using System.IO;
using System.Text.Json;
using Zio.FileSystems;
namespace Sandbox;
///
/// A filesystem. Could be on disk, or in memory, or in the cloud. Could be writable or read only.
/// Or it could be an aggregation of all those things, merged together and read only.
///
public class BaseFileSystem
{
internal static JsonSerializerOptions JsonSerializerOptions { get; set; }
protected Zio.IFileSystem system;
protected Zio.IFileSystemWatcher watcher;
internal List watchers { get; } = new List();
internal HashSet changedFiles { get; } = new HashSet( StringComparer.OrdinalIgnoreCase );
internal BaseFileSystem( Zio.IFileSystem system )
{
this.system = system;
}
internal BaseFileSystem()
{
}
///
public bool IsValid => system != null;
///
/// Returns true if this filesystem is read only
///
public bool IsReadOnly => system is ReadOnlyFileSystem;
internal bool WatchEnabled = true;
internal bool PendingDispose = false;
internal bool TraceChanges = false;
internal virtual void Dispose()
{
lock ( FileWatch.WithChanges )
{
system?.Dispose();
system = null;
watcher?.Dispose();
watcher = null;
foreach ( var watcherSbox in watchers.ToArray() ) watcherSbox.Dispose();
watchers.Clear();
changedFiles.Clear();
}
}
///
/// Get a list of directories
///
public IEnumerable FindDirectory( string folder, string pattern = "*", bool recursive = false )
{
folder = FixPath( folder );
foreach ( var path in system.EnumeratePaths( folder, pattern, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly, Zio.SearchTarget.Directory ) )
{
yield return path.FullName.Substring( folder.Length ).Trim( '/' );
}
}
///
/// Unoptimal, for debugging purposes - don't expose
///
internal int FileCount => FindFile( "/", recursive: true ).ToArray().Length;
///
/// Get a list of files
///
public IEnumerable FindFile( string folder, string pattern = "*", bool recursive = false )
{
folder = FixPath( folder );
List foundFiles = new();
try
{
foreach ( var path in system.EnumeratePaths( folder, pattern, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly, Zio.SearchTarget.File ) )
{
var found = path.FullName.Substring( folder.Length ).Trim( '/' );
foundFiles.Add( found );
}
}
catch ( System.IO.DirectoryNotFoundException ) { }// If directory not found, doesn't matter
return foundFiles;
}
///
/// Delete a folder and optionally all of its contents
///
public void DeleteDirectory( string folder, bool recursive = false )
{
system.DeleteDirectory( FixPath( folder ), recursive );
}
///
/// Delete a file
///
public void DeleteFile( string path )
{
system.DeleteFile( FixPath( path ) );
}
///
/// Create a directory - or a tree of directories.
/// Returns silently if the directory already exists.
///
///
public void CreateDirectory( string folder )
{
if ( string.IsNullOrWhiteSpace( folder ) )
return;
system.CreateDirectory( FixPath( folder ) );
}
///
/// Returns true if the file exists on this filesystem
///
public bool FileExists( string path )
{
ArgumentNullException.ThrowIfNullOrEmpty( path, "path" );
Assert.NotNull( system );
if ( path.Contains( ":" ) ) return false;
return system.FileExists( FixPath( path ) );
}
///
/// Returns true if the directory exists on this filesystem
///
public bool DirectoryExists( string path ) => system.DirectoryExists( FixPath( path ) );
///
/// Returns the full physical path to a file or folder on disk,
/// or null if it isn't on disk.
///
public string GetFullPath( string path )
{
if ( path.Contains( ":" ) )
return path;
if ( system is Zio.FileSystems.SubFileSystem sfs )
{
return sfs.ConvertPathToInternal( FixPath( path ) );
}
if ( system is Zio.FileSystems.AggregateFileSystem afs )
{
// This probably isn't optimal
var entry = afs.FindFirstFileSystemEntry( FixPath( path ) );
if ( entry == null ) return null;
return entry?.FileSystem.ConvertPathToInternal( entry.Path );
}
return null;
}
static string GetRelativePath( Zio.IFileSystem system, string path )
{
if ( system is Zio.FileSystems.SubFileSystem sfs )
{
try
{
return sfs.ConvertPathFromInternal( path ).FullName;
}
catch ( System.ArgumentException ) // System.ArgumentException: Path `C:/git/sbox-boomer/code/UI/Info.razor` must be absolute (Parameter 'path')
{
return null;
}
}
if ( system is Zio.FileSystems.AggregateFileSystem afs )
{
foreach ( var e in afs.GetFileSystems() )
{
try
{
var a = GetRelativePath( e, path );
if ( a is not null )
return a;
}
catch ( System.InvalidOperationException )
{
continue;
}
catch ( System.ArgumentException ) // System.ArgumentException: Path must be absolute (Parameter 'path')
{
continue;
}
}
}
return null;
}
///
/// Returns the relative path
///
internal string GetRelativePath( string path )
{
if ( string.IsNullOrWhiteSpace( path ) ) return null;
return GetRelativePath( system, path.ToLowerInvariant() );
}
///
/// Write the contents to the path. The file will be over-written if the file exists
///
public void WriteAllText( string path, string contents )
{
using ( var stream = OpenWrite( path ) )
using ( var writer = new System.IO.StreamWriter( stream ) )
{
writer.Write( contents );
}
}
///
/// Write the contents to the path. The file will be over-written if the file exists
///
public void WriteAllBytes( string path, byte[] contents )
{
using ( var stream = OpenWrite( path ) )
{
stream.Write( contents, 0, contents.Length );
}
}
///
/// Given a filename, create a path to it
///
void CreatePathForFile( string filePath )
{
var directory = Path.GetDirectoryName( filePath );
if ( string.IsNullOrEmpty( directory ) ) return;
if ( DirectoryExists( directory ) ) return;
CreateDirectory( directory );
}
///
/// Read the contents of path and return it as a string.
/// Returns null if file not found.
///
public string ReadAllText( string path )
{
if ( !FileExists( path ) )
return null;
using ( var f = OpenRead( path, FileMode.Open ) )
using ( var r = new StreamReader( f, Encoding.UTF8, true ) )
{
return r.ReadToEnd();
}
}
///
/// Read the contents of path and return it as a string
///
public Span ReadAllBytes( string path )
{
using ( var f = OpenRead( path ) )
{
var bytes = new byte[f.Length];
f.ReadExactly( bytes, 0, bytes.Length );
return bytes;
}
}
///
/// Read the contents of path and return it as a string
///
public async Task ReadAllBytesAsync( string path )
{
using ( var f = OpenRead( path ) )
{
var bytes = new byte[f.Length];
await f.ReadExactlyAsync( bytes, 0, bytes.Length );
return bytes;
}
}
///
/// Read the contents of path and return it as a string
///
public async Task ReadAllTextAsync( string path )
{
using ( var f = OpenRead( path ) )
using ( var r = new StreamReader( f, Encoding.UTF8, true ) )
{
return await r.ReadToEndAsync();
}
}
///
/// Create a sub-filesystem at the specified path
///
public BaseFileSystem CreateSubSystem( string path )
{
// Log.Trace( $"CreateFileSystem( {path} ) [{GetFullPath(path)}]" );
var sub = new Zio.FileSystems.SubFileSystem( system, FixPath( path ), false );
return new BaseFileSystem( sub );
}
///
/// Open a file for write. If the file exists we'll overwrite it (by default)
///
public System.IO.Stream OpenWrite( string path, FileMode mode = FileMode.Create )
{
CreatePathForFile( path );
return system.OpenFile( FixPath( path ), mode, FileAccess.Write );
}
///
/// Open a file for read. Will throw an exception if it doesn't exist.
///
public System.IO.Stream OpenRead( string path, FileMode mode = FileMode.Open )
{
return system.OpenFile( FixPath( path ), mode, FileAccess.Read, FileShare.Read );
}
///
/// Read Json from a file using System.Text.Json.JsonSerializer. This will throw exceptions
/// if not valid json.
///
public T ReadJson( string filename, T defaultValue = default )
{
var text = ReadAllText( filename );
if ( string.IsNullOrWhiteSpace( text ) )
return defaultValue;
return System.Text.Json.JsonSerializer.Deserialize( text, JsonSerializerOptions );
}
///
/// The same as ReadJson except will return a default value on missing/error.
///
public T ReadJsonOrDefault( string filename, T returnOnError = default )
{
try
{
return ReadJson( filename, returnOnError );
}
catch ( System.Exception )
{
return returnOnError;
}
}
///
/// Convert object to json and write it to the specified file
///
public void WriteJson( string filename, T data )
{
var text = System.Text.Json.JsonSerializer.Serialize( data, JsonSerializerOptions );
WriteAllText( filename, text );
}
///
/// Gets the size in bytes of all the files in a directory
///
public int DirectorySize( string path, bool recursive = false )
{
return (int)FindFile( path, recursive: recursive ).Sum( x => system.GetFileLength( Path.Combine( path, x ) ) );
}
internal FileWatch Watch( string pathglob = null )
{
watcher?.Dispose();
watcher = null;
if ( watcher == null )
{
watcher = system.Watch( "/" );
watcher.NotifyFilter = Zio.NotifyFilters.Attributes | Zio.NotifyFilters.Size | Zio.NotifyFilters.CreationTime | Zio.NotifyFilters.LastWrite | Zio.NotifyFilters.FileName | Zio.NotifyFilters.DirectoryName | Zio.NotifyFilters.Security;
watcher.IncludeSubdirectories = true;
watcher.Changed += OnDirectoryContentsChanged;
watcher.Deleted += OnDirectoryContentsChanged;
watcher.Created += OnDirectoryContentsChanged;
watcher.Renamed += OnDirectoryContentsRenamed;
watcher.Error += OnDirectoryContentsError;
watcher.EnableRaisingEvents = true;
}
FileWatch w = (pathglob != null) ? new FileWatch( this, pathglob ) : new FileWatch( this );
watchers.Add( w );
return w;
}
internal void RemoveWatcher( FileWatch watcher )
{
watchers?.Remove( watcher );
}
internal void AddChangedFile( string path )
{
if ( !WatchEnabled )
return;
path = path.ToLower();
// Ignore common visual studio spam
if ( path.EndsWith( ".tmp" ) || path.EndsWith( "~" ) )
return;
if ( path == "/accesslist.txt" )
return;
lock ( FileWatch.WithChanges )
{
if ( changedFiles.Contains( path ) )
return;
// Log.Info( $"File Changed [{path}]" );
changedFiles.Add( path );
if ( !FileWatch.WithChanges.Contains( this ) )
FileWatch.WithChanges.Add( this );
FileWatch.TimeSinceLastChange = 0;
}
}
void OnDirectoryContentsChanged( object sender, Zio.FileChangedEventArgs e )
{
string path = e.FullPath.FullName;
if ( TraceChanges )
{
Log.Trace( $"File [{e.ChangeType}] - {path} / {e.FullPath} / {e.Name}" );
}
AddChangedFile( path );
}
private static Stopwatch timeSinceActivity = Stopwatch.StartNew();
void OnDirectoryContentsRenamed( object sender, Zio.FileRenamedEventArgs e )
{
string path = e.FullPath.FullName;
string oldpath = e.OldFullPath.FullName;
if ( TraceChanges )
{
Log.Trace( $"File [{e.ChangeType}] - {oldpath} -> {path}" );
}
AddChangedFile( path );
AddChangedFile( oldpath );
}
private void OnDirectoryContentsError( object sender, Zio.FileSystemErrorEventArgs e )
{
if ( TraceChanges )
{
Log.Warning( $"File [Error] - {e.Exception}" );
}
}
///
/// Returns CRC64 of the file contents.
///
/// File path to the file to get CRC of.
/// The CRC64, or 0 if file is not found.
public async Task GetCrcAsync( string filepath )
{
try
{
using ( var s = OpenRead( filepath, FileMode.Open ) )
{
return await Sandbox.Utility.Crc64.FromStreamAsync( s );
}
}
catch ( System.IO.FileNotFoundException )
{
return 0;
}
}
///
/// Returns CRC64 of the file contents.
///
/// File path to the file to get CRC of.
/// The CRC64, or 0 if file is not found.
public ulong GetCrc( string filepath )
{
try
{
using ( var s = OpenRead( filepath, FileMode.Open ) )
{
return Sandbox.Utility.Crc64.FromStream( s );
}
}
catch ( System.IO.FileNotFoundException )
{
return 0;
}
}
///
/// Returns file size of given file.
///
/// File path to the file to look up size of.
/// File size, in bytes.
public long FileSize( string filepath )
{
return system.GetFileLength( FixPath( filepath ) );
}
///
/// Mount this path on the filesystem
///
///
internal void Mount( BaseFileSystem filesystem )
{
if ( filesystem == null ) return;
if ( filesystem.system == null ) return;
if ( system is Zio.FileSystems.AggregateFileSystem fs )
{
if ( fs.GetFileSystems().Contains( filesystem.system ) )
return;
fs.AddFileSystem( filesystem.system );
}
}
internal void UnMount( BaseFileSystem filesystem )
{
if ( filesystem == null ) return;
if ( filesystem.system == null ) return;
(system as Zio.FileSystems.AggregateFileSystem).RemoveFileSystem( filesystem.system );
}
///
/// Mount this path on the filesystem, so it's accessible in Mount
///
internal BaseFileSystem CreateAndMount( BaseFileSystem system, string path )
{
var sub = system.CreateSubSystem( path );
Mount( sub );
return sub;
}
///
/// Mount this path on the filesystem, so it's accessible in Mount
///
internal BaseFileSystem CreateAndMount( string path )
{
var sub = new LocalFileSystem( path );
Mount( sub );
return sub;
}
///
/// Zio wants good paths to start with '/' - so we add it here if it isn't already on
///
internal static string FixPath( string path )
{
// Do not allow 0-32 ASCII stuff
if ( path.Any( c => char.IsControl( c ) ) ) throw new ArgumentException( "Path cannot contain control characters!", "path" );
if ( path.Length < 1 )
return "/";
if ( path[0] == '/' ) return path;
return string.Concat( "/", path );
}
///
/// Lowercase the filename and replace \ with /
///
internal static string NormalizeFilename( string filepath )
{
return filepath.ToLower().Replace( "\\", "/" ); // we can probably do more here
}
}