Merge pull request #6642 from nextcloud/bugfix/file-provider-testing

Make use of NextcloudFileProviderKit in File Provider Module
This commit is contained in:
Claudio Cambra 2024-04-17 16:21:16 +08:00 committed by GitHub
commit aeca31af97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 177 additions and 3230 deletions

View File

@ -1,195 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import OSLog
extension NextcloudFilesDatabaseManager {
func directoryMetadata(account: String, serverUrl: String) -> NextcloudItemMetadataTable? {
// 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 placeholderSeparator = "__TEMP_REPLACE__"
let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(
of: problematicSeparator, with: placeholderSeparator)
var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
let directoryItemFileName = String(splitServerUrl.removeLast())
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 {
return NextcloudItemMetadataTable(value: metadata)
}
return nil
}
func childItemsForDirectory(_ directoryMetadata: NextcloudItemMetadataTable)
-> [NextcloudItemMetadataTable]
{
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"serverUrl BEGINSWITH %@", directoryServerUrl)
return sortedItemMetadatas(metadatas)
}
func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable)
-> [NextcloudItemMetadataTable]
{
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"serverUrl BEGINSWITH %@ AND directory == true", directoryServerUrl)
return sortedItemMetadatas(metadatas)
}
func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable)
-> NextcloudItemMetadataTable?
{
directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
}
func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@ AND directory == true", ocId
).first {
return NextcloudItemMetadataTable(value: metadata)
}
return nil
}
func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND directory == true", account)
return sortedItemMetadatas(metadatas)
}
func directoryMetadatas(account: String, parentDirectoryServerUrl: String)
-> [NextcloudItemMetadataTable]
{
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account,
parentDirectoryServerUrl)
return sortedItemMetadatas(metadatas)
}
// Deletes all metadatas related to the info of the directory provided
func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? {
let database = ncDatabase()
guard
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
}
let directoryMetadataCopy = NextcloudItemMetadataTable(value: directoryMetadata)
let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
let directoryAccount = directoryMetadata.account
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)"
)
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)"
)
return nil
}
var deletedMetadatas: [NextcloudItemMetadataTable] = [directoryMetadataCopy]
let results = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryAccount, directoryUrlPath)
for result in results {
let successfulItemMetadataDelete = deleteItemMetadata(ocId: result.ocId)
if successfulItemMetadataDelete {
deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
}
if localFileMetadataFromOcId(result.ocId) != nil {
deleteLocalFileMetadata(ocId: result.ocId)
}
}
Logger.ncFilesDatabase.debug(
"Completed deletions in directory recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)"
)
return deletedMetadatas
}
func renameDirectoryAndPropagateToChildren(
ocId: String, newServerUrl: String, newFileName: String
) -> [NextcloudItemMetadataTable]? {
let database = ncDatabase()
guard
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
}
let oldItemServerUrl = directoryMetadata.serverUrl
let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName
let newDirectoryServerUrl = newServerUrl + "/" + newFileName
let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account,
oldDirectoryServerUrl)
renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
Logger.ncFilesDatabase.debug("Renamed root renaming directory")
do {
try database.write {
for childItem in childItemResults {
let oldServerUrl = childItem.serverUrl
let movedServerUrl = oldServerUrl.replacingOccurrences(
of: oldDirectoryServerUrl, with: newDirectoryServerUrl)
childItem.serverUrl = movedServerUrl
database.add(childItem, update: .all)
Logger.ncFilesDatabase.debug(
"Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
}
}
} catch {
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
}
let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account,
newDirectoryServerUrl)
return sortedItemMetadatas(updatedChildItemResults)
}
}

View File

@ -1,105 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import OSLog
import RealmSwift
extension NextcloudFilesDatabaseManager {
func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter(
"ocId == %@", ocId
).first {
return NextcloudLocalFileMetadataTable(value: metadata)
}
return nil
}
func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
let database = ncDatabase()
do {
try database.write {
let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
newLocalFileMetadata.ocId = itemMetadata.ocId
newLocalFileMetadata.fileName = itemMetadata.fileName
newLocalFileMetadata.account = itemMetadata.account
newLocalFileMetadata.etag = itemMetadata.etag
newLocalFileMetadata.exifDate = Date()
newLocalFileMetadata.exifLatitude = "-1"
newLocalFileMetadata.exifLongitude = "-1"
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)"
)
}
} 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)"
)
}
}
func deleteLocalFileMetadata(ocId: String) {
let database = ncDatabase()
do {
try database.write {
let results = database.objects(NextcloudLocalFileMetadataTable.self).filter(
"ocId == %@", ocId)
database.delete(results)
}
} catch {
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]
{
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
}
func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter(
"account == %@", account)
return sortedLocalFileMetadatas(results)
}
func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let localFileMetadatas = localFileMetadatas(account: account)
let localFileMetadatasOcIds = Array(localFileMetadatas.map(\.ocId))
var itemMetadatas: [NextcloudItemMetadataTable] = []
for ocId in localFileMetadatasOcIds {
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)"
)
continue
}
itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
}
return itemMetadatas
}
}

View File

@ -1,427 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import NextcloudKit
import OSLog
import RealmSwift
class NextcloudFilesDatabaseManager: NSObject {
static let shared = NextcloudFilesDatabaseManager()
let relativeDatabaseFolderPath = "Database/"
let databaseFilename = "fileproviderextdatabase.realm"
let relativeDatabaseFilePath: String
var databasePath: URL?
let schemaVersion: UInt64 = 100
override init() {
relativeDatabaseFilePath = relativeDatabaseFolderPath + databaseFilename
guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
super.init()
return
}
databasePath = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFilePath)
// 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
let dbFolder = fileProviderDataDirUrl.appendingPathComponent(relativeDatabaseFolderPath)
let dbFolderPath = dbFolder.path
do {
try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
try FileManager.default.setAttributes(
[
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(
fileURL: databasePath,
schemaVersion: schemaVersion,
objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
)
Realm.Configuration.defaultConfiguration = config
do {
_ = try Realm()
Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
} catch let error as NSError {
Logger.ncFilesDatabase.error(
"Error opening Realm db: \(error.localizedDescription, privacy: .public)")
}
super.init()
}
func ncDatabase() -> Realm {
let realm = try! Realm()
realm.refresh()
return realm
}
func anyItemMetadatasForAccount(_ account: String) -> Bool {
!ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
.isEmpty
}
func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
// 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
if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId
).first {
return NextcloudItemMetadataTable(value: itemMetadata)
}
return nil
}
func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>)
-> [NextcloudItemMetadataTable]
{
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
}
func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@", account)
return sortedItemMetadatas(metadatas)
}
func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@", account, serverUrl)
return sortedItemMetadatas(metadatas)
}
func itemMetadatas(
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)
}
func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier)
-> NextcloudItemMetadataTable?
{
let ocId = identifier.rawValue
return itemMetadataFromOcId(ocId)
}
private func processItemMetadatasToDelete(
existingMetadatas: Results<NextcloudItemMetadataTable>,
updatedMetadatas: [NextcloudItemMetadataTable]
) -> [NextcloudItemMetadataTable] {
var deletedMetadatas: [NextcloudItemMetadataTable] = []
for existingMetadata in existingMetadatas {
guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId)
else { continue }
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)"
)
}
return deletedMetadatas
}
private func processItemMetadatasToUpdate(
existingMetadatas: Results<NextcloudItemMetadataTable>,
updatedMetadatas: [NextcloudItemMetadataTable],
updateDirectoryEtags: Bool
) -> (
newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable],
directoriesNeedingRename: [NextcloudItemMetadataTable]
) {
var returningNewMetadatas: [NextcloudItemMetadataTable] = []
var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
for updatedMetadata in updatedMetadatas {
if let existingMetadata = existingMetadatas.first(where: {
$0.ocId == updatedMetadata.ocId
}) {
if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue,
!existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata)
{
if updatedMetadata.directory {
if updatedMetadata.serverUrl != existingMetadata.serverUrl
|| updatedMetadata.fileName != existingMetadata.fileName
{
directoriesNeedingRename.append(
NextcloudItemMetadataTable(value: updatedMetadata))
updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
} else if !updateDirectoryEtags {
updatedMetadata.etag = existingMetadata.etag
}
}
returningUpdatedMetadatas.append(updatedMetadata)
Logger.ncFilesDatabase.debug(
"Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)"
)
} 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)"
)
}
} else { // This is a new metadata
if !updateDirectoryEtags, updatedMetadata.directory {
updatedMetadata.etag = ""
}
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)"
)
}
}
return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
}
func updateItemMetadatas(
account: String,
serverUrl: String,
updatedMetadatas: [NextcloudItemMetadataTable],
updateDirectoryEtags: Bool
) -> (
newMetadatas: [NextcloudItemMetadataTable]?,
updatedMetadatas: [NextcloudItemMetadataTable]?,
deletedMetadatas: [NextcloudItemMetadataTable]?
) {
let database = ncDatabase()
do {
let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter(
"account == %@ AND serverUrl == %@ AND status == %@",
account,
serverUrl,
NextcloudItemMetadataTable.Status.normal.rawValue)
let metadatasToDelete = processItemMetadatasToDelete(
existingMetadatas: existingMetadatas,
updatedMetadatas: updatedMetadatas)
let metadatasToChange = processItemMetadatasToUpdate(
existingMetadatas: existingMetadatas,
updatedMetadatas: updatedMetadatas,
updateDirectoryEtags: updateDirectoryEtags)
var metadatasToUpdate = metadatasToChange.updatedMetadatas
let metadatasToCreate = metadatasToChange.newMetadatas
let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
let metadatasToAdd =
Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) })
+ Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
for metadata in directoriesNeedingRename {
if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(
ocId: metadata.ocId,
newServerUrl: metadata.serverUrl,
newFileName: metadata.fileName)
{
metadatasToUpdate += updatedDirectoryChildren
}
}
try database.write {
for metadata in metadatasToDelete {
// Can't pass copies, we need the originals from the database
database.delete(
ncDatabase().objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", metadata.ocId))
}
for metadata in metadatasToAdd {
database.add(metadata, update: .all)
}
}
return (
newMetadatas: metadatasToCreate,
updatedMetadatas: metadatasToUpdate,
deletedMetadatas: metadatasToDelete
)
} catch {
Logger.ncFilesDatabase.error(
"Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)"
)
return (nil, nil, nil)
}
}
func setStatusForItemMetadata(
_ metadata: NextcloudItemMetadataTable,
status: NextcloudItemMetadataTable.Status,
completionHandler: @escaping (_ updatedMetadata: NextcloudItemMetadataTable?) -> Void
) {
let database = ncDatabase()
do {
try database.write {
guard
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
}
result.status = status.rawValue
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)"
)
completionHandler(NextcloudItemMetadataTable(value: result))
}
} 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)"
)
completionHandler(nil)
}
}
func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
let database = ncDatabase()
do {
try database.write {
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)"
)
}
} 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)"
)
}
}
@discardableResult func deleteItemMetadata(ocId: String) -> Bool {
let database = ncDatabase()
do {
try database.write {
let results = database.objects(NextcloudItemMetadataTable.self).filter(
"ocId == %@", ocId)
Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
database.delete(results)
}
return true
} catch {
Logger.ncFilesDatabase.error(
"Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
)
return false
}
}
func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
let database = ncDatabase()
do {
try database.write {
guard
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
}
let oldFileName = itemMetadata.fileName
let oldServerUrl = itemMetadata.serverUrl
itemMetadata.fileName = newFileName
itemMetadata.fileNameView = newFileName
itemMetadata.serverUrl = newServerUrl
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)"
)
}
} 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)"
)
}
}
func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable)
-> NSFileProviderItemIdentifier?
{
let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
if metadata.serverUrl == homeServerFilesUrl {
return .rootContainer
}
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)"
)
return nil
}
if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.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)"
)
return nil
}
}

View File

@ -1,137 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import NextcloudKit
extension NextcloudItemMetadataTable {
static func fromNKFile(_ file: NKFile, account: String) -> NextcloudItemMetadataTable {
let metadata = NextcloudItemMetadataTable()
metadata.account = account
metadata.checksums = file.checksums
metadata.commentsUnread = file.commentsUnread
metadata.contentType = file.contentType
if let date = file.creationDate {
metadata.creationDate = date as Date
} else {
metadata.creationDate = file.date as Date
}
metadata.dataFingerprint = file.dataFingerprint
metadata.date = file.date as Date
metadata.directory = file.directory
metadata.downloadURL = file.downloadURL
metadata.e2eEncrypted = file.e2eEncrypted
metadata.etag = file.etag
metadata.favorite = file.favorite
metadata.fileId = file.fileId
metadata.fileName = file.fileName
metadata.fileNameView = file.fileName
metadata.hasPreview = file.hasPreview
metadata.iconName = file.iconName
metadata.mountType = file.mountType
metadata.name = file.name
metadata.note = file.note
metadata.ocId = file.ocId
metadata.ownerId = file.ownerId
metadata.ownerDisplayName = file.ownerDisplayName
metadata.lock = file.lock
metadata.lockOwner = file.lockOwner
metadata.lockOwnerEditor = file.lockOwnerEditor
metadata.lockOwnerType = file.lockOwnerType
metadata.lockOwnerDisplayName = file.lockOwnerDisplayName
metadata.lockTime = file.lockTime
metadata.lockTimeOut = file.lockTimeOut
metadata.path = file.path
metadata.permissions = file.permissions
metadata.quotaUsedBytes = file.quotaUsedBytes
metadata.quotaAvailableBytes = file.quotaAvailableBytes
metadata.richWorkspace = file.richWorkspace
metadata.resourceType = file.resourceType
metadata.serverUrl = file.serverUrl
metadata.sharePermissionsCollaborationServices = file.sharePermissionsCollaborationServices
for element in file.sharePermissionsCloudMesh {
metadata.sharePermissionsCloudMesh.append(element)
}
for element in file.shareType {
metadata.shareType.append(element)
}
metadata.size = file.size
metadata.classFile = file.classFile
// 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
{
metadata.classFile = NKCommon.TypeClassFile.document.rawValue
}
if let date = file.uploadDate {
metadata.uploadDate = date as Date
} else {
metadata.uploadDate = file.date as Date
}
metadata.urlBase = file.urlBase
metadata.user = file.user
metadata.userId = file.userId
// Support for finding the correct filename for e2ee files should go here
return metadata
}
static func metadatasFromDirectoryReadNKFiles(
_ files: [NKFile],
account: String,
completionHandler: @escaping (
_ directoryMetadata: NextcloudItemMetadataTable,
_ childDirectoriesMetadatas: [NextcloudItemMetadataTable],
_ metadatas: [NextcloudItemMetadataTable]
) -> Void
) {
var directoryMetadataSet = false
var directoryMetadata = NextcloudItemMetadataTable()
var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = []
var metadatas: [NextcloudItemMetadataTable] = []
let conversionQueue = DispatchQueue(
label: "nkFileToMetadataConversionQueue",
qos: .userInitiated,
attributes: .concurrent)
// appendQueue is a serial queue, not concurrent
let appendQueue = DispatchQueue(label: "metadataAppendQueue", qos: .userInitiated)
let dispatchGroup = DispatchGroup()
for file in files {
if metadatas.isEmpty, !directoryMetadataSet {
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
directoryMetadata = metadata
directoryMetadataSet = true
} else {
conversionQueue.async(group: dispatchGroup) {
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
appendQueue.async(group: dispatchGroup) {
metadatas.append(metadata)
if metadata.directory {
childDirectoriesMetadatas.append(metadata)
}
}
}
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completionHandler(directoryMetadata, childDirectoriesMetadatas, metadatas)
}
}
}

View File

@ -1,219 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import NextcloudKit
import RealmSwift
class NextcloudItemMetadataTable: Object {
enum Status: Int {
case downloadError = -4
case downloading = -3
case inDownload = -2
case waitDownload = -1
case normal = 0
case waitUpload = 1
case inUpload = 2
case uploading = 3
case uploadError = 4
}
enum SharePermissions: Int {
case readShare = 1
case updateShare = 2
case createShare = 4
case deleteShare = 8
case shareShare = 16
case maxFileShare = 19
case maxFolderShare = 31
}
@Persisted(primaryKey: true) var ocId: String
@Persisted var account = ""
@Persisted var assetLocalIdentifier = ""
@Persisted var checksums = ""
@Persisted var chunk: Bool = false
@Persisted var classFile = ""
@Persisted var commentsUnread: Bool = false
@Persisted var contentType = ""
@Persisted var creationDate = Date()
@Persisted var dataFingerprint = ""
@Persisted var date = Date()
@Persisted var directory: Bool = false
@Persisted var deleteAssetLocalIdentifier: Bool = false
@Persisted var downloadURL = ""
@Persisted var e2eEncrypted: Bool = false
@Persisted var edited: Bool = false
@Persisted var etag = ""
@Persisted var etagResource = ""
@Persisted var favorite: Bool = false
@Persisted var fileId = ""
@Persisted var fileName = ""
@Persisted var fileNameView = ""
@Persisted var hasPreview: Bool = false
@Persisted var iconName = ""
@Persisted var iconUrl = ""
@Persisted var isExtractFile: Bool = false
@Persisted var livePhoto: Bool = false
@Persisted var mountType = ""
@Persisted var name = "" // for unifiedSearch is the provider.id
@Persisted var note = ""
@Persisted var ownerId = ""
@Persisted var ownerDisplayName = ""
@Persisted var lock = false
@Persisted var lockOwner = ""
@Persisted var lockOwnerEditor = ""
@Persisted var lockOwnerType = 0
@Persisted var lockOwnerDisplayName = ""
@Persisted var lockTime: Date?
@Persisted var lockTimeOut: Date?
@Persisted var path = ""
@Persisted var permissions = ""
@Persisted var quotaUsedBytes: Int64 = 0
@Persisted var quotaAvailableBytes: Int64 = 0
@Persisted var resourceType = ""
@Persisted var richWorkspace: String?
@Persisted var serverUrl = "" // For parent directory!!
@Persisted var session = ""
@Persisted var sessionError = ""
@Persisted var sessionSelector = ""
@Persisted var sessionTaskIdentifier: Int = 0
@Persisted var sharePermissionsCollaborationServices: Int = 0
// TODO: Find a way to compare these two below in remote state check
let sharePermissionsCloudMesh = List<String>()
let shareType = List<Int>()
@Persisted var size: Int64 = 0
@Persisted var status: Int = 0
@Persisted var subline: String?
@Persisted var trashbinFileName = ""
@Persisted var trashbinOriginalLocation = ""
@Persisted var trashbinDeletionTime = Date()
@Persisted var uploadDate = Date()
@Persisted var url = ""
@Persisted var urlBase = ""
@Persisted var user = ""
@Persisted var userId = ""
var fileExtension: String {
(fileNameView as NSString).pathExtension
}
var fileNoExtension: String {
(fileNameView as NSString).deletingPathExtension
}
var isRenameable: Bool {
lock
}
var isPrintable: Bool {
if isDocumentViewableOnly {
return false
}
if ["application/pdf", "com.adobe.pdf"].contains(contentType)
|| contentType.hasPrefix("text/")
|| classFile == NKCommon.TypeClassFile.image.rawValue
{
return true
}
return false
}
var isDocumentViewableOnly: Bool {
sharePermissionsCollaborationServices == SharePermissions.readShare.rawValue
&& classFile == NKCommon.TypeClassFile.document.rawValue
}
var isCopyableInPasteboard: Bool {
!isDocumentViewableOnly && !directory
}
var isModifiableWithQuickLook: Bool {
if directory || isDocumentViewableOnly {
return false
}
return contentType == "com.adobe.pdf" || contentType == "application/pdf"
|| classFile == NKCommon.TypeClassFile.image.rawValue
}
var isSettableOnOffline: Bool {
session.isEmpty && !isDocumentViewableOnly
}
var canOpenIn: Bool {
session.isEmpty && !isDocumentViewableOnly && !directory
}
var isDownloadUpload: Bool {
status == Status.inDownload.rawValue || status == Status.downloading.rawValue
|| status == Status.inUpload.rawValue || status == Status.uploading.rawValue
}
var isDownload: Bool {
status == Status.inDownload.rawValue || status == Status.downloading.rawValue
}
var isUpload: Bool {
status == Status.inUpload.rawValue || status == Status.uploading.rawValue
}
override func isEqual(_ object: Any?) -> Bool {
if let object = object as? NextcloudItemMetadataTable {
return fileId == object.fileId && account == object.account && path == object.path
&& fileName == object.fileName
}
return false
}
func isInSameDatabaseStoreableRemoteState(_ comparingMetadata: NextcloudItemMetadataTable)
-> Bool
{
comparingMetadata.etag == etag
&& comparingMetadata.fileNameView == fileNameView
&& comparingMetadata.date == date
&& comparingMetadata.permissions == permissions
&& comparingMetadata.hasPreview == hasPreview
&& comparingMetadata.note == note
&& comparingMetadata.lock == lock
&& comparingMetadata.sharePermissionsCollaborationServices
== sharePermissionsCollaborationServices
&& comparingMetadata.favorite == favorite
}
/// 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 {
!lock || (lockOwner == user && lockOwnerType == 0)
}
func thumbnailUrl(size: CGSize) -> URL? {
guard hasPreview else {
return nil
}
let urlBase = urlBase.urlEncoded!
// Leave the leading slash in webdavUrl
let webdavUrl = urlBase + NextcloudAccount.webDavFilesUrlSuffix + user
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"
return URL(string: urlString)
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Foundation
import RealmSwift
class NextcloudLocalFileMetadataTable: Object {
@Persisted(primaryKey: true) var ocId: String
@Persisted var account = ""
@Persisted var etag = ""
@Persisted var exifDate: Date?
@Persisted var exifLatitude = ""
@Persisted var exifLongitude = ""
@Persisted var exifLensModel: String?
@Persisted var favorite: Bool = false
@Persisted var fileName = ""
@Persisted var offline: Bool = false
}

View File

@ -20,17 +20,9 @@ extension Logger {
static let desktopClientConnection = Logger(
subsystem: subsystem, category: "desktopclientconnection")
static let fpUiExtensionService = Logger(subsystem: subsystem, category: "fpUiExtensionService")
static let enumeration = Logger(subsystem: subsystem, category: "enumeration")
static let fileProviderExtension = Logger(
subsystem: subsystem, category: "fileproviderextension")
static let fileTransfer = Logger(subsystem: subsystem, category: "filetransfer")
static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations")
static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase")
static let shares = Logger(subsystem: subsystem, category: "shares")
static let ncAccount = Logger(subsystem: subsystem, category: "ncAccount")
static let materialisedFileHandling = Logger(
subsystem: subsystem, category: "materialisedfilehandling"
)
static let logger = Logger(subsystem: subsystem, category: "logger")
@available(macOSApplicationExtension 12.0, *)

View File

@ -1,60 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import NextcloudKit
extension NKError {
static var noChangesErrorCode: Int {
-200
}
var isCouldntConnectError: Bool {
errorCode == -9999 || errorCode == -1001 || errorCode == -1004 || errorCode == -1005
|| errorCode == -1009 || errorCode == -1012 || errorCode == -1200 || errorCode == -1202
|| errorCode == 500 || errorCode == 503 || errorCode == 200
}
var isUnauthenticatedError: Bool {
errorCode == -1013
}
var isGoingOverQuotaError: Bool {
errorCode == 507
}
var isNotFoundError: Bool {
errorCode == 404
}
var isNoChangesError: Bool {
errorCode == NKError.noChangesErrorCode
}
var fileProviderError: NSFileProviderError {
if isNotFoundError {
NSFileProviderError(.noSuchItem)
} else if isCouldntConnectError {
// Provide something the file provider can do something with
NSFileProviderError(.serverUnreachable)
} else if isUnauthenticatedError {
NSFileProviderError(.notAuthenticated)
} else if isGoingOverQuotaError {
NSFileProviderError(.insufficientQuota)
} else {
NSFileProviderError(.cannotSynchronize)
}
}
}

View File

@ -1,57 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import Alamofire
import Foundation
extension Progress {
func setHandlersFromAfRequest(_ request: Request) {
cancellationHandler = { request.cancel() }
pausingHandler = { request.suspend() }
resumingHandler = { request.resume() }
}
func copyCurrentStateToProgress(_ otherProgress: Progress, includeHandlers: Bool = false) {
if includeHandlers {
otherProgress.cancellationHandler = cancellationHandler
otherProgress.pausingHandler = pausingHandler
otherProgress.resumingHandler = resumingHandler
}
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)
}
}
func copyOfCurrentState(includeHandlers: Bool = false) -> Progress {
let newProgress = Progress()
copyCurrentStateToProgress(newProgress, includeHandlers: includeHandlers)
return newProgress
}
}

View File

@ -1,405 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import NextcloudKit
import OSLog
extension FileProviderEnumerator {
func fullRecursiveScan(
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
scanChangesOnly: Bool,
completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable],
_ newMetadatas: [NextcloudItemMetadataTable],
_ updatedMetadatas: [NextcloudItemMetadataTable],
_ deletedMetadatas: [NextcloudItemMetadataTable],
_ error: NKError?
) -> Void
) {
let rootContainerDirectoryMetadata = NextcloudItemMetadataTable()
rootContainerDirectoryMetadata.directory = true
rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
// Create a serial dispatch queue
let dispatchQueue = DispatchQueue(
label: "recursiveChangeEnumerationQueue", qos: .userInitiated)
dispatchQueue.async {
let results = self.scanRecursively(
rootContainerDirectoryMetadata,
ncAccount: ncAccount,
ncKit: ncKit,
scanChangesOnly: scanChangesOnly)
// 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;
// so we need to check we are not simultaneously marking a moved file as deleted and updated
var checkedDeletedMetadatas = results.deletedMetadatas
for updatedMetadata in results.updatedMetadatas {
guard
let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: {
$0.ocId == updatedMetadata.ocId
})
else {
continue
}
checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx)
}
DispatchQueue.main.async {
completionHandler(
results.metadatas, results.newMetadatas, results.updatedMetadatas,
checkedDeletedMetadatas, results.error)
}
}
}
private func scanRecursively(
_ directoryMetadata: NextcloudItemMetadataTable,
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
scanChangesOnly: Bool
) -> (
metadatas: [NextcloudItemMetadataTable],
newMetadatas: [NextcloudItemMetadataTable],
updatedMetadatas: [NextcloudItemMetadataTable],
deletedMetadatas: [NextcloudItemMetadataTable],
error: NKError?
) {
if isInvalidated {
return ([], [], [], [], nil)
}
assert(directoryMetadata.directory, "Can only recursively scan a directory.")
// Will include results of recursive calls
var allMetadatas: [NextcloudItemMetadataTable] = []
var allNewMetadatas: [NextcloudItemMetadataTable] = []
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
let dbManager = NextcloudFilesDatabaseManager.shared
let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
dispatchGroup.enter()
var criticalError: NKError?
let itemServerUrl =
directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue
? ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: .public)")
FileProviderEnumerator.readServerUrl(
itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly
) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
if readError != nil {
let nkReadError = NKError(error: readError!)
// Is the error is that we have found matching etags on this item, then ignore it
// if we are doing a full rescan
guard nkReadError.isNoChangesError, scanChangesOnly else {
Logger.enumeration.error(
"Finishing enumeration of changes at \(itemServerUrl, privacy: .public) with \(readError!.localizedDescription, privacy: .public)"
)
if nkReadError.isNotFoundError {
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)
{
allDeletedMetadatas += deletedMetadatas
} else {
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
Logger.enumeration.info(
"Error was to say no changed files -- not bad error. No need to check children."
)
} else if nkReadError.isUnauthenticatedError
|| nkReadError.isCouldntConnectError
{
// If it is a critical error then stop, if not then continue
Logger.enumeration.error(
"Error will affect next enumerated items, so stopping enumeration.")
criticalError = nkReadError
}
dispatchGroup.leave()
return
}
}
Logger.enumeration.info(
"Finished reading serverUrl: \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
if let metadatas {
allMetadatas += metadatas
} else {
Logger.enumeration.warning(
"WARNING: Nil metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
}
if let newMetadatas {
allNewMetadatas += newMetadatas
} else {
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 {
allUpdatedMetadatas += updatedMetadatas
} else {
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 {
allDeletedMetadatas += deletedMetadatas
} else {
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.wait()
guard criticalError == nil else {
Logger.enumeration.error(
"Received critical error stopping further scanning: \(criticalError!.errorDescription, privacy: .public)"
)
return ([], [], [], [], error: criticalError)
}
var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
var candidateMetadatas: [NextcloudItemMetadataTable]
if scanChangesOnly, fastEnumeration {
candidateMetadatas = allUpdatedMetadatas
} else if scanChangesOnly {
candidateMetadatas = allUpdatedMetadatas + allNewMetadatas
} else {
candidateMetadatas = allMetadatas
}
for candidateMetadata in candidateMetadatas {
if candidateMetadata.directory {
childDirectoriesToScan.append(candidateMetadata)
}
}
Logger.enumeration.debug("Candidate metadatas for further scan: \(candidateMetadatas, privacy: .public)")
if childDirectoriesToScan.isEmpty {
return (
metadatas: allMetadatas, newMetadatas: allNewMetadatas,
updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil
)
}
for childDirectory in childDirectoriesToScan {
Logger.enumeration.debug(
"About to recursively scan: \(childDirectory.urlBase, privacy: .public) with etag: \(childDirectory.etag, privacy: .public)"
)
let childScanResult = scanRecursively(
childDirectory, ncAccount: ncAccount, ncKit: ncKit, scanChangesOnly: scanChangesOnly
)
allMetadatas += childScanResult.metadatas
allNewMetadatas += childScanResult.newMetadatas
allUpdatedMetadatas += childScanResult.updatedMetadatas
allDeletedMetadatas += childScanResult.deletedMetadatas
}
return (
metadatas: allMetadatas, newMetadatas: allNewMetadatas,
updatedMetadatas: allUpdatedMetadatas,
deletedMetadatas: allDeletedMetadatas, nil
)
}
static func handleDepth1ReadFileOrFolder(
serverUrl: String,
ncAccount: NextcloudAccount,
files: [NKFile],
error: NKError,
completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable]?,
_ newMetadatas: [NextcloudItemMetadataTable]?,
_ updatedMetadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?
) -> Void
) {
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)"
)
completionHandler(nil, nil, nil, nil, error.error)
return
}
Logger.enumeration.debug(
"Starting async conversion of NKFiles for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)"
)
let dbManager = NextcloudFilesDatabaseManager.shared
DispatchQueue.global(qos: .userInitiated).async {
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(
files, account: ncAccount.ncKitAccount
) { directoryMetadata, _, metadatas in
// 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
// unless it's the root container
if serverUrl != ncAccount.davFilesUrl {
dbManager.addItemMetadata(directoryMetadata)
}
// Don't update the etags for folders as we haven't checked their contents.
// When we do a recursive check, if we update the etags now, we will think
// 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.
// (See above)
let changedMetadatas = dbManager.updateItemMetadatas(
account: ncAccount.ncKitAccount, serverUrl: serverUrl,
updatedMetadatas: metadatas,
updateDirectoryEtags: false)
DispatchQueue.main.async {
completionHandler(
metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas,
changedMetadatas.deletedMetadatas, nil)
}
}
}
}
static func readServerUrl(
_ serverUrl: String,
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
stopAtMatchingEtags: Bool = false,
depth: String = "1",
completionHandler: @escaping (
_ metadatas: [NextcloudItemMetadataTable]?,
_ newMetadatas: [NextcloudItemMetadataTable]?,
_ updatedMetadatas: [NextcloudItemMetadataTable]?,
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
_ readError: Error?
) -> Void
) {
let dbManager = NextcloudFilesDatabaseManager.shared
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)"
)
ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: depth, showHiddenFiles: true) {
_, files, _, error in
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)"
)
completionHandler(nil, nil, nil, nil, error.error)
return
}
guard let receivedFile = files.first else {
Logger.enumeration.error(
"Received no items from readFileOrFolder of \(serverUrl, privacy: .public), not much we can do..."
)
completionHandler(nil, nil, nil, nil, error.error)
return
}
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)"
)
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)
return
}
if stopAtMatchingEtags,
let directoryMetadata = dbManager.directoryMetadata(
account: ncKitAccount, serverUrl: serverUrl)
{
let directoryEtag = directoryMetadata.etag
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."
)
let 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)
completionHandler(metadatas, nil, nil, nil, nkError.error)
return
}
}
if depth == "0" {
if serverUrl != ncAccount.davFilesUrl {
let metadata = NextcloudItemMetadataTable.fromNKFile(
receivedFile, account: ncKitAccount)
let isNew = dbManager.itemMetadataFromOcId(metadata.ocId) == nil
let updatedMetadatas = isNew ? [] : [metadata]
let newMetadatas = isNew ? [metadata] : []
dbManager.addItemMetadata(metadata)
DispatchQueue.main.async {
completionHandler([metadata], newMetadatas, updatedMetadatas, nil, nil)
}
}
} else {
handleDepth1ReadFileOrFolder(
serverUrl: serverUrl, ncAccount: ncAccount, files: files, error: error,
completionHandler: completionHandler)
}
}
}
}

View File

@ -1,461 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import NextcloudKit
import OSLog
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private let enumeratedItemIdentifier: NSFileProviderItemIdentifier
private var enumeratedItemMetadata: NextcloudItemMetadataTable?
private var enumeratingSystemIdentifier: Bool {
FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier)
}
// TODO: actually use this in NCKit and server requests
private let anchor = NSFileProviderSyncAnchor(Date().description.data(using: .utf8)!)
private static let maxItemsPerFileProviderPage = 100
let ncAccount: NextcloudAccount
let ncKit: NextcloudKit
let fastEnumeration: Bool
var serverUrl: String = ""
var isInvalidated = false
private static func isSystemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> Bool {
identifier == .rootContainer || identifier == .trashContainer || identifier == .workingSet
}
init(
enumeratedItemIdentifier: NSFileProviderItemIdentifier,
ncAccount: NextcloudAccount,
ncKit: NextcloudKit,
fastEnumeration: Bool = true
) {
self.enumeratedItemIdentifier = enumeratedItemIdentifier
self.ncAccount = ncAccount
self.ncKit = ncKit
self.fastEnumeration = fastEnumeration
if FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) {
Logger.enumeration.debug(
"Providing enumerator for a system defined container: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
serverUrl = ncAccount.davFilesUrl
} else {
Logger.enumeration.debug(
"Providing enumerator for item with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)"
)
let dbManager = NextcloudFilesDatabaseManager.shared
enumeratedItemMetadata = dbManager.itemMetadataFromFileProviderItemIdentifier(
enumeratedItemIdentifier)
if enumeratedItemMetadata != nil {
serverUrl =
enumeratedItemMetadata!.serverUrl + "/" + enumeratedItemMetadata!.fileName
} else {
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)"
)
super.init()
}
func invalidate() {
Logger.enumeration.debug(
"Enumerator is being invalidated for item with identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)"
)
isInvalidated = true
}
// MARK: - Protocol methods
func enumerateItems(
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)
If this is an enumerator for a directory, the root container or all directories:
- perform a server request to fetch directory contents
If this is an enumerator for the working set:
- perform a server request to update 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 that you are finished with this page
*/
if enumeratedItemIdentifier == .trashContainer {
Logger.enumeration.debug(
"Enumerating trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)"
)
// TODO!
observer.finishEnumerating(upTo: nil)
return
}
// Handle the working set as if it were the root container
// If we do a full server scan per the recommendations of the File Provider documentation,
// we will be stuck for a huge period of time without being able to access files as the
// entire server gets scanned. Instead, treat the working set as the root container here.
// Then, when we enumerate changes, we'll go through everything -- while we can still
// navigate a little bit in Finder, file picker, etc
guard serverUrl != "" else {
Logger.enumeration.error(
"Enumerator has empty serverUrl -- can't enumerate that! For identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)"
)
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
return
}
// TODO: Make better use of pagination and handle paging properly
if page == NSFileProviderPage.initialPageSortedByDate 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)"
)
FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit) {
metadatas, _, _, _, readError in
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)"
)
let nkReadError = NKError(error: readError!)
observer.finishEnumeratingWithError(nkReadError.fileProviderError)
return
}
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."
)
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
return
}
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)
}
return
}
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)"
)
// TODO: Handle paging properly
// FileProviderEnumerator.completeObserver(observer, ncKit: ncKit, numPage: numPage, itemMetadatas: nil)
observer.finishEnumerating(upTo: nil)
}
func enumerateChanges(
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)
If this is an enumerator for the working set:
- note the changes in your local database
- inform the observer about item deletions and updates (modifications + insertions)
- inform the observer when you have finished enumerating up to a subsequent sync anchor
*/
if enumeratedItemIdentifier == .workingSet {
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
// have just been moved elsewhere.
fullRecursiveScan(
ncAccount: ncAccount,
ncKit: ncKit,
scanChangesOnly: true
) { _, newMetadatas, updatedMetadatas, deletedMetadatas, error in
if self.isInvalidated {
Logger.enumeration.info(
"Enumerator invalidated during working set change scan. For user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
return
}
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)"
)
observer.finishEnumeratingWithError(error!.fileProviderError)
return
}
Logger.enumeration.info(
"Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public). Enumerating items."
)
FileProviderEnumerator.completeChangesObserver(
observer,
anchor: anchor,
ncKit: self.ncKit,
newMetadatas: newMetadatas,
updatedMetadatas: updatedMetadatas,
deletedMetadatas: deletedMetadatas)
}
return
} else if enumeratedItemIdentifier == .trashContainer {
Logger.enumeration.debug(
"Enumerating changes in trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public)"
)
// TODO!
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
return
}
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
// handling below or from the completeChangesObserver
// TODO: Move to the sync engine extension
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
var currentDeletedMetadatas: [NextcloudItemMetadataTable] = []
if let notNilDeletedMetadatas = deletedMetadatas {
currentDeletedMetadatas = notNilDeletedMetadatas
}
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)"
)
let nkReadError = NKError(error: readError!)
let fpError = nkReadError.fileProviderError
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"
)
guard let itemMetadata = self.enumeratedItemMetadata else {
Logger.enumeration.error(
"Invalid enumeratedItemMetadata, could not delete metadata nor report deletion"
)
observer.finishEnumeratingWithError(fpError)
return
}
let dbManager = NextcloudFilesDatabaseManager.shared
if itemMetadata.directory {
if let deletedDirectoryMetadatas =
dbManager.deleteDirectoryAndSubdirectoriesMetadata(
ocId: itemMetadata.ocId)
{
currentDeletedMetadatas += deletedDirectoryMetadatas
} else {
Logger.enumeration.error(
"Something went wrong when recursively deleting directory not found."
)
}
} else {
dbManager.deleteItemMetadata(ocId: itemMetadata.ocId)
}
FileProviderEnumerator.completeChangesObserver(
observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: nil,
updatedMetadatas: nil,
deletedMetadatas: [itemMetadata])
return
} 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."
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
return
}
observer.finishEnumeratingWithError(fpError)
return
}
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)
}
}
func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
completionHandler(anchor)
}
// MARK: - Helper methods
private static func metadatasToFileProviderItems(
_ itemMetadatas: [NextcloudItemMetadataTable], ncKit: NextcloudKit,
completionHandler: @escaping (_ items: [NSFileProviderItem]) -> Void
) {
var items: [NSFileProviderItem] = []
let conversionQueue = DispatchQueue(
label: "metadataToItemConversionQueue", qos: .userInitiated, attributes: .concurrent)
let appendQueue = DispatchQueue(label: "enumeratorItemAppendQueue", qos: .userInitiated) // Serial queue
let dispatchGroup = DispatchGroup()
for itemMetadata in itemMetadatas {
conversionQueue.async(group: dispatchGroup) {
if itemMetadata.e2eEncrypted {
Logger.enumeration.info(
"Skipping encrypted metadata in enumeration: \(itemMetadata.ocId, privacy: .public) \(itemMetadata.fileName, privacy: .public)"
)
return
}
if let parentItemIdentifier = NextcloudFilesDatabaseManager.shared
.parentItemIdentifierFromMetadata(itemMetadata)
{
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) {
items.append(item)
}
} 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"
)
}
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completionHandler(items)
}
}
private static func fileProviderPageforNumPage(_ numPage: Int) -> NSFileProviderPage {
NSFileProviderPage("\(numPage)".data(using: .utf8)!)
}
private static func completeEnumerationObserver(
_ observer: NSFileProviderEnumerationObserver, ncKit: NextcloudKit, numPage: Int,
itemMetadatas: [NextcloudItemMetadataTable]
) {
metadatasToFileProviderItems(itemMetadatas, ncKit: ncKit) { items in
observer.didEnumerate(items)
Logger.enumeration.info("Did enumerate \(items.count) items")
// TODO: Handle paging properly
/*
if items.count == maxItemsPerFileProviderPage {
let nextPage = numPage + 1
let providerPage = NSFileProviderPage("\(nextPage)".data(using: .utf8)!)
observer.finishEnumerating(upTo: providerPage)
} else {
observer.finishEnumerating(upTo: nil)
}
*/
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
}
}
private static func completeChangesObserver(
_ observer: NSFileProviderChangeObserver, anchor: NSFileProviderSyncAnchor,
ncKit: NextcloudKit,
newMetadatas: [NextcloudItemMetadataTable]?,
updatedMetadatas: [NextcloudItemMetadataTable]?,
deletedMetadatas: [NextcloudItemMetadataTable]?
) {
guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else {
Logger.enumeration.error(
"Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. Finished enumeration of changes with error."
)
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
return
}
// Observer does not care about new vs updated, so join
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
if let newMetadatas {
allUpdatedMetadatas += newMetadatas
}
if let updatedMetadatas {
allUpdatedMetadatas += updatedMetadatas
}
if let deletedMetadatas {
allDeletedMetadatas = deletedMetadatas
}
let allFpItemDeletionsIdentifiers = Array(
allDeletedMetadatas.map { NSFileProviderItemIdentifier($0.ocId) })
if !allFpItemDeletionsIdentifiers.isEmpty {
observer.didDeleteItems(withIdentifiers: allFpItemDeletionsIdentifiers)
}
metadatasToFileProviderItems(allUpdatedMetadatas, ncKit: ncKit) { updatedItems in
if !updatedItems.isEmpty {
observer.didUpdate(updatedItems)
}
Logger.enumeration.info(
"Processed \(updatedItems.count) new or updated metadatas, \(allDeletedMetadatas.count) deleted metadatas."
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
}
}
}

View File

@ -16,6 +16,7 @@ import FileProvider
import Foundation
import NCDesktopClientSocketKit
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
extension FileProviderExtension: NSFileProviderServicing {
@ -89,18 +90,22 @@ extension FileProviderExtension: NSFileProviderServicing {
}
@objc func setupDomainAccount(user: String, serverUrl: String, password: String) {
let newNcAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password)
let newNcAccount = Account(user: user, serverUrl: serverUrl, password: password)
guard newNcAccount != ncAccount else { return }
ncAccount = newNcAccount
ncKit.setup(
user: ncAccount!.username,
userId: ncAccount!.username,
password: ncAccount!.password,
urlBase: ncAccount!.serverUrl,
account: newNcAccount.ncKitAccount,
user: newNcAccount.username,
userId: newNcAccount.username,
password: newNcAccount.password,
urlBase: newNcAccount.serverUrl,
userAgent: "Nextcloud-macOS/FileProviderExt",
nextcloudVersion: 25,
delegate: nil) // TODO: add delegate methods for self
changeObserver = RemoteChangeObserver(ncKit: ncKit, domain: domain)
ncKit.setup(delegate: changeObserver)
Logger.fileProviderExtension.info(
"Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)"
)

View File

@ -15,6 +15,7 @@
import FileProvider
import Foundation
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
extension FileProviderExtension: NSFileProviderThumbnailing {
@ -28,46 +29,12 @@ extension FileProviderExtension: NSFileProviderThumbnailing {
) -> Void,
completionHandler: @escaping (Error?) -> Void
) -> Progress {
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
var progressCounter: Int64 = 0
func finishCurrent() {
progressCounter += 1
if progressCounter == progress.totalUnitCount {
completionHandler(nil)
}
}
for itemIdentifier in itemIdentifiers {
Logger.fileProviderExtension.debug(
"Fetching thumbnail for item with identifier:\(itemIdentifier.rawValue, privacy: .public)"
)
guard
let metadata = NextcloudFilesDatabaseManager.shared
.itemMetadataFromFileProviderItemIdentifier(itemIdentifier),
let thumbnailUrl = metadata.thumbnailUrl(size: size)
else {
Logger.fileProviderExtension.debug("Did not fetch thumbnail URL")
finishCurrent()
continue
}
Logger.fileProviderExtension.debug(
"Fetching thumbnail for file:\(metadata.fileName) at:\(thumbnailUrl.absoluteString, privacy: .public)"
)
ncKit.getPreview(url: thumbnailUrl) { _, data, error in
if error == .success, data != nil {
perThumbnailCompletionHandler(itemIdentifier, data, nil)
} else {
perThumbnailCompletionHandler(
itemIdentifier, nil, NSFileProviderError(.serverUnreachable))
}
finishCurrent()
}
}
return progress
return NextcloudFileProviderKit.fetchThumbnails(
for: itemIdentifiers,
requestedSize: size,
usingKit: self.ncKit,
perThumbnailCompletionHandler: perThumbnailCompletionHandler,
completionHandler: completionHandler
)
}
}

View File

@ -15,13 +15,15 @@
import FileProvider
import NCDesktopClientSocketKit
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
@objc class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate {
let domain: NSFileProviderDomain
let ncKit = NextcloudKit()
let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
var ncAccount: NextcloudAccount?
var ncAccount: Account?
var changeObserver: RemoteChangeObserver?
lazy var ncKitBackground = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)
lazy var socketClient: LocalSocketClient? = {
guard let containerUrl = pathForAppGroupContainer() else {
@ -84,62 +86,25 @@ import OSLog
)
}
// MARK: NSFileProviderReplicatedExtension protocol methods
// MARK: - NSFileProviderReplicatedExtension protocol methods
func item(
for identifier: NSFileProviderItemIdentifier, request _: NSFileProviderRequest,
for identifier: NSFileProviderItemIdentifier,
request _: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress {
// resolve the given identifier to a record in the model
Logger.fileProviderExtension.debug(
"Received item request for item with identifier: \(identifier.rawValue, privacy: .public)"
)
if identifier == .rootContainer {
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not providing item: \(identifier.rawValue, privacy: .public) as account not set up yet"
)
completionHandler(nil, NSFileProviderError(.notAuthenticated))
return Progress()
}
let metadata = NextcloudItemMetadataTable()
metadata.account = ncAccount.ncKitAccount
metadata.directory = true
metadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
metadata.fileName = "root"
metadata.fileNameView = "root"
metadata.serverUrl = ncAccount.serverUrl
metadata.classFile = NKCommon.TypeClassFile.directory.rawValue
completionHandler(
FileProviderItem(
metadata: metadata,
parentItemIdentifier: NSFileProviderItemIdentifier.rootContainer,
ncKit: ncKit), nil)
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
guard let metadata = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier),
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(metadata)
else {
if let item = Item.storedItem(identifier: identifier, usingKit: ncKit) {
completionHandler(item, nil)
} else {
completionHandler(nil, NSFileProviderError(.noSuchItem))
return Progress()
}
completionHandler(
FileProviderItem(
metadata: metadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit), nil)
return Progress()
}
func fetchContents(
for itemIdentifier: NSFileProviderItemIdentifier,
version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest,
version requestedVersion: NSFileProviderItemVersion?,
request: NSFileProviderRequest,
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void
) -> Progress {
Logger.fileProviderExtension.debug(
@ -149,648 +114,206 @@ import OSLog
guard requestedVersion == nil else {
// TODO: Add proper support for file versioning
Logger.fileProviderExtension.error(
"Can't return contents for specific version as this is not supported.")
"Can't return contents for a specific version as this is not supported."
)
completionHandler(
nil, nil,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]))
nil,
nil,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
)
return Progress()
}
guard ncAccount != nil else {
Logger.fileProviderExtension.error(
"Not fetching contents item: \(itemIdentifier.rawValue, privacy: .public) as account not set up yet"
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as account not set up yet.
"""
)
completionHandler(nil, nil, NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let ocId = itemIdentifier.rawValue
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
guard let item = Item.storedItem(identifier: itemIdentifier, usingKit: ncKit) else {
Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
"""
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
as item not found.
"""
)
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return Progress()
}
guard !metadata.isDocumentViewableOnly else {
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))
return Progress()
}
let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
Logger.fileProviderExtension.debug(
"Fetching file with name \(metadata.fileName, privacy: .public) at URL: \(serverUrlFileName, privacy: .public)"
)
let progress = Progress()
// TODO: Handle folders nicely
do {
let fileNameLocalPath = try localPathForNCFile(
ocId: metadata.ocId, fileNameView: metadata.fileNameView, domain: domain)
dbManager.setStatusForItemMetadata(
metadata, status: NextcloudItemMetadataTable.Status.downloading
) { updatedMetadata in
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"
)
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return
}
self.ncKit.download(
serverUrlFileName: serverUrlFileName,
fileNameLocalPath: fileNameLocalPath.path,
requestHandler: { request in
progress.setHandlersFromAfRequest(request)
},
taskHandler: { task in
NSFileProviderManager(for: self.domain)?.register(
task, forItemWithIdentifier: itemIdentifier, completionHandler: { _ in }
)
},
progressHandler: { downloadProgress in
downloadProgress.copyCurrentStateToProgress(progress)
}
) { _, etag, date, _, _, _, error in
if error == .success {
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.sessionError = ""
updatedMetadata.date = (date ?? NSDate()) as Date
updatedMetadata.etag = etag ?? ""
dbManager.addLocalFileMetadataFromItemMetadata(updatedMetadata)
dbManager.addItemMetadata(updatedMetadata)
guard
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(
updatedMetadata)
else {
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
return
}
let fpItem = FileProviderItem(
metadata: updatedMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
completionHandler(fileNameLocalPath, fpItem, nil)
} else {
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.sessionError = error.errorDescription
dbManager.addItemMetadata(updatedMetadata)
completionHandler(nil, nil, error.fileProviderError)
}
}
}
} catch {
Logger.fileProviderExtension.error(
"Could not find local path for file \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)"
Task {
let (localUrl, updatedItem, error) = await item.fetchContents(
domain: self.domain, progress: progress
)
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
completionHandler(localUrl, updatedItem, error)
}
return progress
}
func createItem(
basedOn itemTemplate: NSFileProviderItem, fields _: NSFileProviderItemFields,
contents url: URL?, options: NSFileProviderCreateItemOptions = [],
basedOn itemTemplate: NSFileProviderItem,
fields: NSFileProviderItemFields,
contents url: URL?,
options: NSFileProviderCreateItemOptions = [],
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?)
->
Void
completionHandler: @escaping (
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
) -> Void
) -> Progress {
// TODO: a new item was created on disk, process the item's creation
let tempId = itemTemplate.itemIdentifier.rawValue
Logger.fileProviderExtension.debug(
"Received create item request for item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)"
"""
Received create item request for item with identifier: \(tempId, privacy: .public)
and filename: \(itemTemplate.filename, privacy: .public)
"""
)
guard itemTemplate.contentType != .symbolicLink else {
Logger.fileProviderExtension.error("Cannot create item, symbolic links not supported.")
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo: [:]))
return Progress()
}
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as account not set up yet"
"""
Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public)
as account not set up yet
"""
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false,
NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let parentItemIdentifier = itemTemplate.parentItemIdentifier
let itemTemplateIsFolder =
itemTemplate.contentType == .folder || itemTemplate.contentType == .directory
if options.contains(.mayAlreadyExist) {
// 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"
itemTemplate,
NSFileProviderItemFields(),
false,
NSFileProviderError(.notAuthenticated)
)
completionHandler(
itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
return Progress()
}
var parentItemServerUrl: String
if parentItemIdentifier == .rootContainer {
parentItemServerUrl = ncAccount.davFilesUrl
} else {
guard
let parentItemMetadata = dbManager.directoryMetadata(
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()
}
parentItemServerUrl = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName
}
let fileNameLocalPath = url?.path ?? ""
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)"
)
if itemTemplateIsFolder {
ncKit.createFolder(serverUrlFileName: newServerUrlFileName) { account, _, _, error in
guard error == .success else {
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)
return
}
// Read contents after creation
self.ncKit.readFileOrFolder(
serverUrlFileName: newServerUrlFileName, depth: "0", showHiddenFiles: true
) { account, files, _, error in
guard error == .success else {
Logger.fileTransfer.error(
"Could not read new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)"
)
return
}
DispatchQueue.global().async {
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(
files, account: account
) {
directoryMetadata, _, _ in
dbManager.addItemMetadata(directoryMetadata)
let fpItem = FileProviderItem(
metadata: directoryMetadata,
parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
completionHandler(fpItem, [], true, nil)
}
}
}
}
return Progress()
}
let progress = Progress()
ncKit.upload(
serverUrlFileName: newServerUrlFileName,
fileNameLocalPath: fileNameLocalPath,
requestHandler: { request in
progress.setHandlersFromAfRequest(request)
},
taskHandler: { task in
NSFileProviderManager(for: self.domain)?.register(
task, forItemWithIdentifier: itemTemplate.itemIdentifier,
completionHandler: { _ in })
},
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)
return
}
Logger.fileTransfer.info(
"Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)"
Task {
let (item, error) = await Item.create(
basedOn: itemTemplate,
fields: fields,
contents: url,
request: request,
domain: self.domain,
ncKit: ncKit,
ncAccount: ncAccount,
progress: progress
)
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))"
)
if error != nil {
signalEnumerator(completionHandler: { _ in })
}
let newMetadata = NextcloudItemMetadataTable()
newMetadata.date = (date ?? NSDate()) as Date
newMetadata.etag = etag ?? ""
newMetadata.account = account
newMetadata.fileName = itemTemplate.filename
newMetadata.fileNameView = itemTemplate.filename
newMetadata.ocId = ocId
newMetadata.size = size
newMetadata.contentType = itemTemplate.contentType?.preferredMIMEType ?? ""
newMetadata.directory = itemTemplateIsFolder
newMetadata.serverUrl = parentItemServerUrl
newMetadata.session = ""
newMetadata.sessionError = ""
newMetadata.sessionTaskIdentifier = 0
newMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
dbManager.addItemMetadata(newMetadata)
let fpItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit
completionHandler(
item ?? itemTemplate,
NSFileProviderItemFields(),
false,
error
)
completionHandler(fpItem, [], false, nil)
}
return progress
}
func modifyItem(
_ item: NSFileProviderItem, baseVersion _: NSFileProviderItemVersion,
changedFields: NSFileProviderItemFields, contents newContents: URL?,
options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?)
->
Void
_ 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
// TODO: Handle finder things like tags, other possible item changed fields
let identifier = item.itemIdentifier
let ocId = identifier.rawValue
Logger.fileProviderExtension.debug(
"Received modify item request for item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) and filename: \(item.filename, privacy: .public)"
"""
Received modify item request for item with identifier: \(ocId, privacy: .public)
and filename: \(item.filename, privacy: .public)
"""
)
guard let ncAccount else {
Logger.fileProviderExtension.error(
"Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public) as account not set up yet"
"Not modifying item: \(ocId, privacy: .public) as account not set up yet."
)
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let parentItemIdentifier = item.parentItemIdentifier
let itemTemplateIsFolder = item.contentType == .folder || item.contentType == .directory
if options.contains(.mayAlreadyExist) {
// 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"
guard let existingItem = Item.storedItem(identifier: identifier, usingKit: ncKit) else {
Logger.fileProviderExtension.error(
"Not modifying item: \(ocId, privacy: .public) as item not found."
)
}
var parentItemServerUrl: String
if parentItemIdentifier == .rootContainer {
parentItemServerUrl = ncAccount.davFilesUrl
} else {
guard
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))
return Progress()
}
parentItemServerUrl = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName
}
let fileNameLocalPath = newContents?.path ?? ""
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)"
)
var modifiedItem = item
// Create a serial dispatch queue
// We want to wait for network operations to finish before we fire off subsequent network
// operations, or we might cause explosions (e.g. trying to modify items that have just been
// moved elsewhere)
let dispatchQueue = DispatchQueue(label: "modifyItemQueue", qos: .userInitiated)
if changedFields.contains(.filename) || changedFields.contains(.parentItemIdentifier) {
dispatchQueue.async {
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..."
)
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(item.itemIdentifier.rawValue, privacy: .public)"
)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return
}
var renameError: NSFileProviderError?
let oldServerUrlFileName = metadata.serverUrl + "/" + metadata.fileName
let moveFileOrFolderDispatchGroup = DispatchGroup() // Make this block wait until done
moveFileOrFolderDispatchGroup.enter()
self.ncKit.moveFileOrFolder(
serverUrlFileNameSource: oldServerUrlFileName,
serverUrlFileNameDestination: newServerUrlFileName,
overwrite: false
) { _, error in
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)"
)
renameError = error.fileProviderError
moveFileOrFolderDispatchGroup.leave()
return
}
// 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
if itemTemplateIsFolder {
_ = dbManager.renameDirectoryAndPropagateToChildren(
ocId: ocId, newServerUrl: newServerUrlFileName,
newFileName: item.filename)
self.signalEnumerator { error in
if error != nil {
Logger.fileTransfer.error(
"Error notifying change in moved directory: \(error)")
}
}
} else {
dbManager.renameItemMetadata(
ocId: ocId, newServerUrl: parentItemServerUrl,
newFileName: item.filename)
}
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"
)
renameError = NSFileProviderError(.noSuchItem)
moveFileOrFolderDispatchGroup.leave()
return
}
modifiedItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit)
moveFileOrFolderDispatchGroup.leave()
}
moveFileOrFolderDispatchGroup.wait()
guard renameError == nil else {
Logger.fileTransfer.error(
"Stopping rename of item with ocId \(ocId, privacy: .public) due to error: \(renameError!.localizedDescription, privacy: .public)"
)
completionHandler(modifiedItem, [], false, renameError)
return
}
guard !itemTemplateIsFolder else {
Logger.fileTransfer.debug(
"Only handling renaming for folders. ocId: \(ocId, privacy: .public)")
completionHandler(modifiedItem, [], false, nil)
return
}
}
// Return the progress if item is folder here while the async block runs
guard !itemTemplateIsFolder else {
return Progress()
}
}
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."
)
completionHandler(modifiedItem, [], false, nil)
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
return Progress()
}
let progress = Progress()
if changedFields.contains(.contents) {
dispatchQueue.async {
Logger.fileProviderExtension.debug(
"Item modification for \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public) includes contents"
)
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)"
)
completionHandler(modifiedItem, [], false, NSFileProviderError(.noSuchItem))
return
}
let ocId = item.itemIdentifier.rawValue
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
Logger.fileProviderExtension.error(
"Could not acquire metadata of item with identifier: \(ocId, privacy: .public)"
)
completionHandler(
item, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
return
}
dbManager.setStatusForItemMetadata(
metadata, status: NextcloudItemMetadataTable.Status.uploading
) { updatedMetadata in
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"
)
}
self.ncKit.upload(
serverUrlFileName: newServerUrlFileName,
fileNameLocalPath: fileNameLocalPath,
requestHandler: { request in
progress.setHandlersFromAfRequest(request)
},
taskHandler: { task in
NSFileProviderManager(for: self.domain)?.register(
task, forItemWithIdentifier: item.itemIdentifier,
completionHandler: { _ in })
},
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 {
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()
newMetadata.date = (date ?? NSDate()) as Date
newMetadata.etag = etag ?? ""
newMetadata.account = account
newMetadata.fileName = item.filename
newMetadata.fileNameView = item.filename
newMetadata.ocId = ocId
newMetadata.size = size
newMetadata.contentType = item.contentType?.preferredMIMEType ?? ""
newMetadata.directory = itemTemplateIsFolder
newMetadata.serverUrl = parentItemServerUrl
newMetadata.session = ""
newMetadata.sessionError = ""
newMetadata.sessionTaskIdentifier = 0
newMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
dbManager.addItemMetadata(newMetadata)
modifiedItem = FileProviderItem(
metadata: newMetadata, parentItemIdentifier: parentItemIdentifier,
ncKit: self.ncKit
)
completionHandler(modifiedItem, [], false, nil)
} 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)"
)
metadata.status = NextcloudItemMetadataTable.Status.uploadError.rawValue
metadata.sessionError = error.errorDescription
dbManager.addItemMetadata(metadata)
completionHandler(modifiedItem, [], false, error.fileProviderError)
return
}
}
}
}
} else {
Logger.fileProviderExtension.debug(
"Nothing more to do with \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public), modifications complete"
Task {
let (modifiedItem, error) = await existingItem.modify(
itemTarget: item,
baseVersion: baseVersion,
changedFields: changedFields,
contents: newContents,
options: options,
request: request,
ncAccount: ncAccount,
domain: domain,
progress: progress
)
completionHandler(modifiedItem, [], false, nil)
if error != nil {
signalEnumerator(completionHandler: { _ in })
}
completionHandler(modifiedItem ?? item, [], false, error)
}
return progress
}
func deleteItem(
identifier: NSFileProviderItemIdentifier, baseVersion _: NSFileProviderItemVersion,
options _: NSFileProviderDeleteItemOptions = [], request _: NSFileProviderRequest,
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)"
"Received delete request for item: \(identifier.rawValue, privacy: .public)"
)
guard ncAccount != nil else {
Logger.fileProviderExtension.error(
"Not deleting item: \(identifier.rawValue, privacy: .public) as account not set up yet"
"Not deleting item \(identifier.rawValue, privacy: .public), account not set up yet"
)
completionHandler(NSFileProviderError(.notAuthenticated))
return Progress()
}
let dbManager = NextcloudFilesDatabaseManager.shared
let ocId = identifier.rawValue
guard let itemMetadata = dbManager.itemMetadataFromOcId(ocId) else {
completionHandler(NSFileProviderError(.noSuchItem))
return Progress()
}
let serverFileNameUrl = itemMetadata.serverUrl + "/" + itemMetadata.fileName
guard serverFileNameUrl != "" else {
completionHandler(NSFileProviderError(.noSuchItem))
return Progress()
}
ncKit.deleteFileOrFolder(serverUrlFileName: serverFileNameUrl) { _, error in
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)"
)
completionHandler(error.fileProviderError)
return
}
Logger.fileTransfer.info(
"Successfully deleted item with identifier: \(identifier.rawValue, privacy: .public) at: \(serverFileNameUrl, privacy: .public)"
guard let item = Item.storedItem(identifier: identifier, usingKit: ncKit) else {
Logger.fileProviderExtension.error(
"Not deleting item \(identifier.rawValue, privacy: .public), item not found"
)
if itemMetadata.directory {
_ = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: ocId)
} else {
dbManager.deleteItemMetadata(ocId: ocId)
if dbManager.localFileMetadataFromOcId(ocId) != nil {
dbManager.deleteLocalFileMetadata(ocId: ocId)
}
}
completionHandler(nil)
completionHandler(NSFileProviderError(.noSuchItem))
return Progress()
}
return Progress()
let progress = Progress(totalUnitCount: 1)
Task {
let error = await item.delete()
if error != nil {
signalEnumerator(completionHandler: { _ in })
}
progress.completedUnitCount = 1
completionHandler(await item.delete())
}
return progress
}
func enumerator(
@ -803,10 +326,11 @@ import OSLog
throw NSFileProviderError(.notAuthenticated)
}
return FileProviderEnumerator(
return Enumerator(
enumeratedItemIdentifier: containerItemIdentifier,
ncAccount: ncAccount,
ncKit: ncKit,
domain: domain,
fastEnumeration: config.fastEnumerationEnabled
)
}
@ -828,7 +352,7 @@ import OSLog
}
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
let materialisedObserver = FileProviderMaterialisedEnumerationObserver(
let materialisedObserver = MaterialisedEnumerationObserver(
ncKitAccount: ncAccount.ncKitAccount
) { _ in
completionHandler()

View File

@ -1,164 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import NextcloudKit
import UniformTypeIdentifiers
class FileProviderItem: NSObject, NSFileProviderItem {
enum FileProviderItemTransferError: Error {
case downloadError
case uploadError
}
let metadata: NextcloudItemMetadataTable
let parentItemIdentifier: NSFileProviderItemIdentifier
let ncKit: NextcloudKit
var itemIdentifier: NSFileProviderItemIdentifier {
NSFileProviderItemIdentifier(metadata.ocId)
}
var capabilities: NSFileProviderItemCapabilities {
guard !metadata.directory else {
if #available(macOS 13.0, *) {
// .allowsEvicting deprecated on macOS 13.0+, use contentPolicy instead
return [
.allowsAddingSubItems,
.allowsContentEnumerating,
.allowsReading,
.allowsDeleting,
.allowsRenaming
]
} else {
return [
.allowsAddingSubItems,
.allowsContentEnumerating,
.allowsReading,
.allowsDeleting,
.allowsRenaming,
.allowsEvicting
]
}
}
guard !metadata.lock else {
return [.allowsReading]
}
return [
.allowsWriting,
.allowsReading,
.allowsDeleting,
.allowsRenaming,
.allowsReparenting,
.allowsEvicting
]
}
var itemVersion: NSFileProviderItemVersion {
NSFileProviderItemVersion(
contentVersion: metadata.etag.data(using: .utf8)!,
metadataVersion: metadata.etag.data(using: .utf8)!)
}
var filename: String {
metadata.fileNameView
}
var contentType: UTType {
if itemIdentifier == .rootContainer || metadata.directory {
return .folder
}
let internalType = ncKit.nkCommonInstance.getInternalType(
fileName: metadata.fileNameView,
mimeType: "",
directory: metadata.directory)
return UTType(filenameExtension: internalType.ext) ?? .content
}
var documentSize: NSNumber? {
NSNumber(value: metadata.size)
}
var creationDate: Date? {
metadata.creationDate as Date
}
var lastUsedDate: Date? {
metadata.date as Date
}
var contentModificationDate: Date? {
metadata.date as Date
}
var isDownloaded: Bool {
metadata.directory
|| NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
}
var isDownloading: Bool {
metadata.status == NextcloudItemMetadataTable.Status.downloading.rawValue
}
var downloadingError: Error? {
if metadata.status == NextcloudItemMetadataTable.Status.downloadError.rawValue {
return FileProviderItemTransferError.downloadError
}
return nil
}
var isUploaded: Bool {
NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
}
var isUploading: Bool {
metadata.status == NextcloudItemMetadataTable.Status.uploading.rawValue
}
var uploadingError: Error? {
if metadata.status == NextcloudItemMetadataTable.Status.uploadError.rawValue {
FileProviderItemTransferError.uploadError
} else {
nil
}
}
var childItemCount: NSNumber? {
if metadata.directory {
NSNumber(
integerLiteral: NextcloudFilesDatabaseManager.shared.childItemsForDirectory(
metadata
).count)
} else {
nil
}
}
@available(macOSApplicationExtension 13.0, *)
var contentPolicy: NSFileProviderContentPolicy {
.downloadLazily
}
required init(
metadata: NextcloudItemMetadataTable,
parentItemIdentifier: NSFileProviderItemIdentifier,
ncKit: NextcloudKit
) {
self.metadata = metadata
self.parentItemIdentifier = parentItemIdentifier
self.ncKit = ncKit
super.init()
}
}

View File

@ -14,6 +14,7 @@
import FileProvider
import Foundation
import NextcloudFileProviderKit
import OSLog
class FileProviderMaterialisedEnumerationObserver: NSObject, NSFileProviderEnumerationObserver {
@ -59,7 +60,7 @@ class FileProviderMaterialisedEnumerationObserver: NSObject, NSFileProviderEnume
_ itemIds: Set<String>, account: String,
completionHandler: @escaping (_ deletedOcIds: Set<String>) -> Void
) {
let dbManager = NextcloudFilesDatabaseManager.shared
let dbManager = FilesDatabaseManager.shared
let databaseLocalFileMetadatas = dbManager.localFileMetadatas(account: account)
var noLongerMaterialisedIds = Set<String>()

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
import OSLog
func pathForAppGroupContainer() -> URL? {
guard
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 FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupIdentifier)
}
func pathForFileProviderExtData() -> URL? {
let containerUrl = pathForAppGroupContainer()
return containerUrl?.appendingPathComponent("FileProviderExt/")
}
func pathForFileProviderTempFilesForDomain(_ domain: NSFileProviderDomain) throws -> URL? {
guard let fpManager = NSFileProviderManager(for: domain) else {
Logger.localFileOps.error(
"Unable to get file provider manager for domain: \(domain.displayName, privacy: .public)"
)
throw NSFileProviderError(.providerNotFound)
}
let fileProviderDataUrl = try fpManager.temporaryDirectoryURL()
return fileProviderDataUrl.appendingPathComponent("TemporaryNextcloudFiles/")
}
func localPathForNCFile(ocId _: String, fileNameView: String, domain: NSFileProviderDomain) throws
-> URL
{
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)"
)
throw URLError(.badURL)
}
let filePathUrl = fileProviderFilesPathUrl.appendingPathComponent(fileNameView)
let filePath = filePathUrl.path
if !FileManager.default.fileExists(atPath: filePath) {
FileManager.default.createFile(atPath: filePath, contents: nil)
}
return filePathUrl
}

View File

@ -1,62 +0,0 @@
/*
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
import FileProvider
import Foundation
let ncAccountDictUsernameKey = "usernameKey"
let ncAccountDictPasswordKey = "passwordKey"
let ncAccountDictNcKitAccountKey = "ncKitAccountKey"
let ncAccountDictServerUrlKey = "serverUrlKey"
let ncAccountDictDavFilesUrlKey = "davFilesUrlKey"
struct NextcloudAccount: Equatable {
static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/"
let username, password, ncKitAccount, serverUrl, davFilesUrl: String
init(user: String, serverUrl: String, password: String) {
username = user
self.password = password
ncKitAccount = user + " " + serverUrl
self.serverUrl = serverUrl
davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user
}
init?(dictionary: Dictionary<String, String>) {
guard let username = dictionary[ncAccountDictUsernameKey],
let password = dictionary[ncAccountDictPasswordKey],
let ncKitAccount = dictionary[ncAccountDictNcKitAccountKey],
let serverUrl = dictionary[ncAccountDictServerUrlKey],
let davFilesUrl = dictionary[ncAccountDictDavFilesUrlKey]
else {
return nil
}
self.username = username
self.password = password
self.ncKitAccount = ncKitAccount
self.serverUrl = serverUrl
self.davFilesUrl = davFilesUrl
}
func dictionary() -> Dictionary<String, String> {
return [
ncAccountDictUsernameKey: username,
ncAccountDictPasswordKey: password,
ncAccountDictNcKitAccountKey: ncKitAccount,
ncAccountDictServerUrlKey: serverUrl,
ncAccountDictDavFilesUrlKey: davFilesUrl
]
}
}

View File

@ -8,6 +8,7 @@
import FileProvider
import Foundation
import NextcloudKit
import NextcloudFileProviderKit
import OSLog
class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, FPUIExtensionService {
@ -52,7 +53,7 @@ class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCLi
return nil
}
let dbManager = NextcloudFilesDatabaseManager.shared
let dbManager = FilesDatabaseManager.shared
guard let item = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier) else {
Logger.shares.error("No item \(rawIdentifier, privacy: .public) in db, no shares.")
return nil

View File

@ -8,6 +8,7 @@
import AppKit
import FileProvider
import NextcloudKit
import NextcloudFileProviderKit
import NextcloudCapabilitiesKit
import OSLog
@ -35,7 +36,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
private(set) var shares: [NKShare] = [] {
didSet { Task { @MainActor in sharesTableView?.reloadData() } }
}
private var account: NextcloudAccount? {
private var account: Account? {
didSet {
guard let account = account else { return }
kit = NextcloudKit()
@ -86,7 +87,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
let connection = try await serviceConnection(url: itemURL)
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
let credentials = await connection.credentials() as? Dictionary<String, String>,
let convertedAccount = NextcloudAccount(dictionary: credentials),
let convertedAccount = Account(dictionary: credentials),
!convertedAccount.password.isEmpty
else {
presentError("Failed to get details from File Provider Extension. Retrying.")

View File

@ -9,24 +9,14 @@
/* Begin PBXBuildFile section */
5307A6E62965C6FA001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E52965C6FA001E0C6A /* NextcloudKit */; };
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E72965DAD8001E0C6A /* NextcloudKit */; };
5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6EA2965DB8D001E0C6A /* RealmSwift */; };
5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */; };
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 531522812B8E01C6002E31BE /* ShareTableItemView.xib */; };
5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */; };
5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */; };
5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */; };
5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */; };
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */; };
5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */; };
5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */; };
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */; };
5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */; };
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */; };
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */; };
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */; };
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */; };
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */; };
536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */; };
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5374FD432B95EE1400C78D54 /* ShareController.swift */; };
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */; };
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 537630902B85F4980026BFAB /* ShareViewController.xib */; };
@ -36,8 +26,6 @@
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */; };
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */; };
538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396E27F4765000FA63D5 /* FileProviderItem.swift */; };
538E397127F4765000FA63D5 /* FileProviderEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */; };
538E397627F4765000FA63D5 /* FileProviderExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 538E396727F4765000FA63D5 /* FileProviderExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
53903D1E2956164F00D0B308 /* NCDesktopClientSocketKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D0E2956164F00D0B308 /* NCDesktopClientSocketKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
53903D212956164F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */; };
@ -51,14 +39,12 @@
53903D37295618A400D0B308 /* LineProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D36295618A400D0B308 /* LineProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; };
539158AC27BE71A900816F56 /* FinderSyncSocketLineProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */; };
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */; };
53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D056302970594F00988392 /* LocalFilesUtils.swift */; };
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */; };
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */; };
53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D666602B70C9A70042C03D /* FileProviderConfig.swift */; };
53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */; };
53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */; };
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */; };
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */; };
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53FE14532B8E1219006C4193 /* NextcloudKit */; };
53FE14552B8E28E9006C4193 /* NextcloudAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */; };
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */; };
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */; };
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */; };
@ -161,23 +147,14 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudFilesDatabaseManager.swift; sourceTree = "<group>"; };
531522812B8E01C6002E31BE /* ShareTableItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableItemView.xib; sourceTree = "<group>"; };
5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudItemMetadataTable.swift; sourceTree = "<group>"; };
5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudLocalFileMetadataTable.swift; sourceTree = "<group>"; };
5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderMaterialisedEnumerationObserver.swift; sourceTree = "<group>"; };
5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+Extensions.swift"; sourceTree = "<group>"; };
5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClientCommunicationProtocol.h; sourceTree = "<group>"; };
5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCommunicationService.swift; sourceTree = "<group>"; };
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FileProviderExt-Bridging-Header.h"; sourceTree = "<group>"; };
5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+Directories.swift"; sourceTree = "<group>"; };
5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+LocalFiles.swift"; sourceTree = "<group>"; };
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+Thumbnailing.swift"; sourceTree = "<group>"; };
5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+Extensions.swift"; sourceTree = "<group>"; };
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareeSuggestionsDataSource.swift; sourceTree = "<group>"; };
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderSocketLineProcessor.swift; sourceTree = "<group>"; };
536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudAccount.swift; sourceTree = "<group>"; };
5374FD432B95EE1400C78D54 /* ShareController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareController.swift; sourceTree = "<group>"; };
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
5376307E2B85E5650026BFAB /* FileProviderUIExt.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = FileProviderUIExt.entitlements; sourceTree = "<group>"; };
@ -188,8 +165,6 @@
538E396727F4765000FA63D5 /* FileProviderExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = "<group>"; };
538E396E27F4765000FA63D5 /* FileProviderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderItem.swift; sourceTree = "<group>"; };
538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderEnumerator.swift; sourceTree = "<group>"; };
538E397227F4765000FA63D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
538E397327F4765000FA63D5 /* FileProviderExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderExt.entitlements; sourceTree = "<group>"; };
53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NCDesktopClientSocketKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -203,10 +178,7 @@
53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderUIExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentActionViewController.swift; sourceTree = "<group>"; };
53B979852B84C81F002DA742 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
53D056302970594F00988392 /* LocalFilesUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFilesUtils.swift; sourceTree = "<group>"; };
53D666602B70C9A70042C03D /* FileProviderConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderConfig.swift; sourceTree = "<group>"; };
53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderEnumerator+SyncEngine.swift"; sourceTree = "<group>"; };
53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudItemMetadataTable+NKFile.swift"; sourceTree = "<group>"; };
53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+ClientInterface.swift"; sourceTree = "<group>"; };
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableViewDataSource.swift; sourceTree = "<group>"; };
53FE14572B8E3A7C006C4193 /* FileProviderUIExtRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderUIExtRelease.entitlements; sourceTree = "<group>"; };
@ -234,10 +206,10 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */,
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */,
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */,
53903D302956173F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */,
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -253,6 +225,7 @@
buildActionMask = 2147483647;
files = (
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */,
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */,
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */,
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */,
);
@ -278,19 +251,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5318AD8F29BF406500CBB71C /* Database */ = {
isa = PBXGroup;
children = (
5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */,
5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */,
5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */,
5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */,
53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */,
5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */,
);
path = Database;
sourceTree = "<group>";
};
5350E4C72B0C368B00F276CB /* Services */ = {
isa = PBXGroup;
children = (
@ -306,8 +266,6 @@
isa = PBXGroup;
children = (
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */,
5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */,
5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -332,20 +290,13 @@
538E396B27F4765000FA63D5 /* FileProviderExt */ = {
isa = PBXGroup;
children = (
5318AD8F29BF406500CBB71C /* Database */,
5352E85929B7BFB4002CE85C /* Extensions */,
5350E4C72B0C368B00F276CB /* Services */,
53D666602B70C9A70042C03D /* FileProviderConfig.swift */,
538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */,
53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */,
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */,
53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */,
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */,
538E396E27F4765000FA63D5 /* FileProviderItem.swift */,
5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */,
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */,
53D056302970594F00988392 /* LocalFilesUtils.swift */,
536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */,
538E397327F4765000FA63D5 /* FileProviderExt.entitlements */,
538E397227F4765000FA63D5 /* Info.plist */,
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */,
@ -487,7 +438,7 @@
name = FileProviderExt;
packageProductDependencies = (
5307A6E72965DAD8001E0C6A /* NextcloudKit */,
5307A6EA2965DB8D001E0C6A /* RealmSwift */,
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */,
);
productName = FileProviderExt;
productReference = 538E396727F4765000FA63D5 /* FileProviderExt.appex */;
@ -529,6 +480,7 @@
53FE14532B8E1219006C4193 /* NextcloudKit */,
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */,
53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */,
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */,
);
productName = FileProviderUIExt;
productReference = 53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */;
@ -586,7 +538,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1520;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1240;
TargetAttributes = {
538E396627F4765000FA63D5 = {
@ -628,9 +580,9 @@
mainGroup = C2B573941B1CD88000303B36;
packageReferences = (
5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */,
5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */,
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */,
53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */,
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */,
);
productRefGroup = C2B573B21B1CD91E00303B36 /* Products */;
projectDirPath = "";
@ -713,29 +665,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */,
53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */,
536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */,
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */,
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */,
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */,
53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */,
5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */,
53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */,
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */,
5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */,
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */,
5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */,
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */,
53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */,
538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */,
5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */,
5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */,
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */,
5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */,
5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */,
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */,
538E397127F4765000FA63D5 /* FileProviderEnumerator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -759,7 +697,6 @@
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */,
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */,
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */,
53FE14552B8E28E9006C4193 /* NextcloudAccount.swift in Sources */,
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */,
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */,
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */,
@ -1531,14 +1468,6 @@
minimumVersion = 2.5.9;
};
};
5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/realm/realm-swift.git";
requirement = {
kind = exactVersion;
version = 10.33.0;
};
};
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/claucambra/NextcloudCapabilitiesKit.git";
@ -1555,6 +1484,14 @@
minimumVersion = 1.0.0;
};
};
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/claucambra/NextcloudFileProviderKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -1568,11 +1505,6 @@
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
productName = NextcloudKit;
};
5307A6EA2965DB8D001E0C6A /* RealmSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */;
productName = RealmSwift;
};
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */;
@ -1583,6 +1515,16 @@
package = 53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */;
productName = SuggestionsTextFieldKit;
};
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */ = {
isa = XCSwiftPackageProductDependency;
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
productName = NextcloudFileProviderKit;
};
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */ = {
isa = XCSwiftPackageProductDependency;
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
productName = NextcloudFileProviderKit;
};
53FE14512B8E1213006C4193 /* NextcloudKit */ = {
isa = XCSwiftPackageProductDependency;
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;

View File

@ -49,17 +49,10 @@ private slots:
void disconnectFileProviderDomainForAccount(const OCC::AccountState * const accountState, const QString &reason);
void reconnectFileProviderDomainForAccount(const OCC::AccountState * const accountState);
void trySetupPushNotificationsForAccount(const OCC::Account * const account);
void setupPushNotificationsForAccount(const OCC::Account * const account);
void signalEnumeratorChanged(const OCC::Account * const account);
void slotAccountStateChanged(const OCC::AccountState * const accountState);
void slotEnumeratorSignallingTimerTimeout();
private:
// Starts regular enumerator signalling if no push notifications available
QTimer _enumeratorSignallingTimer;
class MacImplementation;
std::unique_ptr<MacImplementation> d;
};

View File

@ -20,7 +20,6 @@
#include "config.h"
#include "fileproviderdomainmanager.h"
#include "fileprovidersettingscontroller.h"
#include "pushnotifications.h"
#include "gui/accountmanager.h"
#include "libsync/account.h"
@ -67,14 +66,6 @@ QString accountIdFromDomain(NSFileProviderDomain * const domain)
return accountIdFromDomainId(domain.identifier);
}
bool accountFilesPushNotificationsReady(const OCC::AccountPtr &account)
{
const auto pushNotifications = account->pushNotifications();
const auto pushNotificationsCapability = account->capabilities().availablePushNotifications() & OCC::PushNotificationType::Files;
return pushNotificationsCapability && pushNotifications && pushNotifications->isReady();
}
}
namespace OCC {
@ -420,11 +411,6 @@ FileProviderDomainManager::~FileProviderDomainManager() = default;
void FileProviderDomainManager::start()
{
ConfigFile cfg;
std::chrono::milliseconds polltime = cfg.remotePollInterval();
_enumeratorSignallingTimer.setInterval(polltime.count());
connect(&_enumeratorSignallingTimer, &QTimer::timeout,
this, &FileProviderDomainManager::slotEnumeratorSignallingTimerTimeout);
_enumeratorSignallingTimer.start();
setupFileProviderDomains();
@ -498,54 +484,6 @@ void FileProviderDomainManager::addFileProviderDomainForAccount(const AccountSta
connect(accountState, &AccountState::stateChanged, this, [this, accountState] {
slotAccountStateChanged(accountState);
});
// Setup push notifications
const auto accountCapabilities = account->capabilities().isValid();
if (!accountCapabilities) {
connect(account.get(), &Account::capabilitiesChanged, this, [this, account] {
trySetupPushNotificationsForAccount(account.get());
});
return;
}
trySetupPushNotificationsForAccount(account.get());
}
void FileProviderDomainManager::trySetupPushNotificationsForAccount(const Account * const account)
{
if (!d) {
return;
}
Q_ASSERT(account);
const auto pushNotifications = account->pushNotifications();
const auto pushNotificationsCapability = account->capabilities().availablePushNotifications() & PushNotificationType::Files;
if (pushNotificationsCapability && pushNotifications && pushNotifications->isReady()) {
qCDebug(lcMacFileProviderDomainManager) << "Push notifications already ready, connecting them to enumerator signalling."
<< account->displayName();
setupPushNotificationsForAccount(account);
} else if (pushNotificationsCapability) {
qCDebug(lcMacFileProviderDomainManager) << "Push notifications not yet ready, will connect to signalling when ready."
<< account->displayName();
connect(account, &Account::pushNotificationsReady, this, &FileProviderDomainManager::setupPushNotificationsForAccount);
}
}
void FileProviderDomainManager::setupPushNotificationsForAccount(const Account * const account)
{
if (!d) {
return;
}
Q_ASSERT(account);
qCDebug(lcMacFileProviderDomainManager) << "Setting up push notifications for file provider domain for account:"
<< account->displayName();
connect(account->pushNotifications(), &PushNotifications::filesChanged, this, &FileProviderDomainManager::signalEnumeratorChanged);
disconnect(account, &Account::pushNotificationsReady, this, &FileProviderDomainManager::setupPushNotificationsForAccount);
}
void FileProviderDomainManager::signalEnumeratorChanged(const Account * const account)
@ -569,13 +507,6 @@ void FileProviderDomainManager::removeFileProviderDomainForAccount(const Account
Q_ASSERT(account);
d->removeFileProviderDomain(accountState);
if (accountFilesPushNotificationsReady(account)) {
const auto pushNotifications = account->pushNotifications();
disconnect(pushNotifications, &PushNotifications::filesChanged, this, &FileProviderDomainManager::signalEnumeratorChanged);
} else if (const auto hasFilesPushNotificationsCapability = account->capabilities().availablePushNotifications() & PushNotificationType::Files) {
disconnect(account.get(), &Account::pushNotificationsReady, this, &FileProviderDomainManager::setupPushNotificationsForAccount);
}
}
void FileProviderDomainManager::disconnectFileProviderDomainForAccount(const AccountState * const accountState, const QString &reason)
@ -639,27 +570,6 @@ void FileProviderDomainManager::slotAccountStateChanged(const AccountState * con
}
}
void FileProviderDomainManager::slotEnumeratorSignallingTimerTimeout()
{
if (!d) {
return;
}
qCDebug(lcMacFileProviderDomainManager) << "Enumerator signalling timer timed out, notifying domains for accounts without push notifications";
const auto registeredDomainIds = d->configuredDomainIds();
for (const auto &domainId : registeredDomainIds) {
const auto accountUserId = accountIdFromDomainId(domainId);
const auto accountState = AccountManager::instance()->accountFromUserId(accountUserId);
const auto account = accountState->account();
if (!accountFilesPushNotificationsReady(account)) {
qCDebug(lcMacFileProviderDomainManager) << "Notifying domain for account:" << account->userIdAtHostWithPort();
d->signalEnumeratorChanged(account.get());
}
}
}
AccountStatePtr FileProviderDomainManager::accountStateFromFileProviderDomainIdentifier(const QString &domainIdentifier)
{
if (domainIdentifier.isEmpty()) {