mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
603 lines
15 KiB
C#
603 lines
15 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using Zio.FileSystems;
|
|
|
|
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class BaseFileSystem
|
|
{
|
|
internal static JsonSerializerOptions JsonSerializerOptions { get; set; }
|
|
|
|
protected Zio.IFileSystem system;
|
|
protected Zio.IFileSystemWatcher watcher;
|
|
|
|
internal List<FileWatch> watchers { get; } = new List<FileWatch>();
|
|
internal HashSet<string> changedFiles { get; } = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
|
|
|
|
internal BaseFileSystem( Zio.IFileSystem system )
|
|
{
|
|
this.system = system;
|
|
}
|
|
|
|
internal BaseFileSystem()
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc cref="IValid.IsValid"/>
|
|
public bool IsValid => system != null;
|
|
|
|
/// <summary>
|
|
/// Returns true if this filesystem is read only
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a list of directories
|
|
/// </summary>
|
|
public IEnumerable<string> 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( '/' );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unoptimal, for debugging purposes - don't expose
|
|
/// </summary>
|
|
internal int FileCount => FindFile( "/", recursive: true ).ToArray().Length;
|
|
|
|
/// <summary>
|
|
/// Get a list of files
|
|
/// </summary>
|
|
public IEnumerable<string> FindFile( string folder, string pattern = "*", bool recursive = false )
|
|
{
|
|
folder = FixPath( folder );
|
|
|
|
List<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a folder and optionally all of its contents
|
|
/// </summary>
|
|
public void DeleteDirectory( string folder, bool recursive = false )
|
|
{
|
|
system.DeleteDirectory( FixPath( folder ), recursive );
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Delete a file
|
|
/// </summary>
|
|
public void DeleteFile( string path )
|
|
{
|
|
system.DeleteFile( FixPath( path ) );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a directory - or a tree of directories.
|
|
/// Returns silently if the directory already exists.
|
|
/// </summary>
|
|
/// <param name="folder"></param>
|
|
public void CreateDirectory( string folder )
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( folder ) )
|
|
return;
|
|
|
|
system.CreateDirectory( FixPath( folder ) );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the file exists on this filesystem
|
|
/// </summary>
|
|
public bool FileExists( string path )
|
|
{
|
|
ArgumentNullException.ThrowIfNullOrEmpty( path, "path" );
|
|
Assert.NotNull( system );
|
|
|
|
if ( path.Contains( ":" ) ) return false;
|
|
|
|
return system.FileExists( FixPath( path ) );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the directory exists on this filesystem
|
|
/// </summary>
|
|
public bool DirectoryExists( string path ) => system.DirectoryExists( FixPath( path ) );
|
|
|
|
/// <summary>
|
|
/// Returns the full physical path to a file or folder on disk,
|
|
/// or null if it isn't on disk.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the relative path
|
|
/// </summary>
|
|
internal string GetRelativePath( string path )
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( path ) ) return null;
|
|
return GetRelativePath( system, path.ToLowerInvariant() );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write the contents to the path. The file will be over-written if the file exists
|
|
/// </summary>
|
|
public void WriteAllText( string path, string contents )
|
|
{
|
|
using ( var stream = OpenWrite( path ) )
|
|
using ( var writer = new System.IO.StreamWriter( stream ) )
|
|
{
|
|
writer.Write( contents );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write the contents to the path. The file will be over-written if the file exists
|
|
/// </summary>
|
|
public void WriteAllBytes( string path, byte[] contents )
|
|
{
|
|
using ( var stream = OpenWrite( path ) )
|
|
{
|
|
stream.Write( contents, 0, contents.Length );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a filename, create a path to it
|
|
/// </summary>
|
|
void CreatePathForFile( string filePath )
|
|
{
|
|
var directory = Path.GetDirectoryName( filePath );
|
|
if ( string.IsNullOrEmpty( directory ) ) return;
|
|
if ( DirectoryExists( directory ) ) return;
|
|
|
|
CreateDirectory( directory );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the contents of path and return it as a string.
|
|
/// Returns null if file not found.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the contents of path and return it as a string
|
|
/// </summary>
|
|
public Span<byte> ReadAllBytes( string path )
|
|
{
|
|
using ( var f = OpenRead( path ) )
|
|
{
|
|
var bytes = new byte[f.Length];
|
|
f.ReadExactly( bytes, 0, bytes.Length );
|
|
return bytes;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the contents of path and return it as a string
|
|
/// </summary>
|
|
public async Task<byte[]> ReadAllBytesAsync( string path )
|
|
{
|
|
using ( var f = OpenRead( path ) )
|
|
{
|
|
var bytes = new byte[f.Length];
|
|
await f.ReadExactlyAsync( bytes, 0, bytes.Length );
|
|
return bytes;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the contents of path and return it as a string
|
|
/// </summary>
|
|
public async Task<string> ReadAllTextAsync( string path )
|
|
{
|
|
using ( var f = OpenRead( path ) )
|
|
using ( var r = new StreamReader( f, Encoding.UTF8, true ) )
|
|
{
|
|
return await r.ReadToEndAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a sub-filesystem at the specified path
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Open a file for write. If the file exists we'll overwrite it (by default)
|
|
/// </summary>
|
|
public System.IO.Stream OpenWrite( string path, FileMode mode = FileMode.Create )
|
|
{
|
|
CreatePathForFile( path );
|
|
|
|
return system.OpenFile( FixPath( path ), mode, FileAccess.Write );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open a file for read. Will throw an exception if it doesn't exist.
|
|
/// </summary>
|
|
public System.IO.Stream OpenRead( string path, FileMode mode = FileMode.Open )
|
|
{
|
|
return system.OpenFile( FixPath( path ), mode, FileAccess.Read, FileShare.Read );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read Json from a file using System.Text.Json.JsonSerializer. This will throw exceptions
|
|
/// if not valid json.
|
|
/// </summary>
|
|
public T ReadJson<T>( string filename, T defaultValue = default )
|
|
{
|
|
var text = ReadAllText( filename );
|
|
if ( string.IsNullOrWhiteSpace( text ) )
|
|
return defaultValue;
|
|
|
|
return System.Text.Json.JsonSerializer.Deserialize<T>( text, JsonSerializerOptions );
|
|
}
|
|
|
|
/// <summary>
|
|
/// The same as ReadJson except will return a default value on missing/error.
|
|
/// </summary>
|
|
public T ReadJsonOrDefault<T>( string filename, T returnOnError = default )
|
|
{
|
|
try
|
|
{
|
|
return ReadJson<T>( filename, returnOnError );
|
|
}
|
|
catch ( System.Exception )
|
|
{
|
|
return returnOnError;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert object to json and write it to the specified file
|
|
/// </summary>
|
|
public void WriteJson<T>( string filename, T data )
|
|
{
|
|
var text = System.Text.Json.JsonSerializer.Serialize( data, JsonSerializerOptions );
|
|
WriteAllText( filename, text );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the size in bytes of all the files in a directory
|
|
/// </summary>
|
|
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}" );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns CRC64 of the file contents.
|
|
/// </summary>
|
|
/// <param name="filepath">File path to the file to get CRC of.</param>
|
|
/// <returns>The CRC64, or 0 if file is not found.</returns>
|
|
public async Task<ulong> GetCrcAsync( string filepath )
|
|
{
|
|
try
|
|
{
|
|
using ( var s = OpenRead( filepath, FileMode.Open ) )
|
|
{
|
|
return await Sandbox.Utility.Crc64.FromStreamAsync( s );
|
|
}
|
|
}
|
|
catch ( System.IO.FileNotFoundException )
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns CRC64 of the file contents.
|
|
/// </summary>
|
|
/// <param name="filepath">File path to the file to get CRC of.</param>
|
|
/// <returns>The CRC64, or 0 if file is not found.</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns file size of given file.
|
|
/// </summary>
|
|
/// <param name="filepath">File path to the file to look up size of.</param>
|
|
/// <returns>File size, in bytes.</returns>
|
|
public long FileSize( string filepath )
|
|
{
|
|
return system.GetFileLength( FixPath( filepath ) );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mount this path on the filesystem
|
|
/// </summary>
|
|
/// <param name="filesystem"></param>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mount this path on the filesystem, so it's accessible in Mount
|
|
/// </summary>
|
|
internal BaseFileSystem CreateAndMount( BaseFileSystem system, string path )
|
|
{
|
|
var sub = system.CreateSubSystem( path );
|
|
Mount( sub );
|
|
return sub;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mount this path on the filesystem, so it's accessible in Mount
|
|
/// </summary>
|
|
internal BaseFileSystem CreateAndMount( string path )
|
|
{
|
|
var sub = new LocalFileSystem( path );
|
|
Mount( sub );
|
|
return sub;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Zio wants good paths to start with '/' - so we add it here if it isn't already on
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lowercase the filename and replace \ with /
|
|
/// </summary>
|
|
internal static string NormalizeFilename( string filepath )
|
|
{
|
|
return filepath.ToLower().Replace( "\\", "/" ); // we can probably do more here
|
|
}
|
|
}
|