Run swift-format on swift code

Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
This commit is contained in:
Claudio Cambra 2024-01-22 14:40:47 +08:00 committed by Matthieu Gallien
parent f1ed494b31
commit 3928573ff4
18 changed files with 1268 additions and 666 deletions

View File

@ -20,36 +20,51 @@ extension NextcloudFilesDatabaseManager {
// We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com" // We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com"
let problematicSeparator = "://" let problematicSeparator = "://"
let placeholderSeparator = "__TEMP_REPLACE__" let placeholderSeparator = "__TEMP_REPLACE__"
let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(of: problematicSeparator, with: placeholderSeparator) let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(
of: problematicSeparator, with: placeholderSeparator)
var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/") var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
let directoryItemFileName = String(splitServerUrl.removeLast()) let directoryItemFileName = String(splitServerUrl.removeLast())
let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(of: placeholderSeparator, with: problematicSeparator) let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(
of: placeholderSeparator, with: problematicSeparator)
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true", account, directoryItemServerUrl, directoryItemFileName).first { if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true", account,
directoryItemServerUrl, directoryItemFileName
).first {
return NextcloudItemMetadataTable(value: metadata) return NextcloudItemMetadataTable(value: metadata)
} }
return nil return nil
} }
func childItemsForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] { func childItemsForDirectory(_ directoryMetadata: NextcloudItemMetadataTable)
-> [NextcloudItemMetadataTable]
{
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@", directoryServerUrl) let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"serverUrl BEGINSWITH %@", directoryServerUrl)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] { func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable)
-> [NextcloudItemMetadataTable]
{
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@ AND directory == true", directoryServerUrl) let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"serverUrl BEGINSWITH %@ AND directory == true", directoryServerUrl)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudItemMetadataTable? { func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable)
return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl) -> NextcloudItemMetadataTable?
{
directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
} }
func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? { func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first { if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first {
return NextcloudItemMetadataTable(value: metadata) return NextcloudItemMetadataTable(value: metadata)
} }
@ -57,20 +72,31 @@ extension NextcloudFilesDatabaseManager {
} }
func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] { func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND directory == true", account) let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND directory == true", account)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
func directoryMetadatas(account: String, parentDirectoryServerUrl: String) -> [NextcloudItemMetadataTable] { func directoryMetadatas(account: String, parentDirectoryServerUrl: String)
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account, parentDirectoryServerUrl) -> [NextcloudItemMetadataTable]
{
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account,
parentDirectoryServerUrl)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
// Deletes all metadatas related to the info of the directory provided // Deletes all metadatas related to the info of the directory provided
func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? { func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? {
let database = ncDatabase() let database = ncDatabase()
guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else { guard
Logger.ncFilesDatabase.error("Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion") let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first
else {
Logger.ncFilesDatabase.error(
"Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion"
)
return nil return nil
} }
@ -79,20 +105,25 @@ extension NextcloudFilesDatabaseManager {
let directoryAccount = directoryMetadata.account let directoryAccount = directoryMetadata.account
let directoryEtag = directoryMetadata.etag let directoryEtag = directoryMetadata.etag
Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)") Logger.ncFilesDatabase.debug(
"Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
guard deleteItemMetadata(ocId: directoryMetadata.ocId) else { guard deleteItemMetadata(ocId: directoryMetadata.ocId) else {
Logger.ncFilesDatabase.debug("Failure to delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)") Logger.ncFilesDatabase.debug(
"Failure to delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
return nil return nil
} }
var deletedMetadatas: [NextcloudItemMetadataTable] = [directoryMetadataCopy] var deletedMetadatas: [NextcloudItemMetadataTable] = [directoryMetadataCopy]
let results = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryAccount, directoryUrlPath) let results = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryAccount, directoryUrlPath)
for result in results { for result in results {
let successfulItemMetadataDelete = deleteItemMetadata(ocId: result.ocId) let successfulItemMetadataDelete = deleteItemMetadata(ocId: result.ocId)
if (successfulItemMetadataDelete) { if successfulItemMetadataDelete {
deletedMetadatas.append(NextcloudItemMetadataTable(value: result)) deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
} }
@ -101,24 +132,35 @@ extension NextcloudFilesDatabaseManager {
} }
} }
Logger.ncFilesDatabase.debug("Completed deletions in directory recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)") Logger.ncFilesDatabase.debug(
"Completed deletions in directory recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
return deletedMetadatas return deletedMetadatas
} }
func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) -> [NextcloudItemMetadataTable]? { func renameDirectoryAndPropagateToChildren(
ocId: String, newServerUrl: String, newFileName: String
) -> [NextcloudItemMetadataTable]? {
let database = ncDatabase() let database = ncDatabase()
guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else { guard
Logger.ncFilesDatabase.error("Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming") let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first
else {
Logger.ncFilesDatabase.error(
"Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming"
)
return nil return nil
} }
let oldItemServerUrl = directoryMetadata.serverUrl let oldItemServerUrl = directoryMetadata.serverUrl
let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName
let newDirectoryServerUrl = newServerUrl + "/" + newFileName let newDirectoryServerUrl = newServerUrl + "/" + newFileName
let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, oldDirectoryServerUrl) let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account,
oldDirectoryServerUrl)
renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName) renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
Logger.ncFilesDatabase.debug("Renamed root renaming directory") Logger.ncFilesDatabase.debug("Renamed root renaming directory")
@ -127,19 +169,25 @@ extension NextcloudFilesDatabaseManager {
try database.write { try database.write {
for childItem in childItemResults { for childItem in childItemResults {
let oldServerUrl = childItem.serverUrl let oldServerUrl = childItem.serverUrl
let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldDirectoryServerUrl, with: newDirectoryServerUrl) let movedServerUrl = oldServerUrl.replacingOccurrences(
of: oldDirectoryServerUrl, with: newDirectoryServerUrl)
childItem.serverUrl = movedServerUrl childItem.serverUrl = movedServerUrl
database.add(childItem, update: .all) database.add(childItem, update: .all)
Logger.ncFilesDatabase.debug("Moved childItem at \(oldServerUrl) to \(movedServerUrl)") Logger.ncFilesDatabase.debug(
"Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
} }
} }
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)"
)
return nil return nil
} }
let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, newDirectoryServerUrl) let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account,
newDirectoryServerUrl)
return sortedItemMetadatas(updatedChildItemResults) return sortedItemMetadatas(updatedChildItemResults)
} }
} }

View File

@ -13,12 +13,14 @@
*/ */
import Foundation import Foundation
import RealmSwift
import OSLog import OSLog
import RealmSwift
extension NextcloudFilesDatabaseManager { extension NextcloudFilesDatabaseManager {
func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? { func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first { if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter(
"ocId == %@", ocId
).first {
return NextcloudLocalFileMetadataTable(value: metadata) return NextcloudLocalFileMetadataTable(value: metadata)
} }
@ -41,10 +43,14 @@ extension NextcloudFilesDatabaseManager {
newLocalFileMetadata.exifLongitude = "-1" newLocalFileMetadata.exifLongitude = "-1"
database.add(newLocalFileMetadata, update: .all) database.add(newLocalFileMetadata, update: .all)
Logger.ncFilesDatabase.debug("Added local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public)") Logger.ncFilesDatabase.debug(
"Added local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public)"
)
} }
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not add local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not add local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
} }
} }
@ -53,34 +59,42 @@ extension NextcloudFilesDatabaseManager {
do { do {
try database.write { try database.write {
let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId) let results = database.objects(NextcloudLocalFileMetadataTable.self).filter(
"ocId == %@", ocId)
database.delete(results) database.delete(results)
} }
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
} }
} }
private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>) -> [NextcloudLocalFileMetadataTable] { private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>)
-> [NextcloudLocalFileMetadataTable]
{
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true) let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) }) return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
} }
func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] { func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account) let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter(
"account == %@", account)
return sortedLocalFileMetadatas(results) return sortedLocalFileMetadatas(results)
} }
func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] { func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let localFileMetadatas = localFileMetadatas(account: account) let localFileMetadatas = localFileMetadatas(account: account)
let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId }) let localFileMetadatasOcIds = Array(localFileMetadatas.map(\.ocId))
var itemMetadatas: [NextcloudItemMetadataTable] = [] var itemMetadatas: [NextcloudItemMetadataTable] = []
for ocId in localFileMetadatasOcIds { for ocId in localFileMetadatasOcIds {
guard let itemMetadata = itemMetadataFromOcId(ocId) else { guard let itemMetadata = itemMetadataFromOcId(ocId) else {
Logger.ncFilesDatabase.error("Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)") Logger.ncFilesDatabase.error(
continue; "Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)"
)
continue
} }
itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata)) itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))

View File

@ -12,16 +12,14 @@
* for more details. * for more details.
*/ */
import Foundation
import RealmSwift
import FileProvider import FileProvider
import Foundation
import NextcloudKit import NextcloudKit
import OSLog import OSLog
import RealmSwift
class NextcloudFilesDatabaseManager : NSObject { class NextcloudFilesDatabaseManager: NSObject {
static let shared = { static let shared = NextcloudFilesDatabaseManager()
return NextcloudFilesDatabaseManager();
}()
let relativeDatabaseFolderPath = "Database/" let relativeDatabaseFolderPath = "Database/"
let databaseFilename = "fileproviderextdatabase.realm" let databaseFilename = "fileproviderextdatabase.realm"
@ -31,29 +29,36 @@ class NextcloudFilesDatabaseManager : NSObject {
let schemaVersion: UInt64 = 100 let schemaVersion: UInt64 = 100
override init() { override init() {
self.relativeDatabaseFilePath = self.relativeDatabaseFolderPath + self.databaseFilename relativeDatabaseFilePath = relativeDatabaseFolderPath + databaseFilename
guard let fileProviderDataDirUrl = pathForFileProviderExtData() else { guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
super.init() super.init()
return return
} }
self.databasePath = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFilePath) databasePath = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFilePath)
// Disable file protection for directory DB // Disable file protection for directory DB
// https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm // https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm
let dbFolder = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFolderPath) let dbFolder = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFolderPath)
let dbFolderPath = dbFolder.path let dbFolderPath = dbFolder.path
do { do {
try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
try FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: dbFolderPath) try FileManager.default.setAttributes(
} catch let error { [
Logger.ncFilesDatabase.error("Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)") FileAttributeKey.protectionKey: FileProtectionType
.completeUntilFirstUserAuthentication
],
ofItemAtPath: dbFolderPath)
} catch {
Logger.ncFilesDatabase.error(
"Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)"
)
} }
let config = Realm.Configuration( let config = Realm.Configuration(
fileURL: self.databasePath, fileURL: databasePath,
schemaVersion: self.schemaVersion, schemaVersion: schemaVersion,
objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self] objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
) )
@ -63,7 +68,8 @@ class NextcloudFilesDatabaseManager : NSObject {
_ = try Realm() _ = try Realm()
Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt") Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
} catch let error as NSError { } catch let error as NSError {
Logger.ncFilesDatabase.error("Error opening Realm db: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Error opening Realm db: \(error.localizedDescription, privacy: .public)")
} }
super.init() super.init()
@ -76,81 +82,106 @@ class NextcloudFilesDatabaseManager : NSObject {
} }
func anyItemMetadatasForAccount(_ account: String) -> Bool { func anyItemMetadatasForAccount(_ account: String) -> Bool {
return !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account).isEmpty !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
.isEmpty
} }
func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? { func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
// Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db // Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db
// Let's therefore create a copy // Let's therefore create a copy
if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first { if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId
).first {
return NextcloudItemMetadataTable(value: itemMetadata) return NextcloudItemMetadataTable(value: itemMetadata)
} }
return nil return nil
} }
func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>) -> [NextcloudItemMetadataTable] { func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>)
-> [NextcloudItemMetadataTable]
{
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true) let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) }) return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
} }
func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] { func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account) let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@", account)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] { func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@", account, serverUrl) let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@", account, serverUrl)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
func itemMetadatas(account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status) -> [NextcloudItemMetadataTable] { func itemMetadatas(
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, status.rawValue) account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status
)
-> [NextcloudItemMetadataTable]
{
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl,
status.rawValue)
return sortedItemMetadatas(metadatas) return sortedItemMetadatas(metadatas)
} }
func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> NextcloudItemMetadataTable? { func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier)
-> NextcloudItemMetadataTable?
{
let ocId = identifier.rawValue let ocId = identifier.rawValue
return itemMetadataFromOcId(ocId) return itemMetadataFromOcId(ocId)
} }
private func processItemMetadatasToDelete(existingMetadatas: Results<NextcloudItemMetadataTable>, private func processItemMetadatasToDelete(
updatedMetadatas: [NextcloudItemMetadataTable]) -> [NextcloudItemMetadataTable] { existingMetadatas: Results<NextcloudItemMetadataTable>,
updatedMetadatas: [NextcloudItemMetadataTable]
) -> [NextcloudItemMetadataTable] {
var deletedMetadatas: [NextcloudItemMetadataTable] = [] var deletedMetadatas: [NextcloudItemMetadataTable] = []
for existingMetadata in existingMetadatas { for existingMetadata in existingMetadatas {
guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }), guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId) else { continue } let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId)
else { continue }
deletedMetadatas.append(metadataToDelete) deletedMetadatas.append(metadataToDelete)
Logger.ncFilesDatabase.debug("Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: .public)") Logger.ncFilesDatabase.debug(
"Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: .public)"
)
} }
return deletedMetadatas return deletedMetadatas
} }
private func processItemMetadatasToUpdate(existingMetadatas: Results<NextcloudItemMetadataTable>, private func processItemMetadatasToUpdate(
updatedMetadatas: [NextcloudItemMetadataTable], existingMetadatas: Results<NextcloudItemMetadataTable>,
updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], directoriesNeedingRename: [NextcloudItemMetadataTable]) { updatedMetadatas: [NextcloudItemMetadataTable],
updateDirectoryEtags: Bool
) -> (
newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable],
directoriesNeedingRename: [NextcloudItemMetadataTable]
) {
var returningNewMetadatas: [NextcloudItemMetadataTable] = [] var returningNewMetadatas: [NextcloudItemMetadataTable] = []
var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = [] var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var directoriesNeedingRename: [NextcloudItemMetadataTable] = [] var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
for updatedMetadata in updatedMetadatas { for updatedMetadata in updatedMetadatas {
if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) { if let existingMetadata = existingMetadatas.first(where: {
$0.ocId == updatedMetadata.ocId
if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue && }) {
!existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) { if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue,
!existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata)
{
if updatedMetadata.directory { if updatedMetadata.directory {
if updatedMetadata.serverUrl != existingMetadata.serverUrl
if updatedMetadata.serverUrl != existingMetadata.serverUrl || updatedMetadata.fileName != existingMetadata.fileName { || updatedMetadata.fileName != existingMetadata.fileName
{
directoriesNeedingRename.append(NextcloudItemMetadataTable(value: updatedMetadata)) directoriesNeedingRename.append(
updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually NextcloudItemMetadataTable(value: updatedMetadata))
updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
} else if !updateDirectoryEtags { } else if !updateDirectoryEtags {
updatedMetadata.etag = existingMetadata.etag updatedMetadata.etag = existingMetadata.etag
@ -159,49 +190,68 @@ class NextcloudFilesDatabaseManager : NSObject {
returningUpdatedMetadatas.append(updatedMetadata) returningUpdatedMetadatas.append(updatedMetadata)
Logger.ncFilesDatabase.debug(
Logger.ncFilesDatabase.debug("Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)") "Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
} else { } else {
Logger.ncFilesDatabase.debug("Skipping item metadata update; same as existing, or still downloading/uploading. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)") Logger.ncFilesDatabase.debug(
"Skipping item metadata update; same as existing, or still downloading/uploading. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
} }
} else { // This is a new metadata } else { // This is a new metadata
if !updateDirectoryEtags && updatedMetadata.directory { if !updateDirectoryEtags, updatedMetadata.directory {
updatedMetadata.etag = "" updatedMetadata.etag = ""
} }
returningNewMetadatas.append(updatedMetadata) returningNewMetadatas.append(updatedMetadata)
Logger.ncFilesDatabase.debug("Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)") Logger.ncFilesDatabase.debug(
"Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
} }
} }
return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename) return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
} }
func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) { func updateItemMetadatas(
account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable],
updateDirectoryEtags: Bool
) -> (
newMetadatas: [NextcloudItemMetadataTable]?,
updatedMetadatas: [NextcloudItemMetadataTable]?,
deletedMetadatas: [NextcloudItemMetadataTable]?
) {
let database = ncDatabase() let database = ncDatabase()
do { do {
let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue) let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl,
NextcloudItemMetadataTable.Status.normal.rawValue)
let metadatasToDelete = processItemMetadatasToDelete(existingMetadatas: existingMetadatas, let metadatasToDelete = processItemMetadatasToDelete(
updatedMetadatas: updatedMetadatas) existingMetadatas: existingMetadatas,
updatedMetadatas: updatedMetadatas)
let metadatasToChange = processItemMetadatasToUpdate(existingMetadatas: existingMetadatas, let metadatasToChange = processItemMetadatasToUpdate(
updatedMetadatas: updatedMetadatas, existingMetadatas: existingMetadatas,
updateDirectoryEtags: updateDirectoryEtags) updatedMetadatas: updatedMetadatas,
updateDirectoryEtags: updateDirectoryEtags)
var metadatasToUpdate = metadatasToChange.updatedMetadatas var metadatasToUpdate = metadatasToChange.updatedMetadatas
let metadatasToCreate = metadatasToChange.newMetadatas let metadatasToCreate = metadatasToChange.newMetadatas
let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
let metadatasToAdd = Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) }) + let metadatasToAdd =
Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) }) Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) })
+ Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
for metadata in directoriesNeedingRename { for metadata in directoriesNeedingRename {
if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(
if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(ocId: metadata.ocId, newServerUrl: metadata.serverUrl, newFileName: metadata.fileName) { ocId: metadata.ocId, newServerUrl: metadata.serverUrl,
newFileName: metadata.fileName)
{
metadatasToUpdate += updatedDirectoryChildren metadatasToUpdate += updatedDirectoryChildren
} }
} }
@ -209,40 +259,59 @@ class NextcloudFilesDatabaseManager : NSObject {
try database.write { try database.write {
for metadata in metadatasToDelete { for metadata in metadatasToDelete {
// Can't pass copies, we need the originals from the database // Can't pass copies, we need the originals from the database
database.delete(ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId)) database.delete(
ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", metadata.ocId))
} }
for metadata in metadatasToAdd { for metadata in metadatasToAdd {
database.add(metadata, update: .all) database.add(metadata, update: .all)
} }
} }
return (newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate, deletedMetadatas: metadatasToDelete) return (
} catch let error { newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate,
Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)") deletedMetadatas: metadatasToDelete
)
} catch {
Logger.ncFilesDatabase.error(
"Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)"
)
return (nil, nil, nil) return (nil, nil, nil)
} }
} }
func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) { func setStatusForItemMetadata(
_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status,
completionHandler: @escaping (_ updatedMetadata: NextcloudItemMetadataTable?) -> Void
) {
let database = ncDatabase() let database = ncDatabase()
do { do {
try database.write { try database.write {
guard let result = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId).first else { guard
Logger.ncFilesDatabase.debug("Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)") let result = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", metadata.ocId
).first
else {
Logger.ncFilesDatabase.debug(
"Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)"
)
return return
} }
result.status = status.rawValue result.status = status.rawValue
database.add(result, update: .all) database.add(result, update: .all)
Logger.ncFilesDatabase.debug("Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)") Logger.ncFilesDatabase.debug(
"Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
completionHandler(NextcloudItemMetadataTable(value: result)) completionHandler(NextcloudItemMetadataTable(value: result))
} }
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
completionHandler(nil) completionHandler(nil)
} }
} }
@ -253,10 +322,14 @@ class NextcloudFilesDatabaseManager : NSObject {
do { do {
try database.write { try database.write {
database.add(metadata, update: .all) database.add(metadata, update: .all)
Logger.ncFilesDatabase.debug("Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)") Logger.ncFilesDatabase.debug(
"Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
} }
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
} }
} }
@ -265,15 +338,18 @@ class NextcloudFilesDatabaseManager : NSObject {
do { do {
try database.write { try database.write {
let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId) let results = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId)
Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)") Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
database.delete(results) database.delete(results)
} }
return true return true
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
return false return false
} }
} }
@ -283,8 +359,14 @@ class NextcloudFilesDatabaseManager : NSObject {
do { do {
try database.write { try database.write {
guard let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else { guard
Logger.ncFilesDatabase.debug("Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: .public)") let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId
).first
else {
Logger.ncFilesDatabase.debug(
"Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: .public)"
)
return return
} }
@ -297,14 +379,20 @@ class NextcloudFilesDatabaseManager : NSObject {
database.add(itemMetadata, update: .all) database.add(itemMetadata, update: .all)
Logger.ncFilesDatabase.debug("Renamed item \(oldFileName, privacy: .public) to \(newFileName, privacy: .public), moved from serverUrl: \(oldServerUrl, privacy: .public) to serverUrl: \(newServerUrl, privacy: .public)") Logger.ncFilesDatabase.debug(
"Renamed item \(oldFileName, privacy: .public) to \(newFileName, privacy: .public), moved from serverUrl: \(oldServerUrl, privacy: .public) to serverUrl: \(newServerUrl, privacy: .public)"
)
} }
} catch let error { } catch {
Logger.ncFilesDatabase.error("Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: .public) at proposed serverUrl \(newServerUrl, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: .public) at proposed serverUrl \(newServerUrl, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
} }
} }
func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? { func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable)
-> NSFileProviderItemIdentifier?
{
let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
if metadata.serverUrl == homeServerFilesUrl { if metadata.serverUrl == homeServerFilesUrl {
@ -312,7 +400,9 @@ class NextcloudFilesDatabaseManager : NSObject {
} }
guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else { guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
Logger.ncFilesDatabase.error("Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
return nil return nil
} }
@ -320,7 +410,9 @@ class NextcloudFilesDatabaseManager : NSObject {
return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId) return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
} }
Logger.ncFilesDatabase.error("Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)") Logger.ncFilesDatabase.error(
"Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)"
)
return nil return nil
} }
} }

View File

@ -69,8 +69,10 @@ extension NextcloudItemMetadataTable {
} }
metadata.size = file.size metadata.size = file.size
metadata.classFile = file.classFile metadata.classFile = file.classFile
//FIXME: iOS 12.0,* don't detect UTI text/markdown, text/x-markdown // FIXME: iOS 12.0,* don't detect UTI text/markdown, text/x-markdown
if (metadata.contentType == "text/markdown" || metadata.contentType == "text/x-markdown") && metadata.classFile == NKCommon.TypeClassFile.unknow.rawValue { if metadata.contentType == "text/markdown" || metadata.contentType == "text/x-markdown",
metadata.classFile == NKCommon.TypeClassFile.unknow.rawValue
{
metadata.classFile = NKCommon.TypeClassFile.document.rawValue metadata.classFile = NKCommon.TypeClassFile.document.rawValue
} }
if let date = file.uploadDate { if let date = file.uploadDate {
@ -87,26 +89,30 @@ extension NextcloudItemMetadataTable {
return metadata return metadata
} }
static func metadatasFromDirectoryReadNKFiles(_ files: [NKFile], static func metadatasFromDirectoryReadNKFiles(
account: String, _ files: [NKFile],
completionHandler: @escaping (_ directoryMetadata: NextcloudItemMetadataTable, account: String,
_ childDirectoriesMetadatas: [NextcloudItemMetadataTable], completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable]) -> Void) { _ directoryMetadata: NextcloudItemMetadataTable,
_ childDirectoriesMetadatas: [NextcloudItemMetadataTable],
_ metadatas: [NextcloudItemMetadataTable]
) -> Void
) {
var directoryMetadataSet = false var directoryMetadataSet = false
var directoryMetadata = NextcloudItemMetadataTable() var directoryMetadata = NextcloudItemMetadataTable()
var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = [] var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = []
var metadatas: [NextcloudItemMetadataTable] = [] var metadatas: [NextcloudItemMetadataTable] = []
let conversionQueue = DispatchQueue(label: "nkFileToMetadataConversionQueue", qos: .userInitiated, attributes: .concurrent) let conversionQueue = DispatchQueue(
let appendQueue = DispatchQueue(label: "metadataAppendQueue", qos: .userInitiated) // Serial queue label: "nkFileToMetadataConversionQueue", qos: .userInitiated, attributes: .concurrent)
let appendQueue = DispatchQueue(label: "metadataAppendQueue", qos: .userInitiated) // Serial queue
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
for file in files { for file in files {
if metadatas.isEmpty && !directoryMetadataSet { if metadatas.isEmpty, !directoryMetadataSet {
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account) let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
directoryMetadata = metadata; directoryMetadata = metadata
directoryMetadataSet = true; directoryMetadataSet = true
} else { } else {
conversionQueue.async(group: dispatchGroup) { conversionQueue.async(group: dispatchGroup) {
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account) let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)

View File

@ -12,10 +12,10 @@
* for more details. * for more details.
*/ */
import Foundation
import RealmSwift
import FileProvider import FileProvider
import Foundation
import NextcloudKit import NextcloudKit
import RealmSwift
class NextcloudItemMetadataTable: Object { class NextcloudItemMetadataTable: Object {
enum Status: Int { enum Status: Int {
@ -71,7 +71,7 @@ class NextcloudItemMetadataTable: Object {
@Persisted var isExtractFile: Bool = false @Persisted var isExtractFile: Bool = false
@Persisted var livePhoto: Bool = false @Persisted var livePhoto: Bool = false
@Persisted var mountType = "" @Persisted var mountType = ""
@Persisted var name = "" // for unifiedSearch is the provider.id @Persisted var name = "" // for unifiedSearch is the provider.id
@Persisted var note = "" @Persisted var note = ""
@Persisted var ownerId = "" @Persisted var ownerId = ""
@Persisted var ownerDisplayName = "" @Persisted var ownerDisplayName = ""
@ -88,13 +88,13 @@ class NextcloudItemMetadataTable: Object {
@Persisted var quotaAvailableBytes: Int64 = 0 @Persisted var quotaAvailableBytes: Int64 = 0
@Persisted var resourceType = "" @Persisted var resourceType = ""
@Persisted var richWorkspace: String? @Persisted var richWorkspace: String?
@Persisted var serverUrl = "" // For parent directory!! @Persisted var serverUrl = "" // For parent directory!!
@Persisted var session = "" @Persisted var session = ""
@Persisted var sessionError = "" @Persisted var sessionError = ""
@Persisted var sessionSelector = "" @Persisted var sessionSelector = ""
@Persisted var sessionTaskIdentifier: Int = 0 @Persisted var sessionTaskIdentifier: Int = 0
@Persisted var sharePermissionsCollaborationServices: Int = 0 @Persisted var sharePermissionsCollaborationServices: Int = 0
let sharePermissionsCloudMesh = List<String>() // TODO: Find a way to compare these in remote state check let sharePermissionsCloudMesh = List<String>() // TODO: Find a way to compare these in remote state check
let shareType = List<Int>() let shareType = List<Int>()
@Persisted var size: Int64 = 0 @Persisted var size: Int64 = 0
@Persisted var status: Int = 0 @Persisted var status: Int = 0
@ -117,22 +117,25 @@ class NextcloudItemMetadataTable: Object {
} }
var isRenameable: Bool { var isRenameable: Bool {
return lock lock
} }
var isPrintable: Bool { var isPrintable: Bool {
if isDocumentViewableOnly { if isDocumentViewableOnly {
return false return false
} }
if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKCommon.TypeClassFile.image.rawValue { if ["application/pdf", "com.adobe.pdf"].contains(contentType)
|| contentType.hasPrefix("text/")
|| classFile == NKCommon.TypeClassFile.image.rawValue
{
return true return true
} }
return false return false
} }
var isDocumentViewableOnly: Bool { var isDocumentViewableOnly: Bool {
return sharePermissionsCollaborationServices == SharePermissions.readShare.rawValue && sharePermissionsCollaborationServices == SharePermissions.readShare.rawValue
classFile == NKCommon.TypeClassFile.document.rawValue && classFile == NKCommon.TypeClassFile.document.rawValue
} }
var isCopyableInPasteboard: Bool { var isCopyableInPasteboard: Bool {
@ -143,22 +146,21 @@ class NextcloudItemMetadataTable: Object {
if directory || isDocumentViewableOnly { if directory || isDocumentViewableOnly {
return false return false
} }
return contentType == "com.adobe.pdf" || contentType == "application/pdf" || classFile == NKCommon.TypeClassFile.image.rawValue return contentType == "com.adobe.pdf" || contentType == "application/pdf"
|| classFile == NKCommon.TypeClassFile.image.rawValue
} }
var isSettableOnOffline: Bool { var isSettableOnOffline: Bool {
return session.isEmpty && !isDocumentViewableOnly session.isEmpty && !isDocumentViewableOnly
} }
var canOpenIn: Bool { var canOpenIn: Bool {
return session.isEmpty && !isDocumentViewableOnly && !directory session.isEmpty && !isDocumentViewableOnly && !directory
} }
var isDownloadUpload: Bool { var isDownloadUpload: Bool {
return status == Status.inDownload.rawValue || status == Status.inDownload.rawValue || status == Status.downloading.rawValue
status == Status.downloading.rawValue || || status == Status.inUpload.rawValue || status == Status.uploading.rawValue
status == Status.inUpload.rawValue ||
status == Status.uploading.rawValue
} }
var isDownload: Bool { var isDownload: Bool {
@ -171,30 +173,28 @@ class NextcloudItemMetadataTable: Object {
override func isEqual(_ object: Any?) -> Bool { override func isEqual(_ object: Any?) -> Bool {
if let object = object as? NextcloudItemMetadataTable { if let object = object as? NextcloudItemMetadataTable {
return self.fileId == object.fileId && return fileId == object.fileId && account == object.account && path == object.path
self.account == object.account && && fileName == object.fileName
self.path == object.path &&
self.fileName == object.fileName
} }
return false return false
} }
func isInSameDatabaseStoreableRemoteState(_ comparingMetadata: NextcloudItemMetadataTable) -> Bool { func isInSameDatabaseStoreableRemoteState(_ comparingMetadata: NextcloudItemMetadataTable)
return comparingMetadata.etag == self.etag && -> Bool
comparingMetadata.fileNameView == self.fileNameView && {
comparingMetadata.date == self.date && comparingMetadata.etag == etag && comparingMetadata.fileNameView == fileNameView
comparingMetadata.permissions == self.permissions && && comparingMetadata.date == date && comparingMetadata.permissions == permissions
comparingMetadata.hasPreview == self.hasPreview && && comparingMetadata.hasPreview == hasPreview && comparingMetadata.note == note
comparingMetadata.note == self.note && && comparingMetadata.lock == lock
comparingMetadata.lock == self.lock && && comparingMetadata.sharePermissionsCollaborationServices
comparingMetadata.sharePermissionsCollaborationServices == self.sharePermissionsCollaborationServices && == sharePermissionsCollaborationServices
comparingMetadata.favorite == self.favorite && comparingMetadata.favorite == favorite
} }
/// Returns false if the user is lokced out of the file. I.e. The file is locked but by someone else /// Returns false if the user is lokced out of the file. I.e. The file is locked but by someone else
func canUnlock(as user: String) -> Bool { func canUnlock(as user: String) -> Bool {
return !lock || (lockOwner == user && lockOwnerType == 0) !lock || (lockOwner == user && lockOwnerType == 0)
} }
func thumbnailUrl(size: CGSize) -> URL? { func thumbnailUrl(size: CGSize) -> URL? {
@ -203,10 +203,12 @@ class NextcloudItemMetadataTable: Object {
} }
let urlBase = urlBase.urlEncoded! let urlBase = urlBase.urlEncoded!
let webdavUrl = urlBase + NextcloudAccount.webDavFilesUrlSuffix + user // Leave the leading slash let webdavUrl = urlBase + NextcloudAccount.webDavFilesUrlSuffix + user // Leave the leading slash
let serverFileRelativeUrl = serverUrl.replacingOccurrences(of: webdavUrl, with: "") + "/" + fileName let serverFileRelativeUrl =
serverUrl.replacingOccurrences(of: webdavUrl, with: "") + "/" + fileName
let urlString = "\(urlBase)/index.php/core/preview.png?file=\(serverFileRelativeUrl)&x=\(size.width)&y=\(size.height)&a=1&mode=cover" let urlString =
"\(urlBase)/index.php/core/preview.png?file=\(serverFileRelativeUrl)&x=\(size.width)&y=\(size.height)&a=1&mode=cover"
return URL(string: urlString) return URL(string: urlString)
} }

View File

@ -17,11 +17,14 @@ import OSLog
extension Logger { extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier! private static var subsystem = Bundle.main.bundleIdentifier!
static let desktopClientConnection = Logger(subsystem: subsystem, category: "desktopclientconnection") static let desktopClientConnection = Logger(
subsystem: subsystem, category: "desktopclientconnection")
static let enumeration = Logger(subsystem: subsystem, category: "enumeration") static let enumeration = Logger(subsystem: subsystem, category: "enumeration")
static let fileProviderExtension = Logger(subsystem: subsystem, category: "fileproviderextension") static let fileProviderExtension = Logger(
subsystem: subsystem, category: "fileproviderextension")
static let fileTransfer = Logger(subsystem: subsystem, category: "filetransfer") static let fileTransfer = Logger(subsystem: subsystem, category: "filetransfer")
static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations") static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations")
static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase") static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase")
static let materialisedFileHandling = Logger(subsystem: subsystem, category: "materialisedfilehandling") static let materialisedFileHandling = Logger(
subsystem: subsystem, category: "materialisedfilehandling")
} }

View File

@ -12,57 +12,49 @@
* for more details. * for more details.
*/ */
import Foundation
import FileProvider import FileProvider
import Foundation
import NextcloudKit import NextcloudKit
extension NKError { extension NKError {
static var noChangesErrorCode: Int { static var noChangesErrorCode: Int {
return -200 -200
} }
var isCouldntConnectError: Bool { var isCouldntConnectError: Bool {
return errorCode == -9999 || errorCode == -9999 || errorCode == -1001 || errorCode == -1004 || errorCode == -1005
errorCode == -1001 || || errorCode == -1009 || errorCode == -1012 || errorCode == -1200 || errorCode == -1202
errorCode == -1004 || || errorCode == 500 || errorCode == 503 || errorCode == 200
errorCode == -1005 ||
errorCode == -1009 ||
errorCode == -1012 ||
errorCode == -1200 ||
errorCode == -1202 ||
errorCode == 500 ||
errorCode == 503 ||
errorCode == 200
} }
var isUnauthenticatedError: Bool { var isUnauthenticatedError: Bool {
return errorCode == -1013 errorCode == -1013
} }
var isGoingOverQuotaError: Bool { var isGoingOverQuotaError: Bool {
return errorCode == 507 errorCode == 507
} }
var isNotFoundError: Bool { var isNotFoundError: Bool {
return errorCode == 404 errorCode == 404
} }
var isNoChangesError: Bool { var isNoChangesError: Bool {
return errorCode == NKError.noChangesErrorCode errorCode == NKError.noChangesErrorCode
} }
var fileProviderError: NSFileProviderError { var fileProviderError: NSFileProviderError {
if isNotFoundError { if isNotFoundError {
return NSFileProviderError(.noSuchItem) NSFileProviderError(.noSuchItem)
} else if isCouldntConnectError { } else if isCouldntConnectError {
// Provide something the file provider can do something with // Provide something the file provider can do something with
return NSFileProviderError(.serverUnreachable) NSFileProviderError(.serverUnreachable)
} else if isUnauthenticatedError { } else if isUnauthenticatedError {
return NSFileProviderError(.notAuthenticated) NSFileProviderError(.notAuthenticated)
} else if isGoingOverQuotaError { } else if isGoingOverQuotaError {
return NSFileProviderError(.insufficientQuota) NSFileProviderError(.insufficientQuota)
} else { } else {
return NSFileProviderError(.cannotSynchronize) NSFileProviderError(.cannotSynchronize)
} }
} }
} }

View File

@ -12,39 +12,39 @@
* for more details. * for more details.
*/ */
import Foundation
import Alamofire import Alamofire
import Foundation
extension Progress { extension Progress {
func setHandlersFromAfRequest(_ request: Request) { func setHandlersFromAfRequest(_ request: Request) {
self.cancellationHandler = { request.cancel() } cancellationHandler = { request.cancel() }
self.pausingHandler = { request.suspend() } pausingHandler = { request.suspend() }
self.resumingHandler = { request.resume() } resumingHandler = { request.resume() }
} }
func copyCurrentStateToProgress(_ otherProgress: Progress, includeHandlers: Bool = false) { func copyCurrentStateToProgress(_ otherProgress: Progress, includeHandlers: Bool = false) {
if includeHandlers { if includeHandlers {
otherProgress.cancellationHandler = self.cancellationHandler otherProgress.cancellationHandler = cancellationHandler
otherProgress.pausingHandler = self.pausingHandler otherProgress.pausingHandler = pausingHandler
otherProgress.resumingHandler = self.resumingHandler otherProgress.resumingHandler = resumingHandler
} }
otherProgress.totalUnitCount = self.totalUnitCount
otherProgress.completedUnitCount = self.completedUnitCount
otherProgress.estimatedTimeRemaining = self.estimatedTimeRemaining
otherProgress.localizedDescription = self.localizedAdditionalDescription
otherProgress.localizedAdditionalDescription = self.localizedAdditionalDescription
otherProgress.isCancellable = self.isCancellable
otherProgress.isPausable = self.isPausable
otherProgress.fileCompletedCount = self.fileCompletedCount
otherProgress.fileURL = self.fileURL
otherProgress.fileTotalCount = self.fileTotalCount
otherProgress.fileCompletedCount = self.fileCompletedCount
otherProgress.fileOperationKind = self.fileOperationKind
otherProgress.kind = self.kind
otherProgress.throughput = self.throughput
for (key, object) in self.userInfo { otherProgress.totalUnitCount = totalUnitCount
otherProgress.completedUnitCount = completedUnitCount
otherProgress.estimatedTimeRemaining = estimatedTimeRemaining
otherProgress.localizedDescription = localizedAdditionalDescription
otherProgress.localizedAdditionalDescription = localizedAdditionalDescription
otherProgress.isCancellable = isCancellable
otherProgress.isPausable = isPausable
otherProgress.fileCompletedCount = fileCompletedCount
otherProgress.fileURL = fileURL
otherProgress.fileTotalCount = fileTotalCount
otherProgress.fileCompletedCount = fileCompletedCount
otherProgress.fileOperationKind = fileOperationKind
otherProgress.kind = kind
otherProgress.throughput = throughput
for (key, object) in userInfo {
otherProgress.setUserInfoObject(object, forKey: key) otherProgress.setUserInfoObject(object, forKey: key)
} }
} }

View File

@ -17,27 +17,32 @@ import NextcloudKit
import OSLog import OSLog
extension FileProviderEnumerator { extension FileProviderEnumerator {
func fullRecursiveScan(ncAccount: NextcloudAccount, func fullRecursiveScan(
ncKit: NextcloudKit, ncAccount: NextcloudAccount,
scanChangesOnly: Bool, ncKit: NextcloudKit,
completionHandler: @escaping(_ metadatas: [NextcloudItemMetadataTable], scanChangesOnly: Bool,
_ newMetadatas: [NextcloudItemMetadataTable], completionHandler: @escaping (
_ updatedMetadatas: [NextcloudItemMetadataTable], _ metadatas: [NextcloudItemMetadataTable],
_ deletedMetadatas: [NextcloudItemMetadataTable], _ newMetadatas: [NextcloudItemMetadataTable],
_ error: NKError?) -> Void) { _ updatedMetadatas: [NextcloudItemMetadataTable],
_ deletedMetadatas: [NextcloudItemMetadataTable],
_ error: NKError?
) -> Void
) {
let rootContainerDirectoryMetadata = NextcloudItemMetadataTable() let rootContainerDirectoryMetadata = NextcloudItemMetadataTable()
rootContainerDirectoryMetadata.directory = true rootContainerDirectoryMetadata.directory = true
rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
// Create a serial dispatch queue // Create a serial dispatch queue
let dispatchQueue = DispatchQueue(label: "recursiveChangeEnumerationQueue", qos: .userInitiated) let dispatchQueue = DispatchQueue(
label: "recursiveChangeEnumerationQueue", qos: .userInitiated)
dispatchQueue.async { dispatchQueue.async {
let results = self.scanRecursively(rootContainerDirectoryMetadata, let results = self.scanRecursively(
ncAccount: ncAccount, rootContainerDirectoryMetadata,
ncKit: ncKit, ncAccount: ncAccount,
scanChangesOnly: scanChangesOnly) ncKit: ncKit,
scanChangesOnly: scanChangesOnly)
// Run a check to ensure files deleted in one location are not updated in another (e.g. when moved) // Run a check to ensure files deleted in one location are not updated in another (e.g. when moved)
// The recursive scan provides us with updated/deleted metadatas only on a folder by folder basis; // The recursive scan provides us with updated/deleted metadatas only on a folder by folder basis;
@ -45,29 +50,38 @@ extension FileProviderEnumerator {
var checkedDeletedMetadatas = results.deletedMetadatas var checkedDeletedMetadatas = results.deletedMetadatas
for updatedMetadata in results.updatedMetadatas { for updatedMetadata in results.updatedMetadatas {
guard let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: { $0.ocId == updatedMetadata.ocId } ) else { guard
continue; let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: {
$0.ocId == updatedMetadata.ocId
})
else {
continue
} }
checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx) checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
completionHandler(results.metadatas, results.newMetadatas, results.updatedMetadatas, checkedDeletedMetadatas, results.error) completionHandler(
results.metadatas, results.newMetadatas, results.updatedMetadatas,
checkedDeletedMetadatas, results.error)
} }
} }
} }
private func scanRecursively(_ directoryMetadata: NextcloudItemMetadataTable, private func scanRecursively(
ncAccount: NextcloudAccount, _ directoryMetadata: NextcloudItemMetadataTable,
ncKit: NextcloudKit, ncAccount: NextcloudAccount,
scanChangesOnly: Bool) -> (metadatas: [NextcloudItemMetadataTable], ncKit: NextcloudKit,
newMetadatas: [NextcloudItemMetadataTable], scanChangesOnly: Bool
updatedMetadatas: [NextcloudItemMetadataTable], ) -> (
deletedMetadatas: [NextcloudItemMetadataTable], metadatas: [NextcloudItemMetadataTable],
error: NKError?) { newMetadatas: [NextcloudItemMetadataTable],
updatedMetadatas: [NextcloudItemMetadataTable],
if self.isInvalidated { deletedMetadatas: [NextcloudItemMetadataTable],
error: NKError?
) {
if isInvalidated {
return ([], [], [], [], nil) return ([], [], [], [], nil)
} }
@ -80,43 +94,59 @@ extension FileProviderEnumerator {
var allDeletedMetadatas: [NextcloudItemMetadataTable] = [] var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
let dispatchGroup = DispatchGroup() // TODO: Maybe own thread? let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
dispatchGroup.enter() dispatchGroup.enter()
var criticalError: NKError? var criticalError: NKError?
let itemServerUrl = directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue ? let itemServerUrl =
ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue
? ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: .public)") Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: .public)")
FileProviderEnumerator.readServerUrl(itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in FileProviderEnumerator.readServerUrl(
itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly
) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
if readError != nil { if readError != nil {
let nkReadError = NKError(error: readError!) let nkReadError = NKError(error: readError!)
// Is the error is that we have found matching etags on this item, then ignore it // Is the error is that we have found matching etags on this item, then ignore it
// if we are doing a full rescan // if we are doing a full rescan
guard nkReadError.isNoChangesError && scanChangesOnly else { guard nkReadError.isNoChangesError, scanChangesOnly else {
Logger.enumeration.error("Finishing enumeration of changes at \(itemServerUrl, privacy: .public) with \(readError!.localizedDescription, privacy: .public)") Logger.enumeration.error(
"Finishing enumeration of changes at \(itemServerUrl, privacy: .public) with \(readError!.localizedDescription, privacy: .public)"
)
if nkReadError.isNotFoundError { if nkReadError.isNotFoundError {
Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting as deletion without error") Logger.enumeration.info(
"404 error means item no longer exists. Deleting metadata and reporting as deletion without error"
)
if let deletedMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: directoryMetadata.ocId) { if let deletedMetadatas =
dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: directoryMetadata.ocId)
{
allDeletedMetadatas += deletedMetadatas allDeletedMetadatas += deletedMetadatas
} else { } else {
Logger.enumeration.error("An error occurred while trying to delete directory and children not found in recursive scan") Logger.enumeration.error(
"An error occurred while trying to delete directory and children not found in recursive scan"
)
} }
} else if nkReadError.isNoChangesError { // All is well, just no changed etags } else if nkReadError.isNoChangesError { // All is well, just no changed etags
Logger.enumeration.info("Error was to say no changed files -- not bad error. No need to check children.") Logger.enumeration.info(
"Error was to say no changed files -- not bad error. No need to check children."
)
} else if nkReadError.isUnauthenticatedError || nkReadError.isCouldntConnectError { } else if nkReadError.isUnauthenticatedError
|| nkReadError.isCouldntConnectError
{
// If it is a critical error then stop, if not then continue // If it is a critical error then stop, if not then continue
Logger.enumeration.error("Error will affect next enumerated items, so stopping enumeration.") Logger.enumeration.error(
"Error will affect next enumerated items, so stopping enumeration.")
criticalError = nkReadError criticalError = nkReadError
} }
dispatchGroup.leave() dispatchGroup.leave()
@ -124,30 +154,40 @@ extension FileProviderEnumerator {
} }
} }
Logger.enumeration.info("Finished reading serverUrl: \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.info(
"Finished reading serverUrl: \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
if let metadatas = metadatas { if let metadatas {
allMetadatas += metadatas allMetadatas += metadatas
} else { } else {
Logger.enumeration.warning("WARNING: Nil metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.warning(
"WARNING: Nil metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
} }
if let newMetadatas = newMetadatas { if let newMetadatas {
allNewMetadatas += newMetadatas allNewMetadatas += newMetadatas
} else { } else {
Logger.enumeration.warning("WARNING: Nil new metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.warning(
"WARNING: Nil new metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
} }
if let updatedMetadatas = updatedMetadatas { if let updatedMetadatas {
allUpdatedMetadatas += updatedMetadatas allUpdatedMetadatas += updatedMetadatas
} else { } else {
Logger.enumeration.warning("WARNING: Nil updated metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.warning(
"WARNING: Nil updated metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
} }
if let deletedMetadatas = deletedMetadatas { if let deletedMetadatas {
allDeletedMetadatas += deletedMetadatas allDeletedMetadatas += deletedMetadatas
} else { } else {
Logger.enumeration.warning("WARNING: Nil deleted metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.warning(
"WARNING: Nil deleted metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
} }
dispatchGroup.leave() dispatchGroup.leave()
@ -160,13 +200,12 @@ extension FileProviderEnumerator {
} }
var childDirectoriesToScan: [NextcloudItemMetadataTable] = [] var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
var candidateMetadatas: [NextcloudItemMetadataTable] var candidateMetadatas: [NextcloudItemMetadataTable] =
if scanChangesOnly {
if scanChangesOnly { allUpdatedMetadatas + allNewMetadatas
candidateMetadatas = allUpdatedMetadatas + allNewMetadatas } else {
} else { allMetadatas
candidateMetadatas = allMetadatas }
}
for candidateMetadata in candidateMetadatas { for candidateMetadata in candidateMetadatas {
if candidateMetadata.directory { if candidateMetadata.directory {
@ -175,14 +214,18 @@ extension FileProviderEnumerator {
} }
if childDirectoriesToScan.isEmpty { if childDirectoriesToScan.isEmpty {
return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil) return (
metadatas: allMetadatas, newMetadatas: allNewMetadatas,
updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil
)
} }
for childDirectory in childDirectoriesToScan { for childDirectory in childDirectoriesToScan {
let childScanResult = scanRecursively(childDirectory, let childScanResult = scanRecursively(
ncAccount: ncAccount, childDirectory,
ncKit: ncKit, ncAccount: ncAccount,
scanChangesOnly: scanChangesOnly) ncKit: ncKit,
scanChangesOnly: scanChangesOnly)
allMetadatas += childScanResult.metadatas allMetadatas += childScanResult.metadatas
allNewMetadatas += childScanResult.newMetadatas allNewMetadatas += childScanResult.newMetadatas
@ -190,31 +233,44 @@ extension FileProviderEnumerator {
allDeletedMetadatas += childScanResult.deletedMetadatas allDeletedMetadatas += childScanResult.deletedMetadatas
} }
return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil) return (
metadatas: allMetadatas, newMetadatas: allNewMetadatas,
updatedMetadatas: allUpdatedMetadatas,
deletedMetadatas: allDeletedMetadatas, nil
)
} }
static func handleDepth1ReadFileOrFolder(serverUrl: String, static func handleDepth1ReadFileOrFolder(
ncAccount: NextcloudAccount, serverUrl: String,
files: [NKFile], ncAccount: NextcloudAccount,
error: NKError, files: [NKFile],
completionHandler: @escaping (_ metadatas: [NextcloudItemMetadataTable]?, error: NKError,
_ newMetadatas: [NextcloudItemMetadataTable]?, completionHandler: @escaping (
_ updatedMetadatas: [NextcloudItemMetadataTable]?, _ metadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?, _ newMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?) -> Void) { _ updatedMetadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?
) -> Void
) {
guard error == .success else { guard error == .success else {
Logger.enumeration.error("1 depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)") Logger.enumeration.error(
"1 depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(nil, nil, nil, nil, error.error) completionHandler(nil, nil, nil, nil, error.error)
return return
} }
Logger.enumeration.debug("Starting async conversion of NKFiles for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.debug(
"Starting async conversion of NKFiles for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(files, account: ncAccount.ncKitAccount) { directoryMetadata, childDirectoriesMetadata, metadatas in NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(
files, account: ncAccount.ncKitAccount
) { directoryMetadata, _, metadatas in
// STORE DATA FOR CURRENTLY SCANNED DIRECTORY // STORE DATA FOR CURRENTLY SCANNED DIRECTORY
// We have now scanned this directory's contents, so update with etag in order to not check again if not needed // We have now scanned this directory's contents, so update with etag in order to not check again if not needed
@ -228,64 +284,88 @@ extension FileProviderEnumerator {
// that our local copies are up to date -- instead, leave them as the old. // that our local copies are up to date -- instead, leave them as the old.
// They will get updated when they are the subject of a readServerUrl call. // They will get updated when they are the subject of a readServerUrl call.
// (See above) // (See above)
let changedMetadatas = dbManager.updateItemMetadatas(account: ncAccount.ncKitAccount, serverUrl: serverUrl, updatedMetadatas: metadatas, updateDirectoryEtags: false) let changedMetadatas = dbManager.updateItemMetadatas(
account: ncAccount.ncKitAccount, serverUrl: serverUrl,
updatedMetadatas: metadatas,
updateDirectoryEtags: false)
DispatchQueue.main.async { DispatchQueue.main.async {
completionHandler(metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas, changedMetadatas.deletedMetadatas, nil) completionHandler(
metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas,
changedMetadatas.deletedMetadatas, nil)
} }
} }
} }
} }
static func readServerUrl(_ serverUrl: String, static func readServerUrl(
ncAccount: NextcloudAccount, _ serverUrl: String,
ncKit: NextcloudKit, ncAccount: NextcloudAccount,
stopAtMatchingEtags: Bool = false, ncKit: NextcloudKit,
depth: String = "1", stopAtMatchingEtags: Bool = false,
completionHandler: @escaping (_ metadatas: [NextcloudItemMetadataTable]?, depth: String = "1",
_ newMetadatas: [NextcloudItemMetadataTable]?, completionHandler: @escaping (
_ updatedMetadatas: [NextcloudItemMetadataTable]?, _ metadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?, _ newMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?) -> Void) { _ updatedMetadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?
) -> Void
) {
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
let ncKitAccount = ncAccount.ncKitAccount let ncKitAccount = ncAccount.ncKitAccount
Logger.enumeration.debug("Starting to read serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public) at depth \(depth, privacy: .public). NCKit info: userId: \(ncKit.nkCommonInstance.user, privacy: .public), password is empty: \(ncKit.nkCommonInstance.password == "" ? "EMPTY PASSWORD" : "NOT EMPTY PASSWORD"), urlBase: \(ncKit.nkCommonInstance.urlBase, privacy: .public), ncVersion: \(ncKit.nkCommonInstance.nextcloudVersion, privacy: .public)") Logger.enumeration.debug(
"Starting to read serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public) at depth \(depth, privacy: .public). NCKit info: userId: \(ncKit.nkCommonInstance.user, privacy: .public), password is empty: \(ncKit.nkCommonInstance.password == "" ? "EMPTY PASSWORD" : "NOT EMPTY PASSWORD"), urlBase: \(ncKit.nkCommonInstance.urlBase, privacy: .public), ncVersion: \(ncKit.nkCommonInstance.nextcloudVersion, privacy: .public)"
)
ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: depth, showHiddenFiles: true) { _, files, _, error in ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: depth, showHiddenFiles: true) {
_, files, _, error in
guard error == .success else { guard error == .success else {
Logger.enumeration.error("\(depth, privacy: .public) depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)") Logger.enumeration.error(
"\(depth, privacy: .public) depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(nil, nil, nil, nil, error.error) completionHandler(nil, nil, nil, nil, error.error)
return return
} }
guard let receivedFile = files.first else { guard let receivedFile = files.first else {
Logger.enumeration.error("Received no items from readFileOrFolder of \(serverUrl, privacy: .public), not much we can do...") Logger.enumeration.error(
"Received no items from readFileOrFolder of \(serverUrl, privacy: .public), not much we can do..."
)
completionHandler(nil, nil, nil, nil, error.error) completionHandler(nil, nil, nil, nil, error.error)
return return
} }
guard receivedFile.directory else { guard receivedFile.directory else {
Logger.enumeration.debug("Read item is a file. Converting NKfile for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.debug(
let itemMetadata = NextcloudItemMetadataTable.fromNKFile(receivedFile, account: ncKitAccount) "Read item is a file. Converting NKfile for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
dbManager.addItemMetadata(itemMetadata) // TODO: Return some value when it is an update )
let itemMetadata = NextcloudItemMetadataTable.fromNKFile(
receivedFile, account: ncKitAccount)
dbManager.addItemMetadata(itemMetadata) // TODO: Return some value when it is an update
completionHandler([itemMetadata], nil, nil, nil, error.error) completionHandler([itemMetadata], nil, nil, nil, error.error)
return return
} }
if stopAtMatchingEtags, if stopAtMatchingEtags,
let directoryMetadata = dbManager.directoryMetadata(account: ncKitAccount, serverUrl: serverUrl) { let directoryMetadata = dbManager.directoryMetadata(
account: ncKitAccount, serverUrl: serverUrl)
{
let directoryEtag = directoryMetadata.etag let directoryEtag = directoryMetadata.etag
guard directoryEtag == "" || directoryEtag != receivedFile.etag else { guard directoryEtag == "" || directoryEtag != receivedFile.etag else {
Logger.enumeration.debug("Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error.") Logger.enumeration.debug(
"Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error."
)
let description = "Fetched directory etag is same as that stored locally. Not fetching child items." let description =
let nkError = NKError(errorCode: NKError.noChangesErrorCode, errorDescription: description) "Fetched directory etag is same as that stored locally. Not fetching child items."
let nkError = NKError(
errorCode: NKError.noChangesErrorCode, errorDescription: description)
let metadatas = dbManager.itemMetadatas(account: ncKitAccount, serverUrl: serverUrl) let metadatas = dbManager.itemMetadatas(
account: ncKitAccount, serverUrl: serverUrl)
completionHandler(metadatas, nil, nil, nil, nkError.error) completionHandler(metadatas, nil, nil, nil, nkError.error)
return return
@ -294,7 +374,8 @@ extension FileProviderEnumerator {
if depth == "0" { if depth == "0" {
if serverUrl != ncAccount.davFilesUrl { if serverUrl != ncAccount.davFilesUrl {
let metadata = NextcloudItemMetadataTable.fromNKFile(receivedFile, account: ncKitAccount) let metadata = NextcloudItemMetadataTable.fromNKFile(
receivedFile, account: ncKitAccount)
let isNew = dbManager.itemMetadataFromOcId(metadata.ocId) == nil let isNew = dbManager.itemMetadataFromOcId(metadata.ocId) == nil
let updatedMetadatas = isNew ? [] : [metadata] let updatedMetadatas = isNew ? [] : [metadata]
let newMetadatas = isNew ? [metadata] : [] let newMetadatas = isNew ? [metadata] : []
@ -306,7 +387,9 @@ extension FileProviderEnumerator {
} }
} }
} else { } else {
handleDepth1ReadFileOrFolder(serverUrl: serverUrl, ncAccount: ncAccount, files: files, error: error, completionHandler: completionHandler) handleDepth1ReadFileOrFolder(
serverUrl: serverUrl, ncAccount: ncAccount, files: files, error: error,
completionHandler: completionHandler)
} }
} }
} }

View File

@ -17,13 +17,13 @@ import NextcloudKit
import OSLog import OSLog
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private let enumeratedItemIdentifier: NSFileProviderItemIdentifier private let enumeratedItemIdentifier: NSFileProviderItemIdentifier
private var enumeratedItemMetadata: NextcloudItemMetadataTable? private var enumeratedItemMetadata: NextcloudItemMetadataTable?
private var enumeratingSystemIdentifier: Bool { private var enumeratingSystemIdentifier: Bool {
return FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier)
} }
private let anchor = NSFileProviderSyncAnchor(Date().description.data(using: .utf8)!) // TODO: actually use this in NCKit and server requests
private let anchor = NSFileProviderSyncAnchor(Date().description.data(using: .utf8)!) // TODO: actually use this in NCKit and server requests
private static let maxItemsPerFileProviderPage = 100 private static let maxItemsPerFileProviderPage = 100
let ncAccount: NextcloudAccount let ncAccount: NextcloudAccount
let ncKit: NextcloudKit let ncKit: NextcloudKit
@ -31,59 +31,78 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
var isInvalidated = false var isInvalidated = false
private static func isSystemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> Bool { private static func isSystemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> Bool {
return identifier == .rootContainer || identifier == .rootContainer || identifier == .trashContainer || identifier == .workingSet
identifier == .trashContainer ||
identifier == .workingSet
} }
init(enumeratedItemIdentifier: NSFileProviderItemIdentifier, ncAccount: NextcloudAccount, ncKit: NextcloudKit) { init(
enumeratedItemIdentifier: NSFileProviderItemIdentifier, ncAccount: NextcloudAccount,
ncKit: NextcloudKit
) {
self.enumeratedItemIdentifier = enumeratedItemIdentifier self.enumeratedItemIdentifier = enumeratedItemIdentifier
self.ncAccount = ncAccount self.ncAccount = ncAccount
self.ncKit = ncKit self.ncKit = ncKit
if FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) { if FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) {
Logger.enumeration.debug("Providing enumerator for a system defined container: \(enumeratedItemIdentifier.rawValue, privacy: .public)") Logger.enumeration.debug(
self.serverUrl = ncAccount.davFilesUrl "Providing enumerator for a system defined container: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
serverUrl = ncAccount.davFilesUrl
} else { } else {
Logger.enumeration.debug("Providing enumerator for item with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)") Logger.enumeration.debug(
"Providing enumerator for item with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
enumeratedItemMetadata = dbManager.itemMetadataFromFileProviderItemIdentifier(enumeratedItemIdentifier) enumeratedItemMetadata = dbManager.itemMetadataFromFileProviderItemIdentifier(
enumeratedItemIdentifier)
if enumeratedItemMetadata != nil { if enumeratedItemMetadata != nil {
self.serverUrl = enumeratedItemMetadata!.serverUrl + "/" + enumeratedItemMetadata!.fileName serverUrl =
enumeratedItemMetadata!.serverUrl + "/" + enumeratedItemMetadata!.fileName
} else { } else {
Logger.enumeration.error("Could not find itemMetadata for file with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)") Logger.enumeration.error(
"Could not find itemMetadata for file with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
} }
} }
Logger.enumeration.info("Set up enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") Logger.enumeration.info(
"Set up enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
super.init() super.init()
} }
func invalidate() { func invalidate() {
Logger.enumeration.debug("Enumerator is being invalidated for item with identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)") Logger.enumeration.debug(
self.isInvalidated = true "Enumerator is being invalidated for item with identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)"
)
isInvalidated = true
} }
// MARK: - Protocol methods // MARK: - Protocol methods
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) { func enumerateItems(
Logger.enumeration.debug("Received enumerate items request for enumerator with user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage
) {
Logger.enumeration.debug(
"Received enumerate items request for enumerator with user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
/* /*
- inspect the page to determine whether this is an initial or a follow-up request (TODO) - inspect the page to determine whether this is an initial or a follow-up request (TODO)
If this is an enumerator for a directory, the root container or all directories: If this is an enumerator for a directory, the root container or all directories:
- perform a server request to fetch directory contents - perform a server request to fetch directory contents
If this is an enumerator for the working set: If this is an enumerator for the working set:
- perform a server request to update your local database - perform a server request to update your local database
- fetch the working set from your local database - fetch the working set from your local database
- inform the observer about the items returned by the server (possibly multiple times) - inform the observer about the items returned by the server (possibly multiple times)
- inform the observer that you are finished with this page - inform the observer that you are finished with this page
*/ */
if enumeratedItemIdentifier == .trashContainer { if enumeratedItemIdentifier == .trashContainer {
Logger.enumeration.debug("Enumerating trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") Logger.enumeration.debug(
"Enumerating trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// TODO! // TODO!
observer.finishEnumerating(upTo: nil) observer.finishEnumerating(upTo: nil)
@ -98,105 +117,140 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
// navigate a little bit in Finder, file picker, etc // navigate a little bit in Finder, file picker, etc
guard serverUrl != "" else { guard serverUrl != "" else {
Logger.enumeration.error("Enumerator has empty serverUrl -- can't enumerate that! For identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)") Logger.enumeration.error(
"Enumerator has empty serverUrl -- can't enumerate that! For identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)"
)
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem)) observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
return return
} }
// TODO: Make better use of pagination and handle paging properly // TODO: Make better use of pagination and handle paging properly
if page == NSFileProviderPage.initialPageSortedByDate as NSFileProviderPage || if page == NSFileProviderPage.initialPageSortedByDate as NSFileProviderPage
page == NSFileProviderPage.initialPageSortedByName as NSFileProviderPage { || page == NSFileProviderPage.initialPageSortedByName as NSFileProviderPage
{
Logger.enumeration.debug(
"Enumerating initial page for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
Logger.enumeration.debug("Enumerating initial page for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit) {
metadatas, _, _, _, readError in
FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit) { metadatas, _, _, _, readError in
guard readError == nil else { guard readError == nil else {
Logger.enumeration.error("Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error \(readError!.localizedDescription, privacy: .public)") Logger.enumeration.error(
"Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error \(readError!.localizedDescription, privacy: .public)"
)
let nkReadError = NKError(error: readError!) let nkReadError = NKError(error: readError!)
observer.finishEnumeratingWithError(nkReadError.fileProviderError) observer.finishEnumeratingWithError(nkReadError.fileProviderError)
return return
} }
guard let metadatas = metadatas else { guard let metadatas else {
Logger.enumeration.error("Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with invalid metadatas.") Logger.enumeration.error(
"Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with invalid metadatas."
)
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize)) observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
return return
} }
Logger.enumeration.info("Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public). Processed \(metadatas.count) metadatas") Logger.enumeration.info(
"Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public). Processed \(metadatas.count) metadatas"
)
FileProviderEnumerator.completeEnumerationObserver(observer, ncKit: self.ncKit, numPage: 1, itemMetadatas: metadatas) FileProviderEnumerator.completeEnumerationObserver(
observer, ncKit: self.ncKit, numPage: 1, itemMetadatas: metadatas)
} }
return; return
} }
let numPage = Int(String(data: page.rawValue, encoding: .utf8)!)! let numPage = Int(String(data: page.rawValue, encoding: .utf8)!)!
Logger.enumeration.debug("Enumerating page \(numPage, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") Logger.enumeration.debug(
"Enumerating page \(numPage, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// TODO: Handle paging properly // TODO: Handle paging properly
// FileProviderEnumerator.completeObserver(observer, ncKit: ncKit, numPage: numPage, itemMetadatas: nil) // FileProviderEnumerator.completeObserver(observer, ncKit: ncKit, numPage: numPage, itemMetadatas: nil)
observer.finishEnumerating(upTo: nil) observer.finishEnumerating(upTo: nil)
} }
func enumerateChanges(for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor) { func enumerateChanges(
Logger.enumeration.debug("Received enumerate changes request for enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor
) {
Logger.enumeration.debug(
"Received enumerate changes request for enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
/* /*
- query the server for updates since the passed-in sync anchor (TODO) - query the server for updates since the passed-in sync anchor (TODO)
If this is an enumerator for the working set: If this is an enumerator for the working set:
- note the changes in your local database - note the changes in your local database
- inform the observer about item deletions and updates (modifications + insertions) - inform the observer about item deletions and updates (modifications + insertions)
- inform the observer when you have finished enumerating up to a subsequent sync anchor - inform the observer when you have finished enumerating up to a subsequent sync anchor
*/ */
if enumeratedItemIdentifier == .workingSet { if enumeratedItemIdentifier == .workingSet {
Logger.enumeration.debug("Enumerating changes in working set for user: \(self.ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.debug(
"Enumerating changes in working set for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
// Unlike when enumerating items we can't progressively enumerate items as we need to wait to resolve which items are truly deleted and which // Unlike when enumerating items we can't progressively enumerate items as we need to wait to resolve which items are truly deleted and which
// have just been moved elsewhere. // have just been moved elsewhere.
fullRecursiveScan(ncAccount: self.ncAccount, fullRecursiveScan(
ncKit: self.ncKit, ncAccount: ncAccount,
scanChangesOnly: true) { _, newMetadatas, updatedMetadatas, deletedMetadatas, error in ncKit: ncKit,
scanChangesOnly: true
) { _, newMetadatas, updatedMetadatas, deletedMetadatas, error in
if self.isInvalidated { if self.isInvalidated {
Logger.enumeration.info("Enumerator invalidated during working set change scan. For user: \(self.ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.info(
"Enumerator invalidated during working set change scan. For user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize)) observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
return return
} }
guard error == nil else { guard error == nil else {
Logger.enumeration.info("Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with error: \(error!.errorDescription, privacy: .public)") Logger.enumeration.info(
"Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with error: \(error!.errorDescription, privacy: .public)"
)
observer.finishEnumeratingWithError(error!.fileProviderError) observer.finishEnumeratingWithError(error!.fileProviderError)
return return
} }
Logger.enumeration.info("Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public). Enumerating items.") Logger.enumeration.info(
"Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public). Enumerating items."
)
FileProviderEnumerator.completeChangesObserver(observer, FileProviderEnumerator.completeChangesObserver(
anchor: anchor, observer,
ncKit: self.ncKit, anchor: anchor,
newMetadatas: newMetadatas, ncKit: self.ncKit,
updatedMetadatas: updatedMetadatas, newMetadatas: newMetadatas,
deletedMetadatas: deletedMetadatas) updatedMetadatas: updatedMetadatas,
deletedMetadatas: deletedMetadatas)
} }
return return
} else if enumeratedItemIdentifier == .trashContainer { } else if enumeratedItemIdentifier == .trashContainer {
Logger.enumeration.debug("Enumerating changes in trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.debug(
"Enumerating changes in trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
// TODO! // TODO!
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false) observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
return return
} }
Logger.enumeration.info("Enumerating changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)") Logger.enumeration.info(
"Enumerating changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// No matter what happens here we finish enumeration in some way, either from the error // No matter what happens here we finish enumeration in some way, either from the error
// handling below or from the completeChangesObserver // handling below or from the completeChangesObserver
// TODO: Move to the sync engine extension // TODO: Move to the sync engine extension
FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: true) { _, newMetadatas, updatedMetadatas, deletedMetadatas, readError in FileProviderEnumerator.readServerUrl(
serverUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: true
) { _, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
// If we get a 404 we might add more deleted metadatas // If we get a 404 we might add more deleted metadatas
var currentDeletedMetadatas: [NextcloudItemMetadataTable] = [] var currentDeletedMetadatas: [NextcloudItemMetadataTable] = []
@ -205,46 +259,66 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
} }
guard readError == nil else { guard readError == nil else {
Logger.enumeration.error("Finishing enumeration of changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error: \(readError!.localizedDescription, privacy: .public)") Logger.enumeration.error(
"Finishing enumeration of changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error: \(readError!.localizedDescription, privacy: .public)"
)
let nkReadError = NKError(error: readError!) let nkReadError = NKError(error: readError!)
let fpError = nkReadError.fileProviderError let fpError = nkReadError.fileProviderError
if nkReadError.isNotFoundError { if nkReadError.isNotFoundError {
Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting \(self.serverUrl, privacy: .public) as deletion without error") Logger.enumeration.info(
"404 error means item no longer exists. Deleting metadata and reporting \(self.serverUrl, privacy: .public) as deletion without error"
)
guard let itemMetadata = self.enumeratedItemMetadata else { guard let itemMetadata = self.enumeratedItemMetadata else {
Logger.enumeration.error("Invalid enumeratedItemMetadata, could not delete metadata nor report deletion") Logger.enumeration.error(
"Invalid enumeratedItemMetadata, could not delete metadata nor report deletion"
)
observer.finishEnumeratingWithError(fpError) observer.finishEnumeratingWithError(fpError)
return return
} }
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
if itemMetadata.directory { if itemMetadata.directory {
if let deletedDirectoryMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: itemMetadata.ocId) { if let deletedDirectoryMetadatas =
dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: itemMetadata.ocId)
{
currentDeletedMetadatas += deletedDirectoryMetadatas currentDeletedMetadatas += deletedDirectoryMetadatas
} else { } else {
Logger.enumeration.error("Something went wrong when recursively deleting directory not found.") Logger.enumeration.error(
"Something went wrong when recursively deleting directory not found."
)
} }
} else { } else {
dbManager.deleteItemMetadata(ocId: itemMetadata.ocId) dbManager.deleteItemMetadata(ocId: itemMetadata.ocId)
} }
FileProviderEnumerator.completeChangesObserver(observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: nil, updatedMetadatas: nil, deletedMetadatas: [itemMetadata]) FileProviderEnumerator.completeChangesObserver(
observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: nil,
updatedMetadatas: nil,
deletedMetadatas: [itemMetadata])
return return
} else if nkReadError.isNoChangesError { // All is well, just no changed etags } else if nkReadError.isNoChangesError { // All is well, just no changed etags
Logger.enumeration.info("Error was to say no changed files -- not bad error. Finishing change enumeration.") Logger.enumeration.info(
"Error was to say no changed files -- not bad error. Finishing change enumeration."
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false) observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
return; return
} }
observer.finishEnumeratingWithError(fpError) observer.finishEnumeratingWithError(fpError)
return return
} }
Logger.enumeration.info("Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public)") Logger.enumeration.info(
"Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
FileProviderEnumerator.completeChangesObserver(observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: newMetadatas, updatedMetadatas: updatedMetadatas, deletedMetadatas: deletedMetadatas) FileProviderEnumerator.completeChangesObserver(
observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: newMetadatas,
updatedMetadatas: updatedMetadatas, deletedMetadatas: deletedMetadatas)
} }
} }
@ -254,29 +328,43 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
// MARK: - Helper methods // MARK: - Helper methods
private static func metadatasToFileProviderItems(_ itemMetadatas: [NextcloudItemMetadataTable], ncKit: NextcloudKit, completionHandler: @escaping(_ items: [NSFileProviderItem]) -> Void) { private static func metadatasToFileProviderItems(
_ itemMetadatas: [NextcloudItemMetadataTable], ncKit: NextcloudKit,
completionHandler: @escaping (_ items: [NSFileProviderItem]) -> Void
) {
var items: [NSFileProviderItem] = [] var items: [NSFileProviderItem] = []
let conversionQueue = DispatchQueue(label: "metadataToItemConversionQueue", qos: .userInitiated, attributes: .concurrent) let conversionQueue = DispatchQueue(
let appendQueue = DispatchQueue(label: "enumeratorItemAppendQueue", qos: .userInitiated) // Serial queue label: "metadataToItemConversionQueue", qos: .userInitiated, attributes: .concurrent)
let appendQueue = DispatchQueue(label: "enumeratorItemAppendQueue", qos: .userInitiated) // Serial queue
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
for itemMetadata in itemMetadatas { for itemMetadata in itemMetadatas {
conversionQueue.async(group: dispatchGroup) { conversionQueue.async(group: dispatchGroup) {
if itemMetadata.e2eEncrypted { if itemMetadata.e2eEncrypted {
Logger.enumeration.info("Skipping encrypted metadata in enumeration: \(itemMetadata.ocId, privacy: .public) \(itemMetadata.fileName, privacy: .public)") Logger.enumeration.info(
"Skipping encrypted metadata in enumeration: \(itemMetadata.ocId, privacy: .public) \(itemMetadata.fileName, privacy: .public)"
)
return return
} }
if let parentItemIdentifier = NextcloudFilesDatabaseManager.shared.parentItemIdentifierFromMetadata(itemMetadata) { if let parentItemIdentifier = NextcloudFilesDatabaseManager.shared
let item = FileProviderItem(metadata: itemMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit) .parentItemIdentifierFromMetadata(itemMetadata)
Logger.enumeration.debug("Will enumerate item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public)") {
let item = FileProviderItem(
metadata: itemMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: ncKit)
Logger.enumeration.debug(
"Will enumerate item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public)"
)
appendQueue.async(group: dispatchGroup) { appendQueue.async(group: dispatchGroup) {
items.append(item) items.append(item)
} }
} else { } else {
Logger.enumeration.error("Could not get valid parentItemIdentifier for item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public), skipping enumeration") Logger.enumeration.error(
"Could not get valid parentItemIdentifier for item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public), skipping enumeration"
)
} }
} }
} }
@ -287,11 +375,13 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
} }
private static func fileProviderPageforNumPage(_ numPage: Int) -> NSFileProviderPage { private static func fileProviderPageforNumPage(_ numPage: Int) -> NSFileProviderPage {
return NSFileProviderPage("\(numPage)".data(using: .utf8)!) NSFileProviderPage("\(numPage)".data(using: .utf8)!)
} }
private static func completeEnumerationObserver(_ observer: NSFileProviderEnumerationObserver, ncKit: NextcloudKit, numPage: Int, itemMetadatas: [NextcloudItemMetadataTable]) { private static func completeEnumerationObserver(
_ observer: NSFileProviderEnumerationObserver, ncKit: NextcloudKit, numPage: Int,
itemMetadatas: [NextcloudItemMetadataTable]
) {
metadatasToFileProviderItems(itemMetadatas, ncKit: ncKit) { items in metadatasToFileProviderItems(itemMetadatas, ncKit: ncKit) { items in
observer.didEnumerate(items) observer.didEnumerate(items)
Logger.enumeration.info("Did enumerate \(items.count) items") Logger.enumeration.info("Did enumerate \(items.count) items")
@ -310,10 +400,17 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
} }
} }
private static func completeChangesObserver(_ observer: NSFileProviderChangeObserver, anchor: NSFileProviderSyncAnchor, ncKit: NextcloudKit, newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) { private static func completeChangesObserver(
_ observer: NSFileProviderChangeObserver, anchor: NSFileProviderSyncAnchor,
ncKit: NextcloudKit,
newMetadatas: [NextcloudItemMetadataTable]?,
updatedMetadatas: [NextcloudItemMetadataTable]?,
deletedMetadatas: [NextcloudItemMetadataTable]?
) {
guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else { guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else {
Logger.enumeration.error("Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. Finished enumeration of changes with error.") Logger.enumeration.error(
"Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. Finished enumeration of changes with error."
)
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem)) observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
return return
} }
@ -322,19 +419,20 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = [] var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var allDeletedMetadatas: [NextcloudItemMetadataTable] = [] var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
if let newMetadatas = newMetadatas { if let newMetadatas {
allUpdatedMetadatas += newMetadatas allUpdatedMetadatas += newMetadatas
} }
if let updatedMetadatas = updatedMetadatas { if let updatedMetadatas {
allUpdatedMetadatas += updatedMetadatas allUpdatedMetadatas += updatedMetadatas
} }
if let deletedMetadatas = deletedMetadatas { if let deletedMetadatas {
allDeletedMetadatas = deletedMetadatas allDeletedMetadatas = deletedMetadatas
} }
let allFpItemDeletionsIdentifiers = Array(allDeletedMetadatas.map { NSFileProviderItemIdentifier($0.ocId) }) let allFpItemDeletionsIdentifiers = Array(
allDeletedMetadatas.map { NSFileProviderItemIdentifier($0.ocId) })
if !allFpItemDeletionsIdentifiers.isEmpty { if !allFpItemDeletionsIdentifiers.isEmpty {
observer.didDeleteItems(withIdentifiers: allFpItemDeletionsIdentifiers) observer.didDeleteItems(withIdentifiers: allFpItemDeletionsIdentifiers)
} }
@ -345,7 +443,9 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
observer.didUpdate(updatedItems) observer.didUpdate(updatedItems)
} }
Logger.enumeration.info("Processed \(updatedItems.count) new or updated metadatas, \(allDeletedMetadatas.count) deleted metadatas.") Logger.enumeration.info(
"Processed \(updatedItems.count) new or updated metadatas, \(allDeletedMetadatas.count) deleted metadatas."
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false) observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
} }
} }

View File

@ -12,11 +12,11 @@
* for more details. * for more details.
*/ */
import Foundation
import FileProvider import FileProvider
import OSLog import Foundation
import NCDesktopClientSocketKit import NCDesktopClientSocketKit
import NextcloudKit import NextcloudKit
import OSLog
extension FileProviderExtension { extension FileProviderExtension {
func sendFileProviderDomainIdentifier() { func sendFileProviderDomainIdentifier() {
@ -28,7 +28,9 @@ extension FileProviderExtension {
private func signalEnumeratorAfterAccountSetup() { private func signalEnumeratorAfterAccountSetup() {
guard let fpManager = NSFileProviderManager(for: domain) else { guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error("Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup") Logger.fileProviderExtension.error(
"Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup"
)
return return
} }
@ -36,36 +38,47 @@ extension FileProviderExtension {
fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
if error != nil { if error != nil {
Logger.fileProviderExtension.error("Error resolving not authenticated, received error: \(error!.localizedDescription)") Logger.fileProviderExtension.error(
"Error resolving not authenticated, received error: \(error!.localizedDescription)"
)
} }
} }
Logger.fileProviderExtension.debug("Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)") Logger.fileProviderExtension.debug(
"Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)"
)
fpManager.signalEnumerator(for: .workingSet) { error in fpManager.signalEnumerator(for: .workingSet) { error in
if error != nil { if error != nil {
Logger.fileProviderExtension.error("Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)") Logger.fileProviderExtension.error(
"Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)"
)
} }
} }
} }
func setupDomainAccount(user: String, serverUrl: String, password: String) { func setupDomainAccount(user: String, serverUrl: String, password: String) {
ncAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password) ncAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password)
ncKit.setup(user: ncAccount!.username, ncKit.setup(
userId: ncAccount!.username, user: ncAccount!.username,
password: ncAccount!.password, userId: ncAccount!.username,
urlBase: ncAccount!.serverUrl, password: ncAccount!.password,
userAgent: "Nextcloud-macOS/FileProviderExt", urlBase: ncAccount!.serverUrl,
nextcloudVersion: 25, userAgent: "Nextcloud-macOS/FileProviderExt",
delegate: nil) // TODO: add delegate methods for self nextcloudVersion: 25,
delegate: nil) // TODO: add delegate methods for self
Logger.fileProviderExtension.info("Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)") Logger.fileProviderExtension.info(
"Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)"
)
signalEnumeratorAfterAccountSetup() signalEnumeratorAfterAccountSetup()
} }
func removeAccountConfig() { func removeAccountConfig() {
Logger.fileProviderExtension.info("Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)") Logger.fileProviderExtension.info(
"Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)"
)
ncAccount = nil ncAccount = nil
} }
} }

View File

@ -12,19 +12,22 @@
* for more details. * for more details.
*/ */
import Foundation
import FileProvider import FileProvider
import Foundation
import NextcloudKit import NextcloudKit
import OSLog import OSLog
extension FileProviderExtension: NSFileProviderThumbnailing { extension FileProviderExtension: NSFileProviderThumbnailing {
func fetchThumbnails(for itemIdentifiers: [NSFileProviderItemIdentifier], func fetchThumbnails(
requestedSize size: CGSize, for itemIdentifiers: [NSFileProviderItemIdentifier],
perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier, requestedSize size: CGSize,
Data?, perThumbnailCompletionHandler: @escaping (
Error?) -> Void, NSFileProviderItemIdentifier,
completionHandler: @escaping (Error?) -> Void) -> Progress { Data?,
Error?
) -> Void,
completionHandler: @escaping (Error?) -> Void
) -> Progress {
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count)) let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
var progressCounter: Int64 = 0 var progressCounter: Int64 = 0
@ -37,21 +40,29 @@ extension FileProviderExtension: NSFileProviderThumbnailing {
} }
for itemIdentifier in itemIdentifiers { for itemIdentifier in itemIdentifiers {
Logger.fileProviderExtension.debug("Fetching thumbnail for item with identifier:\(itemIdentifier.rawValue, privacy: .public)") Logger.fileProviderExtension.debug(
guard let metadata = NextcloudFilesDatabaseManager.shared.itemMetadataFromFileProviderItemIdentifier(itemIdentifier), "Fetching thumbnail for item with identifier:\(itemIdentifier.rawValue, privacy: .public)"
let thumbnailUrl = metadata.thumbnailUrl(size: size) else { )
guard
let metadata = NextcloudFilesDatabaseManager.shared
.itemMetadataFromFileProviderItemIdentifier(itemIdentifier),
let thumbnailUrl = metadata.thumbnailUrl(size: size)
else {
Logger.fileProviderExtension.debug("Did not fetch thumbnail URL") Logger.fileProviderExtension.debug("Did not fetch thumbnail URL")
finishCurrent() finishCurrent()
continue continue
} }
Logger.fileProviderExtension.debug("Fetching thumbnail for file:\(metadata.fileName) at:\(thumbnailUrl.absoluteString, privacy: .public)") Logger.fileProviderExtension.debug(
"Fetching thumbnail for file:\(metadata.fileName) at:\(thumbnailUrl.absoluteString, privacy: .public)"
)
self.ncKit.getPreview(url: thumbnailUrl) { _, data, error in ncKit.getPreview(url: thumbnailUrl) { _, data, error in
if error == .success && data != nil { if error == .success, data != nil {
perThumbnailCompletionHandler(itemIdentifier, data, nil) perThumbnailCompletionHandler(itemIdentifier, data, nil)
} else { } else {
perThumbnailCompletionHandler(itemIdentifier, nil, NSFileProviderError(.serverUnreachable)) perThumbnailCompletionHandler(
itemIdentifier, nil, NSFileProviderError(.serverUnreachable))
} }
finishCurrent() finishCurrent()
} }

View File

@ -13,9 +13,9 @@
*/ */
import FileProvider import FileProvider
import OSLog
import NCDesktopClientSocketKit import NCDesktopClientSocketKit
import NextcloudKit import NextcloudKit
import OSLog
class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate { class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate {
let domain: NSFileProviderDomain let domain: NSFileProviderDomain
@ -25,15 +25,19 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
return nckb return nckb
}() }()
let appGroupIdentifier: String? = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String let appGroupIdentifier: String? =
Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
var ncAccount: NextcloudAccount? var ncAccount: NextcloudAccount?
lazy var socketClient: LocalSocketClient? = { lazy var socketClient: LocalSocketClient? = {
guard let containerUrl = pathForAppGroupContainer() else { guard let containerUrl = pathForAppGroupContainer() else {
Logger.fileProviderExtension.critical("Could not start file provider socket client properly as could not get container url") Logger.fileProviderExtension.critical(
return nil; "Could not start file provider socket client properly as could not get container url"
)
return nil
} }
let socketPath = containerUrl.appendingPathComponent(".fileprovidersocket", conformingTo: .archive) let socketPath = containerUrl.appendingPathComponent(
".fileprovidersocket", conformingTo: .archive)
let lineProcessor = FileProviderSocketLineProcessor(delegate: self) let lineProcessor = FileProviderSocketLineProcessor(delegate: self)
return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor) return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor)
@ -50,7 +54,9 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
configuration.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData configuration.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData
configuration.sharedContainerIdentifier = appGroupIdentifier configuration.sharedContainerIdentifier = appGroupIdentifier
let session = URLSession(configuration: configuration, delegate: ncKitBackground, delegateQueue: OperationQueue.main) let session = URLSession(
configuration: configuration, delegate: ncKitBackground,
delegateQueue: OperationQueue.main)
return session return session
}() }()
@ -59,23 +65,31 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
// The containing application must create a domain using `NSFileProviderManager.add(_:, completionHandler:)`. The system will then launch the application extension process, call `FileProviderExtension.init(domain:)` to instantiate the extension for that domain, and call methods on the instance. // The containing application must create a domain using `NSFileProviderManager.add(_:, completionHandler:)`. The system will then launch the application extension process, call `FileProviderExtension.init(domain:)` to instantiate the extension for that domain, and call methods on the instance.
super.init() super.init()
self.socketClient?.start() socketClient?.start()
} }
func invalidate() { func invalidate() {
// TODO: cleanup any resources // TODO: cleanup any resources
Logger.fileProviderExtension.debug("Extension for domain \(self.domain.displayName, privacy: .public) is being torn down") Logger.fileProviderExtension.debug(
"Extension for domain \(self.domain.displayName, privacy: .public) is being torn down")
} }
// MARK: NSFileProviderReplicatedExtension protocol methods // MARK: NSFileProviderReplicatedExtension protocol methods
func item(for identifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) -> Progress { func item(
for identifier: NSFileProviderItemIdentifier, request _: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress {
// resolve the given identifier to a record in the model // resolve the given identifier to a record in the model
Logger.fileProviderExtension.debug("Received item request for item with identifier: \(identifier.rawValue, privacy: .public)") Logger.fileProviderExtension.debug(
"Received item request for item with identifier: \(identifier.rawValue, privacy: .public)"
)
if identifier == .rootContainer { if identifier == .rootContainer {
guard let ncAccount = ncAccount else { guard let ncAccount else {
Logger.fileProviderExtension.error("Not providing item: \(identifier.rawValue, privacy: .public) as account not set up yet") Logger.fileProviderExtension.error(
"Not providing item: \(identifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(nil, NSFileProviderError(.notAuthenticated)) completionHandler(nil, NSFileProviderError(.notAuthenticated))
return Progress() return Progress()
} }
@ -90,35 +104,52 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
metadata.serverUrl = ncAccount.serverUrl metadata.serverUrl = ncAccount.serverUrl
metadata.classFile = NKCommon.TypeClassFile.directory.rawValue metadata.classFile = NKCommon.TypeClassFile.directory.rawValue
completionHandler(FileProviderItem(metadata: metadata, parentItemIdentifier: NSFileProviderItemIdentifier.rootContainer, ncKit: ncKit), nil) completionHandler(
FileProviderItem(
metadata: metadata,
parentItemIdentifier: NSFileProviderItemIdentifier.rootContainer,
ncKit: ncKit), nil)
return Progress() return Progress()
} }
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
guard let metadata = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier), guard let metadata = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier),
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(metadata) else { let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(metadata)
else {
completionHandler(nil, NSFileProviderError(.noSuchItem)) completionHandler(nil, NSFileProviderError(.noSuchItem))
return Progress() return Progress()
} }
completionHandler(FileProviderItem(metadata: metadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit), nil) completionHandler(
FileProviderItem(
metadata: metadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit), nil)
return Progress() return Progress()
} }
func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress {
Logger.fileProviderExtension.debug("Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)") func fetchContents(
for itemIdentifier: NSFileProviderItemIdentifier,
version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest,
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void
) -> Progress {
Logger.fileProviderExtension.debug(
"Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
)
guard requestedVersion == nil else { guard requestedVersion == nil else {
// TODO: Add proper support for file versioning // TODO: Add proper support for file versioning
Logger.fileProviderExtension.error("Can't return contents for specific version as this is not supported.") Logger.fileProviderExtension.error(
completionHandler(nil, nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])) "Can't return contents for specific version as this is not supported.")
completionHandler(
nil, nil,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]))
return Progress() return Progress()
} }
guard ncAccount != nil else { guard ncAccount != nil else {
Logger.fileProviderExtension.error("Not fetching contents item: \(itemIdentifier.rawValue, privacy: .public) as account not set up yet") Logger.fileProviderExtension.error(
"Not fetching contents item: \(itemIdentifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(nil, nil, NSFileProviderError(.notAuthenticated)) completionHandler(nil, nil, NSFileProviderError(.notAuthenticated))
return Progress() return Progress()
} }
@ -126,46 +157,65 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
let ocId = itemIdentifier.rawValue let ocId = itemIdentifier.rawValue
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else { guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public)") Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
)
completionHandler(nil, nil, NSFileProviderError(.noSuchItem)) completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return Progress() return Progress()
} }
guard !metadata.isDocumentViewableOnly else { guard !metadata.isDocumentViewableOnly else {
Logger.fileProviderExtension.error("Could not get contents of item as is readonly: \(itemIdentifier.rawValue, privacy: .public) \(metadata.fileName, privacy: .public)") Logger.fileProviderExtension.error(
"Could not get contents of item as is readonly: \(itemIdentifier.rawValue, privacy: .public) \(metadata.fileName, privacy: .public)"
)
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize)) completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
return Progress() return Progress()
} }
let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
Logger.fileProviderExtension.debug("Fetching file with name \(metadata.fileName, privacy: .public) at URL: \(serverUrlFileName, privacy: .public)") Logger.fileProviderExtension.debug(
"Fetching file with name \(metadata.fileName, privacy: .public) at URL: \(serverUrlFileName, privacy: .public)"
)
let progress = Progress() let progress = Progress()
// TODO: Handle folders nicely // TODO: Handle folders nicely
do { do {
let fileNameLocalPath = try localPathForNCFile(ocId: metadata.ocId, fileNameView: metadata.fileNameView, domain: self.domain) let fileNameLocalPath = try localPathForNCFile(
ocId: metadata.ocId, fileNameView: metadata.fileNameView, domain: domain)
dbManager.setStatusForItemMetadata(metadata, status: NextcloudItemMetadataTable.Status.downloading) { updatedMetadata in dbManager.setStatusForItemMetadata(
metadata, status: NextcloudItemMetadataTable.Status.downloading
) { updatedMetadata in
guard let updatedMetadata = updatedMetadata else { guard let updatedMetadata else {
Logger.fileProviderExtension.error("Could not acquire updated metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public), unable to update item status to downloading") Logger.fileProviderExtension.error(
"Could not acquire updated metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public), unable to update item status to downloading"
)
completionHandler(nil, nil, NSFileProviderError(.noSuchItem)) completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return return
} }
self.ncKit.download(serverUrlFileName: serverUrlFileName, self.ncKit.download(
fileNameLocalPath: fileNameLocalPath.path, serverUrlFileName: serverUrlFileName,
requestHandler: { request in fileNameLocalPath: fileNameLocalPath.path,
progress.setHandlersFromAfRequest(request) requestHandler: { request in
}, taskHandler: { task in progress.setHandlersFromAfRequest(request)
NSFileProviderManager(for: self.domain)?.register(task, forItemWithIdentifier: itemIdentifier, completionHandler: { _ in }) },
}, progressHandler: { downloadProgress in taskHandler: { task in
downloadProgress.copyCurrentStateToProgress(progress) NSFileProviderManager(for: self.domain)?.register(
}) { _, etag, date, _, _, _, error in task, forItemWithIdentifier: itemIdentifier, completionHandler: { _ in }
)
},
progressHandler: { downloadProgress in
downloadProgress.copyCurrentStateToProgress(progress)
}
) { _, etag, date, _, _, _, error in
if error == .success { if error == .success {
Logger.fileTransfer.debug("Acquired contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and filename: \(updatedMetadata.fileName, privacy: .public)") Logger.fileTransfer.debug(
"Acquired contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and filename: \(updatedMetadata.fileName, privacy: .public)"
)
updatedMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue updatedMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
updatedMetadata.sessionError = "" updatedMetadata.sessionError = ""
@ -175,18 +225,26 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
dbManager.addLocalFileMetadataFromItemMetadata(updatedMetadata) dbManager.addLocalFileMetadataFromItemMetadata(updatedMetadata)
dbManager.addItemMetadata(updatedMetadata) dbManager.addItemMetadata(updatedMetadata)
guard let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(updatedMetadata) else { guard
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(
updatedMetadata)
else {
completionHandler(nil, nil, NSFileProviderError(.noSuchItem)) completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return return
} }
let fpItem = FileProviderItem(metadata: updatedMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit) let fpItem = FileProviderItem(
metadata: updatedMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
completionHandler(fileNameLocalPath, fpItem, nil) completionHandler(fileNameLocalPath, fpItem, nil)
} else { } else {
Logger.fileTransfer.error("Could not acquire contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and fileName: \(updatedMetadata.fileName, privacy: .public)") Logger.fileTransfer.error(
"Could not acquire contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and fileName: \(updatedMetadata.fileName, privacy: .public)"
)
updatedMetadata.status = NextcloudItemMetadataTable.Status.downloadError.rawValue updatedMetadata.status =
NextcloudItemMetadataTable.Status.downloadError.rawValue
updatedMetadata.sessionError = error.errorDescription updatedMetadata.sessionError = error.errorDescription
dbManager.addItemMetadata(updatedMetadata) dbManager.addItemMetadata(updatedMetadata)
@ -195,40 +253,60 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
} }
} }
} }
} catch let error { } catch {
Logger.fileProviderExtension.error("Could not find local path for file \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)") Logger.fileProviderExtension.error(
"Could not find local path for file \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize)) completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
} }
return progress return progress
} }
func createItem(basedOn itemTemplate: NSFileProviderItem, fields: NSFileProviderItemFields, contents url: URL?, options: NSFileProviderCreateItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress { func createItem(
basedOn itemTemplate: NSFileProviderItem, fields _: NSFileProviderItemFields,
contents url: URL?, options: NSFileProviderCreateItemOptions = [],
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?)
->
Void
) -> Progress {
// TODO: a new item was created on disk, process the item's creation // TODO: a new item was created on disk, process the item's creation
Logger.fileProviderExtension.debug("Received create item request for item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)") Logger.fileProviderExtension.debug(
"Received create item request for item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)"
)
guard itemTemplate.contentType != .symbolicLink else { guard itemTemplate.contentType != .symbolicLink else {
Logger.fileProviderExtension.error("Cannot create item, symbolic links not supported.") Logger.fileProviderExtension.error("Cannot create item, symbolic links not supported.")
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])) completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]))
return Progress() return Progress()
} }
guard let ncAccount = ncAccount else { guard let ncAccount else {
Logger.fileProviderExtension.error("Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as account not set up yet") Logger.fileProviderExtension.error(
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.notAuthenticated)) "Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSFileProviderError(.notAuthenticated))
return Progress() return Progress()
} }
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
let parentItemIdentifier = itemTemplate.parentItemIdentifier let parentItemIdentifier = itemTemplate.parentItemIdentifier
let itemTemplateIsFolder = itemTemplate.contentType == .folder || let itemTemplateIsFolder =
itemTemplate.contentType == .directory itemTemplate.contentType == .folder || itemTemplate.contentType == .directory
if options.contains(.mayAlreadyExist) { if options.contains(.mayAlreadyExist) {
// TODO: This needs to be properly handled with a check in the db // TODO: This needs to be properly handled with a check in the db
Logger.fileProviderExtension.info("Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as it may already exist") Logger.fileProviderExtension.info(
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem)) "Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as it may already exist"
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
return Progress() return Progress()
} }
@ -237,9 +315,16 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
if parentItemIdentifier == .rootContainer { if parentItemIdentifier == .rootContainer {
parentItemServerUrl = ncAccount.davFilesUrl parentItemServerUrl = ncAccount.davFilesUrl
} else { } else {
guard let parentItemMetadata = dbManager.directoryMetadata(ocId: parentItemIdentifier.rawValue) else { guard
Logger.fileProviderExtension.error("Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)") let parentItemMetadata = dbManager.directoryMetadata(
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem)) ocId: parentItemIdentifier.rawValue)
else {
Logger.fileProviderExtension.error(
"Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)"
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSFileProviderError(.noSuchItem))
return Progress() return Progress()
} }
@ -249,29 +334,43 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
let fileNameLocalPath = url?.path ?? "" let fileNameLocalPath = url?.path ?? ""
let newServerUrlFileName = parentItemServerUrl + "/" + itemTemplate.filename let newServerUrlFileName = parentItemServerUrl + "/" + itemTemplate.filename
Logger.fileProviderExtension.debug("About to upload item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) of type: \(itemTemplate.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(itemTemplate.filename) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)") Logger.fileProviderExtension.debug(
"About to upload item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) of type: \(itemTemplate.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(itemTemplate.filename) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)"
)
if itemTemplateIsFolder { if itemTemplateIsFolder {
self.ncKit.createFolder(serverUrlFileName: newServerUrlFileName) { account, ocId, _, error in ncKit.createFolder(serverUrlFileName: newServerUrlFileName) { account, _, _, error in
guard error == .success else { guard error == .success else {
Logger.fileTransfer.error("Could not create new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)") Logger.fileTransfer.error(
"Could not create new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(itemTemplate, [], false, error.fileProviderError) completionHandler(itemTemplate, [], false, error.fileProviderError)
return return
} }
// Read contents after creation // Read contents after creation
self.ncKit.readFileOrFolder(serverUrlFileName: newServerUrlFileName, depth: "0", showHiddenFiles: true) { account, files, _, error in self.ncKit.readFileOrFolder(
serverUrlFileName: newServerUrlFileName, depth: "0", showHiddenFiles: true
) { account, files, _, error in
guard error == .success else { guard error == .success else {
Logger.fileTransfer.error("Could not read new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)") Logger.fileTransfer.error(
"Could not read new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
return return
} }
DispatchQueue.global().async { DispatchQueue.global().async {
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(files, account: account) { directoryMetadata, childDirectoriesMetadata, metadatas in NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(
files, account: account
) {
directoryMetadata, _, _ in
dbManager.addItemMetadata(directoryMetadata) dbManager.addItemMetadata(directoryMetadata)
let fpItem = FileProviderItem(metadata: directoryMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit) let fpItem = FileProviderItem(
metadata: directoryMetadata,
parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
completionHandler(fpItem, [], true, nil) completionHandler(fpItem, [], true, nil)
} }
@ -284,25 +383,37 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
let progress = Progress() let progress = Progress()
self.ncKit.upload(serverUrlFileName: newServerUrlFileName, ncKit.upload(
fileNameLocalPath: fileNameLocalPath, serverUrlFileName: newServerUrlFileName,
requestHandler: { request in fileNameLocalPath: fileNameLocalPath,
progress.setHandlersFromAfRequest(request) requestHandler: { request in
}, taskHandler: { task in progress.setHandlersFromAfRequest(request)
NSFileProviderManager(for: self.domain)?.register(task, forItemWithIdentifier: itemTemplate.itemIdentifier, completionHandler: { _ in }) },
}, progressHandler: { uploadProgress in taskHandler: { task in
uploadProgress.copyCurrentStateToProgress(progress) NSFileProviderManager(for: self.domain)?.register(
}) { account, ocId, etag, date, size, _, _, error in task, forItemWithIdentifier: itemTemplate.itemIdentifier,
guard error == .success, let ocId = ocId else { completionHandler: { _ in })
Logger.fileTransfer.error("Could not upload item with filename: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)") },
progressHandler: { uploadProgress in
uploadProgress.copyCurrentStateToProgress(progress)
}
) { account, ocId, etag, date, size, _, _, error in
guard error == .success, let ocId else {
Logger.fileTransfer.error(
"Could not upload item with filename: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(itemTemplate, [], false, error.fileProviderError) completionHandler(itemTemplate, [], false, error.fileProviderError)
return return
} }
Logger.fileTransfer.info("Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)") Logger.fileTransfer.info(
"Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)"
)
if size != itemTemplate.documentSize as? Int64 { if size != itemTemplate.documentSize as? Int64 {
Logger.fileTransfer.warning("Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(itemTemplate.documentSize??.int64Value ?? 0))") Logger.fileTransfer.warning(
"Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(itemTemplate.documentSize??.int64Value ?? 0))"
)
} }
let newMetadata = NextcloudItemMetadataTable() let newMetadata = NextcloudItemMetadataTable()
@ -324,34 +435,48 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata) dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
dbManager.addItemMetadata(newMetadata) dbManager.addItemMetadata(newMetadata)
let fpItem = FileProviderItem(metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit) let fpItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit
)
completionHandler(fpItem, [], false, nil) completionHandler(fpItem, [], false, nil)
} }
return progress return progress
} }
func modifyItem(_ item: NSFileProviderItem, baseVersion version: NSFileProviderItemVersion, changedFields: NSFileProviderItemFields, contents newContents: URL?, options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress { func modifyItem(
_ item: NSFileProviderItem, baseVersion _: NSFileProviderItemVersion,
changedFields: NSFileProviderItemFields, contents newContents: URL?,
options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?)
->
Void
) -> Progress {
// An item was modified on disk, process the item's modification // An item was modified on disk, process the item's modification
// TODO: Handle finder things like tags, other possible item changed fields // TODO: Handle finder things like tags, other possible item changed fields
Logger.fileProviderExtension.debug("Received modify item request for item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) and filename: \(item.filename, privacy: .public)") Logger.fileProviderExtension.debug(
"Received modify item request for item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) and filename: \(item.filename, privacy: .public)"
)
guard let ncAccount = ncAccount else { guard let ncAccount else {
Logger.fileProviderExtension.error("Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public) as account not set up yet") Logger.fileProviderExtension.error(
"Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated)) completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
return Progress() return Progress()
} }
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
let parentItemIdentifier = item.parentItemIdentifier let parentItemIdentifier = item.parentItemIdentifier
let itemTemplateIsFolder = item.contentType == .folder || let itemTemplateIsFolder = item.contentType == .folder || item.contentType == .directory
item.contentType == .directory
if options.contains(.mayAlreadyExist) { if options.contains(.mayAlreadyExist) {
// TODO: This needs to be properly handled with a check in the db // TODO: This needs to be properly handled with a check in the db
Logger.fileProviderExtension.warning("Modification for item: \(item.itemIdentifier.rawValue, privacy: .public) may already exist") Logger.fileProviderExtension.warning(
"Modification for item: \(item.itemIdentifier.rawValue, privacy: .public) may already exist"
)
} }
var parentItemServerUrl: String var parentItemServerUrl: String
@ -359,8 +484,13 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
if parentItemIdentifier == .rootContainer { if parentItemIdentifier == .rootContainer {
parentItemServerUrl = ncAccount.davFilesUrl parentItemServerUrl = ncAccount.davFilesUrl
} else { } else {
guard let parentItemMetadata = dbManager.directoryMetadata(ocId: parentItemIdentifier.rawValue) else { guard
Logger.fileProviderExtension.error("Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)") let parentItemMetadata = dbManager.directoryMetadata(
ocId: parentItemIdentifier.rawValue)
else {
Logger.fileProviderExtension.error(
"Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)"
)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem)) completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return Progress() return Progress()
} }
@ -371,7 +501,9 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
let fileNameLocalPath = newContents?.path ?? "" let fileNameLocalPath = newContents?.path ?? ""
let newServerUrlFileName = parentItemServerUrl + "/" + item.filename let newServerUrlFileName = parentItemServerUrl + "/" + item.filename
Logger.fileProviderExtension.debug("About to upload modified item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) of type: \(item.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(item.filename, privacy: .public) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)") Logger.fileProviderExtension.debug(
"About to upload modified item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) of type: \(item.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(item.filename, privacy: .public) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)"
)
var modifiedItem = item var modifiedItem = item
@ -384,10 +516,14 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
if changedFields.contains(.filename) || changedFields.contains(.parentItemIdentifier) { if changedFields.contains(.filename) || changedFields.contains(.parentItemIdentifier) {
dispatchQueue.async { dispatchQueue.async {
let ocId = item.itemIdentifier.rawValue let ocId = item.itemIdentifier.rawValue
Logger.fileProviderExtension.debug("Changed fields for item \(ocId, privacy: .public) with filename \(item.filename, privacy: .public) includes filename or parentitemidentifier...") Logger.fileProviderExtension.debug(
"Changed fields for item \(ocId, privacy: .public) with filename \(item.filename, privacy: .public) includes filename or parentitemidentifier..."
)
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else { guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(item.itemIdentifier.rawValue, privacy: .public)") Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(item.itemIdentifier.rawValue, privacy: .public)"
)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem)) completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return return
} }
@ -395,14 +531,18 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
var renameError: NSFileProviderError? var renameError: NSFileProviderError?
let oldServerUrlFileName = metadata.serverUrl + "/" + metadata.fileName let oldServerUrlFileName = metadata.serverUrl + "/" + metadata.fileName
let moveFileOrFolderDispatchGroup = DispatchGroup() // Make this block wait until done let moveFileOrFolderDispatchGroup = DispatchGroup() // Make this block wait until done
moveFileOrFolderDispatchGroup.enter() moveFileOrFolderDispatchGroup.enter()
self.ncKit.moveFileOrFolder(serverUrlFileNameSource: oldServerUrlFileName, self.ncKit.moveFileOrFolder(
serverUrlFileNameDestination: newServerUrlFileName, serverUrlFileNameSource: oldServerUrlFileName,
overwrite: false) { account, error in serverUrlFileNameDestination: newServerUrlFileName,
overwrite: false
) { _, error in
guard error == .success else { guard error == .success else {
Logger.fileTransfer.error("Could not move file or folder: \(oldServerUrlFileName, privacy: .public) to \(newServerUrlFileName, privacy: .public), received error: \(error.errorDescription, privacy: .public)") Logger.fileTransfer.error(
"Could not move file or folder: \(oldServerUrlFileName, privacy: .public) to \(newServerUrlFileName, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
renameError = error.fileProviderError renameError = error.fileProviderError
moveFileOrFolderDispatchGroup.leave() moveFileOrFolderDispatchGroup.leave()
return return
@ -411,37 +551,49 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
// Remember that a folder metadata's serverUrl is its direct server URL, while for // Remember that a folder metadata's serverUrl is its direct server URL, while for
// an item metadata the server URL is the parent folder's URL // an item metadata the server URL is the parent folder's URL
if itemTemplateIsFolder { if itemTemplateIsFolder {
_ = dbManager.renameDirectoryAndPropagateToChildren(ocId: ocId, newServerUrl: newServerUrlFileName, newFileName: item.filename) _ = dbManager.renameDirectoryAndPropagateToChildren(
ocId: ocId, newServerUrl: newServerUrlFileName,
newFileName: item.filename)
self.signalEnumerator { error in self.signalEnumerator { error in
if error != nil { if error != nil {
Logger.fileTransfer.error("Error notifying change in moved directory: \(error)") Logger.fileTransfer.error(
"Error notifying change in moved directory: \(error)")
} }
} }
} else { } else {
dbManager.renameItemMetadata(ocId: ocId, newServerUrl: parentItemServerUrl, newFileName: item.filename) dbManager.renameItemMetadata(
ocId: ocId, newServerUrl: parentItemServerUrl,
newFileName: item.filename)
} }
guard let newMetadata = dbManager.itemMetadataFromOcId(ocId) else { guard let newMetadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileTransfer.error("Could not acquire metadata of item with identifier: \(ocId, privacy: .public), cannot correctly inform of modification") Logger.fileTransfer.error(
"Could not acquire metadata of item with identifier: \(ocId, privacy: .public), cannot correctly inform of modification"
)
renameError = NSFileProviderError(.noSuchItem) renameError = NSFileProviderError(.noSuchItem)
moveFileOrFolderDispatchGroup.leave() moveFileOrFolderDispatchGroup.leave()
return return
} }
modifiedItem = FileProviderItem(metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit) modifiedItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
moveFileOrFolderDispatchGroup.leave() moveFileOrFolderDispatchGroup.leave()
} }
moveFileOrFolderDispatchGroup.wait() moveFileOrFolderDispatchGroup.wait()
guard renameError == nil else { guard renameError == nil else {
Logger.fileTransfer.error("Stopping rename of item with ocId \(ocId, privacy: .public) due to error: \(renameError!.localizedDescription, privacy: .public)") Logger.fileTransfer.error(
"Stopping rename of item with ocId \(ocId, privacy: .public) due to error: \(renameError!.localizedDescription, privacy: .public)"
)
completionHandler(modifiedItem, [], false, renameError) completionHandler(modifiedItem, [], false, renameError)
return return
} }
guard !itemTemplateIsFolder else { guard !itemTemplateIsFolder else {
Logger.fileTransfer.debug("Only handling renaming for folders. ocId: \(ocId, privacy: .public)") Logger.fileTransfer.debug(
"Only handling renaming for folders. ocId: \(ocId, privacy: .public)")
completionHandler(modifiedItem, [], false, nil) completionHandler(modifiedItem, [], false, nil)
return return
} }
@ -454,7 +606,9 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
} }
guard !itemTemplateIsFolder else { guard !itemTemplateIsFolder else {
Logger.fileTransfer.debug("System requested modification for folder with ocID \(item.itemIdentifier.rawValue, privacy: .public) (\(newServerUrlFileName, privacy: .public)) of something other than folder name.") Logger.fileTransfer.debug(
"System requested modification for folder with ocID \(item.itemIdentifier.rawValue, privacy: .public) (\(newServerUrlFileName, privacy: .public)) of something other than folder name."
)
completionHandler(modifiedItem, [], false, nil) completionHandler(modifiedItem, [], false, nil)
return Progress() return Progress()
} }
@ -463,41 +617,62 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
if changedFields.contains(.contents) { if changedFields.contains(.contents) {
dispatchQueue.async { dispatchQueue.async {
Logger.fileProviderExtension.debug("Item modification for \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public) includes contents") Logger.fileProviderExtension.debug(
"Item modification for \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public) includes contents"
)
guard newContents != nil else { guard newContents != nil else {
Logger.fileProviderExtension.warning("WARNING. Could not upload modified contents as was provided nil contents url. ocId: \(item.itemIdentifier.rawValue, privacy: .public)") Logger.fileProviderExtension.warning(
"WARNING. Could not upload modified contents as was provided nil contents url. ocId: \(item.itemIdentifier.rawValue, privacy: .public)"
)
completionHandler(modifiedItem, [], false, NSFileProviderError(.noSuchItem)) completionHandler(modifiedItem, [], false, NSFileProviderError(.noSuchItem))
return return
} }
let ocId = item.itemIdentifier.rawValue let ocId = item.itemIdentifier.rawValue
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else { guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(ocId, privacy: .public)") Logger.fileProviderExtension.error(
completionHandler(item, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem)) "Could not acquire metadata of item with identifier: \(ocId, privacy: .public)"
)
completionHandler(
item, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
return return
} }
dbManager.setStatusForItemMetadata(metadata, status: NextcloudItemMetadataTable.Status.uploading) { updatedMetadata in dbManager.setStatusForItemMetadata(
metadata, status: NextcloudItemMetadataTable.Status.uploading
) { updatedMetadata in
if updatedMetadata == nil { if updatedMetadata == nil {
Logger.fileProviderExtension.warning("Could not acquire updated metadata of item with identifier: \(ocId, privacy: .public), unable to update item status to uploading") Logger.fileProviderExtension.warning(
"Could not acquire updated metadata of item with identifier: \(ocId, privacy: .public), unable to update item status to uploading"
)
} }
self.ncKit.upload(serverUrlFileName: newServerUrlFileName, self.ncKit.upload(
fileNameLocalPath: fileNameLocalPath, serverUrlFileName: newServerUrlFileName,
requestHandler: { request in fileNameLocalPath: fileNameLocalPath,
progress.setHandlersFromAfRequest(request) requestHandler: { request in
}, taskHandler: { task in progress.setHandlersFromAfRequest(request)
NSFileProviderManager(for: self.domain)?.register(task, forItemWithIdentifier: item.itemIdentifier, completionHandler: { _ in }) },
}, progressHandler: { uploadProgress in taskHandler: { task in
uploadProgress.copyCurrentStateToProgress(progress) NSFileProviderManager(for: self.domain)?.register(
}) { account, ocId, etag, date, size, _, _, error in task, forItemWithIdentifier: item.itemIdentifier,
if error == .success, let ocId = ocId { completionHandler: { _ in })
Logger.fileProviderExtension.info("Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(item.filename, privacy: .public)") },
progressHandler: { uploadProgress in
uploadProgress.copyCurrentStateToProgress(progress)
}
) { account, ocId, etag, date, size, _, _, error in
if error == .success, let ocId {
Logger.fileProviderExtension.info(
"Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(item.filename, privacy: .public)"
)
if size != item.documentSize as? Int64 { if size != item.documentSize as? Int64 {
Logger.fileTransfer.warning("Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(item.documentSize??.int64Value ?? 0))") Logger.fileTransfer.warning(
"Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(item.documentSize??.int64Value ?? 0))"
)
} }
let newMetadata = NextcloudItemMetadataTable() let newMetadata = NextcloudItemMetadataTable()
@ -519,10 +694,15 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata) dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
dbManager.addItemMetadata(newMetadata) dbManager.addItemMetadata(newMetadata)
modifiedItem = FileProviderItem(metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit) modifiedItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit
)
completionHandler(modifiedItem, [], false, nil) completionHandler(modifiedItem, [], false, nil)
} else { } else {
Logger.fileTransfer.error("Could not upload item \(item.itemIdentifier.rawValue, privacy: .public) with filename: \(item.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)") Logger.fileTransfer.error(
"Could not upload item \(item.itemIdentifier.rawValue, privacy: .public) with filename: \(item.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
metadata.status = NextcloudItemMetadataTable.Status.uploadError.rawValue metadata.status = NextcloudItemMetadataTable.Status.uploadError.rawValue
metadata.sessionError = error.errorDescription metadata.sessionError = error.errorDescription
@ -536,19 +716,28 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
} }
} }
} else { } else {
Logger.fileProviderExtension.debug("Nothing more to do with \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public), modifications complete") Logger.fileProviderExtension.debug(
"Nothing more to do with \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public), modifications complete"
)
completionHandler(modifiedItem, [], false, nil) completionHandler(modifiedItem, [], false, nil)
} }
return progress return progress
} }
func deleteItem(identifier: NSFileProviderItemIdentifier, baseVersion version: NSFileProviderItemVersion, options: NSFileProviderDeleteItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (Error?) -> Void) -> Progress {
Logger.fileProviderExtension.debug("Received delete item request for item with identifier: \(identifier.rawValue, privacy: .public)") func deleteItem(
identifier: NSFileProviderItemIdentifier, baseVersion _: NSFileProviderItemVersion,
options _: NSFileProviderDeleteItemOptions = [], request _: NSFileProviderRequest,
completionHandler: @escaping (Error?) -> Void
) -> Progress {
Logger.fileProviderExtension.debug(
"Received delete item request for item with identifier: \(identifier.rawValue, privacy: .public)"
)
guard ncAccount != nil else { guard ncAccount != nil else {
Logger.fileProviderExtension.error("Not deleting item: \(identifier.rawValue, privacy: .public) as account not set up yet") Logger.fileProviderExtension.error(
"Not deleting item: \(identifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(NSFileProviderError(.notAuthenticated)) completionHandler(NSFileProviderError(.notAuthenticated))
return Progress() return Progress()
} }
@ -566,14 +755,18 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
return Progress() return Progress()
} }
self.ncKit.deleteFileOrFolder(serverUrlFileName: serverFileNameUrl) { account, error in ncKit.deleteFileOrFolder(serverUrlFileName: serverFileNameUrl) { _, error in
guard error == .success else { guard error == .success else {
Logger.fileTransfer.error("Could not delete item with ocId \(identifier.rawValue, privacy: .public) at \(serverFileNameUrl, privacy: .public), received error: \(error.errorDescription, privacy: .public)") Logger.fileTransfer.error(
"Could not delete item with ocId \(identifier.rawValue, privacy: .public) at \(serverFileNameUrl, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
completionHandler(error.fileProviderError) completionHandler(error.fileProviderError)
return return
} }
Logger.fileTransfer.info("Successfully deleted item with identifier: \(identifier.rawValue, privacy: .public) at: \(serverFileNameUrl, privacy: .public)") Logger.fileTransfer.info(
"Successfully deleted item with identifier: \(identifier.rawValue, privacy: .public) at: \(serverFileNameUrl, privacy: .public)"
)
if itemMetadata.directory { if itemMetadata.directory {
_ = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: ocId) _ = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: ocId)
@ -589,32 +782,41 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
return Progress() return Progress()
} }
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest) throws -> NSFileProviderEnumerator {
guard let ncAccount = ncAccount else { func enumerator(
Logger.fileProviderExtension.error("Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet as account not set up") for containerItemIdentifier: NSFileProviderItemIdentifier, request _: NSFileProviderRequest
) throws -> NSFileProviderEnumerator {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet as account not set up"
)
throw NSFileProviderError(.notAuthenticated) throw NSFileProviderError(.notAuthenticated)
} }
return FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier, ncAccount: ncAccount, ncKit: ncKit) return FileProviderEnumerator(
enumeratedItemIdentifier: containerItemIdentifier, ncAccount: ncAccount, ncKit: ncKit)
} }
func materializedItemsDidChange(completionHandler: @escaping () -> Void) { func materializedItemsDidChange(completionHandler: @escaping () -> Void) {
guard let ncAccount = self.ncAccount else { guard let ncAccount else {
Logger.fileProviderExtension.error("Not purging stale local file metadatas, account not set up") Logger.fileProviderExtension.error(
"Not purging stale local file metadatas, account not set up")
completionHandler() completionHandler()
return return
} }
guard let fpManager = NSFileProviderManager(for: domain) else { guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error("Could not get file provider manager for domain: \(self.domain.displayName, privacy: .public)") Logger.fileProviderExtension.error(
"Could not get file provider manager for domain: \(self.domain.displayName, privacy: .public)"
)
completionHandler() completionHandler()
return return
} }
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems() let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
let materialisedObserver = FileProviderMaterialisedEnumerationObserver(ncKitAccount: ncAccount.ncKitAccount) { _ in let materialisedObserver = FileProviderMaterialisedEnumerationObserver(
ncKitAccount: ncAccount.ncKitAccount
) { _ in
completionHandler() completionHandler()
} }
let startingPage = NSFileProviderPage(NSFileProviderPage.initialPageSortedByName as Data) let startingPage = NSFileProviderPage(NSFileProviderPage.initialPageSortedByName as Data)
@ -622,9 +824,11 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
materialisedEnumerator.enumerateItems(for: materialisedObserver, startingAt: startingPage) materialisedEnumerator.enumerateItems(for: materialisedObserver, startingAt: startingPage)
} }
func signalEnumerator(completionHandler: @escaping(_ error: Error?) -> Void) { func signalEnumerator(completionHandler: @escaping (_ error: Error?) -> Void) {
guard let fpManager = NSFileProviderManager(for: self.domain) else { guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.fileProviderExtension.error("Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts.") Logger.fileProviderExtension.error(
"Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts."
)
return return
} }

View File

@ -13,11 +13,10 @@
*/ */
import FileProvider import FileProvider
import UniformTypeIdentifiers
import NextcloudKit import NextcloudKit
import UniformTypeIdentifiers
class FileProviderItem: NSObject, NSFileProviderItem { class FileProviderItem: NSObject, NSFileProviderItem {
enum FileProviderItemTransferError: Error { enum FileProviderItemTransferError: Error {
case downloadError case downloadError
case uploadError case uploadError
@ -26,71 +25,78 @@ class FileProviderItem: NSObject, NSFileProviderItem {
let metadata: NextcloudItemMetadataTable let metadata: NextcloudItemMetadataTable
let parentItemIdentifier: NSFileProviderItemIdentifier let parentItemIdentifier: NSFileProviderItemIdentifier
let ncKit: NextcloudKit let ncKit: NextcloudKit
var itemIdentifier: NSFileProviderItemIdentifier { var itemIdentifier: NSFileProviderItemIdentifier {
return NSFileProviderItemIdentifier(metadata.ocId) NSFileProviderItemIdentifier(metadata.ocId)
} }
var capabilities: NSFileProviderItemCapabilities { var capabilities: NSFileProviderItemCapabilities {
guard !metadata.directory else { guard !metadata.directory else {
return [ .allowsAddingSubItems, return [
.allowsContentEnumerating, .allowsAddingSubItems,
.allowsReading, .allowsContentEnumerating,
.allowsDeleting, .allowsReading,
.allowsRenaming ] .allowsDeleting,
.allowsRenaming,
]
} }
guard !metadata.lock else { guard !metadata.lock else {
return [ .allowsReading ] return [.allowsReading]
} }
return [ .allowsWriting, return [
.allowsReading, .allowsWriting,
.allowsDeleting, .allowsReading,
.allowsRenaming, .allowsDeleting,
.allowsReparenting ] .allowsRenaming,
.allowsReparenting,
]
} }
var itemVersion: NSFileProviderItemVersion { var itemVersion: NSFileProviderItemVersion {
NSFileProviderItemVersion(contentVersion: metadata.etag.data(using: .utf8)!, NSFileProviderItemVersion(
metadataVersion: metadata.etag.data(using: .utf8)!) contentVersion: metadata.etag.data(using: .utf8)!,
metadataVersion: metadata.etag.data(using: .utf8)!)
} }
var filename: String { var filename: String {
return metadata.fileNameView metadata.fileNameView
} }
var contentType: UTType { var contentType: UTType {
if self.itemIdentifier == .rootContainer || metadata.directory { if itemIdentifier == .rootContainer || metadata.directory {
return .folder return .folder
} }
let internalType = ncKit.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, let internalType = ncKit.nkCommonInstance.getInternalType(
mimeType: "", fileName: metadata.fileNameView,
directory: metadata.directory) mimeType: "",
directory: metadata.directory)
return UTType(filenameExtension: internalType.ext) ?? .content return UTType(filenameExtension: internalType.ext) ?? .content
} }
var documentSize: NSNumber? { var documentSize: NSNumber? {
return NSNumber(value: metadata.size) NSNumber(value: metadata.size)
} }
var creationDate: Date? { var creationDate: Date? {
return metadata.creationDate as Date metadata.creationDate as Date
} }
var lastUsedDate: Date? { var lastUsedDate: Date? {
return metadata.date as Date metadata.date as Date
} }
var contentModificationDate: Date? { var contentModificationDate: Date? {
return metadata.date as Date metadata.date as Date
} }
var isDownloaded: Bool { var isDownloaded: Bool {
return metadata.directory || NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil metadata.directory
|| NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
} }
var isDownloading: Bool { var isDownloading: Bool {
return metadata.status == NextcloudItemMetadataTable.Status.downloading.rawValue metadata.status == NextcloudItemMetadataTable.Status.downloading.rawValue
} }
var downloadingError: Error? { var downloadingError: Error? {
@ -101,30 +107,36 @@ class FileProviderItem: NSObject, NSFileProviderItem {
} }
var isUploaded: Bool { var isUploaded: Bool {
return NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
} }
var isUploading: Bool { var isUploading: Bool {
return metadata.status == NextcloudItemMetadataTable.Status.uploading.rawValue metadata.status == NextcloudItemMetadataTable.Status.uploading.rawValue
} }
var uploadingError: Error? { var uploadingError: Error? {
if metadata.status == NextcloudItemMetadataTable.Status.uploadError.rawValue { if metadata.status == NextcloudItemMetadataTable.Status.uploadError.rawValue {
return FileProviderItemTransferError.uploadError FileProviderItemTransferError.uploadError
} else { } else {
return nil nil
} }
} }
var childItemCount: NSNumber? { var childItemCount: NSNumber? {
if metadata.directory { if metadata.directory {
return NSNumber(integerLiteral: NextcloudFilesDatabaseManager.shared.childItemsForDirectory(metadata).count) NSNumber(
integerLiteral: NextcloudFilesDatabaseManager.shared.childItemsForDirectory(
metadata
).count)
} else { } else {
return nil nil
} }
} }
required init(metadata: NextcloudItemMetadataTable, parentItemIdentifier: NSFileProviderItemIdentifier, ncKit: NextcloudKit) { required init(
metadata: NextcloudItemMetadataTable, parentItemIdentifier: NSFileProviderItemIdentifier,
ncKit: NextcloudKit
) {
self.metadata = metadata self.metadata = metadata
self.parentItemIdentifier = parentItemIdentifier self.parentItemIdentifier = parentItemIdentifier
self.ncKit = ncKit self.ncKit = ncKit

View File

@ -12,44 +12,53 @@
* for more details. * for more details.
*/ */
import Foundation
import FileProvider import FileProvider
import Foundation
import OSLog import OSLog
class FileProviderMaterialisedEnumerationObserver : NSObject, NSFileProviderEnumerationObserver { class FileProviderMaterialisedEnumerationObserver: NSObject, NSFileProviderEnumerationObserver {
let ncKitAccount: String let ncKitAccount: String
let completionHandler: (_ deletedOcIds: Set<String>) -> Void let completionHandler: (_ deletedOcIds: Set<String>) -> Void
var allEnumeratedItemIds: Set<String> = Set<String>() var allEnumeratedItemIds: Set<String> = .init()
required init(ncKitAccount: String, completionHandler: @escaping(_ deletedOcIds: Set<String>) -> Void) { required init(
ncKitAccount: String, completionHandler: @escaping (_ deletedOcIds: Set<String>) -> Void
) {
self.ncKitAccount = ncKitAccount self.ncKitAccount = ncKitAccount
self.completionHandler = completionHandler self.completionHandler = completionHandler
super.init() super.init()
} }
func didEnumerate(_ updatedItems: [NSFileProviderItemProtocol]) { func didEnumerate(_ updatedItems: [NSFileProviderItemProtocol]) {
let updatedItemsIds = Array(updatedItems.map { $0.itemIdentifier.rawValue }) let updatedItemsIds = Array(updatedItems.map(\.itemIdentifier.rawValue))
for updatedItemsId in updatedItemsIds { for updatedItemsId in updatedItemsIds {
allEnumeratedItemIds.insert(updatedItemsId) allEnumeratedItemIds.insert(updatedItemsId)
} }
} }
func finishEnumerating(upTo nextPage: NSFileProviderPage?) { func finishEnumerating(upTo _: NSFileProviderPage?) {
Logger.materialisedFileHandling.debug("Handling enumerated materialised items.") Logger.materialisedFileHandling.debug("Handling enumerated materialised items.")
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(self.allEnumeratedItemIds, FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(
account: self.ncKitAccount, allEnumeratedItemIds,
completionHandler: self.completionHandler) account: ncKitAccount,
completionHandler: completionHandler)
} }
func finishEnumeratingWithError(_ error: Error) { func finishEnumeratingWithError(_ error: Error) {
Logger.materialisedFileHandling.error("Ran into error when enumerating materialised items: \(error.localizedDescription, privacy: .public). Handling items enumerated so far") Logger.materialisedFileHandling.error(
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(self.allEnumeratedItemIds, "Ran into error when enumerating materialised items: \(error.localizedDescription, privacy: .public). Handling items enumerated so far"
account: self.ncKitAccount, )
completionHandler: self.completionHandler) FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(
allEnumeratedItemIds,
account: ncKitAccount,
completionHandler: completionHandler)
} }
static func handleEnumeratedItems(_ itemIds: Set<String>, account: String, completionHandler: @escaping(_ deletedOcIds: Set<String>) -> Void) { static func handleEnumeratedItems(
_ itemIds: Set<String>, account: String,
completionHandler: @escaping (_ deletedOcIds: Set<String>) -> Void
) {
let dbManager = NextcloudFilesDatabaseManager.shared let dbManager = NextcloudFilesDatabaseManager.shared
let databaseLocalFileMetadatas = dbManager.localFileMetadatas(account: account) let databaseLocalFileMetadatas = dbManager.localFileMetadatas(account: account)
var noLongerMaterialisedIds = Set<String>() var noLongerMaterialisedIds = Set<String>()
@ -60,12 +69,13 @@ class FileProviderMaterialisedEnumerationObserver : NSObject, NSFileProviderEnum
guard itemIds.contains(localFileOcId) else { guard itemIds.contains(localFileOcId) else {
noLongerMaterialisedIds.insert(localFileOcId) noLongerMaterialisedIds.insert(localFileOcId)
continue; continue
} }
} }
DispatchQueue.main.async { DispatchQueue.main.async {
Logger.materialisedFileHandling.info("Cleaning up local file metadatas for unmaterialised items") Logger.materialisedFileHandling.info(
"Cleaning up local file metadatas for unmaterialised items")
for itemId in noLongerMaterialisedIds { for itemId in noLongerMaterialisedIds {
dbManager.deleteLocalFileMetadata(ocId: itemId) dbManager.deleteLocalFileMetadata(ocId: itemId)
} }

View File

@ -24,25 +24,27 @@ class FileProviderSocketLineProcessor: NSObject, LineProcessor {
} }
func process(_ line: String) { func process(_ line: String) {
if (line.contains("~")) { // We use this as the separator specifically in ACCOUNT_DETAILS if line.contains("~") { // We use this as the separator specifically in ACCOUNT_DETAILS
Logger.desktopClientConnection.debug("Processing file provider line with potentially sensitive user data") Logger.desktopClientConnection.debug(
"Processing file provider line with potentially sensitive user data")
} else { } else {
Logger.desktopClientConnection.debug("Processing file provider line: \(line, privacy: .public)") Logger.desktopClientConnection.debug(
"Processing file provider line: \(line, privacy: .public)")
} }
let splitLine = line.split(separator: ":", maxSplits: 1) let splitLine = line.split(separator: ":", maxSplits: 1)
guard let commandSubsequence = splitLine.first else { guard let commandSubsequence = splitLine.first else {
Logger.desktopClientConnection.error("Input line did not have a first element") Logger.desktopClientConnection.error("Input line did not have a first element")
return; return
} }
let command = String(commandSubsequence); let command = String(commandSubsequence)
Logger.desktopClientConnection.debug("Received command: \(command, privacy: .public)") Logger.desktopClientConnection.debug("Received command: \(command, privacy: .public)")
if (command == "SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER") { if command == "SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER" {
delegate.sendFileProviderDomainIdentifier() delegate.sendFileProviderDomainIdentifier()
} else if (command == "ACCOUNT_NOT_AUTHENTICATED") { } else if command == "ACCOUNT_NOT_AUTHENTICATED" {
delegate.removeAccountConfig() delegate.removeAccountConfig()
} else if (command == "ACCOUNT_DETAILS") { } else if command == "ACCOUNT_DETAILS" {
guard let accountDetailsSubsequence = splitLine.last else { return } guard let accountDetailsSubsequence = splitLine.last else { return }
let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 2) let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 2)

View File

@ -12,17 +12,22 @@
* for more details. * for more details.
*/ */
import Foundation
import FileProvider import FileProvider
import Foundation
import OSLog import OSLog
func pathForAppGroupContainer() -> URL? { func pathForAppGroupContainer() -> URL? {
guard let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String else { guard
Logger.localFileOps.critical("Could not get container url as missing SocketApiPrefix info in app Info.plist") let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix")
as? String
else {
Logger.localFileOps.critical(
"Could not get container url as missing SocketApiPrefix info in app Info.plist")
return nil return nil
} }
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupIdentifier)
} }
func pathForFileProviderExtData() -> URL? { func pathForFileProviderExtData() -> URL? {
@ -32,7 +37,9 @@ func pathForFileProviderExtData() -> URL? {
func pathForFileProviderTempFilesForDomain(_ domain: NSFileProviderDomain) throws -> URL? { func pathForFileProviderTempFilesForDomain(_ domain: NSFileProviderDomain) throws -> URL? {
guard let fpManager = NSFileProviderManager(for: domain) else { guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.localFileOps.error("Unable to get file provider manager for domain: \(domain.displayName, privacy: .public)") Logger.localFileOps.error(
"Unable to get file provider manager for domain: \(domain.displayName, privacy: .public)"
)
throw NSFileProviderError(.providerNotFound) throw NSFileProviderError(.providerNotFound)
} }
@ -40,9 +47,13 @@ func pathForFileProviderTempFilesForDomain(_ domain: NSFileProviderDomain) throw
return fileProviderDataUrl.appendingPathComponent("TemporaryNextcloudFiles/") return fileProviderDataUrl.appendingPathComponent("TemporaryNextcloudFiles/")
} }
func localPathForNCFile(ocId: String, fileNameView: String, domain: NSFileProviderDomain) throws -> URL { func localPathForNCFile(ocId _: String, fileNameView: String, domain: NSFileProviderDomain) throws
-> URL
{
guard let fileProviderFilesPathUrl = try pathForFileProviderTempFilesForDomain(domain) else { guard let fileProviderFilesPathUrl = try pathForFileProviderTempFilesForDomain(domain) else {
Logger.localFileOps.error("Unable to get path for file provider temp files for domain: \(domain.displayName, privacy: .public)") Logger.localFileOps.error(
"Unable to get path for file provider temp files for domain: \(domain.displayName, privacy: .public)"
)
throw URLError(.badURL) throw URLError(.badURL)
} }

View File

@ -12,21 +12,20 @@
* for more details. * for more details.
*/ */
import Foundation
import FileProvider import FileProvider
import Foundation
class NextcloudAccount: NSObject { class NextcloudAccount: NSObject {
static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/" static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/"
let username, password, ncKitAccount, serverUrl, davFilesUrl: String let username, password, ncKitAccount, serverUrl, davFilesUrl: String
init(user: String, serverUrl: String, password: String) { init(user: String, serverUrl: String, password: String) {
self.username = user username = user
self.password = password self.password = password
self.ncKitAccount = user + " " + serverUrl ncKitAccount = user + " " + serverUrl
self.serverUrl = serverUrl self.serverUrl = serverUrl
self.davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user
super.init() super.init()
} }
} }