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.
211 lines
6.9 KiB
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()
|
|
}
|
|
|
|
}
|
|
|