private async Task InternalDeleteFolder( DbContextScope scope, MediaFolder folder, TreeNode <MediaFolderNode> node, TreeNode <MediaFolderNode> root, FolderDeleteResult result, FileHandling strategy, CancellationToken cancelToken = default) { // (perf) We gonna check file tracks, so we should preload all tracks. await _db.LoadCollectionAsync(folder, (MediaFolder x) => x.Files, false, q => q.Include(f => f.Tracks)); var files = folder.Files.ToList(); var lockedFiles = new List <MediaFile>(files.Count); var trackedFiles = new List <MediaFile>(files.Count); // First delete files if (folder.Files.Any()) { var albumId = strategy == FileHandling.MoveToRoot ? _folderService.FindAlbum(folder.Id).Value.Id : (int?)null; foreach (var batch in files.Slice(500)) { if (cancelToken.IsCancellationRequested) { break; } foreach (var file in batch) { if (cancelToken.IsCancellationRequested) { break; } if (strategy == FileHandling.Delete && file.Tracks.Any()) { // Don't delete tracked files trackedFiles.Add(file); continue; } if (strategy == FileHandling.Delete) { try { result.DeletedFileNames.Add(file.Name); await DeleteFileAsync(file, true); } catch (DeleteTrackedFileException) { trackedFiles.Add(file); } catch (IOException) { lockedFiles.Add(file); } } else if (strategy == FileHandling.SoftDelete) { await DeleteFileAsync(file, false); file.FolderId = null; result.DeletedFileNames.Add(file.Name); } else if (strategy == FileHandling.MoveToRoot) { file.FolderId = albumId; result.DeletedFileNames.Add(file.Name); } } await scope.CommitAsync(cancelToken); } if (lockedFiles.Any()) { // Retry deletion of failed files due to locking. // INFO: By default "LocalFileSystem" waits for 500ms until the lock is revoked or it throws. foreach (var lockedFile in lockedFiles.ToArray()) { if (cancelToken.IsCancellationRequested) { break; } try { await DeleteFileAsync(lockedFile, true); lockedFiles.Remove(lockedFile); } catch { } } await scope.CommitAsync(cancelToken); } } if (!cancelToken.IsCancellationRequested && lockedFiles.Count > 0) { var fullPath = CombinePaths(root.Value.Path, lockedFiles[0].Name); throw new IOException(T("Admin.Media.Exception.InUse", fullPath)); } if (!cancelToken.IsCancellationRequested && lockedFiles.Count == 0 && trackedFiles.Count == 0 && node.Children.All(x => result.DeletedFolderIds.Contains(x.Value.Id))) { // Don't delete folder if a containing file could not be deleted, // any tracked file was found or any of its child folders could not be deleted.. _db.MediaFolders.Remove(folder); await scope.CommitAsync(cancelToken); result.DeletedFolderIds.Add(folder.Id); } result.LockedFileNames = lockedFiles.Select(x => x.Name).ToList(); result.TrackedFileNames = trackedFiles.Select(x => x.Name).ToList(); }
public async Task <MediaFolderInfo> CreateFolderAsync(string path) { Guard.NotEmpty(path, nameof(path)); path = FolderService.NormalizePath(path, false); ValidateFolderPath(path, "CreateFolder", nameof(path)); var dupe = _folderService.GetNodeByPath(path); if (dupe != null) { throw _exceptionFactory.DuplicateFolder(path, dupe.Value); } var sep = "/"; var folderNames = path.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); bool flag = false; int folderId = 0; path = string.Empty; for (int i = 0; i < folderNames.Length; i++) { var folderName = MediaHelper.NormalizeFolderName(folderNames[i]); path += (i > 0 ? sep : string.Empty) + folderName; if (!flag) { // Find the last existing node in path trail var currentNode = _folderService.GetNodeByPath(path)?.Value; if (currentNode != null) { folderId = currentNode.Id; } else { if (i == 0) { throw new NotSupportedException(T("Admin.Media.Exception.TopLevelAlbum", path)); } flag = true; } } if (flag) { using (new DbContextScope(_db, deferCommit: false)) { var mediaFolder = new MediaFolder { Name = folderName, ParentId = folderId }; _db.MediaFolders.Add(mediaFolder); await _db.SaveChangesAsync(); folderId = mediaFolder.Id; } } } return(new MediaFolderInfo(_folderService.GetNodeById(folderId))); }