// // CloudFileStorage.swift // Notes // // Created by Laurent Morvillier on 16/09/2022. // import Foundation import UIKit struct StorageRequest { var filename: String var content: String } let idleTimeBeforeSaving = 2.0 fileprivate extension NSMetadataItem { var url: URL? { return self.value(forAttribute: NSMetadataItemURLKey) as? URL } } /// Should we have a way to go from local to iCloud? /// https://stackoverflow.com/questions/33886846/best-way-to-use-icloud-documents-storage /// Should we store the files locally whatever the iCloud choice is? class FileStorage : FileOperator { static var main: FileStorage = FileStorage() fileprivate var _cloudContainerURL: URL? fileprivate var _cloudStorageDetermined: Bool = false fileprivate let containerTeamId = "notes" fileprivate let containerIdentifier = "notes" fileprivate var _timer: Timer? = nil fileprivate var _downloadTimer: Timer? = nil fileprivate var _downloads: [NSMetadataItem] = [] fileprivate var _storageRequests: [String : String] = [:] init() { let uit = FileManager.default.ubiquityIdentityToken print("ubiquityIdentityToken = \(String(describing: uit))") DispatchQueue.global(qos: .userInteractive).async { self._cloudContainerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil) self._cloudStorageDetermined = true print("Cloud container URL is : \(String(describing: self._cloudContainerURL?.absoluteString))") // self._makeLocalCopyIfNecessary() } } fileprivate func _directoryURL() throws -> URL { let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) return documentDirectory } fileprivate func _makeLocalCopyIfNecessary() { // if the local dir is not empty, do nothing if let docDir = try? self._directoryURL(), let files = try? FileManager.default.contentsOfDirectory(atPath: docDir.absoluteString), files.count > 0 { return } guard let _ = self._cloudContainerURL else { return } NotificationCenter.default.addObserver(self, selector: #selector(metaDataQueryNotified(notification:)), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: nil) let query = NSMetadataQuery() query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] query.start() } @objc func metaDataQueryNotified(notification: Notification) { guard let query = notification.object as? NSMetadataQuery else { print("object = \(notification.object ?? "")") return } for case let item as NSMetadataItem in query.results { if let url = item.value(forAttribute: NSMetadataItemURLKey) as? URL { self._downloadItem(item, url: url) } else { print("item has no URL") } } } fileprivate func _downloadItem(_ item: NSMetadataItem, url: URL) { do { try FileManager.default.startDownloadingUbiquitousItem(at: url) self._startStatusTimer(item: item) } catch { print("error = \(error)") } } fileprivate func _startStatusTimer(item: NSMetadataItem) { self._downloads.append(item) if self._downloadTimer == nil { self._downloadTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self._verifyDownloadStatus), userInfo: nil, repeats: false) } } @objc fileprivate func _verifyDownloadStatus() { let downloads = Array(self._downloads) for item in downloads { if item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String == NSMetadataUbiquitousItemDownloadingStatusCurrent { self._makeLocalCopy(item: item) self._downloads.removeAll(where: { $0.url == item.url }) } } } fileprivate func _makeLocalCopy(item: NSMetadataItem) { guard let fileURL = item.url, let docURL = try? self._directoryURL() else { return } do { try FileManager.default.copyItem(at: fileURL, to: docURL) } catch { print("error: \(error)") // TODO } } // MARK: - FileOperator func requestStorage(filename: String, content: String) { self._storageRequests[filename] = content self._timer?.invalidate() self._timer = Timer.scheduledTimer(timeInterval: idleTimeBeforeSaving, target: self, selector: #selector(self._storageRequested), userInfo: nil, repeats: false) } @objc fileprivate func _storageRequested() { for (filename, content) in self._storageRequests { do { try self._store(filename: filename, content: content) } catch { // TODO show errors to users, possibly by notifications print("error: \(error.localizedDescription)") } } self._storageRequests.removeAll() } fileprivate func _store(filename: String, content: String) throws { let fileURL = try self._directoryURL().appending(path: filename) print("Store file to: \(fileURL.absoluteString)") try content.write(to: fileURL, atomically: true, encoding: .utf8) try self._copyToCloudContainerIfNecessary(fileURL: fileURL, filename: filename) } func getContent(filename: String) throws -> String? { let fileURL = try self._directoryURL().appending(path: filename) return try String(contentsOf: fileURL) } func lastEditDate(filename: String) throws -> Date? { let fileURL = try self._directoryURL().appending(path: filename) let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.absoluteString) return attributes[FileAttributeKey.modificationDate] as? Date } fileprivate func _copyToCloudContainerIfNecessary(fileURL: URL, filename: String) throws { guard let containerURL = self._cloudContainerURL else { print("cloud container is nil, stays local") return } let cloudURL = containerURL.appending(path: filename) print("cloud copy to: \(cloudURL)...") // if FileManager.default.fileExists(atPath: cloudURL.absoluteString) { // try FileManager.default.removeItem(at: cloudURL) // } try FileManager.default.copyItem(at: fileURL, to: cloudURL) } deinit { self._timer?.invalidate() } }