application iOS de notes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notes/Notes/Storage/FileStorage.swift

211 lines
6.9 KiB

//
// 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()
}
}