BucketsProvider.cs
|
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.IO.Compression; using System.Linq; using System.Management.Automation; using System.Management.Automation.Provider; using System.Text; using System.Text.Json; namespace Buckets.Provider { public class BucketObjectInfo { public string LogicalPath { get; set; } public string PhysicalPath { get; set; } public string Key { get; set; } public string Bucket { get; set; } public string Format { get; set; } public bool Compressed { get; set; } public long SizeBytes { get; set; } public DateTime Modified { get; set; } public object Content { get; set; } } public class BucketItemInfo { public string Mode => IsContainer ? "d----" : "-a---"; public DateTime LastWriteTime { get; set; } public long? Length => IsContainer ? (long?)null : SizeBytes; public string Name { get; set; } // Internal use only internal bool IsContainer { get; set; } internal int ItemCount { get; set; } internal string Format { get; set; } internal long SizeBytes { get; set; } internal string PhysicalPath { get; set; } } [CmdletProvider("Buckets", ProviderCapabilities.ShouldProcess)] public class BucketsProvider : NavigationCmdletProvider { private static readonly char[] InvalidChars = { '/', ':', '*', '?', '"', '<', '>', '|', '.', '[', ']' }; private static readonly byte[] GZipMagic = { 0x1F, 0x8B }; private static readonly char Sep = Path.DirectorySeparatorChar; #region Drive Support protected override PSDriveInfo NewDrive(PSDriveInfo drive) { if (drive == null || string.IsNullOrEmpty(drive.Root)) { WriteError(new ErrorRecord( new ArgumentNullException("drive", "Drive root cannot be null or empty."), "NullDrive", ErrorCategory.InvalidArgument, drive)); return null; } string root = drive.Root; if (!Path.IsPathRooted(root)) { var cwd = SessionState.Path.CurrentFileSystemLocation; if (cwd != null) { root = Path.Combine(cwd.Path, root); } } root = Path.GetFullPath(root); if (!Directory.Exists(root)) { Directory.CreateDirectory(root); } // Return PSDriveInfo with drive name as logical root, store physical root separately // This ensures all navigation methods work with logical paths var newDrive = new PSDriveInfo(drive.Name, this.ProviderInfo, drive.Name + ":", drive.Description, drive.Credential); SessionState.PSVariable.Set("__buckets_physical_root_" + drive.Name, root); return newDrive; } private string GetPhysicalRoot() { string varName = "__buckets_physical_root_" + PSDriveInfo.Name; var variable = SessionState.PSVariable.Get(varName); return variable?.Value as string ?? PSDriveInfo.Root; } protected override Collection<PSDriveInfo> InitializeDefaultDrives() { return new Collection<PSDriveInfo>(); } protected override PSDriveInfo RemoveDrive(PSDriveInfo drive) { return null; } #endregion #region Path Resolution /// <summary> /// Convert a provider-logical path to the actual filesystem path. /// Handles both relative paths (users/Alice) and provider-qualified paths (Buckets::...). /// For leaf paths, probes .dat first, then .json. /// </summary> private string ToPhysicalPath(string path) { string root = GetPhysicalRoot(); if (string.IsNullOrEmpty(path)) { return Directory.Exists(root) ? root : null; } // Strip provider qualification: Buckets::... int colonColon = path.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) { path = path.Substring(colonColon + 2); } // Strip drive prefix: buckets: or Buckets: string driveName = PSDriveInfo.Name + ":"; if (path.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { path = path.Substring(driveName.Length); } // Strip leading separators (these are provider separators, NOT filesystem roots) // This is critical: on Unix, "/users" would be treated as absolute by Path.IsPathRooted path = path.TrimStart(Sep, '/', '\\'); if (string.IsNullOrEmpty(path)) { return Directory.Exists(root) ? root : null; } // NOW check if it's a filesystem-absolute path (e.g. passed directly from the engine) if (Path.IsPathRooted(path)) { string normalized = Path.GetFullPath(path); if (Directory.Exists(normalized)) return normalized; if (File.Exists(normalized)) return normalized; string datPath = normalized + ".dat"; if (File.Exists(datPath)) return datPath; string jsonPath = normalized + ".json"; if (File.Exists(jsonPath)) return jsonPath; return null; } // Normalize separators path = path.Replace('/', Sep).Replace('\\', Sep); string physical = Path.Combine(root, path); if (Directory.Exists(physical)) return physical; if (File.Exists(physical)) return physical; string datPath2 = physical + ".dat"; if (File.Exists(datPath2)) return datPath2; string jsonPath2 = physical + ".json"; if (File.Exists(jsonPath2)) return jsonPath2; return null; } /// <summary> /// Convert a physical filesystem path to a provider-logical path. /// </summary> private string ToLogicalPath(string physicalPath) { string root = GetPhysicalRoot(); if (!physicalPath.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { return physicalPath; } string relative = physicalPath.Substring(root.Length).TrimStart(Sep); // Strip known extensions string lower = relative.ToLowerInvariant(); if (lower.EndsWith(".dat")) { relative = relative.Substring(0, relative.Length - 4); } else if (lower.EndsWith(".json")) { relative = relative.Substring(0, relative.Length - 5); } return relative; } #endregion #region Serialization private object DeserializeFile(string physicalPath, out string format, out bool compressed) { format = "Binary"; compressed = false; byte[] bytes = File.ReadAllBytes(physicalPath); string ext = Path.GetExtension(physicalPath).ToLowerInvariant(); if (ext == ".dat") { if (bytes.Length >= 2 && bytes[0] == GZipMagic[0] && bytes[1] == GZipMagic[1]) { compressed = true; using (var ms = new MemoryStream(bytes)) using (var gzip = new GZipStream(ms, CompressionMode.Decompress)) using (var reader = new StreamReader(gzip, Encoding.Unicode)) { string gzXml = reader.ReadToEnd(); return PSSerializer.Deserialize(gzXml); } } // Buckets module writes CLIXML as UTF-8 (not UTF-16LE) string xml = Encoding.UTF8.GetString(bytes); return PSSerializer.Deserialize(xml); } else if (ext == ".json") { format = "JSON"; string json = Encoding.UTF8.GetString(bytes); if (json.StartsWith("\ufeff")) json = json.Substring(1); return PSSerializer.Deserialize(json); } throw new InvalidOperationException($"Unsupported format: {ext}"); } private void SerializeFile(object value, string physicalPath, string format) { string dir = Path.GetDirectoryName(physicalPath); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) { TrySerializeJson(value, physicalPath); } else { string xml = PSSerializer.Serialize(value); File.WriteAllBytes(physicalPath, Encoding.UTF8.GetBytes(xml)); } } private void TrySerializeJson(object value, string physicalPath) { try { object serializable = ConvertToSerializable(value); var options = new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; string json = JsonSerializer.Serialize(serializable, options); File.WriteAllText(physicalPath, json, Encoding.UTF8); } catch (Exception ex) { string datPath = Path.ChangeExtension(physicalPath, ".dat"); WriteWarning($"JSON serialization failed: {ex.Message}. Falling back to binary ({datPath})."); string xml = PSSerializer.Serialize(value); File.WriteAllBytes(datPath, Encoding.Unicode.GetBytes(xml)); } } private object ConvertToSerializable(object value) { if (value is IDictionary dict) { var result = new Dictionary<string, object>(); foreach (DictionaryEntry entry in dict) { result[entry.Key?.ToString() ?? ""] = ConvertToSerializable(entry.Value); } return result; } var pso = value as PSObject; if (pso != null) { var result = new Dictionary<string, object>(); foreach (var prop in pso.Properties.Where(p => p.IsGettable)) { result[prop.Name] = ConvertToSerializable(prop.Value); } return result; } if (value is IEnumerable enumerable && value is not string) { var list = new List<object>(); foreach (var item in enumerable) { list.Add(ConvertToSerializable(item)); } return list; } return value; } #endregion #region Helpers private string SanitizeKey(string key) { string result = key; foreach (var ch in InvalidChars) { result = result.Replace(ch.ToString(), "_"); } return result; } private string GetBucketName(string logicalPath) { string normalized = ToLogicalPath(logicalPath).TrimStart(Sep, '/', '\\'); int sep = normalized.IndexOf(Sep); return sep < 0 ? normalized : normalized.Substring(0, sep); } private string GetKeyName(string logicalPath) { string normalized = ToLogicalPath(logicalPath).TrimStart(Sep, '/', '\\'); int sep = normalized.LastIndexOf(Sep); return sep < 0 ? normalized : normalized.Substring(sep + 1); } private string GetParentLogicalPath(string logicalPath) { string normalized = ToLogicalPath(logicalPath).TrimStart(Sep, '/', '\\'); int sep = normalized.LastIndexOf(Sep); return sep < 0 ? "" : normalized.Substring(0, sep); } #endregion #region Core Provider Methods protected override bool IsValidPath(string path) { return true; } protected override bool ItemExists(string path) { return ToPhysicalPath(path) != null; } protected override bool IsItemContainer(string path) { string physical = ToPhysicalPath(path); return physical != null && Directory.Exists(physical); } protected override void GetItem(string path) { string physical = ToPhysicalPath(path); if (physical == null) { WriteError(new ErrorRecord( new ItemNotFoundException($"Item not found: {path}"), "ItemNotFound", ErrorCategory.ObjectNotFound, path)); return; } if (Directory.Exists(physical)) { var di = new DirectoryInfo(physical); var items = di.GetFiles("*.dat").Concat(di.GetFiles("*.json")); var dirs = di.GetDirectories().Where(d => d.Name != ".buckets"); int count = items.Count() + dirs.Count(); var info = new BucketItemInfo { Name = di.Name, IsContainer = true, ItemCount = count, Format = "", SizeBytes = 0, PhysicalPath = physical, LastWriteTime = di.LastWriteTime }; WriteItemObject(info, ToLogicalPath(physical), true); } else { try { object content = DeserializeFile(physical, out string format, out bool compressed); var fi = new FileInfo(physical); var info = new BucketObjectInfo { LogicalPath = ToLogicalPath(physical), PhysicalPath = physical, Key = GetKeyName(path), Bucket = GetBucketName(path), Format = format, Compressed = compressed, SizeBytes = fi.Length, Modified = fi.LastWriteTime, Content = content }; WriteItemObject(info, ToLogicalPath(physical), false); } catch (Exception ex) { WriteError(new ErrorRecord( new RuntimeException($"Failed to deserialize: {ex.Message}"), "DeserializeFailed", ErrorCategory.ReadError, path)); } } } protected override void GetChildItems(string path, bool recurse) { string physical = ToPhysicalPath(path); if (physical == null || !Directory.Exists(physical)) { return; } uint currentDepth = 0; EnumerateDirectory(physical, recurse, null, ref currentDepth); } protected override void GetChildItems(string path, bool recurse, uint depth) { GetChildItemsInternal(path, recurse, depth); } private void GetChildItemsInternal(string path, bool recurse, uint? depthLimit = null) { string physical = ToPhysicalPath(path); if (physical == null || !Directory.Exists(physical)) { return; } uint currentDepth = 0; EnumerateDirectory(physical, recurse, depthLimit, ref currentDepth); } private void EnumerateDirectory(string directory, bool recurse, uint? depthLimit, ref uint currentDepth) { if (depthLimit.HasValue && currentDepth >= depthLimit.Value) return; var di = new DirectoryInfo(directory); // Subdirectories foreach (var subDir in di.GetDirectories().OrderBy(d => d.Name)) { if (subDir.Name == ".buckets") continue; var files = subDir.GetFiles("*.dat").Concat(subDir.GetFiles("*.json")); var subDirs = subDir.GetDirectories().Where(d => d.Name != ".buckets"); int count = files.Count() + subDirs.Count(); string logical = ToLogicalPath(subDir.FullName); WriteItemObject(new BucketItemInfo { Name = subDir.Name, IsContainer = true, ItemCount = count, Format = "", SizeBytes = 0, PhysicalPath = subDir.FullName, LastWriteTime = subDir.LastWriteTime }, logical, true); if (recurse) { currentDepth++; EnumerateDirectory(subDir.FullName, recurse, depthLimit, ref currentDepth); currentDepth--; } } // Files foreach (var file in di.GetFiles("*.dat").Concat(di.GetFiles("*.json")).OrderBy(f => f.Name)) { string ext = Path.GetExtension(file.Name).ToLowerInvariant(); string logical = ToLogicalPath(file.FullName); WriteItemObject(new BucketItemInfo { Name = Path.GetFileNameWithoutExtension(file.Name), IsContainer = false, ItemCount = 0, Format = ext == ".json" ? "JSON" : "Binary", SizeBytes = file.Length, PhysicalPath = file.FullName, LastWriteTime = file.LastWriteTime }, logical, false); } } protected override void GetChildNames(string path, ReturnContainers returnContainers) { string physical = ToPhysicalPath(path); if (physical == null || !Directory.Exists(physical)) return; var di = new DirectoryInfo(physical); if (returnContainers == ReturnContainers.ReturnAllContainers) { foreach (var subDir in di.GetDirectories().Where(d => d.Name != ".buckets").OrderBy(d => d.Name)) { WriteItemObject(subDir.Name, subDir.Name, true); } } foreach (var file in di.GetFiles("*.dat").Concat(di.GetFiles("*.json")).OrderBy(f => f.Name)) { WriteItemObject(Path.GetFileNameWithoutExtension(file.Name), Path.GetFileNameWithoutExtension(file.Name), false); } } protected override bool HasChildItems(string path) { string physical = ToPhysicalPath(path); if (physical == null || !Directory.Exists(physical)) return false; var di = new DirectoryInfo(physical); return di.GetFiles("*.dat").Length > 0 || di.GetFiles("*.json").Length > 0 || di.GetDirectories().Length > 0; } #endregion #region Navigation protected override string MakePath(string parent, string child) { if (string.IsNullOrEmpty(parent)) return child ?? ""; if (string.IsNullOrEmpty(child)) return parent; string normalized = parent.TrimEnd(Sep, '/', '\\'); if (child == ".") return normalized; if (child == "..") { string driveName = PSDriveInfo.Name + ":"; if (normalized.EndsWith(driveName, StringComparison.OrdinalIgnoreCase) && normalized.Length == driveName.Length) { return normalized; } int lastSep = normalized.LastIndexOf(Sep); if (lastSep < 0) return normalized; normalized = normalized.Substring(0, lastSep); if (string.IsNullOrEmpty(normalized)) return PSDriveInfo.Name + ":"; return normalized; } if (normalized.EndsWith(":", StringComparison.OrdinalIgnoreCase)) { return normalized + Sep + child; } return normalized + Sep + child; } protected override string GetParentPath(string path, string root) { string cleaned = path; int colonColon = cleaned.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) cleaned = cleaned.Substring(colonColon + 2); string driveName = PSDriveInfo.Name + ":"; if (cleaned.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { cleaned = cleaned.Substring(driveName.Length); } cleaned = cleaned.TrimStart(Sep, '/', '\\'); if (string.IsNullOrEmpty(cleaned)) return root; int sep = cleaned.LastIndexOf(Sep); if (sep < 0) return root; string parentRel = cleaned.Substring(0, sep); return string.IsNullOrEmpty(parentRel) ? root : (root + Sep + parentRel); } protected override string GetChildName(string path) { if (string.IsNullOrEmpty(path)) return ""; string cleaned = path; int colonColon = cleaned.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) cleaned = cleaned.Substring(colonColon + 2); string driveName = PSDriveInfo.Name + ":"; if (cleaned.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { cleaned = cleaned.Substring(driveName.Length); } cleaned = cleaned.TrimStart(Sep, '/', '\\'); if (string.IsNullOrEmpty(cleaned)) return ""; int sep = cleaned.LastIndexOf(Sep); return sep < 0 ? cleaned : cleaned.Substring(sep + 1); } protected override string NormalizeRelativePath(string path, string basePath) { if (string.IsNullOrEmpty(path)) return basePath ?? ""; string cleaned = path; int colonColon = cleaned.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) cleaned = cleaned.Substring(colonColon + 2); string driveName = PSDriveInfo.Name + ":"; if (cleaned.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { cleaned = cleaned.Substring(driveName.Length); } cleaned = cleaned.TrimStart(Sep, '/', '\\'); string root = GetPhysicalRoot(); if (cleaned.StartsWith(root, StringComparison.OrdinalIgnoreCase)) { cleaned = cleaned.Substring(root.Length).TrimStart(Sep, '/', '\\'); } if (string.IsNullOrEmpty(cleaned)) return ""; string[] parts = cleaned.Split(new[] { Sep, '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); var stack = new System.Collections.Generic.List<string>(); foreach (var part in parts) { if (part == ".") continue; if (part == "..") { if (stack.Count > 0) stack.RemoveAt(stack.Count - 1); continue; } stack.Add(part); } return string.Join(Sep.ToString(), stack); } #endregion #region Write Operations protected override void NewItem(string path, string itemTypeName, object newItemValue) { if (string.IsNullOrEmpty(path)) { WriteError(new ErrorRecord(new ArgumentException("Path cannot be empty."), "EmptyPath", ErrorCategory.InvalidArgument, path)); return; } if (!ShouldProcess(path, "New Item")) return; string physical = ToPhysicalPath(path); bool wantContainer = string.Equals(itemTypeName, "directory", StringComparison.OrdinalIgnoreCase); // If path points to non-existent location, build the physical path manually if (physical == null) { string root = GetPhysicalRoot(); string logical = path; int colonColon = logical.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) logical = logical.Substring(colonColon + 2); string driveName = PSDriveInfo.Name + ":"; if (logical.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { logical = logical.Substring(driveName.Length); } logical = logical.TrimStart(Sep, '/', '\\', ' '); if (!string.IsNullOrEmpty(logical)) { logical = logical.Replace('/', Sep).Replace('\\', Sep); physical = Path.Combine(root, logical); } } if (wantContainer) { if (!Directory.Exists(physical)) { Directory.CreateDirectory(physical); } WriteItemObject(new BucketItemInfo { Name = GetChildName(path), IsContainer = true, ItemCount = 0, Format = "", SizeBytes = 0, PhysicalPath = physical, LastWriteTime = DateTime.Now }, ToLogicalPath(physical), true); } else { string format = "binary"; if (!string.IsNullOrEmpty(itemTypeName) && itemTypeName.Equals("json", StringComparison.OrdinalIgnoreCase)) { format = "json"; } string ext = format == "json" ? ".json" : ".dat"; if (string.IsNullOrEmpty(Path.GetExtension(physical))) { physical = Path.ChangeExtension(physical, ext); } string dir = Path.GetDirectoryName(physical); string key = Path.GetFileNameWithoutExtension(physical); string sanitizedKey = SanitizeKey(key); if (string.IsNullOrEmpty(sanitizedKey)) { WriteError(new ErrorRecord(new ArgumentException("Key is empty after sanitization."), "EmptyKey", ErrorCategory.InvalidArgument, path)); return; } physical = Path.Combine(dir, sanitizedKey + ext); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } if (File.Exists(physical)) { WriteError(new ErrorRecord(new IOException($"Item already exists: {ToLogicalPath(physical)}"), "ItemAlreadyExists", ErrorCategory.ResourceExists, path)); return; } SerializeFile(newItemValue, physical, format); var fi = new FileInfo(physical); WriteItemObject(new BucketItemInfo { Name = sanitizedKey, IsContainer = false, ItemCount = 0, Format = format == "json" ? "JSON" : "Binary", SizeBytes = fi.Length, PhysicalPath = physical, LastWriteTime = fi.LastWriteTime }, ToLogicalPath(physical), false); } } protected override void SetItem(string path, object value) { string physical = ToPhysicalPath(path); if (physical == null) { WriteError(new ErrorRecord(new ItemNotFoundException($"Item not found: {path}"), "ItemNotFound", ErrorCategory.ObjectNotFound, path)); return; } if (Directory.Exists(physical)) { WriteError(new ErrorRecord(new InvalidOperationException("Cannot set value on a container."), "ContainerSetError", ErrorCategory.InvalidOperation, path)); return; } try { DeserializeFile(physical, out string format, out _); object existing = DeserializeFile(physical, out _, out _); object merged = MergeObjects(existing, value); SerializeFile(merged, physical, format); object updated = DeserializeFile(physical, out _, out _); var fi = new FileInfo(physical); WriteItemObject(new BucketObjectInfo { LogicalPath = ToLogicalPath(physical), PhysicalPath = physical, Key = GetKeyName(path), Bucket = GetBucketName(path), Format = format, Compressed = false, SizeBytes = fi.Length, Modified = fi.LastWriteTime, Content = updated }, ToLogicalPath(physical), false); } catch (Exception ex) { WriteError(new ErrorRecord(new RuntimeException($"Failed to set item: {ex.Message}"), "SetItemFailed", ErrorCategory.WriteError, path)); } } private object MergeObjects(object existing, object newValue) { // Convert existing to a plain dictionary for clean serialization var existingDict = ToDictionary(existing); if (newValue is IDictionary newDict) { foreach (DictionaryEntry entry in newDict) { existingDict[entry.Key?.ToString() ?? ""] = entry.Value; } return existingDict; } var psoNew = newValue as PSObject; if (psoNew != null) { foreach (var prop in psoNew.Properties.Where(p => p.IsGettable)) { existingDict[prop.Name] = prop.Value; } return existingDict; } return newValue; } private Dictionary<string, object> ToDictionary(object value) { var result = new Dictionary<string, object>(); if (value is IDictionary dict) { foreach (DictionaryEntry entry in dict) { result[entry.Key?.ToString() ?? ""] = entry.Value; } return result; } var pso = value as PSObject; if (pso != null) { // If base object is a dictionary, use it as the primary source if (pso.BaseObject is IDictionary baseDict) { foreach (DictionaryEntry entry in baseDict) { result[entry.Key?.ToString() ?? ""] = entry.Value; } return result; } // For non-dictionary PSObjects, extract properties var skip = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Equals", "GetHashCode", "GetType", "ToString", "pstypenames", "psadapted", "psbase", "psextended", "psobject" }; foreach (var prop in pso.Properties.Where(p => p.IsGettable && !skip.Contains(p.Name))) { result[prop.Name] = prop.Value; } return result; } return result; } protected override void RemoveItem(string path, bool recurse) { string physical = ToPhysicalPath(path); if (physical == null) { WriteError(new ErrorRecord(new ItemNotFoundException($"Item not found: {path}"), "ItemNotFound", ErrorCategory.ObjectNotFound, path)); return; } if (!ShouldProcess(path, "Remove Item")) return; if (File.Exists(physical)) { File.Delete(physical); } else if (Directory.Exists(physical)) { if (!IsSafeBucketDirectory(physical)) { WriteWarning($"Bucket '{path}' contains non-bucket files. Refusing to remove."); return; } Directory.Delete(physical, true); } } private bool IsSafeBucketDirectory(string path) { var di = new DirectoryInfo(path); foreach (var file in di.GetFiles()) { string ext = file.Extension.ToLowerInvariant(); if (ext != ".dat" && ext != ".json") return false; } foreach (var subDir in di.GetDirectories()) { if (subDir.Name != ".arrays") return false; foreach (var subSubDir in subDir.GetDirectories()) { foreach (var file in subSubDir.GetFiles()) { string ext = file.Extension.ToLowerInvariant(); if (ext != ".dat" && ext != ".json") return false; } } } return true; } protected override void MoveItem(string path, string destination) { string srcPhysical = ToPhysicalPath(path); if (srcPhysical == null) { WriteError(new ErrorRecord(new ItemNotFoundException($"Source not found: {path}"), "SourceNotFound", ErrorCategory.ObjectNotFound, path)); return; } string destPhysical = ToPhysicalPath(destination); if (destPhysical != null && Directory.Exists(destPhysical)) { destPhysical = Path.Combine(destPhysical, Path.GetFileName(srcPhysical)); } else if (destPhysical == null) { string root = GetPhysicalRoot(); string logical = destination; int colonColon = logical.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) logical = logical.Substring(colonColon + 2); string driveName = PSDriveInfo.Name + ":"; if (logical.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { logical = logical.Substring(driveName.Length); } logical = logical.TrimStart(Sep, '/', '\\', ' '); if (!string.IsNullOrEmpty(logical)) { logical = logical.Replace('/', Sep).Replace('\\', Sep); destPhysical = Path.Combine(root, logical); } if (File.Exists(srcPhysical)) { string ext = Path.GetExtension(srcPhysical); if (string.IsNullOrEmpty(Path.GetExtension(destPhysical))) { destPhysical = Path.ChangeExtension(destPhysical, ext); } } } if (!ShouldProcess($"{path} -> {destination}", "Move Item")) return; string destDir = Path.GetDirectoryName(destPhysical); if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir); if (File.Exists(srcPhysical)) File.Move(srcPhysical, destPhysical); else if (Directory.Exists(srcPhysical)) Directory.Move(srcPhysical, destPhysical); } protected override void CopyItem(string path, string destination, bool recurse) { string srcPhysical = ToPhysicalPath(path); if (srcPhysical == null) { WriteError(new ErrorRecord(new ItemNotFoundException($"Source not found: {path}"), "SourceNotFound", ErrorCategory.ObjectNotFound, path)); return; } string destPhysical = ToPhysicalPath(destination); if (destPhysical != null && Directory.Exists(destPhysical)) { destPhysical = Path.Combine(destPhysical, Path.GetFileName(srcPhysical)); } else if (destPhysical == null) { string root = GetPhysicalRoot(); string logical = destination; int colonColon = logical.IndexOf("::", StringComparison.Ordinal); if (colonColon >= 0) logical = logical.Substring(colonColon + 2); string driveName = PSDriveInfo.Name + ":"; if (logical.StartsWith(driveName, StringComparison.OrdinalIgnoreCase)) { logical = logical.Substring(driveName.Length); } logical = logical.TrimStart(Sep, '/', '\\', ' '); if (!string.IsNullOrEmpty(logical)) { logical = logical.Replace('/', Sep).Replace('\\', Sep); destPhysical = Path.Combine(root, logical); } if (File.Exists(srcPhysical)) { string ext = Path.GetExtension(srcPhysical); if (string.IsNullOrEmpty(Path.GetExtension(destPhysical))) { destPhysical = Path.ChangeExtension(destPhysical, ext); } } } if (!ShouldProcess($"{path} -> {destination}", "Copy Item")) return; if (File.Exists(srcPhysical)) { string destDir = Path.GetDirectoryName(destPhysical); if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir); File.Copy(srcPhysical, destPhysical, true); } else if (Directory.Exists(srcPhysical)) { CopyDirectory(srcPhysical, destPhysical, recurse); } } private void CopyDirectory(string source, string destination, bool recurse) { if (!Directory.Exists(destination)) Directory.CreateDirectory(destination); var di = new DirectoryInfo(source); foreach (var file in di.GetFiles()) file.CopyTo(Path.Combine(destination, file.Name), true); if (recurse) { foreach (var subDir in di.GetDirectories()) { CopyDirectory(subDir.FullName, Path.Combine(destination, subDir.Name), recurse); } } } protected override void RenameItem(string path, string newName) { string physical = ToPhysicalPath(path); if (physical == null) { WriteError(new ErrorRecord(new ItemNotFoundException($"Item not found: {path}"), "ItemNotFound", ErrorCategory.ObjectNotFound, path)); return; } string sanitized = SanitizeKey(newName); if (string.IsNullOrEmpty(sanitized)) { WriteError(new ErrorRecord(new ArgumentException("New name is empty after sanitization."), "EmptyName", ErrorCategory.InvalidArgument, newName)); return; } if (!ShouldProcess($"{path} -> {newName}", "Rename Item")) return; string parent = Path.GetDirectoryName(physical); string newNameWithExt = File.Exists(physical) ? sanitized + Path.GetExtension(physical) : sanitized; string newPhysical = Path.Combine(parent, newNameWithExt); if (File.Exists(newPhysical) || Directory.Exists(newPhysical)) { WriteError(new ErrorRecord(new IOException($"Target already exists: {newNameWithExt}"), "TargetExists", ErrorCategory.ResourceExists, newName)); return; } File.Move(physical, newPhysical); } #endregion } } |