From cb7aa884fef28a574b566a6ab318c273ae111d2c Mon Sep 17 00:00:00 2001 From: Ambroise Decouttere Date: Wed, 23 Oct 2024 13:04:37 +0200 Subject: [PATCH] fix: FileManager as source of truth --- SwissTransferCore/DisplayableFile.swift | 64 ++---- SwissTransferCore/UploadFile.swift | 10 +- .../NewTransferView/FileListView.swift | 24 ++- .../NewTransferFilesCellView.swift | 20 +- .../NewTransferView/NewTransferManager.swift | 184 +++++------------- 5 files changed, 95 insertions(+), 207 deletions(-) diff --git a/SwissTransferCore/DisplayableFile.swift b/SwissTransferCore/DisplayableFile.swift index dc42af73..c59a1061 100644 --- a/SwissTransferCore/DisplayableFile.swift +++ b/SwissTransferCore/DisplayableFile.swift @@ -19,64 +19,36 @@ import Foundation public class DisplayableFile: Identifiable, Hashable { - public let id: String + public var id: String { + url.path() + } + public let name: String public let isFolder: Bool - public var children = [DisplayableFile]() - public var parent: DisplayableFile? - - // Real Files property - public var url: URL? - private var size: Int64 = 0 + public var url: URL + public var size: Int64 = 0 public var mimeType = "" - /// Fake folder init - public init(folderName: String) { - id = UUID().uuidString - name = folderName - isFolder = true - } - - public init(uploadFile: UploadFile) { - id = uploadFile.id - name = uploadFile.url.lastPathComponent - url = uploadFile.url - size = uploadFile.size - mimeType = uploadFile.mimeType - isFolder = false - } - - public var computedSize: Int64 { - if isFolder { - return children.map { $0.computedSize }.reduce(0, +) - } - return size - } - - /// Return all file children in the tree (no folder) - public func computedChildren() async -> [DisplayableFile] { - var array = [DisplayableFile]() - if isFolder { - for element in children { - await array.append(contentsOf: element.computedChildren()) - } - } else { - array.append(self) - } - return array + public init?(url: URL) { + guard let resources = try? url.resourceValues(forKeys: [ + .isDirectoryKey, + .nameKey + ]) else { return nil } + + self.url = url + name = url.lastPathComponent + isFolder = resources.isDirectory ?? false + size = Int64(url.size()) + mimeType = url.typeIdentifier ?? "" } public func hash(into hasher: inout Hasher) { hasher.combine(id) - - _ = children.map { - hasher.combine($0.hashValue) - } } public static func == (lhs: DisplayableFile, rhs: DisplayableFile) -> Bool { - lhs.id == rhs.id && lhs.children == rhs.children + lhs.id == rhs.id } } diff --git a/SwissTransferCore/UploadFile.swift b/SwissTransferCore/UploadFile.swift index c6bdfc29..a7ab2ce3 100644 --- a/SwissTransferCore/UploadFile.swift +++ b/SwissTransferCore/UploadFile.swift @@ -17,6 +17,7 @@ */ import Foundation +import OSLog public class UploadFile: Identifiable { public var id: String { @@ -36,8 +37,15 @@ public class UploadFile: Identifiable { ]), resources.isDirectory == false else { return nil } self.url = url - path = resources.name ?? url.lastPathComponent size = Int64(resources.fileSize ?? 0) mimeType = url.typeIdentifier ?? "" + + do { + let baseURL = try URL.tmpUploadDirectory() + path = String(url.path().trimmingPrefix(baseURL.path())) + } catch { + Logger.general.error("Error while constructing file path: \(url) \(error.localizedDescription)") + return nil + } } } diff --git a/SwissTransferFeatures/NewTransferView/FileListView.swift b/SwissTransferFeatures/NewTransferView/FileListView.swift index 2d3ca2d3..fdae1f94 100644 --- a/SwissTransferFeatures/NewTransferView/FileListView.swift +++ b/SwissTransferFeatures/NewTransferView/FileListView.swift @@ -26,12 +26,7 @@ struct FileListView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var newTransferManager: NewTransferManager - private var files: [DisplayableFile] { - if let folder { - return folder.children - } - return newTransferManager.displayableFiles - } + @State private var files = [DisplayableFile]() private let folder: DisplayableFile? @@ -40,7 +35,7 @@ struct FileListView: View { } private var filesSize: Int64 { - files.map { $0.computedSize }.reduce(0, +) + files.map { $0.size }.reduce(0, +) } private let columns = [ @@ -64,21 +59,21 @@ struct FileListView: View { ForEach(files) { file in if file.isFolder { NavigationLink(value: file) { - LargeFileCell(folderName: file.name, folderSize: file.computedSize) { - Task { - await newTransferManager.remove(file: file) + LargeFileCell(folderName: file.name, folderSize: file.size) { + newTransferManager.remove(file: file) { + files = newTransferManager.filesAt(folderURL: folder?.url) } } } } else { LargeFileCell( fileName: file.name, - fileSize: file.computedSize, + fileSize: file.size, url: file.url, mimeType: file.mimeType ) { - Task { - await newTransferManager.remove(file: file) + newTransferManager.remove(file: file) { + files = newTransferManager.filesAt(folderURL: folder?.url) } } } @@ -88,6 +83,9 @@ struct FileListView: View { } .padding(value: .medium) } + .onAppear { + files = newTransferManager.filesAt(folderURL: folder?.url) + } .onChange(of: files) { _ in if files.isEmpty { dismiss() diff --git a/SwissTransferFeatures/NewTransferView/NewTransferFilesCellView.swift b/SwissTransferFeatures/NewTransferView/NewTransferFilesCellView.swift index 7faaa9ce..bc41b612 100644 --- a/SwissTransferFeatures/NewTransferView/NewTransferFilesCellView.swift +++ b/SwissTransferFeatures/NewTransferView/NewTransferFilesCellView.swift @@ -26,9 +26,10 @@ struct NewTransferFilesCellView: View { @EnvironmentObject private var newTransferManager: NewTransferManager @State private var isShowingFileList = false + @State private var files = [DisplayableFile]() private var filesSize: Int64 { - newTransferManager.displayableFiles.map { $0.computedSize }.reduce(0, +) + files.map { $0.size }.reduce(0, +) } var body: some View { @@ -41,7 +42,7 @@ struct NewTransferFilesCellView: View { NavigationLink(value: DisplayableRootFolder()) { HStack { Text( - "\(STResourcesStrings.Localizable.filesCount(newTransferManager.displayableFiles.count)) · \(filesSize.formatted(.defaultByteCount))" + "\(STResourcesStrings.Localizable.filesCount(files.count)) · \(filesSize.formatted(.defaultByteCount))" ) .font(.ST.callout) .frame(maxWidth: .infinity, alignment: .leading) @@ -56,7 +57,7 @@ struct NewTransferFilesCellView: View { ScrollView(.horizontal) { HStack(spacing: IKPadding.medium) { AddFilesMenuView { urls in - newTransferManager.addFiles(urls: urls) + files = newTransferManager.addFiles(urls: urls) } label: { STResourcesAsset.Images.plus.swiftUIImage .iconSize(.large) @@ -65,19 +66,19 @@ struct NewTransferFilesCellView: View { .background(Color.ST.background, in: .rect(cornerRadius: IKRadius.large)) } - ForEach(newTransferManager.displayableFiles) { file in + ForEach(files) { file in if file.isFolder { NavigationLink(value: file) { SmallThumbnailView { - Task { - await newTransferManager.remove(file: file) + newTransferManager.remove(file: file) { + files = newTransferManager.filesAt(folderURL: nil) } } } } else { SmallThumbnailView(url: file.url, mimeType: file.mimeType) { - Task { - await newTransferManager.remove(file: file) + newTransferManager.remove(file: file) { + files = newTransferManager.filesAt(folderURL: nil) } } } @@ -93,6 +94,9 @@ struct NewTransferFilesCellView: View { .background(Color.ST.cardBackground, in: .rect(cornerRadius: IKRadius.large)) } .padding(.horizontal, value: .medium) + .onAppear { + files = newTransferManager.filesAt(folderURL: nil) + } } } diff --git a/SwissTransferFeatures/NewTransferView/NewTransferManager.swift b/SwissTransferFeatures/NewTransferView/NewTransferManager.swift index acb4831f..3fd66516 100644 --- a/SwissTransferFeatures/NewTransferView/NewTransferManager.swift +++ b/SwissTransferFeatures/NewTransferView/NewTransferManager.swift @@ -43,13 +43,11 @@ private enum TmpDirType: String { @MainActor class NewTransferManager: ObservableObject { - private var uploadFiles = [UploadFile]() - @Published var displayableFiles = [DisplayableFile]() @Published var transferType: TransferType = .qrcode init(urls: [URL]) { cleanTmpDir(type: .upload) - addFiles(urls: urls) + _ = addFiles(urls: urls) cleanTmpDir(type: .cache) } @@ -57,44 +55,31 @@ class NewTransferManager: ObservableObject { cleanTmpDir(type: .all) } - func addFiles(urls: [URL]) { - do { - let tmpUrls = moveToTmp(files: urls) - - try uploadFiles.insert(contentsOf: flatten(urls: tmpUrls), at: 0) - - displayableFiles = prepareForDisplay() - } catch { - Logger.general.error("An error occured while flattening files: \(error)") - } + /// Add files to Upload Folder + /// Return the content of the folder + func addFiles(urls: [URL]) -> [DisplayableFile] { + moveToTmp(files: urls) + return filesAt(folderURL: nil) } /// Removes completely the given file and his children from : /// - FileManager /// - Upload list /// - Displayable list - func remove(file: DisplayableFile) async { - let filesToRemove = await file.computedChildren() - - for fileToRemove in filesToRemove { - uploadFiles.removeAll { $0.id == fileToRemove.id } - guard let url = fileToRemove.url else { continue } - do { - try FileManager.default.removeItem(at: url) - } catch { - Logger.general.error("An error occured while removing file: \(error)") - } + func remove(file: DisplayableFile, completion: () -> Void) { + do { + try FileManager.default.removeItem(at: file.url) + cleanEmptyParent(of: file.url) + completion() + } catch { + Logger.general.error("An error occured while removing file: \(error)") } - - await removeFileAndCleanFolders(file: file) } } extension NewTransferManager { /// Move the imported files/folder in the temporary directory - private func moveToTmp(files: [URL]) -> [URL] { - var urls = [URL]() - + private func moveToTmp(files: [URL]) { do { let tmpDirectory = try URL.tmpUploadDirectory() for file in files { @@ -102,13 +87,10 @@ extension NewTransferManager { _ = file.startAccessingSecurityScopedResource() try FileManager.default.copyItem(at: file, to: destination) file.stopAccessingSecurityScopedResource() - urls.append(destination) } } catch { Logger.general.error("An error occured while moving files to temporary directory: \(error)") } - - return urls } /// Empty the temporary directory @@ -129,134 +111,58 @@ extension NewTransferManager { } } - /// Remove the given file from his parent - /// Start from the file and remove all folders above him who doesn't contain any real file - private func removeFileAndCleanFolders(file: DisplayableFile) async { - file.parent?.children.removeAll { $0.id == file.id } - - guard let parent = file.parent else { - displayableFiles.removeAll { $0.id == file.id } - return - } - - var currentFile: DisplayableFile = parent - while await currentFile.computedChildren().isEmpty { - guard let parent = currentFile.parent else { - // Base of the tree - displayableFiles.removeAll { $0.id == currentFile.id } - break - } - parent.children.removeAll { $0.id == currentFile.id } - currentFile = parent + private func cleanEmptyParent(of url: URL) { + let parent = url.deletingLastPathComponent() + do { + let children = try FileManager.default.contentsOfDirectory(atPath: parent.path()) + guard children.isEmpty else { return } + try FileManager.default.removeItem(at: parent) + cleanEmptyParent(of: parent) + } catch { + Logger.general.error("An error occurred while cleaning parent folder of: \(url.path()) \(error)") } - - objectWillChange.send() } } // MARK: - Tools extension NewTransferManager { - /// Flatten folder + set path - /// Take the list of the imported URLs - /// Flatten the folders of these URLs to get only the File inside them and create a short path for each file + /// Flatten the upload folder /// Then return all the found Files to upload - /// - Parameters: - /// - urls: List of imported URLs /// - Returns: An array of file to Upload (no folder, only file) - private func flatten(urls: [URL]) throws -> [UploadFile] { + public func filesToUpload() throws -> [UploadFile] { let resourceKeys: [URLResourceKey] = [.fileSizeKey, .isDirectoryKey, .nameKey] var result = [UploadFile]() - for url in urls { - guard let resources = try? url.resourceValues(forKeys: Set(resourceKeys)), - let isDirectory = resources.isDirectory else { continue } - - if isDirectory { - let folderEnumerator = FileManager.default.enumerator( - at: url, - includingPropertiesForKeys: resourceKeys, - options: .skipsHiddenFiles - ) - while case let element as URL = folderEnumerator?.nextObject() { - guard var uploadFile = UploadFile(url: element) else { continue } - - let urlToTrim = url.deletingLastPathComponent() - let newPath = uploadFile.url.path().trimmingPrefix(urlToTrim.path()) - uploadFile.path = String(newPath) - - uploadFiles.append(uploadFile) - } - } else { - guard let uploadFile = UploadFile(url: url) else { continue } - result.append(uploadFile) - } + let folderEnumerator = try FileManager.default.enumerator( + at: URL.tmpUploadDirectory(), + includingPropertiesForKeys: resourceKeys, + options: .skipsHiddenFiles + ) + while case let element as URL = folderEnumerator?.nextObject() { + guard let uploadFile = UploadFile(url: element) else { continue } + result.append(uploadFile) } return result } - /// Take the files in uploadFiles and create a tree using fake folders - /// - Returns: The created tree - private func prepareForDisplay() -> [DisplayableFile] { - var tree = [DisplayableFile]() - for file in uploadFiles { - let displayableFile = DisplayableFile(uploadFile: file) - let pathComponents = file.path.components(separatedBy: "/") - - if let parent = findFolder(forPath: pathComponents, in: &tree) { - displayableFile.parent = parent - parent.children.append(displayableFile) - } else { - tree.append(displayableFile) - } - } - - return tree - } - - /// Give the folder in which we need to put the file with the given path - /// - Parameters: - /// - pathComponents: The path of the file (ex: ["parent", "child", "doc.txt"]) - /// - tree: The tree in which we want - /// to put the file (displayableFiles) - /// - Returns: Return the folder - private func findFolder(forPath pathComponents: [String], in tree: inout [DisplayableFile]) -> DisplayableFile? { - var path = pathComponents.dropLast() // Remove the fileName from the path - var result: DisplayableFile? - - // Used to simulate the base of the tree - let fakeFirstParent = DisplayableFile(folderName: "") - fakeFirstParent.children = tree - var currentParent = fakeFirstParent - - while !path.isEmpty { - let currentName = path.removeFirst() - - // Look for the current component in the children of the current parent - if let branch = currentParent.children.first(where: { - $0.name == currentName && $0.isFolder - }) { - result = branch - } else { - // If not found, create a new folder with the current component name - let newFolder = DisplayableFile(folderName: currentName) - newFolder.parent = currentParent - currentParent.children.append(newFolder) + public func filesAt(folderURL: URL?) -> [DisplayableFile] { + let resourceKeys: [URLResourceKey] = [.fileSizeKey, .isDirectoryKey, .nameKey] - result = newFolder + do { + var src = try URL.tmpUploadDirectory() + if let folderURL { + src = folderURL } + let urls = try FileManager.default.contentsOfDirectory(at: src, includingPropertiesForKeys: resourceKeys) - // Update the current parent to the folder we found/created - currentParent = result! - } + let files = urls.compactMap { DisplayableFile(url: $0) } - // Reassign the fake parent children to the tree and remove the fake parent link from the elements of the tree base - tree = fakeFirstParent.children - for baseChild in tree { - baseChild.parent = nil + return files + } catch { + Logger.general.error("An error occurred while getting files from: \(folderURL?.path() ?? "") \(error)") } - - return result + return [] } }