diff --git a/.gitignore b/.gitignore index 65e8c30..2ad59b6 100644 --- a/.gitignore +++ b/.gitignore @@ -358,3 +358,4 @@ elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/wwwroot/lib/ elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/wwwroot/upload/ elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/thumb/ elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/.storage/ +elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/.temp/ diff --git a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Controllers/FilesController.cs b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Controllers/FilesController.cs index 6e2f6cb..18f9930 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Controllers/FilesController.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Controllers/FilesController.cs @@ -119,11 +119,17 @@ private void CustomizeResponse(ConnectorResult connectorResult, IVolume volume, _connector.PluginManager.Features[typeof(QuotaOptions)] = quotaOptions; // Volume initialization - var volume = new Volume(_driver, Startup.MapStoragePath($"./upload/{volumePath}"), - $"/api/files/storage/upload/{volumePath}/", $"/api/files/thumb/") + var volume = new Volume(_driver, + Startup.MapStoragePath($"./upload/{volumePath}"), + Startup.TempPath, + $"/api/files/storage/upload/{volumePath}/", + $"/api/files/thumb/", + thumbnailDirectory: Startup.MapStoragePath($"./thumb/{volumePath}")) { Name = "My volume", - ThumbnailDirectory = Startup.MapStoragePath($"./thumb/{volumePath}") + MaxUploadFiles = 20, + MaxUploadSizeInMb = 10, + MaxUploadConnections = 3 // 3 upload requests at a time }; _connector.AddVolume(volume); @@ -215,7 +221,7 @@ private void CustomizeResponse(ConnectorResult connectorResult, IVolume volume, { Expression = $"file.ext = 'exe'", // Example only FileFilter = (file) => file.Extension == ".exe", - Locked = true, Write = false, ShowOnly = true, Read = false + Write = false, ShowOnly = true, Read = false }, new FilteredObjectAttribute() { diff --git a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationInitResponse.cs b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationInitResponse.cs index e35be50..2da359a 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationInitResponse.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationInitResponse.cs @@ -9,6 +9,7 @@ public ApplicationInitResponse(InitResponse initResp) cwd = initResp.cwd; files = initResp.files; options = initResp.options; + uplMaxFile = initResp.uplMaxFile; } public long usage { get; set; } diff --git a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationOpenResponse.cs b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationOpenResponse.cs index 891f288..f9c9402 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationOpenResponse.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Models/Responses/ApplicationOpenResponse.cs @@ -9,6 +9,7 @@ public ApplicationOpenResponse(OpenResponse openResp) cwd = openResp.cwd; files = openResp.files; options = openResp.options; + uplMaxFile = openResp.uplMaxFile; } public long usage { get; set; } diff --git a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Pages/Index.cshtml b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Pages/Index.cshtml index 06d0f21..a46a184 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Pages/Index.cshtml +++ b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Pages/Index.cshtml @@ -53,6 +53,7 @@ contextmenu: contextMenu, lang: 'vi', requestType: 'post', + uploadMaxChunkSize: 1024 * 1024 * 10, //onlyMimes: ["image", "text/plain"] // Get files of requested mime types only }; diff --git a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Program.cs b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Program.cs index e546b9a..8b7b081 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Program.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Program.cs @@ -26,7 +26,7 @@ public static void Main(string[] args) Id = 2, UserName = "msdiana", VolumePath = "msdiana", - QuotaInBytes = 20 * (long)Math.Pow(1024, 2), + QuotaInBytes = (long)Math.Pow(1024, 3), }); dbContext.SaveChanges(); diff --git a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Startup.cs b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Startup.cs index 824d14c..57b7a54 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Startup.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.AdvancedDemo/Startup.cs @@ -6,6 +6,7 @@ using elFinder.Net.Core.Services.Drawing; using elFinder.Net.Drivers.FileSystem.Extensions; using elFinder.Net.Drivers.FileSystem.Helpers; +using elFinder.Net.Drivers.FileSystem.Services; using elFinder.Net.Plugins.FileSystemQuotaManagement.Extensions; using elFinder.Net.Plugins.LoggingExample.Extensions; using Microsoft.AspNetCore.Authentication.Cookies; @@ -24,16 +25,19 @@ namespace elFinder.Net.AdvancedDemo public class Startup { public const string StorageFolder = ".storage"; + public const string TempFileFolder = ".temp"; public Startup(IConfiguration configuration, IWebHostEnvironment env) { Configuration = configuration; WebRootPath = env.WebRootPath; StoragePath = new DirectoryInfo(StorageFolder).FullName; + TempPath = new DirectoryInfo(TempFileFolder).FullName; } public static string WebRootPath { get; private set; } public static string StoragePath { get; private set; } + public static string TempPath { get; private set; } public static string MapStoragePath(string path) { @@ -49,7 +53,10 @@ public void ConfigureServices(IServiceCollection services) var pluginCollection = new PluginCollection(); services.AddElFinderAspNetCore() - .AddFileSystemDriver() + .AddFileSystemDriver(tempFileCleanerConfig: (opt) => + { + opt.ScanFolders.Add(TempPath, TempFileCleanerOptions.DefaultUnmanagedLifeTime); + }) .AddFileSystemQuotaManagement(pluginCollection) .AddElFinderLoggingExample(pluginCollection) .AddElFinderPlugins(pluginCollection); diff --git a/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Controllers/FilesController.cs b/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Controllers/FilesController.cs index 9fe1198..2a9f320 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Controllers/FilesController.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Controllers/FilesController.cs @@ -47,11 +47,15 @@ private async Task SetupConnectorAsync() for (var i = 0; i < 5; i++) { var volume = new Volume(_driver, - Startup.MapPath($"~/upload/volume-{i}"), $"/upload/volume-{i}", $"/api/files/thumb/") + Startup.MapPath($"~/upload/volume-{i}"), + Startup.TempPath, + $"/upload/volume-{i}", + $"/api/files/thumb/", + thumbnailDirectory: PathHelper.GetFullPath("./thumb")) { StartDirectory = Startup.MapPath($"~/upload/volume-{i}/start"), Name = $"Volume {i}", - ThumbnailDirectory = PathHelper.GetFullPath("./thumb") + MaxUploadConnections = 3 }; _connector.AddVolume(volume); diff --git a/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Startup.cs b/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Startup.cs index b4e3154..fa8adbb 100644 --- a/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Startup.cs +++ b/elFinder.Net.Core/Demos/elFinder.Net.Demo31/Startup.cs @@ -2,6 +2,7 @@ using elFinder.Net.Core; using elFinder.Net.Drivers.FileSystem.Extensions; using elFinder.Net.Drivers.FileSystem.Helpers; +using elFinder.Net.Drivers.FileSystem.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.ResponseCompression; @@ -22,6 +23,7 @@ public Startup(IConfiguration configuration, IWebHostEnvironment env) } public static string WebRootPath { get; private set; } + public static string TempPath { get; } = Path.GetTempPath(); public static string MapPath(string path, string basePath = null) { @@ -41,7 +43,10 @@ public void ConfigureServices(IServiceCollection services) { #region elFinder services.AddElFinderAspNetCore() - .AddFileSystemDriver(); + .AddFileSystemDriver(tempFileCleanerConfig: (opt) => + { + opt.ScanFolders.Add(TempPath, TempFileCleanerOptions.DefaultUnmanagedLifeTime); + }); #endregion services.AddResponseCompression(options => diff --git a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Extensions/ServiceCollectionExtensions.cs b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Extensions/ServiceCollectionExtensions.cs index 9715502..85dbe3a 100644 --- a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Extensions/ServiceCollectionExtensions.cs +++ b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Extensions/ServiceCollectionExtensions.cs @@ -19,10 +19,10 @@ public static IServiceCollection AddFileSystemQuotaManagement(this IServiceColle if (storageManagerOptionsConfig == null) storageManagerOptionsConfig = (options) => { - options.StorageCachingMinutes = StorageManagerOptions.DefaultStorageCachingMinutes; + options.StorageCachingLifeTime = StorageManagerOptions.DefaultStorageCachingLifeTime; options.MaximumItems = StorageManagerOptions.DefaultMaximumItems; options.ReservationsAfterCleanUp = StorageManagerOptions.DefaultReservationsAfterCleanUp; - options.PollingIntervalInMinutes = StorageManagerOptions.DefaultPollingIntervalInMinutes; + options.PollingInterval = StorageManagerOptions.DefaultPollingInterval; }; services.AddScoped() diff --git a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Interceptors/DriverInterceptor.cs b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Interceptors/DriverInterceptor.cs index bad72c6..a37188e 100644 --- a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Interceptors/DriverInterceptor.cs +++ b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/Interceptors/DriverInterceptor.cs @@ -321,29 +321,33 @@ protected virtual void InterceptRm(IInvocation invocation, QuotaOptions quotaOpt { rmLength = file.LengthAsync.Result; } + else if (args is IDirectory dir) + { + rmLength = dir.GetSizeAndCountAsync(false, _ => true, _ => true, cancellationToken: cancellationToken).Result.Size; + } }; driver.OnAfterRemove += (sender, args) => { - if (args is IFile file) - { - var volume = file.Volume; - var volumeDir = driver.CreateDirectory(volume.RootDirectory, volume); - Func> createFunc = (_) => volumeDir.GetPhysicalStorageUsageAsync(cancellationToken); + if (rmLength == 0) return; - var (storageCache, _) = storageManager.Lock(volume.RootDirectory, createFunc); + var volume = args.Volume; + var volumeDir = driver.CreateDirectory(volume.RootDirectory, volume); + Func> createFunc = (_) => volumeDir.GetPhysicalStorageUsageAsync(cancellationToken); - try - { - storageCache.Storage -= rmLength.Value; - rmLength = null; - proceededDirs.Add(volumeDir); - } - finally - { - if (storageCache != null) - storageManager.Unlock(storageCache); - } + var (storageCache, _) = storageManager.Lock(volume.RootDirectory, createFunc); + + try + { + storageCache.Storage -= rmLength.Value; + rmLength = null; + proceededDirs.Add(volumeDir); + } + finally + { + if (storageCache != null) + storageManager.Unlock(storageCache); + storageCache = null; } }; } @@ -370,15 +374,14 @@ protected virtual void InterceptUpload(IInvocation invocation, QuotaOptions quot var cmd = invocation.Arguments[0] as UploadCommand; var volume = cmd.TargetPath.Volume; var volumeDir = driver.CreateDirectory(volume.RootDirectory, volume); + var proceededDirs = new HashSet(); var cancellationToken = (CancellationToken)invocation.Arguments.Last(); double? maximum = null; + DirectoryStorageCache storageCache = null; if (quotaOptions.Enabled && quotaOptions.Quotas.TryGetValue(volume.VolumeId, out var volumeQuota)) maximum = volumeQuota.MaxStorageSize; - DirectoryStorageCache storageCache = null; - bool proceeded = false; - if (!_registeredHandlers.Contains(nameof(InterceptUpload))) { _registeredHandlers.Add(nameof(InterceptUpload)); @@ -391,6 +394,30 @@ protected virtual void InterceptUpload(IInvocation invocation, QuotaOptions quot { (storageCache, _) = storageManager.Lock(volume.RootDirectory, createFunc); + if (args.IsChunking) + { + try + { + var totalUploadLength = cmd.RangeInfo.TotalBytes; + + if (args.IsOverwrite) + { + var destLength = args.DestFile.LengthAsync.Result; + totalUploadLength -= destLength; + } + + if (storageCache.Storage + totalUploadLength > maximum) + throw new QuotaException(maximum.Value, storageCache.Storage, quotaOptions); + + return; + } + finally + { + storageManager.Unlock(storageCache); + storageCache = null; + } + } + uploadLength = args.FormFile.Length; if (args.IsOverwrite) { @@ -400,13 +427,69 @@ protected virtual void InterceptUpload(IInvocation invocation, QuotaOptions quot if (storageCache.Storage + uploadLength > maximum) throw new QuotaException(maximum.Value, storageCache.Storage, quotaOptions); - proceeded = true; + proceededDirs.Add(volumeDir); }; driver.OnAfterUpload += (sender, args) => { - if (storageCache == null) return; + if (storageCache == null && uploadLength == 0) return; storageCache.Storage += uploadLength; + uploadLength = 0; + storageManager.Unlock(storageCache); + storageCache = null; + }; + + long transferLength = 0; + long originalLength = 0; + long tempTransferedLength = 0; + + driver.OnBeforeChunkMerged += (sender, args) => + { + originalLength = args.IsOverwrite ? args.File.LengthAsync.Result : 0; + }; + + driver.OnBeforeChunkTransfer += (sender, args) => + { + (storageCache, _) = storageManager.Lock(volume.RootDirectory, createFunc); + + if (originalLength > 0) + { + tempTransferedLength -= originalLength; + originalLength = 0; + } + + transferLength = args.ChunkFile.LengthAsync.Result; + tempTransferedLength += transferLength; + + if (storageCache.Storage + tempTransferedLength > maximum) + throw new QuotaException(maximum.Value, storageCache.Storage, quotaOptions); + + proceededDirs.Add(volumeDir); + }; + + driver.OnAfterChunkTransfer += (sender, args) => + { + if (storageCache != null && tempTransferedLength > 0) + { + storageCache.Storage += tempTransferedLength; + tempTransferedLength = 0; + } + + storageManager.Unlock(storageCache); + storageCache = null; + }; + + driver.OnAfterChunkMerged += (sender, arge) => + { + if (storageCache == null) + (storageCache, _) = storageManager.Lock(volume.RootDirectory, createFunc); + + if (tempTransferedLength < 0) + { + storageCache.Storage += tempTransferedLength; + tempTransferedLength = 0; + } + storageManager.Unlock(storageCache); storageCache = null; }; @@ -415,9 +498,32 @@ protected virtual void InterceptUpload(IInvocation invocation, QuotaOptions quot { storageManager.Unlock(storageCache); storageCache = null; + if (exception is QuotaException) throw exception; }; + + long rmLength = 0; + + driver.OnBeforeRollbackChunk += (sender, args) => + { + if (args is IFile file) + { + rmLength = file.LengthAsync.Result; + proceededDirs.Add(volumeDir); + } + }; + + driver.OnAfterRollbackChunk += (sender, args) => + { + if (rmLength == 0 || args is IDirectory) return; + + if (storageCache == null) + (storageCache, _) = storageManager.Lock(volume.RootDirectory, createFunc); + + storageCache.Storage -= rmLength; + rmLength = 0; + }; } try @@ -429,8 +535,8 @@ protected virtual void InterceptUpload(IInvocation invocation, QuotaOptions quot storageManager.Unlock(storageCache); storageCache = null; - if (proceeded) - storageManager.StartSizeCalculationThread(volumeDir); + foreach (var dir in proceededDirs) + storageManager.StartSizeCalculationThread(dir); } } @@ -602,7 +708,7 @@ protected virtual void InterceptArchive(IInvocation invocation, QuotaOptions quo { try { - file.DeleteAsync().Wait(); + file.DeleteAsync(cancellationToken: cancellationToken).Wait(); } finally { diff --git a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManager.cs b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManager.cs index c06afe3..4d83134 100644 --- a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManager.cs +++ b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManager.cs @@ -27,7 +27,7 @@ public long Storage public DateTimeOffset? LastAccessTime { get; set; } } - public interface IStorageManager + public interface IStorageManager : IDisposable { (long Storage, bool IsInit) GetOrCreateDirectoryStorage(string dir, Func> createFunc); bool RemoveDirectoryStorage(string dir); @@ -41,10 +41,14 @@ public class StorageManager : IStorageManager protected readonly ConcurrentDictionary directoryStorageCaches; protected readonly IOptionsMonitor options; + private bool _disposedValue; + private readonly CancellationTokenSource _tokenSource; + public StorageManager(IOptionsMonitor options) { this.options = options; directoryStorageCaches = new ConcurrentDictionary(); + _tokenSource = new CancellationTokenSource(); StartCachesCleaner(); } @@ -163,22 +167,17 @@ protected virtual void CheckAndClearCaches() protected virtual void StartCachesCleaner() { - var expiredTimespan = TimeSpan.FromMinutes(options.CurrentValue.StorageCachingMinutes); - var running = true; - Thread thread = new Thread(() => { - while (running) + while (!_tokenSource.IsCancellationRequested) { - var sleepMins = options.CurrentValue.PollingIntervalInMinutes == 0 ? - StorageManagerOptions.DefaultPollingIntervalInMinutes : options.CurrentValue.PollingIntervalInMinutes; - - Thread.Sleep(TimeSpan.FromMinutes(sleepMins)); + Thread.Sleep(options.CurrentValue.PollingInterval); + _tokenSource.Token.ThrowIfCancellationRequested(); var expiredCaches = directoryStorageCaches.Where(cache => { var lifeTime = DateTimeOffset.UtcNow - cache.Value.LastAccessTime; - return lifeTime >= expiredTimespan; + return lifeTime >= options.CurrentValue.StorageCachingLifeTime; }).ToArray(); foreach (var cache in expiredCaches) @@ -191,5 +190,36 @@ protected virtual void StartCachesCleaner() thread.IsBackground = true; thread.Start(); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~StorageManager() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManagerOptions.cs b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManagerOptions.cs index 2f67388..d2f3bac 100644 --- a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManagerOptions.cs +++ b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/StorageManagerOptions.cs @@ -1,15 +1,17 @@ -namespace elFinder.Net.Plugins.FileSystemQuotaManagement +using System; + +namespace elFinder.Net.Plugins.FileSystemQuotaManagement { public class StorageManagerOptions { - public const int DefaultStorageCachingMinutes = 30; + public static readonly TimeSpan DefaultStorageCachingLifeTime = TimeSpan.FromMinutes(30); + public static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); public const int DefaultMaximumItems = 10000; - public const int DefaultPollingIntervalInMinutes = 5; public const int DefaultReservationsAfterCleanUp = 100; - public int StorageCachingMinutes { get; set; } = DefaultStorageCachingMinutes; + public TimeSpan StorageCachingLifeTime { get; set; } = DefaultStorageCachingLifeTime; + public TimeSpan PollingInterval { get; set; } = DefaultPollingInterval; public int MaximumItems { get; set; } = DefaultMaximumItems; public int ReservationsAfterCleanUp { get; set; } = DefaultReservationsAfterCleanUp; - public int PollingIntervalInMinutes { get; set; } = DefaultPollingIntervalInMinutes; } } diff --git a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/elFinder.Net.Plugins.FileSystemQuotaManagement.csproj b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/elFinder.Net.Plugins.FileSystemQuotaManagement.csproj index 5e651f6..76abedc 100644 --- a/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/elFinder.Net.Plugins.FileSystemQuotaManagement.csproj +++ b/elFinder.Net.Core/Plugins/elFinder.Net.Plugins.FileSystemQuotaManagement/elFinder.Net.Plugins.FileSystemQuotaManagement.csproj @@ -12,7 +12,7 @@ https://github.com/trannamtrung1st/elFinder.Net.Core OSS elFinder, connector, file-manager, driver, plugins - 1.2.5 + 1.3.3 diff --git a/elFinder.Net.Core/elFinder.Net.AspNetCore/elFinder.Net.AspNetCore.csproj b/elFinder.Net.Core/elFinder.Net.AspNetCore/elFinder.Net.AspNetCore.csproj index a42813d..95836bd 100644 --- a/elFinder.Net.Core/elFinder.Net.AspNetCore/elFinder.Net.AspNetCore.csproj +++ b/elFinder.Net.Core/elFinder.Net.AspNetCore/elFinder.Net.AspNetCore.csproj @@ -14,7 +14,7 @@ true This package enables ASP.NET Core 2.2 projects to easily integrate the elFinder.Net.Core connector. See the example project on Github for usage detail. logo.png - 1.2.6.1 + 1.3.3 diff --git a/elFinder.Net.Core/elFinder.Net.Core/Connector.cs b/elFinder.Net.Core/elFinder.Net.Core/Connector.cs index 5c247ed..adadaad 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Connector.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Connector.cs @@ -26,6 +26,7 @@ Task ParsePathAsync(string target, Task> ParsePathsAsync(IEnumerable targets, CancellationToken cancellationToken = default); Task GetThumbAsync(string target, CancellationToken cancellationToken = default); string AddVolume(IVolume volume); + Task AbortAsync(RequestCommand cmd, CancellationToken cancellationToken = default); } public class Connector : IConnector @@ -61,14 +62,14 @@ public virtual async Task ProcessAsync(ConnectorCommand cmd, Ca var hasReqId = !string.IsNullOrEmpty(cmd.ReqId); if (hasReqId) - connectorManager.AddCancellationTokenSource(cmd.ReqId, cancellationTokenSource); + connectorManager.AddCancellationTokenSource(new RequestCommand(cmd), cancellationTokenSource); var cookies = new Dictionary(); var connResult = await ProcessCoreAsync(cmd, cookies, cancellationToken); connResult.Cookies = cookies; if (hasReqId) - connectorManager.Release(cmd.ReqId); + connectorManager.ReleaseRequest(cmd.ReqId); return connResult; } @@ -101,7 +102,12 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand var abortCmd = new AbortCommand(); abortCmd.Id = args.GetValueOrDefault(ConnectorCommand.Param_Id); - var success = await connectorManager.AbortAsync(abortCmd.Id, cancellationToken); + var (success, reqCmd) = await connectorManager.AbortAsync(abortCmd.Id, cancellationToken: cancellationToken); + + if (success) + { + await AbortAsync(reqCmd, cancellationToken: cancellationToken); + } //return ConnectorResult.NoContent(new AbortResponse { success = success }); return ConnectorResult.Success(new AbortResponse @@ -129,7 +135,7 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand openCmd.Target = args.GetValueOrDefault(ConnectorCommand.Param_Target); openCmd.TargetPath = await ParsePathAsync(openCmd.Target, createIfNotExists: false, cancellationToken: cancellationToken); openCmd.Mimes = options.MimeDetect == MimeDetectOption.Internal - ? args.GetValueOrDefault(ConnectorCommand.Param_Mimes) + ? args.GetValueOrDefault(ConnectorCommand.Param_MimesArr) : default; if (byte.TryParse(args.GetValueOrDefault(ConnectorCommand.Param_Init), out var init)) openCmd.Init = init; @@ -354,7 +360,7 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand lsCmd.TargetPath = await ParsePathAsync(lsCmd.Target, cancellationToken: cancellationToken); lsCmd.Intersect = args.GetValueOrDefault(ConnectorCommand.Param_Intersect); lsCmd.Mimes = options.MimeDetect == MimeDetectOption.Internal - ? args.GetValueOrDefault(ConnectorCommand.Param_Mimes) + ? args.GetValueOrDefault(ConnectorCommand.Param_MimesArr) : default; cmd.CmdObject = lsCmd; @@ -473,7 +479,7 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand searchCmd.Target = args.GetValueOrDefault(ConnectorCommand.Param_Target); searchCmd.TargetPath = await ParsePathAsync(searchCmd.Target, cancellationToken: cancellationToken); searchCmd.Q = args.GetValueOrDefault(ConnectorCommand.Param_Q); - searchCmd.Mimes = args.GetValueOrDefault(ConnectorCommand.Param_Mimes); + searchCmd.Mimes = args.GetValueOrDefault(ConnectorCommand.Param_MimesArr); cmd.CmdObject = searchCmd; SearchResponse finalSearchResp; @@ -499,12 +505,15 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand return ConnectorResult.Success(finalSearchResp); } + + // Remember to update AbortAsync case ConnectorCommand.Cmd_Upload: { var uploadCmd = new UploadCommand(); uploadCmd.Target = args.GetValueOrDefault(ConnectorCommand.Param_Target); uploadCmd.TargetPath = await ParsePathAsync(uploadCmd.Target, cancellationToken: cancellationToken); - uploadCmd.Upload = cmd.Files.Where(o => o.Name == ConnectorCommand.Param_Upload); + uploadCmd.Mimes = args.GetValueOrDefault(ConnectorCommand.Param_Mimes); + uploadCmd.Upload = cmd.Files.Where(o => o.Name == ConnectorCommand.Param_Upload).ToArray(); uploadCmd.UploadPath = args.GetValueOrDefault(ConnectorCommand.Param_UploadPath); uploadCmd.UploadPathInfos = await ParsePathsAsync(uploadCmd.UploadPath, cancellationToken); uploadCmd.MTime = args.GetValueOrDefault(ConnectorCommand.Param_MTime); @@ -515,18 +524,45 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand .ToDictionary(o => o.Key, o => (string)o.Value); if (byte.TryParse(args.GetValueOrDefault(ConnectorCommand.Param_Overwrite), out var overwrite)) uploadCmd.Overwrite = overwrite; + + // Chunked upload processing + uploadCmd.UploadName = args.GetValueOrDefault(ConnectorCommand.Param_Upload); + uploadCmd.Chunk = args.GetValueOrDefault(ConnectorCommand.Param_Chunk); + uploadCmd.Range = args.GetValueOrDefault(ConnectorCommand.Param_Range); + uploadCmd.Cid = args.GetValueOrDefault(ConnectorCommand.Param_Cid); + var isChunking = uploadCmd.Chunk.ToString().Length > 0; + var isChunkMerge = isChunking && uploadCmd.Cid.ToString().Length == 0; + var uploadCount = uploadCmd.Upload.Count(); + cmd.CmdObject = uploadCmd; var volume = uploadCmd.TargetPath.Volume; + if (uploadCmd.UploadPathInfos.Any(path => path.Volume != volume)) + throw new CommandParamsException(ConnectorCommand.Cmd_Upload); + + if (uploadCmd.UploadName == UploadCommand.ChunkFail && uploadCmd.Mimes == UploadCommand.ChunkFail) + { + await volume.Driver.AbortUploadAsync(uploadCmd, cancellationToken); + throw new ConnectionAbortedException(); + } + else if (isChunking && !isChunkMerge + && (uploadCmd.Upload.Count() != 1 || uploadCmd.UploadPathInfos.Count() != 1 + || uploadCmd.Upload.Single().Length > uploadCmd.RangeInfo.TotalBytes)) + { + throw new CommandParamsException(ConnectorCommand.Cmd_Upload); + } + else if (isChunkMerge && (string.IsNullOrWhiteSpace(uploadCmd.UploadName) + || string.IsNullOrWhiteSpace(uploadCmd.Chunk))) + { + throw new CommandParamsException(ConnectorCommand.Cmd_Upload); + } + if (volume.MaxUploadSize.HasValue) { if (uploadCmd.Upload.Any(file => file.Length > volume.MaxUploadSize)) throw new UploadFileSizeException(); } - if (uploadCmd.UploadPathInfos.Any(path => path.Volume != volume)) - throw new CommandParamsException(ConnectorCommand.Cmd_Upload); - var uploadResp = await volume.Driver.UploadAsync(uploadCmd, cancellationToken); return ConnectorResult.Success(uploadResp); } @@ -575,10 +611,6 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand { errResp = ErrorResponse.Factory.AccessDenied(uaEx); } - else if (rootCause is IOException ioEx) - { - errResp = ErrorResponse.Factory.AccessDenied(ioEx); - } else if (rootCause is FileNotFoundException fnfEx) { errResp = ErrorResponse.Factory.FileNotFound(fnfEx); @@ -587,6 +619,18 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand { errResp = ErrorResponse.Factory.FolderNotFound(dnfEx); } + else if (rootCause is TaskCanceledException taskEx) + { + errResp = ErrorResponse.Factory.ConnectionAborted(taskEx); + } + else if (rootCause is OperationCanceledException opEx) + { + errResp = ErrorResponse.Factory.ConnectionAborted(opEx); + } + else if (rootCause is IOException ioEx) + { + errResp = ErrorResponse.Factory.AccessDenied(ioEx); + } else if (rootCause is ArgumentException argEx) { errResp = ErrorResponse.Factory.CommandParams(argEx, cmd.Cmd); @@ -598,6 +642,8 @@ protected virtual async Task ProcessCoreAsync(ConnectorCommand } } + // If the error response is returned too fast, elFinder client will be likely to miss it. + Thread.Sleep(options.DefaultErrResponseTimeoutMs); return ConnectorResult.Error(errResp, errSttCode); } @@ -652,5 +698,77 @@ public virtual async Task> ParsePathsAsync(IEnumerable kvp.Key.StartsWith(ConnectorCommand.Param_Hashes_Start)) + .ToDictionary(o => o.Key, o => (string)o.Value); + if (byte.TryParse(args.GetValueOrDefault(ConnectorCommand.Param_Overwrite), out var overwrite)) + uploadCmd.Overwrite = overwrite; + + // Chunked upload processing + uploadCmd.UploadName = args.GetValueOrDefault(ConnectorCommand.Param_Upload); + uploadCmd.Chunk = args.GetValueOrDefault(ConnectorCommand.Param_Chunk); + uploadCmd.Range = args.GetValueOrDefault(ConnectorCommand.Param_Range); + uploadCmd.Cid = args.GetValueOrDefault(ConnectorCommand.Param_Cid); + var isChunking = uploadCmd.Chunk.ToString().Length > 0; + var isChunkMerge = isChunking && uploadCmd.Cid.ToString().Length == 0; + var uploadCount = uploadCmd.Upload.Count(); + + if (uploadCmd.UploadName == UploadCommand.ChunkFail && uploadCmd.Mimes == UploadCommand.ChunkFail) + { + return; + } + + if (isChunking && !isChunkMerge && uploadCmd.UploadPathInfos.Count() != 1) + throw new CommandParamsException(ConnectorCommand.Cmd_Upload); + + if (isChunkMerge && (string.IsNullOrWhiteSpace(uploadCmd.UploadName) + || string.IsNullOrWhiteSpace(uploadCmd.Chunk))) + throw new CommandParamsException(ConnectorCommand.Cmd_Upload); + + var volume = uploadCmd.TargetPath.Volume; + + if (volume.MaxUploadSize.HasValue) + { + if (uploadCmd.Upload.Any(file => file.Length > volume.MaxUploadSize)) + throw new UploadFileSizeException(); + } + + if (uploadCmd.UploadPathInfos.Any(path => path.Volume != volume)) + throw new CommandParamsException(ConnectorCommand.Cmd_Upload); + + await volume.Driver.AbortUploadAsync(uploadCmd, cancellationToken); + return; + } + } + } } } diff --git a/elFinder.Net.Core/elFinder.Net.Core/ConnectorManager.cs b/elFinder.Net.Core/elFinder.Net.Core/ConnectorManager.cs index ffbc4a9..cd45d20 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/ConnectorManager.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/ConnectorManager.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using elFinder.Net.Core.Models.Command; +using Microsoft.Extensions.Options; using System; using System.Collections.Concurrent; using System.Linq; @@ -9,116 +10,111 @@ namespace elFinder.Net.Core { public interface IConnectorManager { - void AddCancellationTokenSource(string reqId, CancellationTokenSource cancellationTokenSource); - Task AbortAsync(string reqId, CancellationToken cancellationToken = default); - bool Release(string reqId); - void LockDirectoryAndProceed(string dir, Func action); + void AddCancellationTokenSource(RequestCommand cmd, CancellationTokenSource cancellationTokenSource); + Task<(bool Success, RequestCommand Cmd)> AbortAsync(string reqId, CancellationToken cancellationToken = default); + bool ReleaseRequest(string reqId); + T GetLock(string key, Func lockCreate) where T : ConnectorLock; + T GetLock(string key) where T : ConnectorLock; + bool ReleaseLockCache(string key); } public class ConnectorManagerOptions { - public const int DefaultCcTokenSourceCachingMinutes = 30; public const int DefaultMaximumItems = 10000; - public const int DefaultPollingIntervalInMinutes = 5; + public static readonly TimeSpan DefaultCcTokenSourceCachingLifeTime = TimeSpan.FromMinutes(30); + public static readonly TimeSpan DefaultLockCachingLifeTime = TimeSpan.FromMinutes(10); + public static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); - public int TokenSourceCachingMinutes { get; set; } = DefaultCcTokenSourceCachingMinutes; public int MaximumItems { get; set; } = DefaultMaximumItems; - public int PollingIntervalInMinutes { get; set; } = DefaultPollingIntervalInMinutes; + public TimeSpan TokenSourceCachingLifeTime { get; set; } = DefaultCcTokenSourceCachingLifeTime; + public TimeSpan LockCachingLifeTime { get; set; } = DefaultLockCachingLifeTime; + public TimeSpan PollingInterval { get; set; } = DefaultPollingInterval; } public class ConnectorManager : IConnectorManager { - protected readonly ConcurrentDictionary directoryLocks; - protected readonly ConcurrentDictionary directoryLockStatuses; - protected readonly ConcurrentDictionary tokenMaps; + protected readonly ConcurrentDictionary tokenMaps; protected readonly IOptionsMonitor options; + private readonly ConcurrentDictionary _connectorLocks; + private bool _disposedValue; + private readonly CancellationTokenSource _tokenSource; + public ConnectorManager(IOptionsMonitor options) { - directoryLocks = new ConcurrentDictionary(); - directoryLockStatuses = new ConcurrentDictionary(); - tokenMaps = new ConcurrentDictionary(); + _connectorLocks = new ConcurrentDictionary(); + tokenMaps = new ConcurrentDictionary(); this.options = options; + _tokenSource = new CancellationTokenSource(); StartRequestIdCleaner(); + StartLockCleaner(); } - public virtual Task AbortAsync(string reqId, CancellationToken cancellationToken = default) + public virtual Task<(bool Success, RequestCommand Cmd)> AbortAsync(string reqId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - (CancellationTokenSource Source, DateTimeOffset CreatedTime) token; + (RequestCommand Cmd, CancellationTokenSource Source, DateTimeOffset CreatedTime) token; if (tokenMaps.TryRemove(reqId, out token)) { token.Source.Cancel(); - return Task.FromResult(true); + return Task.FromResult((true, token.Cmd)); } - return Task.FromResult(false); + return Task.FromResult((false, default(RequestCommand))); } - public virtual void AddCancellationTokenSource(string reqId, CancellationTokenSource cancellationTokenSource) + public virtual void AddCancellationTokenSource(RequestCommand cmd, CancellationTokenSource cancellationTokenSource) { if (tokenMaps.Count >= options.CurrentValue.MaximumItems) return; - (CancellationTokenSource Source, DateTimeOffset CreatedTime) currentToken; - - if (tokenMaps.TryRemove(reqId, out currentToken)) - currentToken.Source.Cancel(); + if (tokenMaps.ContainsKey(cmd.ReqId)) + throw new InvalidOperationException(); - tokenMaps[reqId] = (cancellationTokenSource, DateTimeOffset.UtcNow); + tokenMaps[cmd.ReqId] = (cmd, cancellationTokenSource, DateTimeOffset.UtcNow); } - public virtual bool Release(string reqId) + public T GetLock(string key, Func lockCreate) where T : ConnectorLock { - return tokenMaps.TryRemove(reqId, out _); + var lockObj = _connectorLocks.GetOrAdd(key, lockCreate); + lockObj.LastAccess = DateTimeOffset.UtcNow; + return lockObj as T; } - #region Directory lock - public void LockDirectoryAndProceed(string dir, Func action) + public T GetLock(string key) where T : ConnectorLock { - directoryLockStatuses.GetOrAdd(dir, 0); - directoryLockStatuses[dir]++; - var volumeLock = directoryLocks.GetOrAdd(dir, (key) => new object()); + ConnectorLock lockObj; - lock (volumeLock) - { - directoryLockStatuses[dir]--; + if (_connectorLocks.TryGetValue(key, out lockObj)) + lockObj.LastAccess = DateTimeOffset.UtcNow; - try - { - action().Wait(); - } - finally - { - if (directoryLockStatuses[dir] == 0) - { - directoryLockStatuses.TryRemove(dir, out _); - directoryLocks.TryRemove(dir, out _); - } - } - } + return lockObj as T; } - #endregion - protected virtual void StartRequestIdCleaner() + public bool ReleaseLockCache(string key) + { + return _connectorLocks.TryRemove(key, out _); + } + + public virtual bool ReleaseRequest(string reqId) { - var expiredTimespan = TimeSpan.FromMinutes(options.CurrentValue.TokenSourceCachingMinutes); - var running = true; + return tokenMaps.TryRemove(reqId, out _); + } + protected virtual void StartRequestIdCleaner() + { Thread thread = new Thread(() => { - while (running) + while (!_tokenSource.IsCancellationRequested) { - var sleepMins = options.CurrentValue.PollingIntervalInMinutes == 0 ? - ConnectorManagerOptions.DefaultPollingIntervalInMinutes : options.CurrentValue.PollingIntervalInMinutes; - - Thread.Sleep(TimeSpan.FromMinutes(sleepMins)); + Thread.Sleep(options.CurrentValue.PollingInterval); + _tokenSource.Token.ThrowIfCancellationRequested(); var expiredTokens = tokenMaps.Where(token => { var lifeTime = DateTimeOffset.UtcNow - token.Value.CreatedTime; - return lifeTime >= expiredTimespan; + return lifeTime >= options.CurrentValue.TokenSourceCachingLifeTime; }).ToArray(); foreach (var token in expiredTokens) @@ -136,5 +132,76 @@ protected virtual void StartRequestIdCleaner() thread.IsBackground = true; thread.Start(); } + + protected virtual void StartLockCleaner() + { + Thread thread = new Thread(() => + { + while (!_tokenSource.IsCancellationRequested) + { + Thread.Sleep(options.CurrentValue.PollingInterval); + _tokenSource.Token.ThrowIfCancellationRequested(); + + var expiredLocks = _connectorLocks.Where(lockObj => + { + var lifeTime = DateTimeOffset.UtcNow - lockObj.Value.LastAccess; + return lifeTime >= options.CurrentValue.LockCachingLifeTime; + }).ToArray(); + + foreach (var lockObj in expiredLocks) + { + try + { + _connectorLocks.TryRemove(lockObj.Key, out _); + } + catch (Exception) { } + } + } + }); + + thread.IsBackground = true; + thread.Start(); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~DefaultThumbnailBackgroundGenerator() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + public class ConnectorLock + { + public DateTimeOffset LastAccess { get; internal set; } + + public void Deactivate() + { + LastAccess = DateTimeOffset.MinValue; + } } } diff --git a/elFinder.Net.Core/elFinder.Net.Core/ConnectorOptions.cs b/elFinder.Net.Core/elFinder.Net.Core/ConnectorOptions.cs index 864a2a2..2abd26d 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/ConnectorOptions.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/ConnectorOptions.cs @@ -8,5 +8,6 @@ public class ConnectorOptions public virtual MimeDetectOption MimeDetect { get; set; } = MimeDetectOption.Internal; public virtual IEnumerable EnabledCommands { get; set; } = ConnectorCommand.AllCommands; public virtual IEnumerable DisabledUICommands { get; set; } = ConnectorCommand.NotSupportedUICommands; + public virtual int DefaultErrResponseTimeoutMs { get; set; } = 500; } } diff --git a/elFinder.Net.Core/elFinder.Net.Core/Exceptions/ConnectionAbortedException.cs b/elFinder.Net.Core/elFinder.Net.Core/Exceptions/ConnectionAbortedException.cs new file mode 100644 index 0000000..327227d --- /dev/null +++ b/elFinder.Net.Core/elFinder.Net.Core/Exceptions/ConnectionAbortedException.cs @@ -0,0 +1,15 @@ +using elFinder.Net.Core.Models.Response; + +namespace elFinder.Net.Core.Exceptions +{ + public class ConnectionAbortedException : ConnectorException + { + public ConnectionAbortedException() + { + ErrorResponse = new ErrorResponse(this) + { + error = ErrorResponse.ConnectionAborted + }; + } + } +} diff --git a/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileExtensions.cs b/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileExtensions.cs index b3d4d3f..e30a77c 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileExtensions.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileExtensions.cs @@ -122,8 +122,10 @@ public static bool CanEditImage(this IFile file) return file.ObjectAttribute.Read && file.ObjectAttribute.Write; } - public static async Task CanArchiveToAsync(this IFile destination) + public static async Task CanArchiveToAsync(this IFile destination, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (await destination.ExistsAsync) return destination.ObjectAttribute.Write; diff --git a/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileSystemExtensions.cs b/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileSystemExtensions.cs index 4cfc137..abb2965 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileSystemExtensions.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Extensions/IFileSystemExtensions.cs @@ -1,4 +1,5 @@ using elFinder.Net.Core.Services; +using System.Threading; using System.Threading.Tasks; namespace elFinder.Net.Core.Extensions @@ -48,32 +49,40 @@ public static bool CanMove(this IFileSystem fileSystem) return !fileSystem.ObjectAttribute.Locked; } - public static async Task CanCopyToAsync(this IFileSystem destination) + public static async Task CanCopyToAsync(this IFileSystem destination, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (await destination.ExistsAsync) return destination.ObjectAttribute.Write; return destination.Parent?.ObjectAttribute.Write != false; } - public static async Task CanWriteAsync(this IFileSystem destination) + public static async Task CanWriteAsync(this IFileSystem destination, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (await destination.ExistsAsync) return destination.ObjectAttribute.Write; return destination.Parent?.ObjectAttribute.Write != false; } - public static async Task CanMoveToAsync(this IFileSystem destination) + public static async Task CanMoveToAsync(this IFileSystem destination, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (await destination.ExistsAsync) return destination.ObjectAttribute.Write; return destination.Parent?.ObjectAttribute.Write != false; } - public static async Task CanExtractToAsync(this IFileSystem destination) + public static async Task CanExtractToAsync(this IFileSystem destination, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + if (await destination.ExistsAsync) return destination.ObjectAttribute.Write; diff --git a/elFinder.Net.Core/elFinder.Net.Core/Extensions/ServiceCollectionExtensions.cs b/elFinder.Net.Core/elFinder.Net.Core/Extensions/ServiceCollectionExtensions.cs index 01fb6eb..a64ee27 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Extensions/ServiceCollectionExtensions.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Extensions/ServiceCollectionExtensions.cs @@ -32,15 +32,16 @@ public static IServiceCollection AddElFinderCore(this IServiceCollection service connectorManagerConfig = (options) => { options.MaximumItems = ConnectorManagerOptions.DefaultMaximumItems; - options.TokenSourceCachingMinutes = ConnectorManagerOptions.DefaultCcTokenSourceCachingMinutes; - options.PollingIntervalInMinutes = ConnectorManagerOptions.DefaultPollingIntervalInMinutes; + options.TokenSourceCachingLifeTime = ConnectorManagerOptions.DefaultCcTokenSourceCachingLifeTime; + options.LockCachingLifeTime = ConnectorManagerOptions.DefaultLockCachingLifeTime; + options.PollingInterval = ConnectorManagerOptions.DefaultPollingInterval; }; services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .Configure(connectorManagerConfig); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .Configure(connectorManagerConfig); return services.AddScoped() .AddScoped() diff --git a/elFinder.Net.Core/elFinder.Net.Core/Helpers/FileHelper.cs b/elFinder.Net.Core/elFinder.Net.Core/Helpers/FileHelper.cs new file mode 100644 index 0000000..5d431bd --- /dev/null +++ b/elFinder.Net.Core/elFinder.Net.Core/Helpers/FileHelper.cs @@ -0,0 +1,25 @@ +using System.Linq; + +namespace elFinder.Net.Core.Helpers +{ + public static class FileHelper + { + public static (string UploadingFileName, int CurrentChunkNo, int TotalChunks) GetChunkInfo(string chunkName) + { + var fileName = chunkName.Substring(0, chunkName.LastIndexOf('.')); + fileName = fileName.Substring(0, fileName.LastIndexOf('.')); + var fileParts = chunkName.Split('.'); + var chunkInfo = fileParts[fileParts.Length - 2].Split('_'); + var totalChunks = int.Parse(chunkInfo[chunkInfo.Length - 1]) + 1; + var chunkNo = int.Parse(chunkInfo[0]); + return (fileName, chunkNo, totalChunks); + } + + public static (long StartByte, long ChunkLength, long TotalBytes) GetRangeInfo(string range) + { + var rangeParts = range.Split(',').Select(r => long.Parse(r)).ToArray(); + + return (rangeParts[0], rangeParts[1], rangeParts[2]); + } + } +} diff --git a/elFinder.Net.Core/elFinder.Net.Core/IDriver.cs b/elFinder.Net.Core/elFinder.Net.Core/IDriver.cs index ec7f01f..79a2504 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/IDriver.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/IDriver.cs @@ -13,6 +13,9 @@ namespace elFinder.Net.Core public interface IDriver { #region Events + event EventHandler OnBeforeRemoveThumb; + event EventHandler OnAfterRemoveThumb; + event EventHandler OnRemoveThumbError; event EventHandler OnBeforeMakeDir; event EventHandler OnAfterMakeDir; event EventHandler OnBeforeMakeFile; @@ -21,8 +24,14 @@ public interface IDriver event EventHandler<(IFileSystem FileSystem, string PrevName)> OnAfterRename; event EventHandler OnBeforeRemove; event EventHandler OnAfterRemove; - event EventHandler<(IFile File, IFormFileWrapper FormFile, bool IsOverwrite)> OnBeforeUpload; - event EventHandler<(IFile File, IFormFileWrapper FormFile, bool IsOverwrite)> OnAfterUpload; + event EventHandler OnBeforeRollbackChunk; + event EventHandler OnAfterRollbackChunk; + event EventHandler<(IFile File, IFile DestFile, IFormFileWrapper FormFile, bool IsOverwrite, bool IsChunking)> OnBeforeUpload; + event EventHandler<(IFile File, IFile DestFile, IFormFileWrapper FormFile, bool IsOverwrite, bool IsChunking)> OnAfterUpload; + event EventHandler<(IFile File, bool IsOverwrite)> OnBeforeChunkMerged; + event EventHandler<(IFile File, bool IsOverwrite)> OnAfterChunkMerged; + event EventHandler<(IFile ChunkFile, IFile DestFile, bool IsOverwrite)> OnBeforeChunkTransfer; + event EventHandler<(IFile ChunkFile, IFile DestFile, bool IsOverwrite)> OnAfterChunkTransfer; event EventHandler OnUploadError; event EventHandler<(IFileSystem FileSystem, string NewDest, bool IsOverwrite)> OnBeforeMove; event EventHandler<(IFileSystem FileSystem, IFileSystem NewFileSystem, bool IsOverwrite)> OnAfterMove; @@ -65,6 +74,7 @@ public interface IDriver Task TreeAsync(TreeCommand cmd, CancellationToken cancellationToken = default); Task SearchAsync(SearchCommand cmd, CancellationToken cancellationToken = default); Task UploadAsync(UploadCommand cmd, CancellationToken cancellationToken = default); + Task AbortUploadAsync(UploadCommand cmd, CancellationToken cancellationToken = default); Task ResizeAsync(ResizeCommand cmd, CancellationToken cancellationToken = default); Task ZipdlAsync(ZipdlCommand cmd, CancellationToken cancellationToken = default); Task ZipdlRawAsync(ZipdlCommand cmd, CancellationToken cancellationToken = default); diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Command/ConnectorCommand.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Command/ConnectorCommand.cs index 8086c0b..91f7558 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Command/ConnectorCommand.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Command/ConnectorCommand.cs @@ -6,6 +6,27 @@ namespace elFinder.Net.Core.Models.Command { + public class RequestCommand + { + public RequestCommand(ConnectorCommand cmd) + { + ReqId = cmd.ReqId; + Cmd = cmd.Cmd; + Method = cmd.Method; + RequestHeaders = cmd.RequestHeaders; + Query = cmd.Query; + Form = cmd.Form; + } + + public string ReqId { get; set; } + public string Cmd { get; set; } + public HttpMethod Method { get; set; } + public IReadOnlyDictionary RequestHeaders { get; set; } + public IReadOnlyDictionary Query { get; set; } + public IReadOnlyDictionary Form { get; set; } + public IReadOnlyDictionary Args => Method == HttpMethod.Get ? Query : Form; + } + public class ConnectorCommand { public static readonly IEnumerable AllCommands; @@ -41,7 +62,8 @@ static ConnectorCommand() public const string Param_Cmd = "cmd"; public const string Param_Target = "target"; public const string Param_Q = "q"; - public const string Param_Mimes = "mimes[]"; + public const string Param_MimesArr = "mimes[]"; + public const string Param_Mimes = "mimes"; public const string Param_Init = "init"; public const string Param_Tree = "tree"; public const string Param_Name = "name"; @@ -76,6 +98,9 @@ static ConnectorCommand() public const string Param_Bg = "bg"; public const string Param_Quality = "quality"; public const string Param_Id = "id"; + public const string Param_Chunk = "chunk"; + public const string Param_Cid = "cid"; + public const string Param_Range = "range"; public const string Header_ReqId = "X-elFinderReqid"; diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Command/UploadCommand.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Command/UploadCommand.cs index be3363d..7a6ebf0 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Command/UploadCommand.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Command/UploadCommand.cs @@ -1,17 +1,22 @@ -using elFinder.Net.Core.Http; +using elFinder.Net.Core.Helpers; +using elFinder.Net.Core.Http; using Microsoft.Extensions.Primitives; +using System; using System.Collections.Generic; namespace elFinder.Net.Core.Models.Command { public class UploadCommand : TargetCommand { + public const string ChunkFail = "chunkfail"; + public UploadCommand() { Hashes = new Dictionary(); } public IEnumerable Upload { get; set; } + public string Mimes { get; set; } public StringValues UploadPath { get; set; } public StringValues MTime { get; set; } public StringValues Name { get; set; } @@ -21,5 +26,31 @@ public UploadCommand() public byte? Overwrite { get; set; } public IEnumerable UploadPathInfos { get; set; } + + #region Chunked upload + public string UploadName { get; set; } + public StringValues Chunk { get; set; } + public StringValues Cid { get; set; } + public StringValues Range { get; set; } + public (string UploadingFileName, int CurrentChunkNo, int TotalChunks) ChunkInfo + { + get + { + if (Cid.ToString().Length == 0) throw new InvalidOperationException(); + + return FileHelper.GetChunkInfo(Chunk); + } + } + + public (long StartByte, long ChunkLength, long TotalBytes) RangeInfo + { + get + { + if (Range.ToString().Length == 0) throw new InvalidOperationException(); + + return FileHelper.GetRangeInfo(Range); + } + } + #endregion } } diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Options/ConnectorResponseOptions.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Options/ConnectorResponseOptions.cs index 8bf7da1..6c91711 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Options/ConnectorResponseOptions.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Options/ConnectorResponseOptions.cs @@ -45,7 +45,7 @@ public ConnectorResponseOptions(IDirectory directory, IEnumerable disabl public string trashHash => string.Empty; - public int uploadMaxConn => -1; + public int uploadMaxConn { get; set; } = 1; public string uploadMaxSize { get; set; } diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/ErrorResponse.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/ErrorResponse.cs index 2f0d554..f387a3a 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/ErrorResponse.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/ErrorResponse.cs @@ -19,6 +19,14 @@ public ErrorResponse(Exception ex) public static class Factory { + public static ErrorResponse ConnectionAborted(Exception ex) + { + return new ErrorResponse(ex) + { + error = ErrorResponse.ConnectionAborted + }; + } + public static ErrorResponse AccessDenied(Exception ex) { return new ErrorResponse(ex) @@ -78,6 +86,7 @@ public static ErrorResponse Unknown(Exception ex) public const string FileNotFound = "errFileNotFound"; public const string FolderNotFound = "errFolderNotFound"; public const string CommandParams = "errCmdParams"; + public const string ConnectionAborted = "errAbort"; public const string Unknown = "errUnknown"; public const string PermissionDenied = "errPerm"; public const string UploadFileSize = "errUploadFileSize"; diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/InitResponse.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/InitResponse.cs index db4442d..9875b09 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/InitResponse.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/InitResponse.cs @@ -1,20 +1,16 @@ using elFinder.Net.Core.Models.FileInfo; using elFinder.Net.Core.Models.Options; -using System.Collections.Generic; namespace elFinder.Net.Core.Models.Response { public class InitResponse : OpenResponse { - private static readonly string[] _empty = new string[0]; - public InitResponse() : base() { } - public InitResponse(BaseInfoResponse cwd, ConnectorResponseOptions options) : base(cwd, options) + public InitResponse(BaseInfoResponse cwd, ConnectorResponseOptions options, IVolume volume) : base(cwd, options, volume) { } public string api => ApiValues.Version; - public IEnumerable netDrivers => _empty; } } diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/OpenResponse.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/OpenResponse.cs index 298a9ed..3a58121 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/OpenResponse.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/OpenResponse.cs @@ -6,6 +6,7 @@ namespace elFinder.Net.Core.Models.Response { public class OpenResponse { + private static readonly string[] _empty = new string[0]; private static readonly DebugResponse _debug = new DebugResponse(); public OpenResponse() @@ -13,20 +14,22 @@ public OpenResponse() files = new List(); } - public OpenResponse(BaseInfoResponse cwd, ConnectorResponseOptions options) + public OpenResponse(BaseInfoResponse cwd, ConnectorResponseOptions options, + IVolume volume) { files = new List(); this.cwd = cwd; this.options = options; files.Add(cwd); + uplMaxFile = volume.MaxUploadFiles; } public BaseInfoResponse cwd { get; protected set; } - public DebugResponse debug => _debug; - public List files { get; protected set; } - public ConnectorResponseOptions options { get; protected set; } + public IEnumerable netDrivers => _empty; + public int? uplMaxFile { get; protected set; } + public string uplMaxSize => options.uploadMaxSize; } } diff --git a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/UploadResponse.cs b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/UploadResponse.cs index 8b2bb80..4b063c5 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Models/Response/UploadResponse.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Models/Response/UploadResponse.cs @@ -18,6 +18,11 @@ public UploadResponse() private List _warning; public List warning => _warning.Count > 0 ? _warning : null; + #region Chunked upload + public string _chunkmerged { get; set; } + public string _name { get; set; } + #endregion + public List GetWarnings() { return _warning; diff --git a/elFinder.Net.Core/elFinder.Net.Core/Volume.cs b/elFinder.Net.Core/elFinder.Net.Core/Volume.cs index a247b74..55ce4cc 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/Volume.cs +++ b/elFinder.Net.Core/elFinder.Net.Core/Volume.cs @@ -10,9 +10,12 @@ public interface IVolume string Name { get; set; } string Url { get; } string RootDirectory { get; } + string TempDirectory { get; } + string TempArchiveDirectory { get; } + string ChunkDirectory { get; } + string ThumbnailDirectory { get; } string StartDirectory { get; set; } string ThumbnailUrl { get; set; } - string ThumbnailDirectory { get; set; } int ThumbnailSize { get; set; } char DirectorySeparatorChar { get; set; } bool UploadOverwrite { get; set; } @@ -20,6 +23,8 @@ public interface IVolume bool IsReadOnly { get; set; } bool IsLocked { get; set; } bool IsShowOnly { get; set; } + int? MaxUploadFiles { get; set; } + int MaxUploadConnections { get; set; } /// /// Get or sets maximum upload file size. This size is per files in bytes. /// Note: you still to configure maxupload limits in web.config for whole application @@ -52,16 +57,33 @@ public class Volume : IVolume public const string HashSeparator = "_"; public Volume(IDriver driver, - string rootDirectory, string url, string thumbUrl, + string rootDirectory, + string tempDirectory, + string url, + string thumbUrl, + string tempArchiveDirectory = null, + string chunkDirectory = null, + string thumbnailDirectory = null, char directorySeparatorChar = default) { if (rootDirectory == null) throw new ArgumentNullException(nameof(rootDirectory)); + if (tempDirectory == null) + throw new ArgumentNullException(nameof(tempDirectory)); if (url == null) throw new ArgumentNullException(nameof(url)); Driver = driver; RootDirectory = rootDirectory; + TempDirectory = tempDirectory; + TempArchiveDirectory = tempArchiveDirectory ?? tempDirectory; + ChunkDirectory = chunkDirectory ?? tempDirectory; + DirectorySeparatorChar = directorySeparatorChar == default ? Path.DirectorySeparatorChar : directorySeparatorChar; + ThumbnailDirectory = thumbnailDirectory ?? $"{tempDirectory}{DirectorySeparatorChar}.tmb"; + + if (Own(TempDirectory) || Own(TempArchiveDirectory) || Own(ChunkDirectory) || Own(ThumbnailDirectory)) + throw new InvalidOperationException("Nested directories are not allowed"); + Url = url; if (!string.IsNullOrEmpty(thumbUrl)) @@ -69,8 +91,6 @@ public Volume(IDriver driver, ThumbnailUrl = thumbUrl; } - DirectorySeparatorChar = directorySeparatorChar == default ? Path.DirectorySeparatorChar : directorySeparatorChar; - ThumbnailDirectory = $"{Path.GetTempPath()}{DirectorySeparatorChar}.{nameof(elFinder)}tmb"; IsLocked = false; Name = Path.GetFileNameWithoutExtension(rootDirectory); UploadOverwrite = true; @@ -82,6 +102,10 @@ public Volume(IDriver driver, public virtual string Name { get; set; } public virtual string Url { get; } public virtual string RootDirectory { get; } + public virtual string TempDirectory { get; } + public virtual string TempArchiveDirectory { get; } + public virtual string ChunkDirectory { get; } + public virtual string ThumbnailDirectory { get; } private string _startDirectory; public virtual string StartDirectory @@ -95,7 +119,6 @@ public virtual string StartDirectory } } public virtual string ThumbnailUrl { get; set; } - public virtual string ThumbnailDirectory { get; set; } public virtual int ThumbnailSize { get; set; } public virtual char DirectorySeparatorChar { get; set; } public virtual bool UploadOverwrite { get; set; } @@ -116,6 +139,10 @@ public virtual ObjectAttribute DefaultObjectAttribute } } + public virtual int? MaxUploadFiles { get; set; } = 20; + + public int MaxUploadConnections { get; set; } = 1; + public virtual double? MaxUploadSize { get; set; } public virtual double? MaxUploadSizeInKb diff --git a/elFinder.Net.Core/elFinder.Net.Core/elFinder.Net.Core.csproj b/elFinder.Net.Core/elFinder.Net.Core/elFinder.Net.Core.csproj index 617039d..b5d329b 100644 --- a/elFinder.Net.Core/elFinder.Net.Core/elFinder.Net.Core.csproj +++ b/elFinder.Net.Core/elFinder.Net.Core/elFinder.Net.Core.csproj @@ -17,7 +17,7 @@ See the example project on Github for usage detail. logo.png - 1.2.6 + 1.3.3 diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/IDriverExtensions.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/IDriverExtensions.cs index 3f57972..3925cc7 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/IDriverExtensions.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/IDriverExtensions.cs @@ -18,7 +18,7 @@ public static void SetupBackgroundThumbnailGenerator(this IDriver driver, MediaType? mediaType = null; if ((mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify: false)) == null) return; - await file.RefreshAsync(); + await file.RefreshAsync(cancellationToken: cancellationToken); var tmbFilePath = await driver.GenerateThumbPathAsync(file, cancellationToken: cancellationToken); var tmbFile = driver.CreateFile(tmbFilePath, file.Volume); generator.TryAddToQueue(file, tmbFile, file.Volume.ThumbnailSize, keepRatio, mediaType); @@ -29,7 +29,7 @@ public static void SetupBackgroundThumbnailGenerator(this IDriver driver, MediaType? mediaType = null; if (args.NewFileSystem is IFile file && (mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify: false)) != null) { - await file.RefreshAsync(); + await file.RefreshAsync(cancellationToken: cancellationToken); var tmbFilePath = await driver.GenerateThumbPathAsync(file, cancellationToken: cancellationToken); var tmbFile = driver.CreateFile(tmbFilePath, file.Volume); generator.TryAddToQueue(file, tmbFile, file.Volume.ThumbnailSize, keepRatio, mediaType); @@ -43,7 +43,7 @@ public static void SetupBackgroundThumbnailGenerator(this IDriver driver, if ((mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify: false)) == null) return; - await file.RefreshAsync(); + await file.RefreshAsync(cancellationToken: cancellationToken); var tmbFilePath = await driver.GenerateThumbPathAsync(file, cancellationToken: cancellationToken); var tmbFile = driver.CreateFile(tmbFilePath, file.Volume); generator.TryAddToQueue(file, tmbFile, file.Volume.ThumbnailSize, keepRatio, mediaType); @@ -54,7 +54,7 @@ public static void SetupBackgroundThumbnailGenerator(this IDriver driver, MediaType? mediaType = null; if (args.NewFileSystem is IFile file && (mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify: false)) != null) { - await file.RefreshAsync(); + await file.RefreshAsync(cancellationToken: cancellationToken); var tmbFilePath = await driver.GenerateThumbPathAsync(file, cancellationToken: cancellationToken); var tmbFile = driver.CreateFile(tmbFilePath, file.Volume); generator.TryAddToQueue(file, tmbFile, file.Volume.ThumbnailSize, keepRatio, mediaType); @@ -66,7 +66,7 @@ public static void SetupBackgroundThumbnailGenerator(this IDriver driver, MediaType? mediaType = null; if (args.FileSystem is IFile file && (mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify: false)) != null) { - await file.RefreshAsync(); + await file.RefreshAsync(cancellationToken: cancellationToken); var tmbFilePath = await driver.GenerateThumbPathAsync(file, cancellationToken: cancellationToken); var tmbFile = driver.CreateFile(tmbFilePath, file.Volume); generator.TryAddToQueue(file, tmbFile, file.Volume.ThumbnailSize, keepRatio, mediaType); diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/ServiceCollectionExtensions.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/ServiceCollectionExtensions.cs index 6675784..01c9737 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/ServiceCollectionExtensions.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Extensions/ServiceCollectionExtensions.cs @@ -1,17 +1,25 @@ using elFinder.Net.Core; using elFinder.Net.Drivers.FileSystem.Services; using Microsoft.Extensions.DependencyInjection; +using System; namespace elFinder.Net.Drivers.FileSystem.Extensions { public static class ServiceCollectionExtensions { - public static IServiceCollection AddFileSystemDriver(this IServiceCollection services) + public static IServiceCollection AddFileSystemDriver(this IServiceCollection services, + Action tempFileCleanerConfig = null) { + if (tempFileCleanerConfig == null) + tempFileCleanerConfig = (opt) => { }; + return services.AddScoped() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .Configure(tempFileCleanerConfig); } } } diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDirectory.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDirectory.cs index 78601e4..4319b08 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDirectory.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDirectory.cs @@ -264,7 +264,7 @@ public virtual async Task MoveToAsync(string newDest, IVolume destVo if (verify && !this.CanMove()) throw new PermissionDeniedException(); var destInfo = new FileSystemDirectory(newDest, destVolume); - if (verify && !await destInfo.CanMoveToAsync()) + if (verify && !await destInfo.CanMoveToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (destInfo.FileExists()) diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDriver.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDriver.cs index 30f025b..d780de0 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDriver.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemDriver.cs @@ -28,6 +28,7 @@ namespace elFinder.Net.Drivers.FileSystem { public class FileSystemDriver : IDriver { + public const string ChunkingFolderPrefix = "_uploading_"; public const string DefaultThumbExt = ".png"; private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); @@ -38,7 +39,13 @@ public class FileSystemDriver : IDriver protected readonly IZipFileArchiver zipFileArchiver; protected readonly IThumbnailBackgroundGenerator thumbnailBackgroundGenerator; protected readonly IConnector connector; + protected readonly IConnectorManager connectorManager; + protected readonly ICryptographyProvider cryptographyProvider; + protected readonly ITempFileCleaner tempFileCleaner; + public event EventHandler OnBeforeRemoveThumb; + public event EventHandler OnAfterRemoveThumb; + public event EventHandler OnRemoveThumbError; public event EventHandler OnBeforeMakeDir; public event EventHandler OnAfterMakeDir; public event EventHandler OnBeforeMakeFile; @@ -47,8 +54,14 @@ public class FileSystemDriver : IDriver public event EventHandler<(IFileSystem FileSystem, string PrevName)> OnAfterRename; public event EventHandler OnBeforeRemove; public event EventHandler OnAfterRemove; - public event EventHandler<(IFile File, IFormFileWrapper FormFile, bool IsOverwrite)> OnBeforeUpload; - public event EventHandler<(IFile File, IFormFileWrapper FormFile, bool IsOverwrite)> OnAfterUpload; + public event EventHandler OnBeforeRollbackChunk; + public event EventHandler OnAfterRollbackChunk; + public event EventHandler<(IFile File, IFile DestFile, IFormFileWrapper FormFile, bool IsOverwrite, bool IsChunking)> OnBeforeUpload; + public event EventHandler<(IFile File, IFile DestFile, IFormFileWrapper FormFile, bool IsOverwrite, bool IsChunking)> OnAfterUpload; + public event EventHandler<(IFile File, bool IsOverwrite)> OnBeforeChunkMerged; + public event EventHandler<(IFile File, bool IsOverwrite)> OnAfterChunkMerged; + public event EventHandler<(IFile ChunkFile, IFile DestFile, bool IsOverwrite)> OnBeforeChunkTransfer; + public event EventHandler<(IFile ChunkFile, IFile DestFile, bool IsOverwrite)> OnAfterChunkTransfer; public event EventHandler OnUploadError; public event EventHandler<(IFileSystem FileSystem, string NewDest, bool IsOverwrite)> OnBeforeMove; public event EventHandler<(IFileSystem FileSystem, IFileSystem NewFileSystem, bool IsOverwrite)> OnAfterMove; @@ -74,7 +87,10 @@ public FileSystemDriver(IPathParser pathParser, IZipDownloadPathProvider zipDownloadPathProvider, IZipFileArchiver zipFileArchiver, IThumbnailBackgroundGenerator thumbnailBackgroundGenerator, - IConnector connector) + ICryptographyProvider cryptographyProvider, + IConnector connector, + IConnectorManager connectorManager, + ITempFileCleaner tempFileCleaner) { this.pathParser = pathParser; this.pictureEditor = pictureEditor; @@ -82,7 +98,10 @@ public FileSystemDriver(IPathParser pathParser, this.zipDownloadPathProvider = zipDownloadPathProvider; this.zipFileArchiver = zipFileArchiver; this.thumbnailBackgroundGenerator = thumbnailBackgroundGenerator; + this.cryptographyProvider = cryptographyProvider; this.connector = connector; + this.connectorManager = connectorManager; + this.tempFileCleaner = tempFileCleaner; } public virtual async Task LsAsync(LsCommand cmd, CancellationToken cancellationToken = default) @@ -248,17 +267,20 @@ public virtual async Task OpenAsync(OpenCommand cmd, CancellationT InitResponse initResp; if (fileInfo is RootInfoResponse rootInfo) { - initResp = new InitResponse(rootInfo, rootInfo.options); + initResp = new InitResponse(rootInfo, rootInfo.options, cwd.Volume); } else { initResp = new InitResponse(fileInfo, - new ConnectorResponseOptions(cwd, connector.Options.DisabledUICommands, currentVolume.DirectorySeparatorChar)); + new ConnectorResponseOptions(cwd, connector.Options.DisabledUICommands, currentVolume.DirectorySeparatorChar), + cwd.Volume); await AddParentsToListAsync(targetPath, initResp.files, cancellationToken: cancellationToken); } if (currentVolume.MaxUploadSize.HasValue) - initResp.options.uploadMaxSize = $"{currentVolume.MaxUploadSizeInKb.Value}K"; + initResp.options.uploadMaxSize = $"{currentVolume.MaxUploadSizeInMb.Value}M"; + + initResp.options.uploadMaxConn = currentVolume.MaxUploadConnections; openResp = initResp; } @@ -271,12 +293,13 @@ public virtual async Task OpenAsync(OpenCommand cmd, CancellationT if (fileInfo is RootInfoResponse rootInfo) { - openResp = new OpenResponse(rootInfo, rootInfo.options); + openResp = new OpenResponse(rootInfo, rootInfo.options, cwd.Volume); } else { openResp = new OpenResponse(fileInfo, - new ConnectorResponseOptions(cwd, connector.Options.DisabledUICommands, currentVolume.DirectorySeparatorChar)); + new ConnectorResponseOptions(cwd, connector.Options.DisabledUICommands, currentVolume.DirectorySeparatorChar), + cwd.Volume); } } @@ -433,25 +456,47 @@ public virtual async Task RmAsync(RmCommand cmd, CancellationToken c return rmResp; } - public virtual async Task SetupVolumeAsync(IVolume volume, CancellationToken cancellationToken = default) + public virtual Task SetupVolumeAsync(IVolume volume, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); + Action CreateAndHideDirectory = (str) => + { + var dir = new DirectoryInfo(str); + + if (!dir.Exists) + dir.Create(); + + if (!dir.Attributes.HasFlag(FileAttributes.Hidden)) + dir.Attributes = FileAttributes.Hidden; + }; + if (volume.ThumbnailDirectory != null) { - var tmbDirObj = new FileSystemDirectory(volume.ThumbnailDirectory, volume); + CreateAndHideDirectory(volume.ThumbnailDirectory); + } - if (!await tmbDirObj.ExistsAsync) - await tmbDirObj.CreateAsync(cancellationToken: cancellationToken); + if (volume.TempDirectory != null) + { + CreateAndHideDirectory(volume.TempDirectory); + } + + if (volume.TempArchiveDirectory != null) + { + CreateAndHideDirectory(volume.TempArchiveDirectory); + } - if (!tmbDirObj.Attributes.HasFlag(FileAttributes.Hidden)) - tmbDirObj.Attributes = FileAttributes.Hidden; + if (volume.ChunkDirectory != null) + { + CreateAndHideDirectory(volume.ChunkDirectory); } if (!Directory.Exists(volume.RootDirectory)) { Directory.CreateDirectory(volume.RootDirectory); } + + return Task.CompletedTask; } public virtual async Task TmbAsync(TmbCommand cmd, CancellationToken cancellationToken = default) @@ -488,7 +533,17 @@ public virtual async Task UploadAsync(UploadCommand cmd, Cancell if (cmd.Renames.Any(name => !IsObjectNameValid(name))) throw new InvalidFileNameException(); - if (!IsObjectNameValid(cmd.Suffix)) + if (!IsObjectNameValid(cmd.Suffix) || !IsObjectNameValid(cmd.UploadName)) + throw new InvalidFileNameException(); + + var isChunking = cmd.Chunk.ToString().Length > 0; + var isChunkMerge = isChunking && cmd.Cid.ToString().Length == 0; + var isFinalUploading = !isChunking || isChunkMerge; + + if (isChunking && (cmd.Chunk.Any(name => !IsObjectNameValid(name)))) + throw new InvalidFileNameException(); + + if (!isFinalUploading && !IsObjectNameValid(cmd.ChunkInfo.UploadingFileName)) throw new InvalidFileNameException(); var uploadResp = new UploadResponse(); @@ -498,56 +553,58 @@ public virtual async Task UploadAsync(UploadCommand cmd, Cancell var warningDetails = uploadResp.GetWarningDetails(); var setNewParents = new HashSet(); - foreach (var uploadPath in cmd.UploadPathInfos.Distinct()) + if (isFinalUploading) { - var directory = uploadPath.Directory; - string lastParentHash = null; - - while (!volume.IsRoot(directory)) + foreach (var uploadPath in cmd.UploadPathInfos.Distinct()) { - var hash = lastParentHash ?? directory.GetHash(volume, pathParser); - lastParentHash = directory.GetParentHash(volume, pathParser); + var directory = uploadPath.Directory; + string lastParentHash = null; + + while (!volume.IsRoot(directory)) + { + var hash = lastParentHash ?? directory.GetHash(volume, pathParser); + lastParentHash = directory.GetParentHash(volume, pathParser); - if (!await directory.ExistsAsync && setNewParents.Add(directory)) - uploadResp.added.Add(await directory.ToFileInfoAsync(hash, lastParentHash, volume, connector.Options, cancellationToken: cancellationToken)); + if (!await directory.ExistsAsync && setNewParents.Add(directory)) + uploadResp.added.Add(await directory.ToFileInfoAsync(hash, lastParentHash, volume, connector.Options, cancellationToken: cancellationToken)); - directory = directory.Parent; + directory = directory.Parent; + } } } - var uploadCount = cmd.Upload.Count(); - for (var idx = 0; idx < uploadCount; idx++) + if (isChunkMerge) { - var formFile = cmd.Upload.ElementAt(idx); - IDirectory dest; - string destHash; + FileSystemDirectory chunkingDir = null; + FileSystemFile uploadFileInfo = null; try { - if (cmd.UploadPath.Count > idx) - { - dest = cmd.UploadPathInfos.ElementAt(idx).Directory; - destHash = cmd.UploadPath[idx]; - } - else - { - dest = targetPath.Directory; - destHash = targetPath.HashedTarget; - } + string uploadingFileName = Path.GetFileName(cmd.UploadName); + string chunkMergeName = Path.GetFileName(cmd.Chunk); - if (!dest.CanCreateObject()) - throw new PermissionDeniedException($"Permission denied: {volume.GetRelativePath(dest)}"); + var uploadDir = cmd.UploadPath.Count > 0 ? cmd.UploadPathInfos.Single().Directory : cmd.TargetPath.Directory; + var uploadDirHash = cmd.UploadPath.Count > 0 ? cmd.UploadPath.Single() : cmd.Target; + var chunkingDirFullName = PathHelper.SafelyCombine(uploadDir.Volume.ChunkDirectory, + uploadDir.Volume.ChunkDirectory, chunkMergeName); + chunkingDir = new FileSystemDirectory(chunkingDirFullName, volume); - string uploadFullName = PathHelper.SafelyCombine(dest.FullName, dest.FullName, Path.GetFileName(formFile.FileName)); - var uploadFileInfo = new FileSystemFile(uploadFullName, volume); + if (!await chunkingDir.ExistsAsync) + throw new DirectoryNotFoundException(); + + if (!uploadDir.CanCreateObject()) + throw new PermissionDeniedException($"Permission denied: {volume.GetRelativePath(uploadDir)}"); + + var uploadFullName = PathHelper.SafelyCombine(uploadDir.FullName, uploadDir.FullName, uploadingFileName); + uploadFileInfo = new FileSystemFile(uploadFullName, volume); var isOverwrite = false; if (await uploadFileInfo.ExistsAsync) { - if (cmd.Renames.Contains(formFile.FileName)) + if (cmd.Renames.Contains(uploadingFileName)) { - var fileNameWithoutExt = Path.GetFileNameWithoutExtension(formFile.FileName); - var ext = Path.GetExtension(formFile.FileName); + var fileNameWithoutExt = Path.GetFileNameWithoutExtension(uploadingFileName); + var ext = Path.GetExtension(uploadingFileName); var backupName = $"{fileNameWithoutExt}{cmd.Suffix}{ext}"; var fullBakName = PathHelper.SafelyCombine(uploadFileInfo.Parent.FullName, uploadFileInfo.Parent.FullName, backupName); var bakFile = new FileSystemFile(fullBakName, volume); @@ -560,7 +617,7 @@ public virtual async Task UploadAsync(UploadCommand cmd, Cancell await uploadFileInfo.RenameAsync(backupName, cancellationToken: cancellationToken); OnAfterRename?.Invoke(this, (uploadFileInfo, prevName)); - uploadResp.added.Add(await uploadFileInfo.ToFileInfoAsync(destHash, volume, pathParser, pictureEditor, videoEditor, cancellationToken: cancellationToken)); + uploadResp.added.Add(await uploadFileInfo.ToFileInfoAsync(uploadDirHash, volume, pathParser, pictureEditor, videoEditor, cancellationToken: cancellationToken)); uploadFileInfo = new FileSystemFile(uploadFullName, volume); } else if (cmd.Overwrite == 0 || (cmd.Overwrite == null && !volume.UploadOverwrite)) @@ -575,30 +632,287 @@ public virtual async Task UploadAsync(UploadCommand cmd, Cancell else isOverwrite = true; } - OnBeforeUpload?.Invoke(this, (uploadFileInfo, formFile, isOverwrite)); - using (var fileStream = await uploadFileInfo.OpenWriteAsync(cancellationToken: cancellationToken)) - { - await formFile.CopyToAsync(fileStream, cancellationToken: cancellationToken); - } - OnAfterUpload?.Invoke(this, (uploadFileInfo, formFile, isOverwrite)); + var chunkedUploadInfo = connectorManager.GetLock(chunkingDir.FullName); + + if (chunkedUploadInfo == null) + throw new ConnectionAbortedException(); + + OnBeforeChunkMerged?.Invoke(this, (uploadFileInfo, isOverwrite)); + chunkedUploadInfo.IsFileTouched = true; + await MergeChunksAsync(uploadFileInfo, chunkingDir, isOverwrite, cancellationToken: cancellationToken); + OnAfterChunkMerged?.Invoke(this, (uploadFileInfo, isOverwrite)); + + connectorManager.ReleaseLockCache(chunkingDir.FullName); await uploadFileInfo.RefreshAsync(cancellationToken); - uploadResp.added.Add(await uploadFileInfo.ToFileInfoAsync(destHash, volume, pathParser, pictureEditor, videoEditor, cancellationToken: cancellationToken)); + uploadResp.added.Add(await uploadFileInfo.ToFileInfoAsync(uploadDirHash, volume, pathParser, pictureEditor, videoEditor, cancellationToken: cancellationToken)); } catch (Exception ex) { - var rootCause = ex.GetRootCause(); + var chunkedUploadInfo = connectorManager.GetLock(chunkingDir.FullName); + + if (chunkedUploadInfo != null) + { + lock (chunkedUploadInfo) + { + chunkedUploadInfo.Exception = ex.GetRootCause(); + + if (chunkingDir != null) + { + chunkingDir.RefreshAsync().Wait(); + + if (chunkingDir.ExistsAsync.Result) + { + OnBeforeRollbackChunk?.Invoke(this, chunkingDir); + OnBeforeRemove?.Invoke(this, chunkingDir); + chunkingDir.DeleteAsync().Wait(); + OnAfterRemove?.Invoke(this, chunkingDir); + OnAfterRollbackChunk?.Invoke(this, chunkingDir); + } + } + + if (uploadFileInfo != null && chunkedUploadInfo.IsFileTouched) + { + uploadFileInfo.RefreshAsync().Wait(); + + if (uploadFileInfo.ExistsAsync.Result) + { + OnBeforeRollbackChunk?.Invoke(this, uploadFileInfo); + OnBeforeRemove?.Invoke(this, uploadFileInfo); + uploadFileInfo.DeleteAsync().Wait(); + OnAfterRemove?.Invoke(this, uploadFileInfo); + OnAfterRollbackChunk?.Invoke(this, uploadFileInfo); + } + } + } + } + OnUploadError?.Invoke(this, ex); + throw ex; + } + } + else + { + var uploadCount = cmd.Upload.Count(); + for (var idx = 0; idx < uploadCount; idx++) + { + var formFile = cmd.Upload.ElementAt(idx); + IDirectory dest = null; + IDirectory finalDest = null; + string destHash = null; + string uploadingFileName = "unknown", cleanFileName; - if (rootCause is PermissionDeniedException pEx) + try { - warning.Add(string.IsNullOrEmpty(pEx.Message) ? $"Permission denied: {formFile.FileName}" : pEx.Message); - warningDetails.Add(ErrorResponse.Factory.UploadFile(pEx, formFile.FileName)); + (string UploadFileName, int CurrentChunkNo, int TotalChunks)? chunkInfo = null; + + if (isChunking) + { + chunkInfo = cmd.ChunkInfo; + uploadingFileName = Path.GetFileName(chunkInfo.Value.UploadFileName); + cleanFileName = Path.GetFileName(cmd.Chunk); + } + else + { + uploadingFileName = Path.GetFileName(formFile.FileName); + cleanFileName = uploadingFileName; + } + + if (cmd.UploadPath.Count > idx) + { + if (isFinalUploading) + { + dest = cmd.UploadPathInfos.ElementAt(idx).Directory; + finalDest = dest; + destHash = cmd.UploadPath[idx]; + } + else + { + finalDest = cmd.UploadPathInfos.ElementAt(idx).Directory; + var tempDest = GetChunkDirectory(finalDest, uploadingFileName, cmd.Cid); + dest = new FileSystemDirectory(tempDest, volume); + destHash = cmd.UploadPath[idx]; + } + } + else + { + if (isFinalUploading) + { + dest = targetPath.Directory; + finalDest = dest; + destHash = targetPath.HashedTarget; + } + else + { + finalDest = targetPath.Directory; + var tempDest = GetChunkDirectory(finalDest, uploadingFileName, cmd.Cid); + dest = new FileSystemDirectory(tempDest, volume); + destHash = targetPath.HashedTarget; + } + } + + if (isChunking) + { + var chunkedUploadInfo = connectorManager.GetLock(dest.FullName, _ => new ChunkedUploadInfo()); + lock (chunkedUploadInfo) + { + if (chunkedUploadInfo.Exception != null) throw chunkedUploadInfo.Exception; + + if (!dest.ExistsAsync.Result) + { + if (!dest.CanCreate()) throw new PermissionDeniedException(); + + chunkedUploadInfo.TotalUploaded = 0; + OnBeforeMakeDir?.Invoke(this, dest); + dest.CreateAsync(cancellationToken: cancellationToken).Wait(); + OnAfterMakeDir?.Invoke(this, dest); + + WriteStatusFileAsync(dest).Wait(); + } + } + } + + if (!finalDest.CanCreateObject()) + throw new PermissionDeniedException($"Permission denied: {volume.GetRelativePath(finalDest)}"); + + var uploadFullName = PathHelper.SafelyCombine(dest.FullName, dest.FullName, cleanFileName); + var uploadFileInfo = new FileSystemFile(uploadFullName, volume); + var finalUploadFullName = PathHelper.SafelyCombine(finalDest.FullName, finalDest.FullName, uploadingFileName); + var finalUploadFileInfo = isChunking ? new FileSystemFile(finalUploadFullName, volume) : uploadFileInfo; + var isOverwrite = false; + + if (!isFinalUploading && await uploadFileInfo.ExistsAsync) + { + throw new PermissionDeniedException(); + } + + if (await finalUploadFileInfo.ExistsAsync) + { + if (cmd.Renames.Contains(uploadingFileName)) + { + var fileNameWithoutExt = Path.GetFileNameWithoutExtension(uploadingFileName); + var ext = Path.GetExtension(uploadingFileName); + var backupName = $"{fileNameWithoutExt}{cmd.Suffix}{ext}"; + var fullBakName = PathHelper.SafelyCombine(finalUploadFileInfo.Parent.FullName, finalUploadFileInfo.Parent.FullName, backupName); + var bakFile = new FileSystemFile(fullBakName, volume); + + if (await bakFile.ExistsAsync) + backupName = await bakFile.GetCopyNameAsync(cmd.Suffix, cancellationToken: cancellationToken); + + var prevName = finalUploadFileInfo.Name; + OnBeforeRename?.Invoke(this, (finalUploadFileInfo, backupName)); + await finalUploadFileInfo.RenameAsync(backupName, cancellationToken: cancellationToken); + OnAfterRename?.Invoke(this, (finalUploadFileInfo, prevName)); + + uploadResp.added.Add(await finalUploadFileInfo.ToFileInfoAsync(destHash, volume, pathParser, pictureEditor, videoEditor, cancellationToken: cancellationToken)); + finalUploadFileInfo = new FileSystemFile(finalUploadFullName, volume); + } + else if (cmd.Overwrite == 0 || (cmd.Overwrite == null && !volume.UploadOverwrite)) + { + string newName = await finalUploadFileInfo.GetCopyNameAsync(cmd.Suffix, cancellationToken: cancellationToken); + finalUploadFullName = PathHelper.SafelyCombine(finalUploadFileInfo.DirectoryName, finalUploadFileInfo.DirectoryName, newName); + finalUploadFileInfo = new FileSystemFile(finalUploadFullName, volume); + isOverwrite = false; + } + else if (!finalUploadFileInfo.ObjectAttribute.Write) + throw new PermissionDeniedException(); + else isOverwrite = true; + } + + uploadFileInfo = isChunking ? uploadFileInfo : finalUploadFileInfo; + + if (isChunking) + { + await WriteStatusFileAsync(dest); + } + + OnBeforeUpload?.Invoke(this, (uploadFileInfo, finalUploadFileInfo, formFile, isOverwrite, isChunking)); + using (var fileStream = await uploadFileInfo.OpenWriteAsync(cancellationToken: cancellationToken)) + { + await formFile.CopyToAsync(fileStream, cancellationToken: cancellationToken); + } + OnAfterUpload?.Invoke(this, (uploadFileInfo, finalUploadFileInfo, formFile, isOverwrite, isChunking)); + + if (isFinalUploading) + { + await finalUploadFileInfo.RefreshAsync(cancellationToken); + uploadResp.added.Add(await finalUploadFileInfo.ToFileInfoAsync(destHash, volume, pathParser, pictureEditor, videoEditor, cancellationToken: cancellationToken)); + } + else + { + var chunkedUploadInfo = connectorManager.GetLock(dest.FullName); + + if (chunkedUploadInfo != null) + { + lock (chunkedUploadInfo) + { + if (chunkedUploadInfo.Exception != null) throw chunkedUploadInfo.Exception; + + chunkedUploadInfo.TotalUploaded++; + + if (chunkedUploadInfo.TotalUploaded == cmd.ChunkInfo.TotalChunks) + { + uploadResp._chunkmerged = dest.Name; + uploadResp._name = uploadingFileName; + } + } + } + } } - else + catch (Exception ex) { - warning.Add($"Failed to upload: {formFile.FileName}"); - warningDetails.Add(ErrorResponse.Factory.UploadFile(ex, formFile.FileName)); + var rootCause = ex.GetRootCause(); + + if (isChunking) + { + var chunkedUploadInfo = connectorManager.GetLock(dest.FullName); + var isExceptionReturned = false; + + if (chunkedUploadInfo != null) + { + lock (chunkedUploadInfo) + { + isExceptionReturned = chunkedUploadInfo.Exception != null; + + if (!isExceptionReturned) + { + chunkedUploadInfo.Exception = rootCause; + } + + if (dest != null) + { + dest.RefreshAsync().Wait(); + + if (dest.ExistsAsync.Result) + { + OnBeforeRollbackChunk?.Invoke(this, dest); + OnBeforeRemove?.Invoke(this, dest); + dest.DeleteAsync().Wait(); + OnAfterRemove?.Invoke(this, dest); + OnAfterRollbackChunk?.Invoke(this, dest); + } + } + } + } + + if (isExceptionReturned) return new UploadResponse(); + + OnUploadError?.Invoke(this, ex); + throw ex; + } + + OnUploadError?.Invoke(this, ex); + + if (rootCause is PermissionDeniedException pEx) + { + warning.Add(string.IsNullOrEmpty(pEx.Message) ? $"Permission denied: {uploadingFileName}" : pEx.Message); + warningDetails.Add(ErrorResponse.Factory.UploadFile(pEx, uploadingFileName)); + } + else + { + warning.Add($"Failed to upload: {uploadingFileName}"); + warningDetails.Add(ErrorResponse.Factory.UploadFile(ex, uploadingFileName)); + } } } } @@ -606,6 +920,69 @@ public virtual async Task UploadAsync(UploadCommand cmd, Cancell return uploadResp; } + public Task AbortUploadAsync(UploadCommand cmd, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (cmd.Name.Any(name => !IsObjectNameValid(name))) + throw new InvalidFileNameException(); + + if (cmd.Renames.Any(name => !IsObjectNameValid(name))) + throw new InvalidFileNameException(); + + if (!IsObjectNameValid(cmd.Suffix) || !IsObjectNameValid(cmd.UploadName)) + throw new InvalidFileNameException(); + + var isChunking = cmd.Chunk.ToString().Length > 0; + var isChunkMerge = isChunking && cmd.Cid.ToString().Length == 0; + var isFinalUploading = !isChunking || isChunkMerge; + + if (isChunking && (cmd.Chunk.Any(name => !IsObjectNameValid(name)))) + throw new InvalidFileNameException(); + + if (!isFinalUploading && !IsObjectNameValid(cmd.ChunkInfo.UploadingFileName)) + throw new InvalidFileNameException(); + + if (isFinalUploading) return Task.CompletedTask; + + var targetPath = cmd.TargetPath; + var volume = targetPath.Volume; + IDirectory dest = null; + + (string UploadFileName, int CurrentChunkNo, int TotalChunks)? chunkInfo = null; + string uploadingFileName, cleanFileName; + + chunkInfo = cmd.ChunkInfo; + uploadingFileName = chunkInfo.Value.UploadFileName; + cleanFileName = Path.GetFileName(cmd.Chunk); + + var uploadDir = cmd.UploadPathInfos.FirstOrDefault()?.Directory ?? cmd.TargetPath.Directory; + var tempDest = GetChunkDirectory(uploadDir, uploadingFileName, cmd.Cid); + dest = new FileSystemDirectory(tempDest, volume); + + var chunkedUploadInfo = connectorManager.GetLock(dest.FullName); + + if (chunkedUploadInfo != null) + { + lock (chunkedUploadInfo) + { + chunkedUploadInfo.Exception = new ConnectionAbortedException(); + + if (!dest.ExistsAsync.Result) + throw new DirectoryNotFoundException(); + + if (!dest.CanDelete()) + throw new PermissionDeniedException($"Permission denied: {volume.GetRelativePath(dest)}"); + + OnBeforeRemove?.Invoke(this, dest); + dest.DeleteAsync(cancellationToken: cancellationToken).Wait(); + OnAfterRemove?.Invoke(this, dest); + } + } + + return Task.CompletedTask; + } + public virtual async Task TreeAsync(TreeCommand cmd, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -790,9 +1167,9 @@ public virtual async Task PasteAsync(PasteCommand cmd, Cancellati { OnBeforeMove?.Invoke(this, (src.Directory, newDest, true)); pastedDir = await MergeAsync(src.Directory, newDest, dstVolume, copyOverwrite, cancellationToken: cancellationToken); - OnBeforeRemove(this, src.Directory); + OnBeforeRemove?.Invoke(this, src.Directory); await src.Directory.DeleteAsync(cancellationToken: cancellationToken); - OnAfterRemove(this, src.Directory); + OnAfterRemove?.Invoke(this, src.Directory); OnAfterMove?.Invoke(this, (src.Directory, pastedDir, true)); } else @@ -959,7 +1336,7 @@ public virtual async Task ArchiveAsync(ArchiveCommand cmd, Canc var archivePath = PathHelper.SafelyCombine(directory.FullName, directory.FullName, filename); var newFile = new FileSystemFile(archivePath, volume); - if (!await newFile.CanArchiveToAsync()) + if (!await newFile.CanArchiveToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (newFile.DirectoryExists()) @@ -1018,7 +1395,7 @@ public virtual async Task ExtractAsync(ExtractCommand cmd, Canc fromPath = PathHelper.SafelyCombine(fromPath, fromPath, Path.GetFileNameWithoutExtension(targetPath.File.Name)); fromDir = new FileSystemDirectory(fromPath, volume); - if (!await fromDir.CanExtractToAsync()) + if (!await fromDir.CanExtractToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (fromDir.FileExists()) @@ -1047,7 +1424,7 @@ public virtual async Task ExtractAsync(ExtractCommand cmd, Canc if (string.IsNullOrEmpty(entry.Name)) { var dir = new FileSystemDirectory(fullName, volume); - if (!await dir.CanExtractToAsync()) + if (!await dir.CanExtractToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (dir.FileExists()) @@ -1071,7 +1448,7 @@ public virtual async Task ExtractAsync(ExtractCommand cmd, Canc { var file = new FileSystemFile(fullName, volume); - if (!await file.CanExtractToAsync()) throw new PermissionDeniedException(); + if (!await file.CanExtractToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (file.DirectoryExists()) throw new ExistsException(file.Name); @@ -1272,7 +1649,8 @@ public virtual async Task ZipdlAsync(ZipdlCommand cmd, Cancell var volume = cmd.TargetPaths.Select(p => p.Volume).First(); var zipExt = $".{FileExtensions.Zip}"; - var (archivePath, archiveFileKey) = await zipDownloadPathProvider.GetFileForArchivingAsync(); + var (archivePath, archiveFileKey) = await zipDownloadPathProvider.GetFileForArchivingAsync( + volume.TempArchiveDirectory, cancellationToken: cancellationToken); var newFile = new FileSystemFile(archivePath, volume); try @@ -1319,7 +1697,9 @@ public virtual async Task ZipdlRawAsync(ZipdlCommand cmd, Cancella if (!IsObjectNameValid(cmd.DownloadFileName)) throw new InvalidFileNameException(); - var archiveFile = await zipDownloadPathProvider.ParseArchiveFileKeyAsync(cmd.ArchiveFileKey); + var volume = cmd.CwdPath.Volume; + var archiveFile = await zipDownloadPathProvider.ParseArchiveFileKeyAsync( + volume.TempArchiveDirectory, cmd.ArchiveFileKey, cancellationToken); var tempFileInfo = new FileInfo(archiveFile); if (!tempFileInfo.Exists) throw new PermissionDeniedException($"Malformed key"); @@ -1412,9 +1792,9 @@ public virtual Task GenerateThumbPathAsync(IDirectory directory, Cancell { cancellationToken.ThrowIfCancellationRequested(); - MediaType? mediaType = null; + MediaType? mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify); - if ((mediaType = file.CanGetThumb(pictureEditor, videoEditor, verify)) == null) return (null, null, mediaType); + if (mediaType == null) return (null, null, mediaType); var thumbPath = await GenerateThumbPathAsync(file, cancellationToken: cancellationToken); @@ -1454,6 +1834,7 @@ public IDirectory CreateDirectory(string fullPath, IVolume volume) return new FileSystemDirectory(fullPath, volume); } + // Or use Path.GetFileName(name) to remove all paths and keep the fileName only. protected bool IsObjectNameValid(string name) { return name == null || !name.Any(ch => InvalidFileNameChars.Contains(ch)); @@ -1539,7 +1920,7 @@ protected virtual async Task CopyToAsync(IDirectory directory, strin if (!directory.CanCopy()) throw new PermissionDeniedException(); var destInfo = new FileSystemDirectory(newDest, destVolume); - if (!await destInfo.CanCopyToAsync()) + if (!await destInfo.CanCopyToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (destInfo.FileExists()) @@ -1584,7 +1965,7 @@ protected virtual async Task MergeAsync(IDirectory srcDir, string ne cancellationToken.ThrowIfCancellationRequested(); var destInfo = new FileSystemDirectory(newDest, destVolume); - if (!await destInfo.CanMoveToAsync()) + if (!await destInfo.CanMoveToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (destInfo.FileExists()) @@ -1601,9 +1982,9 @@ protected virtual async Task MergeAsync(IDirectory srcDir, string ne if (!await currentNewDest.ExistsAsync) { - OnBeforeMakeDir(this, currentNewDest); + OnBeforeMakeDir?.Invoke(this, currentNewDest); await currentNewDest.CreateAsync(cancellationToken: cancellationToken); - OnAfterMakeDir(this, currentNewDest); + OnAfterMakeDir?.Invoke(this, currentNewDest); } foreach (var dir in await currentDir.GetDirectoriesAsync(cancellationToken: cancellationToken)) @@ -1619,7 +2000,7 @@ protected virtual async Task MergeAsync(IDirectory srcDir, string ne } } - await destInfo.RefreshAsync(); + await destInfo.RefreshAsync(cancellationToken: cancellationToken); return destInfo; } @@ -1651,21 +2032,30 @@ protected virtual async Task RemoveThumbsAsync(PathInfo path, CancellationToken { cancellationToken.ThrowIfCancellationRequested(); - if (path.IsDirectory) + try { - string thumbPath = await GenerateThumbPathAsync(path.Directory, cancellationToken: cancellationToken); - if (!string.IsNullOrEmpty(thumbPath) && Directory.Exists(thumbPath)) + OnBeforeRemoveThumb?.Invoke(this, path); + if (path.IsDirectory) { - Directory.Delete(thumbPath, true); + string thumbPath = await GenerateThumbPathAsync(path.Directory, cancellationToken: cancellationToken); + if (!string.IsNullOrEmpty(thumbPath) && Directory.Exists(thumbPath)) + { + Directory.Delete(thumbPath, true); + } } - } - else - { - string thumbPath = await GenerateThumbPathAsync(path.File, cancellationToken: cancellationToken); - if (!string.IsNullOrEmpty(thumbPath) && File.Exists(thumbPath)) + else { - File.Delete(thumbPath); + string thumbPath = await GenerateThumbPathAsync(path.File, cancellationToken: cancellationToken); + if (!string.IsNullOrEmpty(thumbPath) && File.Exists(thumbPath)) + { + File.Delete(thumbPath); + } } + OnAfterRemoveThumb?.Invoke(this, path); + } + catch (Exception ex) + { + OnRemoveThumbError?.Invoke(this, ex); } } @@ -1695,5 +2085,69 @@ protected virtual byte[] ParseDataURIScheme(string dataUri, string fromcmd) return Convert.FromBase64String(parts[1]); } + + protected virtual async Task MergeChunksAsync(IFile destFile, IDirectory chunkingDir, + bool isOverwrite, CancellationToken cancellationToken = default) + { + var files = (await chunkingDir.GetFilesAsync(cancellationToken: cancellationToken)) + .Where(f => f.Name != tempFileCleaner.Options.StatusFile) + .OrderBy(f => FileHelper.GetChunkInfo(f.Name).CurrentChunkNo).ToArray(); + + using (var fileStream = await destFile.OpenWriteAsync(cancellationToken: cancellationToken)) + { + foreach (var file in files) + { + await WriteStatusFileAsync(chunkingDir); + + OnBeforeChunkTransfer?.Invoke(this, (file, destFile, isOverwrite)); + using (var readStream = await file.OpenReadAsync(cancellationToken: cancellationToken)) + using (var memStream = new MemoryStream()) + { + await readStream.CopyToAsync(memStream); + var bytes = memStream.ToArray(); + await fileStream.WriteAsync(bytes, 0, bytes.Length); + } + OnAfterChunkTransfer?.Invoke(this, (file, destFile, isOverwrite)); + + OnBeforeRemove?.Invoke(this, file); + await file.DeleteAsync(cancellationToken: cancellationToken); + OnAfterRemove?.Invoke(this, file); + } + } + + OnBeforeRemove?.Invoke(this, chunkingDir); + await chunkingDir.DeleteAsync(cancellationToken: cancellationToken); + OnAfterRemove?.Invoke(this, chunkingDir); + } + + private string GetChunkDirectory(IDirectory uploadDir, string uploadingFileName, string cid) + { + var bytes = Encoding.UTF8.GetBytes(uploadDir.FullName + uploadingFileName + cid); + var signature = BitConverter.ToString(cryptographyProvider.HMACSHA1ComputeHash( + nameof(GetChunkDirectory), bytes)).Replace("-", string.Empty); + var tempFileName = $"{ChunkingFolderPrefix}_{signature}"; + var tempDest = PathHelper.SafelyCombine(uploadDir.Volume.ChunkDirectory, uploadDir.Volume.ChunkDirectory, tempFileName); + return tempDest; + } + + private async Task WriteStatusFileAsync(IDirectory directory) + { + try + { + if (await directory.ExistsAsync) + { + var statusFile = PathHelper.SafelyCombine(directory.FullName, directory.FullName, tempFileCleaner.Options.StatusFile); + using (var file = File.OpenWrite(statusFile)) { } + } + } + catch (Exception) { } + } + + class ChunkedUploadInfo : ConnectorLock + { + public Exception Exception { get; set; } + public int TotalUploaded { get; set; } + public bool IsFileTouched { get; set; } + } } } diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemFile.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemFile.cs index 5d8d4d1..4ed28dc 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemFile.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/FileSystemFile.cs @@ -114,7 +114,7 @@ public virtual async Task OverwriteAsync(Stream stream, bool verify = true, Canc { cancellationToken.ThrowIfCancellationRequested(); - if (verify && !await this.CanWriteAsync()) + if (verify && !await this.CanWriteAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (this.DirectoryExists()) @@ -131,7 +131,7 @@ public virtual async Task OpenWriteAsync(bool verify = true, FileMode fi { cancellationToken.ThrowIfCancellationRequested(); - if (verify && !await this.CanWriteAsync()) + if (verify && !await this.CanWriteAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (this.DirectoryExists()) @@ -222,7 +222,7 @@ public virtual async Task CopyToAsync(string newDest, IVolume destVolume, if (verify && !this.CanCopy()) throw new PermissionDeniedException(); var destInfo = new FileSystemFile(newDest, destVolume); - if (verify && !await destInfo.CanCopyToAsync()) + if (verify && !await destInfo.CanCopyToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (destInfo.DirectoryExists()) @@ -241,7 +241,7 @@ public virtual async Task MoveToAsync(string newDest, IVolume destVolume, if (verify && !this.CanMove()) throw new PermissionDeniedException(); var destInfo = new FileSystemFile(newDest, destVolume); - if (verify && !await destInfo.CanMoveToAsync()) + if (verify && !await destInfo.CanMoveToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (destInfo.DirectoryExists()) diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/IZipDownloadPathProvider.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/IZipDownloadPathProvider.cs deleted file mode 100644 index 1b3bcb1..0000000 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/IZipDownloadPathProvider.cs +++ /dev/null @@ -1,48 +0,0 @@ -using elFinder.Net.Core.Exceptions; -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; - -namespace elFinder.Net.Drivers.FileSystem -{ - public interface IZipDownloadPathProvider - { - Task<(string ArchiveFilePath, string ArchiveFileKey)> GetFileForArchivingAsync(); - Task ParseArchiveFileKeyAsync(string archiveFileKey); - } - - public class TempZipDownloadPathProvider : IZipDownloadPathProvider - { - private readonly HMAC _hmac = new HMACSHA256(); - private static readonly string Postfix = '_' + nameof(elFinder); - - public Task<(string ArchiveFilePath, string ArchiveFileKey)> GetFileForArchivingAsync() - { - var bytes = Encoding.UTF8.GetBytes(Path.GetTempFileName() + Guid.NewGuid().ToString()); - var tempFile = Path.Combine(Path.GetTempPath(), - BitConverter.ToString(_hmac.ComputeHash(bytes)).Replace("-", string.Empty) + Postfix); - var tempFileName = Path.GetFileName(tempFile); - return Task.FromResult((tempFile, tempFileName)); - } - - public Task ParseArchiveFileKeyAsync(string archiveFileKey) - { - var tempDirPath = Path.GetTempPath(); - - if (Path.IsPathRooted(archiveFileKey)) throw new PermissionDeniedException("Malformed key"); - - var fullPath = Path.GetFullPath(Path.Combine(tempDirPath, archiveFileKey)); - if (!fullPath.StartsWith(tempDirPath.EndsWith($"{Path.DirectorySeparatorChar}") - ? tempDirPath : (tempDirPath + Path.DirectorySeparatorChar))) - throw new PermissionDeniedException("Malformed key"); - - var fileName = Path.GetFileName(fullPath); - if (!fileName.EndsWith(Postfix)) - throw new PermissionDeniedException("Malformed key"); - - return Task.FromResult(fullPath); - } - } -} diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/DefaultThumbnailBackgroundGenerator.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/DefaultThumbnailBackgroundGenerator.cs index d5f6777..ae16550 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/DefaultThumbnailBackgroundGenerator.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/DefaultThumbnailBackgroundGenerator.cs @@ -2,6 +2,7 @@ using elFinder.Net.Core.Services.Drawing; using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -22,6 +23,9 @@ public class DefaultThumbnailBackgroundGenerator : IThumbnailBackgroundGenerator private readonly ConcurrentQueue _imageQueue; private readonly ManualResetEventSlim _imageSignal; + private bool _disposedValue; + private readonly CancellationTokenSource _tokenSource; + public DefaultThumbnailBackgroundGenerator(IPictureEditor pictureEditor, IVideoEditor videoEditor) { @@ -33,6 +37,7 @@ public DefaultThumbnailBackgroundGenerator(IPictureEditor pictureEditor, _videoMaps = new ConcurrentDictionary(); _imageQueue = new ConcurrentQueue(); _videoQueue = new ConcurrentQueue(); + _tokenSource = new CancellationTokenSource(); _videoWorker = new Thread(async () => await RunVideoGeneratorAsync()); _videoWorker.IsBackground = true; _videoWorker.Start(); @@ -68,10 +73,9 @@ public void TryAddToQueue(IFile file, IFile tmbFile, int size, bool keepAspectRa private async Task RunVideoGeneratorAsync() { - var running = true; - - while (running) + while (!_tokenSource.IsCancellationRequested) { + _tokenSource.Token.ThrowIfCancellationRequested(); _videoSignal.Wait(); string nextFile; @@ -79,26 +83,27 @@ private async Task RunVideoGeneratorAsync() { _videoMaps.TryRemove(nextFile, out var tuple); var (file, tmbFile, size, keepAspectRatio) = tuple; + try { - await file.RefreshAsync(); + await file.RefreshAsync(_tokenSource.Token); if (!await file.ExistsAsync || await tmbFile.ExistsAsync) continue; - var thumb = await _videoEditor.GenerateThumbnailInBackgroundAsync(file, size, keepAspectRatio); + var thumb = await _videoEditor.GenerateThumbnailInBackgroundAsync(file, size, keepAspectRatio, cancellationToken: _tokenSource.Token); if (thumb != null) { using (thumb) { thumb.ImageStream.Position = 0; - await tmbFile.OverwriteAsync(thumb.ImageStream, verify: false); + await tmbFile.OverwriteAsync(thumb.ImageStream, verify: false, cancellationToken: _tokenSource.Token); } } } catch (Exception ex) { - Console.WriteLine(ex); + Debug.WriteLine(ex); } } @@ -110,10 +115,9 @@ private async Task RunVideoGeneratorAsync() private async Task RunImageGeneratorAsync() { - var running = true; - - while (running) + while (!_tokenSource.IsCancellationRequested) { + _tokenSource.Token.ThrowIfCancellationRequested(); _imageSignal.Wait(); string nextFile; @@ -121,14 +125,15 @@ private async Task RunImageGeneratorAsync() { _imageMaps.TryRemove(nextFile, out var tuple); var (file, tmbFile, size, keepAspectRatio) = tuple; + try { - await file.RefreshAsync(); + await file.RefreshAsync(_tokenSource.Token); if (!await file.ExistsAsync || await tmbFile.ExistsAsync) continue; ImageWithMimeType thumb; - using (var fileStream = await file.OpenReadAsync(verify: false)) + using (var fileStream = await file.OpenReadAsync(verify: false, cancellationToken: _tokenSource.Token)) { thumb = _pictureEditor.GenerateThumbnail(fileStream, size, keepAspectRatio); } @@ -138,13 +143,13 @@ private async Task RunImageGeneratorAsync() using (thumb) { thumb.ImageStream.Position = 0; - await tmbFile.OverwriteAsync(thumb.ImageStream, verify: false); + await tmbFile.OverwriteAsync(thumb.ImageStream, verify: false, cancellationToken: _tokenSource.Token); } } } catch (Exception ex) { - Console.WriteLine(ex); + Debug.WriteLine(ex); } } @@ -153,5 +158,36 @@ private async Task RunImageGeneratorAsync() _imageSignal.Reset(); } } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~DefaultThumbnailBackgroundGenerator() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/ICryptography.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/ICryptography.cs new file mode 100644 index 0000000..7f3a612 --- /dev/null +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/ICryptography.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; + +namespace elFinder.Net.Drivers.FileSystem.Services +{ + public interface ICryptographyProvider + { + byte[] HMACSHA256ComputeHash(string key, byte[] buffer); + byte[] HMACSHA1ComputeHash(string key, byte[] buffer); + } + + public class DefaultCryptographyProvider : ICryptographyProvider + { + protected readonly ConcurrentDictionary hmac256Map; + protected readonly ConcurrentDictionary hmac1Map; + + public DefaultCryptographyProvider() + { + hmac256Map = new ConcurrentDictionary(); + hmac1Map = new ConcurrentDictionary(); + } + + public byte[] HMACSHA256ComputeHash(string key, byte[] buffer) + { + var hash = hmac256Map.GetOrAdd(key, _ => new HMACSHA256()); + lock (hash) + { + return hash.ComputeHash(buffer); + } + } + + public byte[] HMACSHA1ComputeHash(string key, byte[] buffer) + { + var hash = hmac1Map.GetOrAdd(key, _ => new HMACSHA1()); + lock (hash) + { + return hash.ComputeHash(buffer); + } + } + } +} diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/ITempFileCleaner.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/ITempFileCleaner.cs new file mode 100644 index 0000000..1fd8eeb --- /dev/null +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/ITempFileCleaner.cs @@ -0,0 +1,254 @@ +using Microsoft.Extensions.Options; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace elFinder.Net.Drivers.FileSystem.Services +{ + public interface ITempFileCleaner : IDisposable + { + TempFileCleanerOptions Options { get; } + Task AddEntryAsync(TempFileEntry entry); + Task RemovEntryAsync(string entryKey); + } + + public class TempFileEntry + { + public string FullName { get; set; } + public bool IsDirectory { get; set; } + public DateTimeOffset Expired { get; set; } + } + + public class TempFileCleanerOptions + { + public static readonly TimeSpan DefaultUnmanagedLifeTime = TimeSpan.FromMinutes(10); + public static readonly TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); + public const string DefaultStatusFile = ".status"; + + public string StatusFile { get; set; } = DefaultStatusFile; + public TimeSpan PollingInterval { get; set; } = DefaultPollingInterval; + public IDictionary ScanFolders { get; set; } = new Dictionary(); + } + + public class DefaultTempFileCleaner : ITempFileCleaner + { + protected readonly ConcurrentDictionary managedEntries; + protected readonly IOptionsMonitor options; + + private bool _disposedValue; + private readonly CancellationTokenSource _tokenSource; + + public DefaultTempFileCleaner(IOptionsMonitor options) + { + this.options = options; + managedEntries = new ConcurrentDictionary(); + _tokenSource = new CancellationTokenSource(); + StartCleanerThread(); + } + + public TempFileCleanerOptions Options => options.CurrentValue; + + public Task AddEntryAsync(TempFileEntry entry) + { + if (entry == null) throw new ArgumentNullException(nameof(entry)); + + return Task.FromResult(managedEntries.TryAdd(entry.FullName, entry)); + } + + public Task RemovEntryAsync(string entryKey) + { + return Task.FromResult(managedEntries.TryRemove(entryKey, out _)); + } + + protected virtual void StartCleanerThread() + { + Thread thread = new Thread(() => + { + while (!_tokenSource.IsCancellationRequested) + { + Thread.Sleep(options.CurrentValue.PollingInterval); + _tokenSource.Token.ThrowIfCancellationRequested(); + + var expiredEntries = managedEntries.Where(entry => + { + return DateTimeOffset.UtcNow >= entry.Value.Expired; + }).ToArray(); + + foreach (var entry in expiredEntries) + { + _tokenSource.Token.ThrowIfCancellationRequested(); + + try + { + managedEntries.TryRemove(entry.Key, out _); + var fullName = entry.Value.FullName; + + if (entry.Value.IsDirectory) + { + if (Directory.Exists(fullName)) + Directory.Delete(fullName); + } + else if (File.Exists(fullName)) + { + File.Delete(fullName); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + + if (options.CurrentValue.ScanFolders == null) continue; + + foreach (var scanFolderEntry in options.CurrentValue.ScanFolders) + { + _tokenSource.Token.ThrowIfCancellationRequested(); + var scanFolder = scanFolderEntry.Key; + + try + { + var notExpiredFolders = Directory.EnumerateDirectories(scanFolder, "*", SearchOption.AllDirectories) + .Where(f => + { + var statusFile = Path.Combine(f, options.CurrentValue.StatusFile); + if (File.Exists(statusFile)) + { + var lastWriteTime = File.GetLastWriteTimeUtc(statusFile); + var lifeTime = DateTimeOffset.UtcNow - lastWriteTime; + return lifeTime < scanFolderEntry.Value; + } + + return false; + }).ToArray(); + + var expiredFiles = Directory.EnumerateFiles(scanFolder, "*", SearchOption.AllDirectories) + .Where(f => !managedEntries.ContainsKey(f)) + .Where(f => + { + try + { + var fInfo = new FileInfo(f); + + if (notExpiredFolders.Contains(fInfo.Directory.FullName)) + return false; + + var lastAccessTime = fInfo.LastAccessTimeUtc; + var lastWriteTime = fInfo.LastWriteTimeUtc; + var finalTime = lastAccessTime > lastWriteTime ? lastAccessTime : lastWriteTime; + var lifeTime = DateTimeOffset.UtcNow - finalTime; + return lifeTime >= scanFolderEntry.Value; + } + catch (Exception ex) + { + Debug.WriteLine(ex); + return false; + } + }).ToArray(); + + foreach (var file in expiredFiles) + { + _tokenSource.Token.ThrowIfCancellationRequested(); + + try + { + File.Delete(file); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + + var expiredFolders = Directory.EnumerateDirectories(scanFolder, "*", SearchOption.AllDirectories) + .Where(f => !managedEntries.ContainsKey(f)) + .Where(f => + { + try + { + var isEmpty = Directory.EnumerateFiles(f, "*", SearchOption.AllDirectories).Count() == 0; + + if (isEmpty) + { + var dInfo = new DirectoryInfo(f); + + if (notExpiredFolders.Contains(dInfo.FullName)) + return false; + + var lastWriteTime = dInfo.LastWriteTimeUtc; + var lifeTime = DateTimeOffset.UtcNow - lastWriteTime; + return lifeTime >= scanFolderEntry.Value; + } + + return false; + } + catch (Exception ex) + { + Debug.WriteLine(ex); + return false; + } + }).ToArray(); + + foreach (var dir in expiredFolders) + { + _tokenSource.Token.ThrowIfCancellationRequested(); + + try + { + Directory.Delete(dir); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + } + }); + + thread.IsBackground = true; + thread.Start(); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~DefaultTempFileCleaner() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IThumbnailBackgroundGenerator.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IThumbnailBackgroundGenerator.cs index 5f3a703..5f394f5 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IThumbnailBackgroundGenerator.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IThumbnailBackgroundGenerator.cs @@ -1,8 +1,9 @@ using elFinder.Net.Core; +using System; namespace elFinder.Net.Drivers.FileSystem.Services { - public interface IThumbnailBackgroundGenerator + public interface IThumbnailBackgroundGenerator : IDisposable { void TryAddToQueue(IFile file, IFile tmbFile, int size, bool keepAspectRatio, MediaType? mediaType); } diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipDownloadPathProvider.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipDownloadPathProvider.cs new file mode 100644 index 0000000..d54f106 --- /dev/null +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipDownloadPathProvider.cs @@ -0,0 +1,49 @@ +using elFinder.Net.Core.Exceptions; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace elFinder.Net.Drivers.FileSystem.Services +{ + public interface IZipDownloadPathProvider + { + Task<(string ArchiveFilePath, string ArchiveFileKey)> GetFileForArchivingAsync(string filePath, CancellationToken cancellationToken = default); + Task ParseArchiveFileKeyAsync(string filePath, string archiveFileKey, CancellationToken cancellationToken = default); + } + + public class TempZipDownloadPathProvider : IZipDownloadPathProvider + { + private static readonly string Prefix = nameof(elFinder); + + public TempZipDownloadPathProvider() + { + } + + public Task<(string ArchiveFilePath, string ArchiveFileKey)> GetFileForArchivingAsync(string filePath, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var tempFileName = $"{Prefix}_{Guid.NewGuid()}_{DateTimeOffset.UtcNow.Ticks}"; + var tempFile = Path.Combine(filePath, tempFileName); + return Task.FromResult((tempFile, tempFileName)); + } + + public Task ParseArchiveFileKeyAsync(string filePath, string archiveFileKey, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (Path.IsPathRooted(archiveFileKey)) throw new PermissionDeniedException("Malformed key"); + + var fullPath = Path.GetFullPath(Path.Combine(filePath, archiveFileKey)); + if (!fullPath.StartsWith(filePath.EndsWith($"{Path.DirectorySeparatorChar}") + ? filePath : filePath + Path.DirectorySeparatorChar)) + throw new PermissionDeniedException("Malformed key"); + + var fileName = Path.GetFileName(fullPath); + if (!fileName.StartsWith(Prefix)) + throw new PermissionDeniedException("Malformed key"); + + return Task.FromResult(fullPath); + } + } +} diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipFileArchiver.cs b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipFileArchiver.cs index 7f48bec..b9f024e 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipFileArchiver.cs +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/Services/IZipFileArchiver.cs @@ -67,7 +67,7 @@ public async Task ExtractToAsync(ZipArchiveEntry entry, IFile dest, bool overwri { cancellationToken.ThrowIfCancellationRequested(); - if (!await dest.CanExtractToAsync()) throw new PermissionDeniedException(); + if (!await dest.CanExtractToAsync(cancellationToken: cancellationToken)) throw new PermissionDeniedException(); if (dest.DirectoryExists()) throw new ExistsException(dest.Name); diff --git a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/elFinder.Net.Drivers.FileSystem.csproj b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/elFinder.Net.Drivers.FileSystem.csproj index 109f8ab..bd928a6 100644 --- a/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/elFinder.Net.Drivers.FileSystem.csproj +++ b/elFinder.Net.Core/elFinder.Net.Drivers.FileSystem/elFinder.Net.Drivers.FileSystem.csproj @@ -13,7 +13,7 @@ true elFinder.Net.Core Local File System driver. logo.png - 1.2.6 + 1.3.3