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 } }