using System.Text; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; using Mono.Unix.Native; using MoonCore.Unix.SecureFs; using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers; using MoonlightServers.DaemonShared.Enums; namespace MoonlightServers.Daemon.Helpers; public class ServerFileSystem { private readonly SecureFileSystem FileSystem; public ServerFileSystem(SecureFileSystem fileSystem) { FileSystem = fileSystem; } public Task ListAsync(string inputPath) { var path = Normalize(inputPath); var entries = FileSystem.ReadDir(path); IEnumerable entryQuery = entries; // Filter all lost+found directories on the root of the file system // to hide the folder shown by virtual disk volumes if (string.IsNullOrEmpty(inputPath) || inputPath == "/") entryQuery = entryQuery.Where(x => x.Name != "lost+found"); var result = entryQuery .Select(x => new ServerFileSystemResponse() { Name = x.Name, IsFolder = x.IsDirectory, Size = x.Size, UpdatedAt = x.LastChanged, CreatedAt = x.CreatedAt }) .ToArray(); return Task.FromResult(result); } public Task MoveAsync(string inputOldPath, string inputNewPath) { var oldPath = Normalize(inputOldPath); var newPath = Normalize(inputNewPath); FileSystem.Rename(oldPath, newPath); return Task.CompletedTask; } public Task DeleteAsync(string inputPath) { var path = Normalize(inputPath); FileSystem.RemoveAll(path); return Task.CompletedTask; } public Task MkdirAsync(string inputPath) { var path = Normalize(inputPath); FileSystem.MkdirAll(path, FilePermissions.ACCESSPERMS); return Task.CompletedTask; } public Task TouchAsync(string inputPath) { var path = Normalize(inputPath); var parentDirectory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/") FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS); FileSystem.OpenFileWrite( path, _ => { }, OpenFlags.O_CREAT ); // We use these custom flags to ensure we aren't overwriting the file return Task.CompletedTask; } public Task CreateChunkAsync(string inputPath, long totalSize, long positionToSkip, Stream chunkStream) { var path = Normalize(inputPath); var parentDirectory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/") FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS); FileSystem.OpenFileWrite(path, fileStream => { if (fileStream.Length != totalSize) fileStream.SetLength(totalSize); fileStream.Position = positionToSkip; chunkStream.CopyTo(fileStream); fileStream.Flush(); }, OpenFlags.O_CREAT | OpenFlags.O_RDWR); // We use these custom flags to ensure we aren't overwriting the file return Task.CompletedTask; } public Task CreateAsync(string inputPath, Stream dataStream) { var path = Normalize(inputPath); var parentDirectory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/") FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS); FileSystem.OpenFileWrite(path, stream => { stream.Position = 0; dataStream.CopyTo(stream); stream.Flush(); }); return Task.CompletedTask; } public Task ReadAsync(string inputPath, Func onHandle) { var path = Normalize(inputPath); FileSystem.OpenFileRead(path, stream => { // No try catch here because the safe fs abstraction already handles every error occuring in the handle onHandle.Invoke(stream).Wait(); }); return Task.CompletedTask; } #region Compression public Task CompressAsync(string[] itemsInput, string destinationInput, CompressType type) { var destination = Normalize(destinationInput); var items = itemsInput.Select(Normalize); if (type == CompressType.Zip) { FileSystem.OpenFileWrite(destination, stream => { using var zipStream = new ZipOutputStream(stream); foreach (var item in items) AddItemToZip(item, zipStream); zipStream.Flush(); stream.Flush(); zipStream.Close(); }); } else if (type == CompressType.TarGz) { FileSystem.OpenFileWrite(destination, stream => { using var gzStream = new GZipOutputStream(stream); using var tarStream = new TarOutputStream(gzStream, Encoding.UTF8); foreach (var item in items) AddItemToTar(item, tarStream); tarStream.Flush(); gzStream.Flush(); stream.Flush(); tarStream.Close(); gzStream.Close(); }); } return Task.CompletedTask; } public Task DecompressAsync(string pathInput, string destinationInput, CompressType type) { var path = Normalize(pathInput); var destination = Normalize(destinationInput); if (type == CompressType.Zip) { FileSystem.OpenFileRead(path, fileStream => { var zipInputStream = new ZipInputStream(fileStream); ExtractZip(zipInputStream, destination); }); } else if (type == CompressType.TarGz) { FileSystem.OpenFileRead(path, fileStream => { var gzInputStream = new GZipInputStream(fileStream); var zipInputStream = new TarInputStream(gzInputStream, Encoding.UTF8); ExtractTar(zipInputStream, destination); }); } return Task.CompletedTask; } private void AddItemToZip(string path, ZipOutputStream outputStream) { var item = FileSystem.Stat(path); if (item.IsDirectory) { var contents = FileSystem.ReadDir(path); foreach (var content in contents) { AddItemToZip( Path.Combine(path, content.Name), outputStream ); } } else { var entry = new ZipEntry(path) { Size = item.Size, DateTime = item.LastChanged }; outputStream.PutNextEntry(entry); FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); }); outputStream.CloseEntry(); } } private void AddItemToTar(string path, TarOutputStream outputStream) { var item = FileSystem.Stat(path); if (item.IsDirectory) { var contents = FileSystem.ReadDir(path); foreach (var content in contents) { AddItemToTar( Path.Combine(path, content.Name), outputStream ); } } else { var entry = TarEntry.CreateTarEntry(path); entry.Name = path; entry.Size = item.Size; entry.ModTime = item.LastChanged; outputStream.PutNextEntry(entry); FileSystem.OpenFileRead(path, stream => { stream.CopyTo(outputStream); }); outputStream.CloseEntry(); } } private void ExtractZip(ZipInputStream inputStream, string destination) { while (true) { var entry = inputStream.GetNextEntry(); if (entry == null) break; if (entry.IsDirectory) continue; var fileDestination = Path.Combine(destination, entry.Name); var parentDirectory = Path.GetDirectoryName(fileDestination); if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/") FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS); FileSystem.OpenFileWrite(fileDestination, stream => { stream.Position = 0; inputStream.CopyTo(stream); stream.Flush(); }); // This will override the file if it exists } } private void ExtractTar(TarInputStream inputStream, string destination) { while (true) { var entry = inputStream.GetNextEntry(); if (entry == null) break; if (entry.IsDirectory) continue; var fileDestination = Path.Combine(destination, entry.Name); var parentDirectory = Path.GetDirectoryName(fileDestination); if (!string.IsNullOrEmpty(parentDirectory) && parentDirectory != "/") FileSystem.MkdirAll(parentDirectory, FilePermissions.ACCESSPERMS); FileSystem.OpenFileWrite(fileDestination, stream => { stream.Position = 0; inputStream.CopyTo(stream); stream.Flush(); }); // This will override the file if it exists } } #endregion private string Normalize(string path) { return path .Replace("//", "/") .Replace("..", "") .TrimStart('/'); } }