PDF-Archiver/PDF-Archiver

View on GitHub
ArchiveCore/Sources/ArchiveBackend/PathManager/PathManager.swift

Summary

Maintainability
A
2 hrs
Test Coverage
//
//  PathManager.swift
//  
//
//  Created by Julian Kahnert on 16.11.20.
//

import ArchiveSharedConstants
import Foundation

extension UserDefaults {
    var archivePathType: PathManager.ArchivePathType? {
        get {

            do {
                var staleBookmarkData = false
                if let type: PathManager.ArchivePathType = try? getObject(forKey: .archivePathType) {
                    return type
                } else if let bookmarkData = object(forKey: Names.archivePathType.rawValue) as? Data {
                    #if os(macOS)
                    let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &staleBookmarkData)
                    if staleBookmarkData {
                        set(nil, forKey: Names.archivePathType.rawValue)
                        log.errorAndAssert("Found stale bookmark data.")
                        return nil
                    }
                    return .local(url)
                    #else
                    let url = try URL(resolvingBookmarkData: bookmarkData, bookmarkDataIsStale: &staleBookmarkData)
                    guard !staleBookmarkData else {
                        // Handle stale data here.
                        log.errorAndAssert("Error while getting archive url. Stale bookmark data.")
                        return nil
                    }
                    return .local(url)
                    #endif
                } else {
                    return nil
                }
            } catch {
                set(nil, forKey: Names.archivePathType.rawValue)
                log.errorAndAssert("Error while getting archive url.", metadata: ["error": "\(String(describing: error))"])
                NotificationCenter.default.postAlert(error)
                return nil
            }
        }
        set {
            do {
                switch newValue {
                    case .local(let url):
                        #if os(macOS)
                        let bookmark = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
                        set(bookmark, forKey: Names.archivePathType.rawValue)
                        #else
                        // Securely access the URL to save a bookmark
                        guard url.startAccessingSecurityScopedResource() else {
                            // Handle the failure here.
                            return
                        }
                        // We have to stop accessing the resource no matter what
                        defer { url.stopAccessingSecurityScopedResource() }
                        do {
                            // Make sure the bookmark is minimal!
                            let bookmark = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil)
                            set(bookmark, forKey: Names.archivePathType.rawValue)
                        } catch {
                            print("Bookmark error \(error)")
                        }
                        #endif
                    default:
                        try setObject(newValue, forKey: .archivePathType)
                }
            } catch {
                set(nil, forKey: Names.archivePathType.rawValue)
                log.errorAndAssert("Failed to set ArchivePathType.", metadata: ["error": "\(error)"])
                NotificationCenter.default.postAlert(error)
            }
        }
    }
}

public final class PathManager: Log {

    public static let shared = PathManager()
    private static let userDefaults: UserDefaults = .appGroup

    public private(set) var archivePathType: ArchivePathType
    private let fileManager = FileManager.default

    private init() {
        let iCloudDriveAvailable = FileManager.default.iCloudDriveURL != nil
        if iCloudDriveAvailable {
            archivePathType = PathManager.userDefaults.archivePathType ?? .iCloudDrive
        } else {
            #if os(macOS)
            archivePathType = PathManager.userDefaults.archivePathType ?? .local(FileManager.default.documentsDirectoryURL.appendingPathComponent("PDFArchiver"))
            #else
            archivePathType = PathManager.userDefaults.archivePathType ?? .appContainer
            #endif
        }
    }

    public func getArchiveUrl() throws -> URL {
        let archiveURL: URL
        if UserDefaults.isInDemoMode {
            archiveURL = fileManager.temporaryDirectory
        } else {
            archiveURL = try archivePathType.getArchiveUrl()
        }
        try FileManager.default.createFolderIfNotExists(archiveURL)
        return archiveURL
    }

    public func getUntaggedUrl() throws -> URL {
        let untaggedURL = try getArchiveUrl().appendingPathComponent("untagged")
        try FileManager.default.createFolderIfNotExists(untaggedURL)
        return untaggedURL
    }

    public func setArchiveUrl(with type: ArchivePathType) throws {
        if type == .iCloudDrive {
            guard fileManager.iCloudDriveURL != nil else { throw PathError.iCloudDriveNotFound }
        }

        log.debug("Setting new archive type.", metadata: ["type": "\(type)"])

        let newArchiveUrl = try type.getArchiveUrl()
        let oldArchiveUrl = try archivePathType.getArchiveUrl()

        guard newArchiveUrl != oldArchiveUrl else {
            log.errorAndAssert("Old and new archive url should be different", metadata: ["newArchiveUrl": "\(newArchiveUrl)"])
            return
        }

        let contents = try fileManager.contentsOfDirectory(at: oldArchiveUrl, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
            .filter(\.hasDirectoryPath)
            .filter { folderUrl in
                folderUrl.lastPathComponent.isNumeric || folderUrl.lastPathComponent == "untagged"
            }

        var moveError: Error?
        for folder in contents {
            let destination = newArchiveUrl.appendingPathComponent(folder.lastPathComponent)

            do {
                if fileManager.directoryExists(at: destination) {
                    try fileManager.moveContents(of: folder, to: destination)
                } else {
                    try fileManager.moveItem(at: folder, to: destination)
                }
            } catch {
                // we do not want to abort the move process - save error and show it later
                moveError = error
            }
        }

        self.archivePathType = type
        Self.userDefaults.archivePathType = type

        if let moveError = moveError {
            throw moveError
        }
    }
}