using System.Text;
using XFESystem.Core;
namespace XFESystem.Storage;
public sealed class MountedWorkspace : IWorkspaceBinaryStorage
{
public const string DisksPath="X:\\Disks";
readonly object gate=new();
readonly IWorkspaceBinaryStorage workspace;
readonly List<MountedVolume> volumes=[];
readonly List<string> unavailable=[];
readonly string reportPath;
public bool IsPersistent=>workspace.IsPersistent;
public MountedWorkspace(IWorkspaceBinaryStorage workspace,IEnumerable<IPhysicalDisk> disks,Action<string>? trace=null)
{
this.workspace=workspace;
reportPath=DisksPath+"\\MountedVolumes.txt";
for(int suffix=1;workspace.Exists(reportPath);suffix++)reportPath=DisksPath+"\\MountedVolumes-"+suffix+".txt";
foreach(var disk in disks)
{
try
{
var partitions=DiskPartitions.Read(disk);
foreach(var partition in partitions)
{
string root=DisksPath+"\\"+disk.Name+"P"+partition.Number;
if(workspace.Exists(root)){unavailable.Add(root+": existing workspace path was preserved");continue;}
try
{
var volume=new MountedVolume(disk,partition,root);volumes.Add(volume);
trace?.Invoke("XFE_VOLUME_MOUNT|"+root+"|FS|"+volume.FileSystem+"|Writable|"+(volume.Writable?1:0)+"|Label|"+volume.Label);
}
catch(Exception error)when(error is IOException or ArgumentException or NotSupportedException or OverflowException or UnauthorizedAccessException)
{unavailable.Add(root+": "+error.Message);trace?.Invoke("XFE_VOLUME_UNAVAILABLE|"+root+"|"+error.GetType().Name);}
}
}
catch(Exception error)when(error is IOException or ArgumentException or OverflowException)
{unavailable.Add(disk.Name+": "+error.Message);trace?.Invoke("XFE_DISK_UNAVAILABLE|"+disk.Name+"|"+error.GetType().Name);}
}
}
static bool Is(string a,string b)=>a.Equals(b,StringComparison.OrdinalIgnoreCase);
static bool Under(string path,string root)=>Is(path,root)||path.StartsWith(root+"\\",StringComparison.OrdinalIgnoreCase);
MountedVolume? Volume(string path)=>volumes.FirstOrDefault(v=>Under(path,v.Path));
string ReportPath=>reportPath;
string Report()=>"Mounted disks\n"+string.Join('\n',volumes.Select(v=>v.Path+" | "+v.FileSystem+" | "+v.Label+" | "+(v.Writable?"Read/write":"Read-only: "+v.ReadOnlyReason)))+
"\n"+string.Join('\n',unavailable)+"\n";
void Protect(string path){if(Is(path,DisksPath)||Is(path,ReportPath))throw new IOException("The disk list is managed by the system.");}
public bool Exists(string path)
{path=SystemPaths.Normalize(path);lock(gate)return Is(path,DisksPath)||Is(path,ReportPath)||(Volume(path)is{}v?v.Exists(path):workspace.Exists(path));}
public WorkspaceEntry[] List(string path)
{
path=SystemPaths.Normalize(path);lock(gate)
{
if(Volume(path)is{}v)return v.List(path);
var entries=workspace.Exists(path)?workspace.List(path).ToList():Is(path,DisksPath)?[]:throw new DirectoryNotFoundException(path);
if(Is(path,SystemPaths.Root)&&!entries.Any(e=>Is(e.Path,DisksPath)))entries.Add(new(DisksPath,true,0));
if(Is(path,DisksPath))
{entries.AddRange(volumes.Select(v=>new WorkspaceEntry(v.Path,true,0)));if(!entries.Any(e=>Is(e.Path,ReportPath)))entries.Add(new(ReportPath,false,Encoding.UTF8.GetByteCount(Report())));}
return entries.OrderBy(e=>!e.IsDirectory).ThenBy(e=>e.Name,StringComparer.OrdinalIgnoreCase).ToArray();
}
}
public string ReadText(string path)
{path=SystemPaths.Normalize(path);lock(gate)return Is(path,ReportPath)?Report():Volume(path)is{}v?v.ReadText(path):workspace.ReadText(path);}
public byte[] ReadBytes(string path)
{path=SystemPaths.Normalize(path);lock(gate)return Is(path,ReportPath)?Encoding.UTF8.GetBytes(Report()):Volume(path)is{}v?v.ReadBytes(path):workspace.ReadBytes(path);}
public void WriteText(string path,string content)
{path=SystemPaths.Normalize(path);lock(gate){Protect(path);if(Volume(path)is{}v)v.WriteText(path,content);else workspace.WriteText(path,content);}}
public void WriteBytes(string path,ReadOnlySpan<byte> content)
{path=SystemPaths.Normalize(path);lock(gate){Protect(path);if(Volume(path)is{}v)v.WriteBytes(path,content);else workspace.WriteBytes(path,content);}}
public void CreateDirectory(string path)
{path=SystemPaths.Normalize(path);lock(gate){if(Is(path,DisksPath))return;Protect(path);if(Volume(path)is{}v)v.CreateDirectory(path);else workspace.CreateDirectory(path);}}
public void Delete(string path)
{path=SystemPaths.Normalize(path);lock(gate){Protect(path);if(Volume(path)is{}v)v.Delete(path);else workspace.Delete(path);}}
public void Move(string source,string destination)
{
source=SystemPaths.Normalize(source);destination=SystemPaths.Normalize(destination);lock(gate)
{
Protect(source);Protect(destination);var from=Volume(source);var to=Volume(destination);
if(from==to){if(from!=null)from.Move(source,destination);else workspace.Move(source,destination);return;}
if(Exists(destination))throw new IOException("Destination already exists.");
// Cross-volume files commit and verify the destination before the
// source is removed. Directory trees require explicit copy steps.
byte[] bytes=ReadBytes(source);WriteBytes(destination,bytes);
if(!ReadBytes(destination).AsSpan().SequenceEqual(bytes))throw new IOException("Cross-volume copy verification failed; source was preserved.");
Delete(source);
}
}
}
using System.Text;
using XFESystem.Core;
namespace XFESystem.Storage;
public sealed class MountedWorkspace : IWorkspaceBinaryStorage
{
public const string DisksPath="X:\\Disks";
readonly object gate=new();
readonly IWorkspaceBinaryStorage workspace;
readonly List<MountedVolume> volumes=[];
readonly List<string> unavailable=[];
readonly string reportPath;
public bool IsPersistent=>workspace.IsPersistent;
public MountedWorkspace(IWorkspaceBinaryStorage workspace,IEnumerable<IPhysicalDisk> disks,Action<string>? trace=null)
{
this.workspace=workspace;
reportPath=DisksPath+"\\MountedVolumes.txt";
for(int suffix=1;workspace.Exists(reportPath);suffix++)reportPath=DisksPath+"\\MountedVolumes-"+suffix+".txt";
foreach(var disk in disks)
{
try
{
var partitions=DiskPartitions.Read(disk);
foreach(var partition in partitions)
{
string root=DisksPath+"\\"+disk.Name+"P"+partition.Number;
if(workspace.Exists(root)){unavailable.Add(root+": existing workspace path was preserved");continue;}
try
{
var volume=new MountedVolume(disk,partition,root);volumes.Add(volume);
trace?.Invoke("XFE_VOLUME_MOUNT|"+root+"|FS|"+volume.FileSystem+"|Writable|"+(volume.Writable?1:0)+"|Label|"+volume.Label);
}
catch(Exception error)when(error is IOException or ArgumentException or NotSupportedException or OverflowException or UnauthorizedAccessException)
{unavailable.Add(root+": "+error.Message);trace?.Invoke("XFE_VOLUME_UNAVAILABLE|"+root+"|"+error.GetType().Name);}
}
}
catch(Exception error)when(error is IOException or ArgumentException or OverflowException)
{unavailable.Add(disk.Name+": "+error.Message);trace?.Invoke("XFE_DISK_UNAVAILABLE|"+disk.Name+"|"+error.GetType().Name);}
}
}
static bool Is(string a,string b)=>a.Equals(b,StringComparison.OrdinalIgnoreCase);
static bool Under(string path,string root)=>Is(path,root)||path.StartsWith(root+"\\",StringComparison.OrdinalIgnoreCase);
MountedVolume? Volume(string path)=>volumes.FirstOrDefault(v=>Under(path,v.Path));
string ReportPath=>reportPath;
string Report()=>"Mounted disks\n"+string.Join('\n',volumes.Select(v=>v.Path+" | "+v.FileSystem+" | "+v.Label+" | "+(v.Writable?"Read/write":"Read-only: "+v.ReadOnlyReason)))+
"\n"+string.Join('\n',unavailable)+"\n";
void Protect(string path){if(Is(path,DisksPath)||Is(path,ReportPath))throw new IOException("The disk list is managed by the system.");}
public bool Exists(string path)
{path=SystemPaths.Normalize(path);lock(gate)return Is(path,DisksPath)||Is(path,ReportPath)||(Volume(path)is{}v?v.Exists(path):workspace.Exists(path));}
public WorkspaceEntry[] List(string path)
{
path=SystemPaths.Normalize(path);lock(gate)
{
if(Volume(path)is{}v)return v.List(path);
var entries=workspace.Exists(path)?workspace.List(path).ToList():Is(path,DisksPath)?[]:throw new DirectoryNotFoundException(path);
if(Is(path,SystemPaths.Root)&&!entries.Any(e=>Is(e.Path,DisksPath)))entries.Add(new(DisksPath,true,0));
if(Is(path,DisksPath))
{entries.AddRange(volumes.Select(v=>new WorkspaceEntry(v.Path,true,0)));if(!entries.Any(e=>Is(e.Path,ReportPath)))entries.Add(new(ReportPath,false,Encoding.UTF8.GetByteCount(Report())));}
return entries.OrderBy(e=>!e.IsDirectory).ThenBy(e=>e.Name,StringComparer.OrdinalIgnoreCase).ToArray();
}
}
public string ReadText(string path)
{path=SystemPaths.Normalize(path);lock(gate)return Is(path,ReportPath)?Report():Volume(path)is{}v?v.ReadText(path):workspace.ReadText(path);}
public byte[] ReadBytes(string path)
{path=SystemPaths.Normalize(path);lock(gate)return Is(path,ReportPath)?Encoding.UTF8.GetBytes(Report()):Volume(path)is{}v?v.ReadBytes(path):workspace.ReadBytes(path);}
public void WriteText(string path,string content)
{path=SystemPaths.Normalize(path);lock(gate){Protect(path);if(Volume(path)is{}v)v.WriteText(path,content);else workspace.WriteText(path,content);}}
public void WriteBytes(string path,ReadOnlySpan<byte> content)
{path=SystemPaths.Normalize(path);lock(gate){Protect(path);if(Volume(path)is{}v)v.WriteBytes(path,content);else workspace.WriteBytes(path,content);}}
public void CreateDirectory(string path)
{path=SystemPaths.Normalize(path);lock(gate){if(Is(path,DisksPath))return;Protect(path);if(Volume(path)is{}v)v.CreateDirectory(path);else workspace.CreateDirectory(path);}}
public void Delete(string path)
{path=SystemPaths.Normalize(path);lock(gate){Protect(path);if(Volume(path)is{}v)v.Delete(path);else workspace.Delete(path);}}
public void Move(string source,string destination)
{
source=SystemPaths.Normalize(source);destination=SystemPaths.Normalize(destination);lock(gate)
{
Protect(source);Protect(destination);var from=Volume(source);var to=Volume(destination);
if(from==to){if(from!=null)from.Move(source,destination);else workspace.Move(source,destination);return;}
if(Exists(destination))throw new IOException("Destination already exists.");
// Cross-volume files commit and verify the destination before the
// source is removed. Directory trees require explicit copy steps.
byte[] bytes=ReadBytes(source);WriteBytes(destination,bytes);
if(!ReadBytes(destination).AsSpan().SequenceEqual(bytes))throw new IOException("Cross-volume copy verification failed; source was preserved.");
Delete(source);
}
}
}