mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 15:06:08 +03:00
Merge pull request #5527 from nextcloud/feature/file-provider-try-2
Implement File Provider file synchronisation engine for macOS
This commit is contained in:
commit
c4d12115a9
59 changed files with 5771 additions and 749 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -88,6 +88,7 @@ dlldata.c
|
||||||
# macOS specific
|
# macOS specific
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
**/Carthage/
|
||||||
|
|
||||||
# Visual C++ cache files
|
# Visual C++ cache files
|
||||||
ipch/
|
ipch/
|
||||||
|
|
|
@ -187,6 +187,11 @@ else()
|
||||||
unset(CMAKE_CXX_CLANG_TIDY)
|
unset(CMAKE_CXX_CLANG_TIDY)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
# build macOS File Provider module
|
||||||
|
option(BUILD_FILE_PROVIDER_MODULE "BUILD_FILE_PROVIDER_MODULE" ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
# When this option is enabled, 5xx errors are not added to the blacklist
|
# When this option is enabled, 5xx errors are not added to the blacklist
|
||||||
# Normally you don't want to enable this option because if a particular file
|
# Normally you don't want to enable this option because if a particular file
|
||||||
# triggers a bug on the server, you want the file to be blacklisted.
|
# triggers a bug on the server, you want the file to be blacklisted.
|
||||||
|
|
|
@ -1,26 +1,55 @@
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(OC_OEM_SHARE_ICNS "${CMAKE_BINARY_DIR}/src/gui/${APPLICATION_ICON_NAME}.icns")
|
set(OC_OEM_SHARE_ICNS "${CMAKE_BINARY_DIR}/src/gui/${APPLICATION_ICON_NAME}.icns")
|
||||||
|
|
||||||
|
if (CMAKE_BUILD_TYPE MATCHES "Debug" OR CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo")
|
||||||
|
set(XCODE_TARGET_CONFIGURATION "Debug")
|
||||||
|
else()
|
||||||
|
set(XCODE_TARGET_CONFIGURATION "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
# The bundle identifier and application group need to have compatible values with the client
|
# The bundle identifier and application group need to have compatible values with the client
|
||||||
# to be able to open a Mach port across the extension's sandbox boundary.
|
# to be able to open a Mach port across the extension's sandbox boundary.
|
||||||
# Pass the info through the xcodebuild command line and make sure that the project uses
|
# Pass the info through the xcodebuild command line and make sure that the project uses
|
||||||
# those user-defined settings to build the plist.
|
# those user-defined settings to build the plist.
|
||||||
add_custom_target( mac_overlayplugin ALL
|
add_custom_target( mac_overlayplugin ALL
|
||||||
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
|
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
|
||||||
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj
|
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj
|
||||||
-target FinderSyncExt -configuration Release "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
|
-target FinderSyncExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
|
||||||
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
|
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
|
||||||
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
|
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
|
||||||
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
|
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
|
||||||
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
|
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
|
||||||
COMMENT building Mac Overlay icons
|
COMMENT building Mac Overlay icons
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
|
|
||||||
|
if (BUILD_FILE_PROVIDER_MODULE)
|
||||||
|
add_custom_target( mac_fileproviderplugin ALL
|
||||||
|
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
|
||||||
|
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj
|
||||||
|
-target FileProviderExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
|
||||||
|
"OC_APPLICATION_EXECUTABLE_NAME=${APPLICATION_EXECUTABLE}"
|
||||||
|
"OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}"
|
||||||
|
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
|
||||||
|
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
|
||||||
|
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
|
||||||
|
COMMENT building macOS File Provider extension
|
||||||
|
VERBATIM)
|
||||||
|
|
||||||
|
add_dependencies(mac_overlayplugin mac_fileproviderplugin nextcloud) # for the ownCloud.icns to be generated
|
||||||
|
else()
|
||||||
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
|
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
|
||||||
|
endif()
|
||||||
|
|
||||||
if (BUILD_OWNCLOUD_OSX_BUNDLE)
|
if (BUILD_OWNCLOUD_OSX_BUNDLE)
|
||||||
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
|
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
|
||||||
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
|
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
|
||||||
USE_SOURCE_PERMISSIONS)
|
USE_SOURCE_PERMISSIONS)
|
||||||
|
|
||||||
|
if (BUILD_FILE_PROVIDER_MODULE)
|
||||||
|
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FileProviderExt.appex
|
||||||
|
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
|
||||||
|
USE_SOURCE_PERMISSIONS)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
7
shell_integration/MacOSX/Nextcloud.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
shell_integration/MacOSX/Nextcloud.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:NextcloudIntegration/NextcloudIntegration.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
* 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? {
|
||||||
|
return 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 let error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
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 let error {
|
||||||
|
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 let error {
|
||||||
|
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 { $0.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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,326 @@
|
||||||
|
/*
|
||||||
|
* 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 Foundation
|
||||||
|
import RealmSwift
|
||||||
|
import FileProvider
|
||||||
|
import NextcloudKit
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
class NextcloudFilesDatabaseManager : NSObject {
|
||||||
|
static let shared = {
|
||||||
|
return NextcloudFilesDatabaseManager();
|
||||||
|
}()
|
||||||
|
|
||||||
|
let relativeDatabaseFolderPath = "Database/"
|
||||||
|
let databaseFilename = "fileproviderextdatabase.realm"
|
||||||
|
let relativeDatabaseFilePath: String
|
||||||
|
var databasePath: URL?
|
||||||
|
|
||||||
|
let schemaVersion: UInt64 = 100
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.relativeDatabaseFilePath = self.relativeDatabaseFolderPath + self.databaseFilename
|
||||||
|
|
||||||
|
guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
|
||||||
|
super.init()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.databasePath = fileProviderDataDirUrl.appendingPathComponent(self.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(self.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 let error {
|
||||||
|
Logger.ncFilesDatabase.error("Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Realm.Configuration(
|
||||||
|
fileURL: self.databasePath,
|
||||||
|
schemaVersion: self.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 {
|
||||||
|
return !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 let error {
|
||||||
|
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 let error {
|
||||||
|
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 let error {
|
||||||
|
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 let error {
|
||||||
|
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 let error {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
let appendQueue = DispatchQueue(label: "metadataAppendQueue", qos: .userInitiated) // Serial queue
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import FileProvider
|
||||||
|
import NextcloudKit
|
||||||
|
|
||||||
|
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
|
||||||
|
let sharePermissionsCloudMesh = List<String>() // TODO: Find a way to compare these in remote state check
|
||||||
|
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 {
|
||||||
|
return 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 {
|
||||||
|
return 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 {
|
||||||
|
return session.isEmpty && !isDocumentViewableOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
var canOpenIn: Bool {
|
||||||
|
return session.isEmpty && !isDocumentViewableOnly && !directory
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDownloadUpload: Bool {
|
||||||
|
return 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 self.fileId == object.fileId &&
|
||||||
|
self.account == object.account &&
|
||||||
|
self.path == object.path &&
|
||||||
|
self.fileName == object.fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInSameDatabaseStoreableRemoteState(_ comparingMetadata: NextcloudItemMetadataTable) -> Bool {
|
||||||
|
return comparingMetadata.etag == self.etag &&
|
||||||
|
comparingMetadata.fileNameView == self.fileNameView &&
|
||||||
|
comparingMetadata.date == self.date &&
|
||||||
|
comparingMetadata.permissions == self.permissions &&
|
||||||
|
comparingMetadata.hasPreview == self.hasPreview &&
|
||||||
|
comparingMetadata.note == self.note &&
|
||||||
|
comparingMetadata.lock == self.lock &&
|
||||||
|
comparingMetadata.sharePermissionsCollaborationServices == self.sharePermissionsCollaborationServices &&
|
||||||
|
comparingMetadata.favorite == self.favorite
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns false if the user is lokced out of the file. I.e. The file is locked but by somone else
|
||||||
|
func canUnlock(as user: String) -> Bool {
|
||||||
|
return !lock || (lockOwner == user && lockOwnerType == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func thumbnailUrl(size: CGSize) -> URL? {
|
||||||
|
guard hasPreview else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlBase = urlBase.urlEncoded!
|
||||||
|
let webdavUrl = urlBase + NextcloudAccount.webDavFilesUrlSuffix + user // Leave the leading slash
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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 OSLog
|
||||||
|
|
||||||
|
extension Logger {
|
||||||
|
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||||
|
|
||||||
|
static let desktopClientConnection = Logger(subsystem: subsystem, category: "desktopclientconnection")
|
||||||
|
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 materialisedFileHandling = Logger(subsystem: subsystem, category: "materialisedfilehandling")
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* 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 FileProvider
|
||||||
|
import NextcloudKit
|
||||||
|
|
||||||
|
extension NKError {
|
||||||
|
static var noChangesErrorCode: Int {
|
||||||
|
return -200
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCouldntConnectError: Bool {
|
||||||
|
return 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 {
|
||||||
|
return errorCode == -1013
|
||||||
|
}
|
||||||
|
|
||||||
|
var isGoingOverQuotaError: Bool {
|
||||||
|
return errorCode == 507
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNotFoundError: Bool {
|
||||||
|
return errorCode == 404
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNoChangesError: Bool {
|
||||||
|
return errorCode == NKError.noChangesErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileProviderError: NSFileProviderError {
|
||||||
|
if isNotFoundError {
|
||||||
|
return NSFileProviderError(.noSuchItem)
|
||||||
|
} else if isCouldntConnectError {
|
||||||
|
// Provide something the file provider can do something with
|
||||||
|
return NSFileProviderError(.serverUnreachable)
|
||||||
|
} else if isUnauthenticatedError {
|
||||||
|
return NSFileProviderError(.notAuthenticated)
|
||||||
|
} else if isGoingOverQuotaError {
|
||||||
|
return NSFileProviderError(.insufficientQuota)
|
||||||
|
} else {
|
||||||
|
return NSFileProviderError(.cannotSynchronize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 Alamofire
|
||||||
|
|
||||||
|
extension Progress {
|
||||||
|
func setHandlersFromAfRequest(_ request: Request) {
|
||||||
|
self.cancellationHandler = { request.cancel() }
|
||||||
|
self.pausingHandler = { request.suspend() }
|
||||||
|
self.resumingHandler = { request.resume() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyCurrentStateToProgress(_ otherProgress: Progress, includeHandlers: Bool = false) {
|
||||||
|
if includeHandlers {
|
||||||
|
otherProgress.cancellationHandler = self.cancellationHandler
|
||||||
|
otherProgress.pausingHandler = self.pausingHandler
|
||||||
|
otherProgress.resumingHandler = self.resumingHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
otherProgress.totalUnitCount = self.totalUnitCount
|
||||||
|
otherProgress.completedUnitCount = self.completedUnitCount
|
||||||
|
otherProgress.estimatedTimeRemaining = self.estimatedTimeRemaining
|
||||||
|
otherProgress.localizedDescription = self.localizedAdditionalDescription
|
||||||
|
otherProgress.localizedAdditionalDescription = self.localizedAdditionalDescription
|
||||||
|
otherProgress.isCancellable = self.isCancellable
|
||||||
|
otherProgress.isPausable = self.isPausable
|
||||||
|
otherProgress.fileCompletedCount = self.fileCompletedCount
|
||||||
|
otherProgress.fileURL = self.fileURL
|
||||||
|
otherProgress.fileTotalCount = self.fileTotalCount
|
||||||
|
otherProgress.fileCompletedCount = self.fileCompletedCount
|
||||||
|
otherProgress.fileOperationKind = self.fileOperationKind
|
||||||
|
otherProgress.kind = self.kind
|
||||||
|
otherProgress.throughput = self.throughput
|
||||||
|
|
||||||
|
for (key, object) in self.userInfo {
|
||||||
|
otherProgress.setUserInfoObject(object, forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyOfCurrentState(includeHandlers: Bool = false) -> Progress {
|
||||||
|
let newProgress = Progress()
|
||||||
|
copyCurrentStateToProgress(newProgress, includeHandlers: includeHandlers)
|
||||||
|
return newProgress
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,313 @@
|
||||||
|
/*
|
||||||
|
* 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 self.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 = 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 = 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 = 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 = 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 {
|
||||||
|
return ([], [], [], [], error: criticalError)
|
||||||
|
}
|
||||||
|
|
||||||
|
var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
|
||||||
|
var candidateMetadatas: [NextcloudItemMetadataTable]
|
||||||
|
|
||||||
|
if scanChangesOnly {
|
||||||
|
candidateMetadatas = allUpdatedMetadatas + allNewMetadatas
|
||||||
|
} else {
|
||||||
|
candidateMetadatas = allMetadatas
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidateMetadata in candidateMetadatas {
|
||||||
|
if candidateMetadata.directory {
|
||||||
|
childDirectoriesToScan.append(candidateMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if childDirectoriesToScan.isEmpty {
|
||||||
|
return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for childDirectory in childDirectoriesToScan {
|
||||||
|
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, childDirectoriesMetadata, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,352 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
return FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier)
|
||||||
|
}
|
||||||
|
private let anchor = NSFileProviderSyncAnchor(Date().description.data(using: .utf8)!) // TODO: actually use this in NCKit and server requests
|
||||||
|
private static let maxItemsPerFileProviderPage = 100
|
||||||
|
let ncAccount: NextcloudAccount
|
||||||
|
let ncKit: NextcloudKit
|
||||||
|
var serverUrl: String = ""
|
||||||
|
var isInvalidated = false
|
||||||
|
|
||||||
|
private static func isSystemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> Bool {
|
||||||
|
return identifier == .rootContainer ||
|
||||||
|
identifier == .trashContainer ||
|
||||||
|
identifier == .workingSet
|
||||||
|
}
|
||||||
|
|
||||||
|
init(enumeratedItemIdentifier: NSFileProviderItemIdentifier, ncAccount: NextcloudAccount, ncKit: NextcloudKit) {
|
||||||
|
self.enumeratedItemIdentifier = enumeratedItemIdentifier
|
||||||
|
self.ncAccount = ncAccount
|
||||||
|
self.ncKit = ncKit
|
||||||
|
|
||||||
|
if FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) {
|
||||||
|
Logger.enumeration.debug("Providing enumerator for a system defined container: \(enumeratedItemIdentifier.rawValue, privacy: .public)")
|
||||||
|
self.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 {
|
||||||
|
self.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)")
|
||||||
|
self.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 = 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: self.ncAccount,
|
||||||
|
ncKit: self.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 {
|
||||||
|
return 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 = newMetadatas {
|
||||||
|
allUpdatedMetadatas += newMetadatas
|
||||||
|
}
|
||||||
|
|
||||||
|
if let updatedMetadatas = updatedMetadatas {
|
||||||
|
allUpdatedMetadatas += updatedMetadatas
|
||||||
|
}
|
||||||
|
|
||||||
|
if let deletedMetadatas = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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 FileProvider
|
||||||
|
import OSLog
|
||||||
|
import NCDesktopClientSocketKit
|
||||||
|
import NextcloudKit
|
||||||
|
|
||||||
|
extension FileProviderExtension {
|
||||||
|
func sendFileProviderDomainIdentifier() {
|
||||||
|
let command = "FILE_PROVIDER_DOMAIN_IDENTIFIER_REQUEST_REPLY"
|
||||||
|
let argument = domain.identifier.rawValue
|
||||||
|
let message = command + ":" + argument + "\n"
|
||||||
|
socketClient?.sendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func signalEnumeratorAfterAccountSetup() {
|
||||||
|
guard let fpManager = NSFileProviderManager(for: domain) else {
|
||||||
|
Logger.fileProviderExtension.error("Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(ncAccount != nil)
|
||||||
|
|
||||||
|
fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
|
||||||
|
if error != nil {
|
||||||
|
Logger.fileProviderExtension.error("Error resolving not authenticated, received error: \(error!.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.fileProviderExtension.debug("Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)")
|
||||||
|
|
||||||
|
fpManager.signalEnumerator(for: .workingSet) { error in
|
||||||
|
if error != nil {
|
||||||
|
Logger.fileProviderExtension.error("Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDomainAccount(user: String, serverUrl: String, password: String) {
|
||||||
|
ncAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password)
|
||||||
|
ncKit.setup(user: ncAccount!.username,
|
||||||
|
userId: ncAccount!.username,
|
||||||
|
password: ncAccount!.password,
|
||||||
|
urlBase: ncAccount!.serverUrl,
|
||||||
|
userAgent: "Nextcloud-macOS/FileProviderExt",
|
||||||
|
nextcloudVersion: 25,
|
||||||
|
delegate: nil) // TODO: add delegate methods for self
|
||||||
|
|
||||||
|
Logger.fileProviderExtension.info("Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)")
|
||||||
|
|
||||||
|
signalEnumeratorAfterAccountSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAccountConfig() {
|
||||||
|
Logger.fileProviderExtension.info("Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)")
|
||||||
|
ncAccount = nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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 FileProvider
|
||||||
|
import NextcloudKit
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
extension FileProviderExtension: NSFileProviderThumbnailing {
|
||||||
|
func fetchThumbnails(for itemIdentifiers: [NSFileProviderItemIdentifier],
|
||||||
|
requestedSize size: CGSize,
|
||||||
|
perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier,
|
||||||
|
Data?,
|
||||||
|
Error?) -> 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)")
|
||||||
|
|
||||||
|
self.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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,633 @@
|
||||||
|
/*
|
||||||
|
* 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 OSLog
|
||||||
|
import NCDesktopClientSocketKit
|
||||||
|
import NextcloudKit
|
||||||
|
|
||||||
|
class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate {
|
||||||
|
let domain: NSFileProviderDomain
|
||||||
|
let ncKit = NextcloudKit()
|
||||||
|
lazy var ncKitBackground: NKBackground = {
|
||||||
|
let nckb = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)
|
||||||
|
return nckb
|
||||||
|
}()
|
||||||
|
|
||||||
|
let appGroupIdentifier: String? = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
|
||||||
|
var ncAccount: NextcloudAccount?
|
||||||
|
lazy var socketClient: LocalSocketClient? = {
|
||||||
|
guard let containerUrl = pathForAppGroupContainer() else {
|
||||||
|
Logger.fileProviderExtension.critical("Could not start file provider socket client properly as could not get container url")
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
let socketPath = containerUrl.appendingPathComponent(".fileprovidersocket", conformingTo: .archive)
|
||||||
|
let lineProcessor = FileProviderSocketLineProcessor(delegate: self)
|
||||||
|
|
||||||
|
return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
let urlSessionIdentifier: String = "com.nextcloud.session.upload.fileproviderext"
|
||||||
|
let urlSessionMaximumConnectionsPerHost = 5
|
||||||
|
lazy var urlSession: URLSession = {
|
||||||
|
let configuration = URLSessionConfiguration.background(withIdentifier: urlSessionIdentifier)
|
||||||
|
configuration.allowsCellularAccess = true
|
||||||
|
configuration.sessionSendsLaunchEvents = true
|
||||||
|
configuration.isDiscretionary = false
|
||||||
|
configuration.httpMaximumConnectionsPerHost = urlSessionMaximumConnectionsPerHost
|
||||||
|
configuration.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData
|
||||||
|
configuration.sharedContainerIdentifier = appGroupIdentifier
|
||||||
|
|
||||||
|
let session = URLSession(configuration: configuration, delegate: ncKitBackground, delegateQueue: OperationQueue.main)
|
||||||
|
return session
|
||||||
|
}()
|
||||||
|
|
||||||
|
required init(domain: NSFileProviderDomain) {
|
||||||
|
self.domain = domain
|
||||||
|
// The containing application must create a domain using `NSFileProviderManager.add(_:, completionHandler:)`. The system will then launch the application extension process, call `FileProviderExtension.init(domain:)` to instantiate the extension for that domain, and call methods on the instance.
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
self.socketClient?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
// TODO: cleanup any resources
|
||||||
|
Logger.fileProviderExtension.debug("Extension for domain \(self.domain.displayName, privacy: .public) is being torn down")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: NSFileProviderReplicatedExtension protocol methods
|
||||||
|
|
||||||
|
func item(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 = 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 {
|
||||||
|
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, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress {
|
||||||
|
|
||||||
|
Logger.fileProviderExtension.debug("Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)")
|
||||||
|
|
||||||
|
guard requestedVersion == nil else {
|
||||||
|
// TODO: Add proper support for file versioning
|
||||||
|
Logger.fileProviderExtension.error("Can't return contents for specific version as this is not supported.")
|
||||||
|
completionHandler(nil, nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:]))
|
||||||
|
return Progress()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard ncAccount != nil else {
|
||||||
|
Logger.fileProviderExtension.error("Not fetching contents 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 {
|
||||||
|
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public)")
|
||||||
|
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: self.domain)
|
||||||
|
|
||||||
|
dbManager.setStatusForItemMetadata(metadata, status: NextcloudItemMetadataTable.Status.downloading) { updatedMetadata in
|
||||||
|
|
||||||
|
guard let updatedMetadata = 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 let error {
|
||||||
|
Logger.fileProviderExtension.error("Could not find local path for file \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func createItem(basedOn itemTemplate: NSFileProviderItem, fields: NSFileProviderItemFields, contents url: URL?, options: NSFileProviderCreateItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
|
||||||
|
// TODO: a new item was created on disk, process the item's creation
|
||||||
|
|
||||||
|
Logger.fileProviderExtension.debug("Received create item request for item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)")
|
||||||
|
|
||||||
|
guard itemTemplate.contentType != .symbolicLink else {
|
||||||
|
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 = ncAccount else {
|
||||||
|
Logger.fileProviderExtension.error("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")
|
||||||
|
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 {
|
||||||
|
self.ncKit.createFolder(serverUrlFileName: newServerUrlFileName) { account, ocId, _, 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, childDirectoriesMetadata, metadatas in
|
||||||
|
|
||||||
|
dbManager.addItemMetadata(directoryMetadata)
|
||||||
|
|
||||||
|
let fpItem = FileProviderItem(metadata: directoryMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit)
|
||||||
|
|
||||||
|
completionHandler(fpItem, [], true, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Progress()
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = Progress()
|
||||||
|
|
||||||
|
self.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 = 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)")
|
||||||
|
|
||||||
|
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))")
|
||||||
|
}
|
||||||
|
|
||||||
|
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(fpItem, [], false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func modifyItem(_ item: NSFileProviderItem, baseVersion version: NSFileProviderItemVersion, changedFields: NSFileProviderItemFields, contents newContents: URL?, options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
|
||||||
|
// An item was modified on disk, process the item's modification
|
||||||
|
// TODO: Handle finder things like tags, other possible item changed fields
|
||||||
|
|
||||||
|
Logger.fileProviderExtension.debug("Received modify item request for item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) and filename: \(item.filename, privacy: .public)")
|
||||||
|
|
||||||
|
guard let ncAccount = ncAccount else {
|
||||||
|
Logger.fileProviderExtension.error("Not modifying item: \(item.itemIdentifier.rawValue, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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) { account, 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)
|
||||||
|
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 = 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")
|
||||||
|
completionHandler(modifiedItem, [], false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteItem(identifier: NSFileProviderItemIdentifier, baseVersion version: NSFileProviderItemVersion, options: NSFileProviderDeleteItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (Error?) -> Void) -> Progress {
|
||||||
|
|
||||||
|
Logger.fileProviderExtension.debug("Received delete item request for item with identifier: \(identifier.rawValue, privacy: .public)")
|
||||||
|
|
||||||
|
guard ncAccount != nil else {
|
||||||
|
Logger.fileProviderExtension.error("Not deleting item: \(identifier.rawValue, privacy: .public) as 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ncKit.deleteFileOrFolder(serverUrlFileName: serverFileNameUrl) { account, 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)")
|
||||||
|
|
||||||
|
if itemMetadata.directory {
|
||||||
|
_ = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: ocId)
|
||||||
|
} else {
|
||||||
|
dbManager.deleteItemMetadata(ocId: ocId)
|
||||||
|
if dbManager.localFileMetadataFromOcId(ocId) != nil {
|
||||||
|
dbManager.deleteLocalFileMetadata(ocId: ocId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Progress()
|
||||||
|
}
|
||||||
|
|
||||||
|
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest) throws -> NSFileProviderEnumerator {
|
||||||
|
|
||||||
|
guard let ncAccount = ncAccount else {
|
||||||
|
Logger.fileProviderExtension.error("Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet as account not set up")
|
||||||
|
throw NSFileProviderError(.notAuthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier, ncAccount: ncAccount, ncKit: ncKit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func materializedItemsDidChange(completionHandler: @escaping () -> Void) {
|
||||||
|
guard let ncAccount = self.ncAccount else {
|
||||||
|
Logger.fileProviderExtension.error("Not purging stale local file metadatas, account not set up")
|
||||||
|
completionHandler()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let fpManager = NSFileProviderManager(for: domain) else {
|
||||||
|
Logger.fileProviderExtension.error("Could not get file provider manager for domain: \(self.domain.displayName, privacy: .public)")
|
||||||
|
completionHandler()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
|
||||||
|
let materialisedObserver = FileProviderMaterialisedEnumerationObserver(ncKitAccount: ncAccount.ncKitAccount) { _ in
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
let startingPage = NSFileProviderPage(NSFileProviderPage.initialPageSortedByName as Data)
|
||||||
|
|
||||||
|
materialisedEnumerator.enumerateItems(for: materialisedObserver, startingAt: startingPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalEnumerator(completionHandler: @escaping(_ error: Error?) -> Void) {
|
||||||
|
guard let fpManager = NSFileProviderManager(for: self.domain) else {
|
||||||
|
Logger.fileProviderExtension.error("Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fpManager.signalEnumerator(for: .workingSet, completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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 UniformTypeIdentifiers
|
||||||
|
import NextcloudKit
|
||||||
|
|
||||||
|
class FileProviderItem: NSObject, NSFileProviderItem {
|
||||||
|
|
||||||
|
enum FileProviderItemTransferError: Error {
|
||||||
|
case downloadError
|
||||||
|
case uploadError
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata: NextcloudItemMetadataTable
|
||||||
|
let parentItemIdentifier: NSFileProviderItemIdentifier
|
||||||
|
let ncKit: NextcloudKit
|
||||||
|
|
||||||
|
var itemIdentifier: NSFileProviderItemIdentifier {
|
||||||
|
return NSFileProviderItemIdentifier(metadata.ocId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var capabilities: NSFileProviderItemCapabilities {
|
||||||
|
guard !metadata.directory else {
|
||||||
|
return [ .allowsAddingSubItems,
|
||||||
|
.allowsContentEnumerating,
|
||||||
|
.allowsReading,
|
||||||
|
.allowsDeleting,
|
||||||
|
.allowsRenaming ]
|
||||||
|
}
|
||||||
|
guard !metadata.lock else {
|
||||||
|
return [ .allowsReading ]
|
||||||
|
}
|
||||||
|
return [ .allowsWriting,
|
||||||
|
.allowsReading,
|
||||||
|
.allowsDeleting,
|
||||||
|
.allowsRenaming,
|
||||||
|
.allowsReparenting ]
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemVersion: NSFileProviderItemVersion {
|
||||||
|
NSFileProviderItemVersion(contentVersion: metadata.etag.data(using: .utf8)!,
|
||||||
|
metadataVersion: metadata.etag.data(using: .utf8)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename: String {
|
||||||
|
return metadata.fileNameView
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType: UTType {
|
||||||
|
if self.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? {
|
||||||
|
return NSNumber(value: metadata.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
var creationDate: Date? {
|
||||||
|
return metadata.creationDate as Date
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastUsedDate: Date? {
|
||||||
|
return metadata.date as Date
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentModificationDate: Date? {
|
||||||
|
return metadata.date as Date
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDownloaded: Bool {
|
||||||
|
return metadata.directory || NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDownloading: Bool {
|
||||||
|
return metadata.status == NextcloudItemMetadataTable.Status.downloading.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadingError: Error? {
|
||||||
|
if metadata.status == NextcloudItemMetadataTable.Status.downloadError.rawValue {
|
||||||
|
return FileProviderItemTransferError.downloadError
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isUploaded: Bool {
|
||||||
|
return NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isUploading: Bool {
|
||||||
|
return metadata.status == NextcloudItemMetadataTable.Status.uploading.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadingError: Error? {
|
||||||
|
if metadata.status == NextcloudItemMetadataTable.Status.uploadError.rawValue {
|
||||||
|
return FileProviderItemTransferError.uploadError
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var childItemCount: NSNumber? {
|
||||||
|
if metadata.directory {
|
||||||
|
return NSNumber(integerLiteral: NextcloudFilesDatabaseManager.shared.childItemsForDirectory(metadata).count)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(metadata: NextcloudItemMetadataTable, parentItemIdentifier: NSFileProviderItemIdentifier, ncKit: NextcloudKit) {
|
||||||
|
self.metadata = metadata
|
||||||
|
self.parentItemIdentifier = parentItemIdentifier
|
||||||
|
self.ncKit = ncKit
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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 FileProvider
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
class FileProviderMaterialisedEnumerationObserver : NSObject, NSFileProviderEnumerationObserver {
|
||||||
|
let ncKitAccount: String
|
||||||
|
let completionHandler: (_ deletedOcIds: Set<String>) -> Void
|
||||||
|
var allEnumeratedItemIds: Set<String> = Set<String>()
|
||||||
|
|
||||||
|
required init(ncKitAccount: String, completionHandler: @escaping(_ deletedOcIds: Set<String>) -> Void) {
|
||||||
|
self.ncKitAccount = ncKitAccount
|
||||||
|
self.completionHandler = completionHandler
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func didEnumerate(_ updatedItems: [NSFileProviderItemProtocol]) {
|
||||||
|
let updatedItemsIds = Array(updatedItems.map { $0.itemIdentifier.rawValue })
|
||||||
|
|
||||||
|
for updatedItemsId in updatedItemsIds {
|
||||||
|
allEnumeratedItemIds.insert(updatedItemsId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishEnumerating(upTo nextPage: NSFileProviderPage?) {
|
||||||
|
Logger.materialisedFileHandling.debug("Handling enumerated materialised items.")
|
||||||
|
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(self.allEnumeratedItemIds,
|
||||||
|
account: self.ncKitAccount,
|
||||||
|
completionHandler: self.completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishEnumeratingWithError(_ error: Error) {
|
||||||
|
Logger.materialisedFileHandling.error("Ran into error when enumerating materialised items: \(error.localizedDescription, privacy: .public). Handling items enumerated so far")
|
||||||
|
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(self.allEnumeratedItemIds,
|
||||||
|
account: self.ncKitAccount,
|
||||||
|
completionHandler: self.completionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func handleEnumeratedItems(_ itemIds: Set<String>, account: String, completionHandler: @escaping(_ deletedOcIds: Set<String>) -> Void) {
|
||||||
|
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||||
|
let databaseLocalFileMetadatas = dbManager.localFileMetadatas(account: account)
|
||||||
|
var noLongerMaterialisedIds = Set<String>()
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
for localFile in databaseLocalFileMetadatas {
|
||||||
|
let localFileOcId = localFile.ocId
|
||||||
|
|
||||||
|
guard itemIds.contains(localFileOcId) else {
|
||||||
|
noLongerMaterialisedIds.insert(localFileOcId)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Logger.materialisedFileHandling.info("Cleaning up local file metadatas for unmaterialised items")
|
||||||
|
for itemId in noLongerMaterialisedIds {
|
||||||
|
dbManager.deleteLocalFileMetadata(ocId: itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(noLongerMaterialisedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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 Foundation
|
||||||
|
import NCDesktopClientSocketKit
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
class FileProviderSocketLineProcessor: NSObject, LineProcessor {
|
||||||
|
var delegate: FileProviderExtension
|
||||||
|
|
||||||
|
required init(delegate: FileProviderExtension) {
|
||||||
|
self.delegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(_ line: String) {
|
||||||
|
if (line.contains("~")) { // We use this as the separator specifically in ACCOUNT_DETAILS
|
||||||
|
Logger.desktopClientConnection.debug("Processing file provider line with potentially sensitive user data")
|
||||||
|
} else {
|
||||||
|
Logger.desktopClientConnection.debug("Processing file provider line: \(line, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let splitLine = line.split(separator: ":", maxSplits: 1)
|
||||||
|
guard let commandSubsequence = splitLine.first else {
|
||||||
|
Logger.desktopClientConnection.error("Input line did not have a first element")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let command = String(commandSubsequence);
|
||||||
|
|
||||||
|
Logger.desktopClientConnection.debug("Received command: \(command, privacy: .public)")
|
||||||
|
if (command == "SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER") {
|
||||||
|
delegate.sendFileProviderDomainIdentifier()
|
||||||
|
} else if (command == "ACCOUNT_NOT_AUTHENTICATED") {
|
||||||
|
delegate.removeAccountConfig()
|
||||||
|
} else if (command == "ACCOUNT_DETAILS") {
|
||||||
|
guard let accountDetailsSubsequence = splitLine.last else { return }
|
||||||
|
let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 2)
|
||||||
|
|
||||||
|
let user = String(splitAccountDetails[0])
|
||||||
|
let serverUrl = String(splitAccountDetails[1])
|
||||||
|
let password = String(splitAccountDetails[2])
|
||||||
|
|
||||||
|
delegate.setupDomainAccount(user: user, serverUrl: serverUrl, password: password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>$(OC_APPLICATION_NAME) File Provider Extension</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)</string>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionFileProviderDocumentGroup</key>
|
||||||
|
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
|
||||||
|
<key>NSExtensionFileProviderSupportsEnumeration</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.fileprovider-nonui</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).FileProviderExtension</string>
|
||||||
|
</dict>
|
||||||
|
<key>SocketApiPrefix</key>
|
||||||
|
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 FileProvider
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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 Foundation
|
||||||
|
import FileProvider
|
||||||
|
|
||||||
|
class NextcloudAccount: NSObject {
|
||||||
|
static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/"
|
||||||
|
let username, password, ncKitAccount, serverUrl, davFilesUrl: String
|
||||||
|
|
||||||
|
init(user: String, serverUrl: String, password: String) {
|
||||||
|
self.username = user
|
||||||
|
self.password = password
|
||||||
|
self.ncKitAccount = user + " " + serverUrl
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,9 +15,10 @@
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <FinderSync/FinderSync.h>
|
#import <FinderSync/FinderSync.h>
|
||||||
|
#import <NCDesktopClientSocketKit/LocalSocketClient.h>
|
||||||
|
|
||||||
#import "SyncClient.h"
|
#import "SyncClient.h"
|
||||||
#import "LineProcessor.h"
|
#import "FinderSyncSocketLineProcessor.h"
|
||||||
#import "LocalSocketClient.h"
|
|
||||||
|
|
||||||
@interface FinderSync : FIFinderSync <SyncClientDelegate>
|
@interface FinderSync : FIFinderSync <SyncClientDelegate>
|
||||||
{
|
{
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
NSCondition *_menuIsComplete;
|
NSCondition *_menuIsComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property LineProcessor *lineProcessor;
|
@property FinderSyncSocketLineProcessor *lineProcessor;
|
||||||
@property LocalSocketClient *localSocketClient;
|
@property LocalSocketClient *localSocketClient;
|
||||||
|
|
||||||
@end
|
@end
|
|
@ -62,17 +62,15 @@
|
||||||
NSLog(@"Socket path: %@", socketPath.path);
|
NSLog(@"Socket path: %@", socketPath.path);
|
||||||
|
|
||||||
if (socketPath.path) {
|
if (socketPath.path) {
|
||||||
self.lineProcessor = [[LineProcessor alloc] initWithDelegate:self];
|
self.lineProcessor = [[FinderSyncSocketLineProcessor alloc] initWithDelegate:self];
|
||||||
self.localSocketClient = [[LocalSocketClient alloc] init:socketPath.path
|
self.localSocketClient = [[LocalSocketClient alloc] initWithSocketPath:socketPath.path
|
||||||
lineProcessor:self.lineProcessor];
|
lineProcessor:self.lineProcessor];
|
||||||
[self.localSocketClient start];
|
[self.localSocketClient start];
|
||||||
|
[self.localSocketClient askOnSocket:@"" query:@"GET_STRINGS"];
|
||||||
} else {
|
} else {
|
||||||
NSLog(@"No socket path. Not initiating local socket client.");
|
NSLog(@"No socket path. Not initiating local socket client.");
|
||||||
self.localSocketClient = nil;
|
self.localSocketClient = nil;
|
||||||
}
|
}
|
||||||
_registeredDirectories = [[NSMutableSet alloc] init];
|
|
||||||
_strings = [[NSMutableDictionary alloc] init];
|
|
||||||
_menuIsComplete = [[NSCondition alloc] init];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
|
@ -12,22 +12,24 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#import <NCDesktopClientSocketKit/LineProcessor.h>
|
||||||
|
|
||||||
#import "SyncClient.h"
|
#import "SyncClient.h"
|
||||||
|
|
||||||
#ifndef LineProcessor_h
|
#ifndef FinderSyncSocketLineProcessor_h
|
||||||
#define LineProcessor_h
|
#define FinderSyncSocketLineProcessor_h
|
||||||
|
|
||||||
/// This class is in charge of dispatching all work that must be done on the UI side of the extension.
|
/// This class is in charge of dispatching all work that must be done on the UI side of the extension.
|
||||||
/// Tasks are dispatched on the main UI thread for this reason.
|
/// Tasks are dispatched on the main UI thread for this reason.
|
||||||
///
|
///
|
||||||
/// These tasks are parsed from byte data (UTF9 strings) acquired from the socket; look at the
|
/// These tasks are parsed from byte data (UTF8 strings) acquired from the socket; look at the
|
||||||
/// LocalSocketClient for more detail on how data is read from and written to the socket.
|
/// LocalSocketClient for more detail on how data is read from and written to the socket.
|
||||||
|
|
||||||
@interface LineProcessor : NSObject
|
@interface FinderSyncSocketLineProcessor : NSObject<LineProcessor>
|
||||||
@property(nonatomic, weak)id<SyncClientDelegate> delegate;
|
|
||||||
|
@property(nonatomic, weak) id<SyncClientDelegate> delegate;
|
||||||
|
|
||||||
- (instancetype)initWithDelegate:(id<SyncClientDelegate>)delegate;
|
- (instancetype)initWithDelegate:(id<SyncClientDelegate>)delegate;
|
||||||
- (void)process:(NSString*)line;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
#endif /* LineProcessor_h */
|
#endif /* LineProcessor_h */
|
|
@ -13,9 +13,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "LineProcessor.h"
|
#import "FinderSyncSocketLineProcessor.h"
|
||||||
|
|
||||||
@implementation LineProcessor
|
@implementation FinderSyncSocketLineProcessor
|
||||||
|
|
||||||
-(instancetype)initWithDelegate:(id<SyncClientDelegate>)delegate
|
-(instancetype)initWithDelegate:(id<SyncClientDelegate>)delegate
|
||||||
{
|
{
|
|
@ -2,8 +2,6 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>SocketApiPrefix</key>
|
|
||||||
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
|
@ -39,5 +37,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>SocketApiPrefix</key>
|
||||||
|
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LineProcessor_h
|
||||||
|
#define LineProcessor_h
|
||||||
|
|
||||||
|
@protocol LineProcessor<NSObject>
|
||||||
|
|
||||||
|
- (void)process:(NSString*)line;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#endif /* LineProcessor_h */
|
|
@ -12,7 +12,7 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import "LineProcessor.h"
|
#import <NCDesktopClientSocketKit/LineProcessor.h>
|
||||||
|
|
||||||
#ifndef LocalSocketClient_h
|
#ifndef LocalSocketClient_h
|
||||||
#define LocalSocketClient_h
|
#define LocalSocketClient_h
|
||||||
|
@ -37,26 +37,20 @@
|
||||||
|
|
||||||
@interface LocalSocketClient : NSObject
|
@interface LocalSocketClient : NSObject
|
||||||
|
|
||||||
@property NSString* socketPath;
|
- (instancetype)initWithSocketPath:(NSString*)socketPath
|
||||||
@property LineProcessor* lineProcessor;
|
lineProcessor:(id<LineProcessor>)lineProcessor;
|
||||||
@property int sock;
|
|
||||||
@property dispatch_queue_t localSocketQueue;
|
@property (readonly) BOOL isConnected;
|
||||||
@property dispatch_source_t readSource;
|
|
||||||
@property dispatch_source_t writeSource;
|
|
||||||
@property NSMutableData* inBuffer;
|
|
||||||
@property NSMutableData* outBuffer;
|
|
||||||
|
|
||||||
- (instancetype)init:(NSString*)socketPath lineProcessor:(LineProcessor*)lineProcessor;
|
|
||||||
- (BOOL)isConnected;
|
|
||||||
- (void)start;
|
- (void)start;
|
||||||
- (void)restart;
|
- (void)restart;
|
||||||
- (void)closeConnection;
|
- (void)closeConnection;
|
||||||
- (NSString*)strErr;
|
|
||||||
- (void)askOnSocket:(NSString*)path query:(NSString*)verb;
|
- (void)sendMessage:(NSString*)message;
|
||||||
- (void)askForIcon:(NSString*)path isDirectory:(BOOL)isDirectory;
|
- (void)askOnSocket:(NSString*)path
|
||||||
- (void)readFromSocket;
|
query:(NSString*)verb;
|
||||||
- (void)writeToSocket;
|
- (void)askForIcon:(NSString*)path
|
||||||
- (void)processInBuffer;
|
isDirectory:(BOOL)isDirectory;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
#endif /* LocalSocketClient_h */
|
#endif /* LocalSocketClient_h */
|
|
@ -13,28 +13,45 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "LocalSocketClient.h"
|
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#import "LocalSocketClient.h"
|
||||||
|
|
||||||
|
@interface LocalSocketClient()
|
||||||
|
{
|
||||||
|
NSString* _socketPath;
|
||||||
|
id<LineProcessor> _lineProcessor;
|
||||||
|
|
||||||
|
int _sock;
|
||||||
|
dispatch_queue_t _localSocketQueue;
|
||||||
|
dispatch_source_t _readSource;
|
||||||
|
dispatch_source_t _writeSource;
|
||||||
|
NSMutableData* _inBuffer;
|
||||||
|
NSMutableData* _outBuffer;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation LocalSocketClient
|
@implementation LocalSocketClient
|
||||||
|
|
||||||
- (instancetype)init:(NSString*)socketPath lineProcessor:(LineProcessor*)lineProcessor
|
- (instancetype)initWithSocketPath:(NSString*)socketPath
|
||||||
|
lineProcessor:(id<LineProcessor>)lineProcessor
|
||||||
{
|
{
|
||||||
NSLog(@"Initiating local socket client.");
|
NSLog(@"Initiating local socket client pointing to %@", socketPath);
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
|
||||||
if(self) {
|
if(self) {
|
||||||
self.socketPath = socketPath;
|
_socketPath = socketPath;
|
||||||
self.lineProcessor = lineProcessor;
|
_lineProcessor = lineProcessor;
|
||||||
|
|
||||||
self.sock = -1;
|
_sock = -1;
|
||||||
self.localSocketQueue = dispatch_queue_create("localSocketQueue", DISPATCH_QUEUE_SERIAL);
|
_localSocketQueue = dispatch_queue_create("localSocketQueue", DISPATCH_QUEUE_SERIAL);
|
||||||
|
|
||||||
self.inBuffer = [NSMutableData data];
|
_inBuffer = [NSMutableData data];
|
||||||
self.outBuffer = [NSMutableData data];
|
_outBuffer = [NSMutableData data];
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
|
@ -42,8 +59,8 @@
|
||||||
|
|
||||||
- (BOOL)isConnected
|
- (BOOL)isConnected
|
||||||
{
|
{
|
||||||
NSLog(@"Checking is connected: %@", self.sock != -1 ? @"YES" : @"NO");
|
NSLog(@"Checking is connected: %@", _sock != -1 ? @"YES" : @"NO");
|
||||||
return self.sock != -1;
|
return _sock != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)start
|
- (void)start
|
||||||
|
@ -54,44 +71,44 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sockaddr_un localSocketAddr;
|
struct sockaddr_un localSocketAddr;
|
||||||
unsigned long socketPathByteCount = [self.socketPath lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; // add 1 for the NUL terminator char
|
unsigned long socketPathByteCount = [_socketPath lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; // add 1 for the NUL terminator char
|
||||||
int maxByteCount = sizeof(localSocketAddr.sun_path);
|
int maxByteCount = sizeof(localSocketAddr.sun_path);
|
||||||
|
|
||||||
if(socketPathByteCount > maxByteCount) {
|
if(socketPathByteCount > maxByteCount) {
|
||||||
// LOG THAT THE SOCKET PATH IS TOO LONG HERE
|
// LOG THAT THE SOCKET PATH IS TOO LONG HERE
|
||||||
NSLog(@"Socket path '%@' is too long: maximum socket path length is %i, this path is of length %lu", self.socketPath, maxByteCount, socketPathByteCount);
|
NSLog(@"Socket path '%@' is too long: maximum socket path length is %i, this path is of length %lu", _socketPath, maxByteCount, socketPathByteCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"Opening local socket...");
|
NSLog(@"Opening local socket...");
|
||||||
|
|
||||||
// LOG THAT THE SOCKET IS BEING OPENED HERE
|
// LOG THAT THE SOCKET IS BEING OPENED HERE
|
||||||
self.sock = socket(AF_LOCAL, SOCK_STREAM, 0);
|
_sock = socket(AF_LOCAL, SOCK_STREAM, 0);
|
||||||
|
|
||||||
if(self.sock == -1) {
|
if(_sock == -1) {
|
||||||
NSLog(@"Cannot open socket: '%@'", [self strErr]);
|
NSLog(@"Cannot open socket: '%@'", [self strErr]);
|
||||||
[self restart];
|
[self restart];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"Local socket opened. Connecting to '%@' ...", self.socketPath);
|
NSLog(@"Local socket opened. Connecting to '%@' ...", _socketPath);
|
||||||
|
|
||||||
localSocketAddr.sun_family = AF_LOCAL & 0xff;
|
localSocketAddr.sun_family = AF_LOCAL & 0xff;
|
||||||
|
|
||||||
const char* pathBytes = [self.socketPath UTF8String];
|
const char* pathBytes = [_socketPath UTF8String];
|
||||||
strcpy(localSocketAddr.sun_path, pathBytes);
|
strcpy(localSocketAddr.sun_path, pathBytes);
|
||||||
|
|
||||||
int connectionStatus = connect(self.sock, (struct sockaddr*)&localSocketAddr, sizeof(localSocketAddr));
|
int connectionStatus = connect(_sock, (struct sockaddr*)&localSocketAddr, sizeof(localSocketAddr));
|
||||||
|
|
||||||
if(connectionStatus == -1) {
|
if(connectionStatus == -1) {
|
||||||
NSLog(@"Could not connect to '%@': '%@'", self.socketPath, [self strErr]);
|
NSLog(@"Could not connect to '%@': '%@'", _socketPath, [self strErr]);
|
||||||
[self restart];
|
[self restart];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int flags = fcntl(self.sock, F_GETFL, 0);
|
int flags = fcntl(_sock, F_GETFL, 0);
|
||||||
|
|
||||||
if(fcntl(self.sock, F_SETFL, flags | O_NONBLOCK) == -1) {
|
if(fcntl(_sock, F_SETFL, flags | O_NONBLOCK) == -1) {
|
||||||
NSLog(@"Could not set socket to non-blocking mode: '%@'", [self strErr]);
|
NSLog(@"Could not set socket to non-blocking mode: '%@'", [self strErr]);
|
||||||
[self restart];
|
[self restart];
|
||||||
return;
|
return;
|
||||||
|
@ -99,17 +116,17 @@
|
||||||
|
|
||||||
NSLog(@"Connected to socket. Setting up dispatch sources...");
|
NSLog(@"Connected to socket. Setting up dispatch sources...");
|
||||||
|
|
||||||
self.readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self.sock, 0, self.localSocketQueue);
|
_readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, _sock, 0, _localSocketQueue);
|
||||||
dispatch_source_set_event_handler(self.readSource, ^(void){ [self readFromSocket]; });
|
dispatch_source_set_event_handler(_readSource, ^(void){ [self readFromSocket]; });
|
||||||
dispatch_source_set_cancel_handler(self.readSource, ^(void){
|
dispatch_source_set_cancel_handler(_readSource, ^(void){
|
||||||
self.readSource = nil;
|
self->_readSource = nil;
|
||||||
[self closeConnection];
|
[self closeConnection];
|
||||||
});
|
});
|
||||||
|
|
||||||
self.writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, self.sock, 0, self.localSocketQueue);
|
_writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, _sock, 0, _localSocketQueue);
|
||||||
dispatch_source_set_event_handler(self.writeSource, ^(void){ [self writeToSocket]; });
|
dispatch_source_set_event_handler(_writeSource, ^(void){ [self writeToSocket]; });
|
||||||
dispatch_source_set_cancel_handler(self.writeSource, ^(void){
|
dispatch_source_set_cancel_handler(_writeSource, ^(void){
|
||||||
self.writeSource = nil;
|
self->_writeSource = nil;
|
||||||
[self closeConnection];
|
[self closeConnection];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,8 +136,7 @@
|
||||||
|
|
||||||
NSLog(@"Starting to read from socket");
|
NSLog(@"Starting to read from socket");
|
||||||
|
|
||||||
dispatch_resume(self.readSource);
|
dispatch_resume(_readSource);
|
||||||
[self askOnSocket:@"" query:@"GET_STRINGS"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)restart
|
- (void)restart
|
||||||
|
@ -138,35 +154,35 @@
|
||||||
{
|
{
|
||||||
NSLog(@"Closing connection.");
|
NSLog(@"Closing connection.");
|
||||||
|
|
||||||
if(self.readSource) {
|
if(_readSource) {
|
||||||
// Since dispatch_source_cancel works asynchronously, if we deallocate the dispatch source here then we can
|
// Since dispatch_source_cancel works asynchronously, if we deallocate the dispatch source here then we can
|
||||||
// cause a crash. So instead we strongly hold a reference to the read source and deallocate it asynchronously
|
// cause a crash. So instead we strongly hold a reference to the read source and deallocate it asynchronously
|
||||||
// with the handler.
|
// with the handler.
|
||||||
__block dispatch_source_t previousReadSource = self.readSource;
|
__block dispatch_source_t previousReadSource = _readSource;
|
||||||
dispatch_source_set_cancel_handler(self.readSource, ^{
|
dispatch_source_set_cancel_handler(_readSource, ^{
|
||||||
previousReadSource = nil;
|
previousReadSource = nil;
|
||||||
});
|
});
|
||||||
dispatch_source_cancel(self.readSource);
|
dispatch_source_cancel(_readSource);
|
||||||
// The readSource is still alive due to the other reference and will be deallocated by the cancel handler
|
// The readSource is still alive due to the other reference and will be deallocated by the cancel handler
|
||||||
self.readSource = nil;
|
_readSource = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(self.writeSource) {
|
if(_writeSource) {
|
||||||
// Same deal with the write source
|
// Same deal with the write source
|
||||||
__block dispatch_source_t previousWriteSource = self.writeSource;
|
__block dispatch_source_t previousWriteSource = _writeSource;
|
||||||
dispatch_source_set_cancel_handler(self.writeSource, ^{
|
dispatch_source_set_cancel_handler(_writeSource, ^{
|
||||||
previousWriteSource = nil;
|
previousWriteSource = nil;
|
||||||
});
|
});
|
||||||
dispatch_source_cancel(self.writeSource);
|
dispatch_source_cancel(_writeSource);
|
||||||
self.writeSource = nil;
|
_writeSource = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
[self.inBuffer setLength:0];
|
[_inBuffer setLength:0];
|
||||||
[self.outBuffer setLength: 0];
|
[_outBuffer setLength: 0];
|
||||||
|
|
||||||
if(self.sock != -1) {
|
if(_sock != -1) {
|
||||||
close(self.sock);
|
close(_sock);
|
||||||
self.sock = -1;
|
_sock = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,45 +199,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)askOnSocket:(NSString *)path query:(NSString *)verb
|
- (void)sendMessage:(NSString *)message
|
||||||
{
|
{
|
||||||
NSString *line = [NSString stringWithFormat:@"%@:%@\n", verb, path];
|
dispatch_async(_localSocketQueue, ^(void) {
|
||||||
dispatch_async(self.localSocketQueue, ^(void) {
|
|
||||||
if(![self isConnected]) {
|
if(![self isConnected]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL writeSourceIsSuspended = [self.outBuffer length] == 0;
|
BOOL writeSourceIsSuspended = [self->_outBuffer length] == 0;
|
||||||
|
|
||||||
[self.outBuffer appendData:[line dataUsingEncoding:NSUTF8StringEncoding]];
|
[self->_outBuffer appendData:[message dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
|
||||||
NSLog(@"Writing to out buffer: '%@'", line);
|
NSLog(@"Writing to out buffer: '%@'", message);
|
||||||
NSLog(@"Out buffer now %li bytes", [self.outBuffer length]);
|
NSLog(@"Out buffer now %li bytes", [self->_outBuffer length]);
|
||||||
|
|
||||||
if(writeSourceIsSuspended) {
|
if(writeSourceIsSuspended) {
|
||||||
NSLog(@"Resuming write dispatch source.");
|
NSLog(@"Resuming write dispatch source.");
|
||||||
dispatch_resume(self.writeSource);
|
dispatch_resume(self->_writeSource);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)askOnSocket:(NSString *)path query:(NSString *)verb
|
||||||
|
{
|
||||||
|
NSString *line = [NSString stringWithFormat:@"%@:%@\n", verb, path];
|
||||||
|
[self sendMessage:line];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)writeToSocket
|
- (void)writeToSocket
|
||||||
{
|
{
|
||||||
if(![self isConnected]) {
|
if(![self isConnected]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if([self.outBuffer length] == 0) {
|
if([_outBuffer length] == 0) {
|
||||||
NSLog(@"Empty out buffer, suspending write dispatch source.");
|
NSLog(@"Empty out buffer, suspending write dispatch source.");
|
||||||
dispatch_suspend(self.writeSource);
|
dispatch_suspend(_writeSource);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLog(@"About to write %li bytes from outbuffer to socket.", [self.outBuffer length]);
|
NSLog(@"About to write %li bytes from outbuffer to socket.", [_outBuffer length]);
|
||||||
|
|
||||||
long bytesWritten = write(self.sock, [self.outBuffer bytes], [self.outBuffer length]);
|
long bytesWritten = write(_sock, [_outBuffer bytes], [_outBuffer length]);
|
||||||
char lineWritten[[self.outBuffer length]];
|
char lineWritten[[_outBuffer length]];
|
||||||
memcpy(lineWritten, [self.outBuffer bytes], [self.outBuffer length]);
|
memcpy(lineWritten, [_outBuffer bytes], [_outBuffer length]);
|
||||||
NSLog(@"Wrote %li bytes to socket. Line written was: '%@'", bytesWritten, [NSString stringWithUTF8String:lineWritten]);
|
NSLog(@"Wrote %li bytes to socket. Line written was: '%@'", bytesWritten, [NSString stringWithUTF8String:lineWritten]);
|
||||||
|
|
||||||
if(bytesWritten == 0) {
|
if(bytesWritten == 0) {
|
||||||
|
@ -240,13 +261,13 @@
|
||||||
[self restart];
|
[self restart];
|
||||||
}
|
}
|
||||||
} else if(bytesWritten > 0) {
|
} else if(bytesWritten > 0) {
|
||||||
[self.outBuffer replaceBytesInRange:NSMakeRange(0, bytesWritten) withBytes:NULL length:0];
|
[_outBuffer replaceBytesInRange:NSMakeRange(0, bytesWritten) withBytes:NULL length:0];
|
||||||
|
|
||||||
NSLog(@"Out buffer cleared. Now count is %li bytes.", [self.outBuffer length]);
|
NSLog(@"Out buffer cleared. Now count is %li bytes.", [_outBuffer length]);
|
||||||
|
|
||||||
if([self.outBuffer length] == 0) {
|
if([_outBuffer length] == 0) {
|
||||||
NSLog(@"Out buffer has been emptied, suspending write dispatch source.");
|
NSLog(@"Out buffer has been emptied, suspending write dispatch source.");
|
||||||
dispatch_suspend(self.writeSource);
|
dispatch_suspend(_writeSource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,7 +298,7 @@
|
||||||
char buffer[bufferLength];
|
char buffer[bufferLength];
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
long bytesRead = read(self.sock, buffer, bufferLength);
|
long bytesRead = read(_sock, buffer, bufferLength);
|
||||||
|
|
||||||
NSLog(@"Read %li bytes from socket.", bytesRead);
|
NSLog(@"Read %li bytes from socket.", bytesRead);
|
||||||
|
|
||||||
|
@ -297,7 +318,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
[self.inBuffer appendBytes:buffer length:bytesRead];
|
[_inBuffer appendBytes:buffer length:bytesRead];
|
||||||
[self processInBuffer];
|
[self processInBuffer];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -305,22 +326,22 @@
|
||||||
|
|
||||||
- (void)processInBuffer
|
- (void)processInBuffer
|
||||||
{
|
{
|
||||||
NSLog(@"Processing in buffer. In buffer length %li", [self.inBuffer length]);
|
NSLog(@"Processing in buffer. In buffer length %li", [_inBuffer length]);
|
||||||
UInt8 separator[] = {0xa}; // Byte value for "\n"
|
UInt8 separator[] = {0xa}; // Byte value for "\n"
|
||||||
while(true) {
|
while(true) {
|
||||||
NSRange firstSeparatorIndex = [self.inBuffer rangeOfData:[NSData dataWithBytes:separator length:1] options:0 range:NSMakeRange(0, [self.inBuffer length])];
|
NSRange firstSeparatorIndex = [_inBuffer rangeOfData:[NSData dataWithBytes:separator length:1] options:0 range:NSMakeRange(0, [_inBuffer length])];
|
||||||
|
|
||||||
if(firstSeparatorIndex.location == NSNotFound) {
|
if(firstSeparatorIndex.location == NSNotFound) {
|
||||||
NSLog(@"No separator found. Stopping.");
|
NSLog(@"No separator found. Stopping.");
|
||||||
return; // No separator, nope out
|
return; // No separator, nope out
|
||||||
} else {
|
} else {
|
||||||
unsigned char *buffer = [self.inBuffer mutableBytes];
|
unsigned char *buffer = [_inBuffer mutableBytes];
|
||||||
buffer[firstSeparatorIndex.location] = 0; // Add NULL terminator, so we can use C string methods
|
buffer[firstSeparatorIndex.location] = 0; // Add NULL terminator, so we can use C string methods
|
||||||
|
|
||||||
NSString *newLine = [NSString stringWithUTF8String:[self.inBuffer bytes]];
|
NSString *newLine = [NSString stringWithUTF8String:[_inBuffer bytes]];
|
||||||
|
|
||||||
[self.inBuffer replaceBytesInRange:NSMakeRange(0, firstSeparatorIndex.location + 1) withBytes:NULL length:0];
|
[_inBuffer replaceBytesInRange:NSMakeRange(0, firstSeparatorIndex.location + 1) withBytes:NULL length:0];
|
||||||
[self.lineProcessor process:newLine];
|
[_lineProcessor process:newLine];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// NCDesktopClientSocketKit.h
|
||||||
|
// NCDesktopClientSocketKit
|
||||||
|
//
|
||||||
|
// Created by Claudio Cambra on 23/12/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
//! Project version number for NCDesktopClientSocketKit.
|
||||||
|
FOUNDATION_EXPORT double NCDesktopClientSocketKitVersionNumber;
|
||||||
|
|
||||||
|
//! Project version string for NCDesktopClientSocketKit.
|
||||||
|
FOUNDATION_EXPORT const unsigned char NCDesktopClientSocketKitVersionString[];
|
||||||
|
|
||||||
|
// In this header, you should import all the public headers of your framework using statements like #import <NCDesktopClientSocketKit/PublicHeader.h>
|
||||||
|
|
||||||
|
#import <NCDesktopClientSocketKit/LocalSocketClient.h>
|
||||||
|
#import <NCDesktopClientSocketKit/LineProcessor.h>
|
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,6 @@
|
||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj">
|
location = "self:">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -18,7 +18,7 @@
|
||||||
BlueprintIdentifier = "C2B573D61B1CD9CE00303B36"
|
BlueprintIdentifier = "C2B573D61B1CD9CE00303B36"
|
||||||
BuildableName = "FinderSyncExt.appex"
|
BuildableName = "FinderSyncExt.appex"
|
||||||
BlueprintName = "FinderSyncExt"
|
BlueprintName = "FinderSyncExt"
|
||||||
ReferencedContainer = "container:OwnCloudFinderSync.xcodeproj">
|
ReferencedContainer = "container:NextcloudIntegration.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
BlueprintIdentifier = "C2B573B01B1CD91E00303B36"
|
BlueprintIdentifier = "C2B573B01B1CD91E00303B36"
|
||||||
BuildableName = "desktopclient.app"
|
BuildableName = "desktopclient.app"
|
||||||
BlueprintName = "desktopclient"
|
BlueprintName = "desktopclient"
|
||||||
ReferencedContainer = "container:OwnCloudFinderSync.xcodeproj">
|
ReferencedContainer = "container:NextcloudIntegration.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
|
@ -48,10 +48,21 @@
|
||||||
BlueprintIdentifier = "C2B573D61B1CD9CE00303B36"
|
BlueprintIdentifier = "C2B573D61B1CD9CE00303B36"
|
||||||
BuildableName = "FinderSyncExt.appex"
|
BuildableName = "FinderSyncExt.appex"
|
||||||
BlueprintName = "FinderSyncExt"
|
BlueprintName = "FinderSyncExt"
|
||||||
ReferencedContainer = "container:OwnCloudFinderSync.xcodeproj">
|
ReferencedContainer = "container:NextcloudIntegration.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
<Testables>
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "53903D142956164F00D0B308"
|
||||||
|
BuildableName = "NCDesktopClientSocketKitTests.xctest"
|
||||||
|
BlueprintName = "NCDesktopClientSocketKitTests"
|
||||||
|
ReferencedContainer = "container:NextcloudIntegration.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
|
@ -73,7 +84,7 @@
|
||||||
BlueprintIdentifier = "C2B573B01B1CD91E00303B36"
|
BlueprintIdentifier = "C2B573B01B1CD91E00303B36"
|
||||||
BuildableName = "desktopclient.app"
|
BuildableName = "desktopclient.app"
|
||||||
BlueprintName = "desktopclient"
|
BlueprintName = "desktopclient"
|
||||||
ReferencedContainer = "container:OwnCloudFinderSync.xcodeproj">
|
ReferencedContainer = "container:NextcloudIntegration.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
|
@ -92,7 +103,7 @@
|
||||||
BlueprintIdentifier = "C2B573B01B1CD91E00303B36"
|
BlueprintIdentifier = "C2B573B01B1CD91E00303B36"
|
||||||
BuildableName = "desktopclient.app"
|
BuildableName = "desktopclient.app"
|
||||||
BlueprintName = "desktopclient"
|
BlueprintName = "desktopclient"
|
||||||
ReferencedContainer = "container:OwnCloudFinderSync.xcodeproj">
|
ReferencedContainer = "container:NextcloudIntegration.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
|
@ -1,600 +0,0 @@
|
||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 46;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
539158AC27BE71A900816F56 /* LineProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 539158AB27BE71A900816F56 /* LineProcessor.m */; };
|
|
||||||
539158B327BEC98A00816F56 /* LocalSocketClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 539158B227BEC98A00816F56 /* LocalSocketClient.m */; };
|
|
||||||
C2B573BA1B1CD91E00303B36 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; };
|
|
||||||
C2B573D21B1CD94B00303B36 /* main.m in Resources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; };
|
|
||||||
C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573DD1B1CD9CE00303B36 /* FinderSync.m */; };
|
|
||||||
C2B573E21B1CD9CE00303B36 /* FinderSyncExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
|
||||||
C2B573F31B1DAD6400303B36 /* error.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573EB1B1DAD6400303B36 /* error.iconset */; };
|
|
||||||
C2B573F41B1DAD6400303B36 /* ok_swm.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573EC1B1DAD6400303B36 /* ok_swm.iconset */; };
|
|
||||||
C2B573F51B1DAD6400303B36 /* ok.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573ED1B1DAD6400303B36 /* ok.iconset */; };
|
|
||||||
C2B573F71B1DAD6400303B36 /* sync.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573EF1B1DAD6400303B36 /* sync.iconset */; };
|
|
||||||
C2B573F91B1DAD6400303B36 /* warning.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573F11B1DAD6400303B36 /* warning.iconset */; };
|
|
||||||
/* End PBXBuildFile section */
|
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
C2B573DF1B1CD9CE00303B36 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = C2B573951B1CD88000303B36 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = C2B573D61B1CD9CE00303B36;
|
|
||||||
remoteInfo = FinderSyncExt;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
|
||||||
C2B573E11B1CD9CE00303B36 /* Embed App Extensions */ = {
|
|
||||||
isa = PBXCopyFilesBuildPhase;
|
|
||||||
buildActionMask = 8;
|
|
||||||
dstPath = "";
|
|
||||||
dstSubfolderSpec = 13;
|
|
||||||
files = (
|
|
||||||
C2B573E21B1CD9CE00303B36 /* FinderSyncExt.appex in Embed App Extensions */,
|
|
||||||
);
|
|
||||||
name = "Embed App Extensions";
|
|
||||||
runOnlyForDeploymentPostprocessing = 1;
|
|
||||||
};
|
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
539158A927BE606500816F56 /* LineProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LineProcessor.h; sourceTree = "<group>"; };
|
|
||||||
539158AA27BE67CC00816F56 /* SyncClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SyncClient.h; sourceTree = "<group>"; };
|
|
||||||
539158AB27BE71A900816F56 /* LineProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LineProcessor.m; sourceTree = "<group>"; };
|
|
||||||
539158B127BE891500816F56 /* LocalSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalSocketClient.h; sourceTree = "<group>"; };
|
|
||||||
539158B227BEC98A00816F56 /* LocalSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalSocketClient.m; sourceTree = "<group>"; };
|
|
||||||
C2B573B11B1CD91E00303B36 /* desktopclient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktopclient.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
C2B573B51B1CD91E00303B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
C2B573B91B1CD91E00303B36 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
|
||||||
C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FinderSyncExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
C2B573DA1B1CD9CE00303B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
C2B573DB1B1CD9CE00303B36 /* FinderSyncExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = FinderSyncExt.entitlements; sourceTree = "<group>"; };
|
|
||||||
C2B573DC1B1CD9CE00303B36 /* FinderSync.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FinderSync.h; sourceTree = "<group>"; };
|
|
||||||
C2B573DD1B1CD9CE00303B36 /* FinderSync.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FinderSync.m; sourceTree = "<group>"; };
|
|
||||||
C2B573EB1B1DAD6400303B36 /* error.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = error.iconset; path = ../../icons/nopadding/error.iconset; sourceTree = SOURCE_ROOT; };
|
|
||||||
C2B573EC1B1DAD6400303B36 /* ok_swm.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = ok_swm.iconset; path = ../../icons/nopadding/ok_swm.iconset; sourceTree = SOURCE_ROOT; };
|
|
||||||
C2B573ED1B1DAD6400303B36 /* ok.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = ok.iconset; path = ../../icons/nopadding/ok.iconset; sourceTree = SOURCE_ROOT; };
|
|
||||||
C2B573EF1B1DAD6400303B36 /* sync.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = sync.iconset; path = ../../icons/nopadding/sync.iconset; sourceTree = SOURCE_ROOT; };
|
|
||||||
C2B573F11B1DAD6400303B36 /* warning.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = warning.iconset; path = ../../icons/nopadding/warning.iconset; sourceTree = SOURCE_ROOT; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
C2B573AE1B1CD91E00303B36 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
C2B573D41B1CD9CE00303B36 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
C2B573941B1CD88000303B36 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C2B573B31B1CD91E00303B36 /* desktopclient */,
|
|
||||||
C2B573D81B1CD9CE00303B36 /* FinderSyncExt */,
|
|
||||||
C2B573B21B1CD91E00303B36 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C2B573B21B1CD91E00303B36 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C2B573B11B1CD91E00303B36 /* desktopclient.app */,
|
|
||||||
C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C2B573B31B1CD91E00303B36 /* desktopclient */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C2B573B41B1CD91E00303B36 /* Supporting Files */,
|
|
||||||
);
|
|
||||||
path = desktopclient;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C2B573B41B1CD91E00303B36 /* Supporting Files */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C2B573B51B1CD91E00303B36 /* Info.plist */,
|
|
||||||
C2B573B91B1CD91E00303B36 /* main.m */,
|
|
||||||
);
|
|
||||||
name = "Supporting Files";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C2B573D81B1CD9CE00303B36 /* FinderSyncExt */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
539158AA27BE67CC00816F56 /* SyncClient.h */,
|
|
||||||
C2B573DC1B1CD9CE00303B36 /* FinderSync.h */,
|
|
||||||
C2B573DD1B1CD9CE00303B36 /* FinderSync.m */,
|
|
||||||
539158A927BE606500816F56 /* LineProcessor.h */,
|
|
||||||
539158AB27BE71A900816F56 /* LineProcessor.m */,
|
|
||||||
539158B127BE891500816F56 /* LocalSocketClient.h */,
|
|
||||||
539158B227BEC98A00816F56 /* LocalSocketClient.m */,
|
|
||||||
C2B573D91B1CD9CE00303B36 /* Supporting Files */,
|
|
||||||
);
|
|
||||||
path = FinderSyncExt;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
C2B573D91B1CD9CE00303B36 /* Supporting Files */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
C2B573EB1B1DAD6400303B36 /* error.iconset */,
|
|
||||||
C2B573EC1B1DAD6400303B36 /* ok_swm.iconset */,
|
|
||||||
C2B573ED1B1DAD6400303B36 /* ok.iconset */,
|
|
||||||
C2B573EF1B1DAD6400303B36 /* sync.iconset */,
|
|
||||||
C2B573F11B1DAD6400303B36 /* warning.iconset */,
|
|
||||||
C2B573DA1B1CD9CE00303B36 /* Info.plist */,
|
|
||||||
C2B573DB1B1CD9CE00303B36 /* FinderSyncExt.entitlements */,
|
|
||||||
);
|
|
||||||
name = "Supporting Files";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
C2B573B01B1CD91E00303B36 /* desktopclient */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = C2B573CC1B1CD91E00303B36 /* Build configuration list for PBXNativeTarget "desktopclient" */;
|
|
||||||
buildPhases = (
|
|
||||||
C2B573AD1B1CD91E00303B36 /* Sources */,
|
|
||||||
C2B573AE1B1CD91E00303B36 /* Frameworks */,
|
|
||||||
C2B573AF1B1CD91E00303B36 /* Resources */,
|
|
||||||
C2B573E11B1CD9CE00303B36 /* Embed App Extensions */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
C2B573E01B1CD9CE00303B36 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
name = desktopclient;
|
|
||||||
productName = desktopclient;
|
|
||||||
productReference = C2B573B11B1CD91E00303B36 /* desktopclient.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
C2B573D61B1CD9CE00303B36 /* FinderSyncExt */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = C2B573E31B1CD9CE00303B36 /* Build configuration list for PBXNativeTarget "FinderSyncExt" */;
|
|
||||||
buildPhases = (
|
|
||||||
C2B573D31B1CD9CE00303B36 /* Sources */,
|
|
||||||
C2B573D41B1CD9CE00303B36 /* Frameworks */,
|
|
||||||
C2B573D51B1CD9CE00303B36 /* Resources */,
|
|
||||||
5B3335471CA058E200E11A45 /* ShellScript */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
name = FinderSyncExt;
|
|
||||||
productName = FinderSyncExt;
|
|
||||||
productReference = C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */;
|
|
||||||
productType = "com.apple.product-type.app-extension";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
C2B573951B1CD88000303B36 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
LastUpgradeCheck = 1240;
|
|
||||||
TargetAttributes = {
|
|
||||||
C2B573B01B1CD91E00303B36 = {
|
|
||||||
CreatedOnToolsVersion = 6.3.1;
|
|
||||||
DevelopmentTeam = 9B5WD74GWJ;
|
|
||||||
ProvisioningStyle = Manual;
|
|
||||||
};
|
|
||||||
C2B573D61B1CD9CE00303B36 = {
|
|
||||||
CreatedOnToolsVersion = 6.3.1;
|
|
||||||
DevelopmentTeam = 9B5WD74GWJ;
|
|
||||||
ProvisioningStyle = Manual;
|
|
||||||
SystemCapabilities = {
|
|
||||||
com.apple.ApplicationGroups.Mac = {
|
|
||||||
enabled = 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = C2B573981B1CD88000303B36 /* Build configuration list for PBXProject "OwnCloudFinderSync" */;
|
|
||||||
compatibilityVersion = "Xcode 3.2";
|
|
||||||
developmentRegion = English;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
English,
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = C2B573941B1CD88000303B36;
|
|
||||||
productRefGroup = C2B573B21B1CD91E00303B36 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
C2B573B01B1CD91E00303B36 /* desktopclient */,
|
|
||||||
C2B573D61B1CD9CE00303B36 /* FinderSyncExt */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
C2B573AF1B1CD91E00303B36 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
C2B573D21B1CD94B00303B36 /* main.m in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
C2B573D51B1CD9CE00303B36 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
C2B573F91B1DAD6400303B36 /* warning.iconset in Resources */,
|
|
||||||
C2B573F31B1DAD6400303B36 /* error.iconset in Resources */,
|
|
||||||
C2B573F71B1DAD6400303B36 /* sync.iconset in Resources */,
|
|
||||||
C2B573F41B1DAD6400303B36 /* ok_swm.iconset in Resources */,
|
|
||||||
C2B573F51B1DAD6400303B36 /* ok.iconset in Resources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
|
||||||
5B3335471CA058E200E11A45 /* ShellScript */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "if [[ ${OC_OEM_SHARE_ICNS} ]]; then\n cp ${OC_OEM_SHARE_ICNS} ${BUILT_PRODUCTS_DIR}/FinderSyncExt.appex/Contents/Resources/app.icns\nfi";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
C2B573AD1B1CD91E00303B36 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
C2B573BA1B1CD91E00303B36 /* main.m in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
C2B573D31B1CD9CE00303B36 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
539158B327BEC98A00816F56 /* LocalSocketClient.m in Sources */,
|
|
||||||
539158AC27BE71A900816F56 /* LineProcessor.m in Sources */,
|
|
||||||
C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
C2B573E01B1CD9CE00303B36 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = C2B573D61B1CD9CE00303B36 /* FinderSyncExt */;
|
|
||||||
targetProxy = C2B573DF1B1CD9CE00303B36 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
C2B573991B1CD88000303B36 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
C2B5739A1B1CD88000303B36 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
C2B573CD1B1CD91E00303B36 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_IDENTITY = "-";
|
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
INFOPLIST_FILE = desktopclient/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE = "";
|
|
||||||
SDKROOT = macosx;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
C2B573CE1B1CD91E00303B36 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_IDENTITY = "-";
|
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
INFOPLIST_FILE = desktopclient/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE = "";
|
|
||||||
SDKROOT = macosx;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
C2B573E41B1CD9CE00303B36 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = FinderSyncExt/FinderSyncExt.entitlements;
|
|
||||||
CODE_SIGN_IDENTITY = "-";
|
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
INFOPLIST_FILE = FinderSyncExt/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks";
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
|
||||||
OC_APPLICATION_NAME = ownCloud;
|
|
||||||
OC_APPLICATION_REV_DOMAIN = com.owncloud.desktopclient;
|
|
||||||
OC_OEM_SHARE_ICNS = "";
|
|
||||||
OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX = "";
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE = "";
|
|
||||||
SDKROOT = macosx;
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
C2B573E51B1CD9CE00303B36 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = FinderSyncExt/FinderSyncExt.entitlements;
|
|
||||||
CODE_SIGN_IDENTITY = "-";
|
|
||||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
INFOPLIST_FILE = FinderSyncExt/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks";
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
OC_APPLICATION_NAME = ownCloud;
|
|
||||||
OC_APPLICATION_REV_DOMAIN = com.owncloud.desktopclient;
|
|
||||||
OC_OEM_SHARE_ICNS = "";
|
|
||||||
OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX = "";
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE = "";
|
|
||||||
SDKROOT = macosx;
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
C2B573981B1CD88000303B36 /* Build configuration list for PBXProject "OwnCloudFinderSync" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
C2B573991B1CD88000303B36 /* Debug */,
|
|
||||||
C2B5739A1B1CD88000303B36 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
C2B573CC1B1CD91E00303B36 /* Build configuration list for PBXNativeTarget "desktopclient" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
C2B573CD1B1CD91E00303B36 /* Debug */,
|
|
||||||
C2B573CE1B1CD91E00303B36 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
C2B573E31B1CD9CE00303B36 /* Build configuration list for PBXNativeTarget "FinderSyncExt" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
C2B573E41B1CD9CE00303B36 /* Debug */,
|
|
||||||
C2B573E51B1CD9CE00303B36 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
};
|
|
||||||
rootObject = C2B573951B1CD88000303B36 /* Project object */;
|
|
||||||
}
|
|
|
@ -281,6 +281,19 @@ IF( APPLE )
|
||||||
list(APPEND client_SRCS cocoainitializer_mac.mm)
|
list(APPEND client_SRCS cocoainitializer_mac.mm)
|
||||||
list(APPEND client_SRCS systray.mm)
|
list(APPEND client_SRCS systray.mm)
|
||||||
|
|
||||||
|
if (BUILD_FILE_PROVIDER_MODULE)
|
||||||
|
list(APPEND client_SRCS
|
||||||
|
macOS/fileprovider.h
|
||||||
|
macOS/fileprovider_mac.mm
|
||||||
|
macOS/fileproviderdomainmanager.h
|
||||||
|
macOS/fileproviderdomainmanager_mac.mm
|
||||||
|
macOS/fileprovidersocketcontroller.h
|
||||||
|
macOS/fileprovidersocketcontroller.cpp
|
||||||
|
macOS/fileprovidersocketserver.h
|
||||||
|
macOS/fileprovidersocketserver.cpp
|
||||||
|
macOS/fileprovidersocketserver_mac.mm)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(SPARKLE_FOUND AND BUILD_UPDATER)
|
if(SPARKLE_FOUND AND BUILD_UPDATER)
|
||||||
# Define this, we need to check in updater.cpp
|
# Define this, we need to check in updater.cpp
|
||||||
add_definitions(-DHAVE_SPARKLE)
|
add_definitions(-DHAVE_SPARKLE)
|
||||||
|
@ -653,7 +666,12 @@ endif()
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
find_package(Qt5 COMPONENTS MacExtras)
|
find_package(Qt5 COMPONENTS MacExtras)
|
||||||
|
|
||||||
|
if (BUILD_FILE_PROVIDER_MODULE)
|
||||||
|
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications -framework FileProvider")
|
||||||
|
else()
|
||||||
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications")
|
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WITH_CRASHREPORTER)
|
if(WITH_CRASHREPORTER)
|
||||||
|
|
|
@ -51,6 +51,8 @@
|
||||||
|
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN)
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
#include "macOS/fileprovider.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(WITH_CRASHREPORTER)
|
#if defined(WITH_CRASHREPORTER)
|
||||||
|
@ -371,7 +373,7 @@ Application::Application(int &argc, char **argv)
|
||||||
}
|
}
|
||||||
|
|
||||||
_folderManager.reset(new FolderMan);
|
_folderManager.reset(new FolderMan);
|
||||||
#ifdef Q_OS_WIN
|
#if defined(Q_OS_WIN)
|
||||||
_shellExtensionsServer.reset(new ShellExtensionsServer);
|
_shellExtensionsServer.reset(new ShellExtensionsServer);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -400,6 +402,10 @@ Application::Application(int &argc, char **argv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(Q_OS_MACOS)
|
||||||
|
_fileProvider.reset(new Mac::FileProvider);
|
||||||
|
#endif
|
||||||
|
|
||||||
FolderMan::instance()->setSyncEnabled(true);
|
FolderMan::instance()->setSyncEnabled(true);
|
||||||
|
|
||||||
setQuitOnLastWindowClosed(false);
|
setQuitOnLastWindowClosed(false);
|
||||||
|
|
|
@ -49,6 +49,12 @@ class Folder;
|
||||||
class ShellExtensionsServer;
|
class ShellExtensionsServer;
|
||||||
class SslErrorDialog;
|
class SslErrorDialog;
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
namespace Mac {
|
||||||
|
class FileProvider;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The Application class
|
* @brief The Application class
|
||||||
* @ingroup gui
|
* @ingroup gui
|
||||||
|
@ -152,8 +158,10 @@ private:
|
||||||
QScopedPointer<CrashReporter::Handler> _crashHandler;
|
QScopedPointer<CrashReporter::Handler> _crashHandler;
|
||||||
#endif
|
#endif
|
||||||
QScopedPointer<FolderMan> _folderManager;
|
QScopedPointer<FolderMan> _folderManager;
|
||||||
#ifdef Q_OS_WIN
|
#if defined(Q_OS_WIN)
|
||||||
QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
|
QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
|
||||||
|
#elif defined(Q_OS_MACOS)
|
||||||
|
QScopedPointer<Mac::FileProvider> _fileProvider;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
52
src/gui/macOS/fileprovider.h
Normal file
52
src/gui/macOS/fileprovider.h
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "fileproviderdomainmanager.h"
|
||||||
|
#include "fileprovidersocketserver.h"
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
// NOTE: For the file provider extension to work, the app bundle will
|
||||||
|
// need to be correctly codesigned!
|
||||||
|
|
||||||
|
class FileProvider : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
static FileProvider *instance();
|
||||||
|
~FileProvider() override;
|
||||||
|
|
||||||
|
static bool fileProviderAvailable();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<FileProviderDomainManager> _domainManager;
|
||||||
|
std::unique_ptr<FileProviderSocketServer> _socketServer;
|
||||||
|
|
||||||
|
static FileProvider *_instance;
|
||||||
|
explicit FileProvider(QObject * const parent = nullptr);
|
||||||
|
|
||||||
|
friend class OCC::Application;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
} // namespace OCC
|
92
src/gui/macOS/fileprovider_mac.mm
Normal file
92
src/gui/macOS/fileprovider_mac.mm
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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 <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
#include "configfile.h"
|
||||||
|
|
||||||
|
#include "fileprovider.h"
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcMacFileProvider, "nextcloud.gui.macfileprovider", QtInfoMsg)
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
FileProvider* FileProvider::_instance = nullptr;
|
||||||
|
|
||||||
|
FileProvider::FileProvider(QObject * const parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
Q_ASSERT(!_instance);
|
||||||
|
|
||||||
|
if (!fileProviderAvailable()) {
|
||||||
|
qCInfo(lcMacFileProvider) << "File provider system is not available on this version of macOS.";
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
} else if (!ConfigFile().macFileProviderModuleEnabled()) {
|
||||||
|
qCInfo(lcMacFileProvider) << "File provider module is not enabled in application config.";
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCInfo(lcMacFileProvider) << "Initialising file provider domain manager.";
|
||||||
|
_domainManager = std::make_unique<FileProviderDomainManager>(new FileProviderDomainManager(this));
|
||||||
|
|
||||||
|
if (_domainManager) {
|
||||||
|
qCDebug(lcMacFileProvider()) << "Initialized file provider domain manager";
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProvider) << "Initialising file provider socket server.";
|
||||||
|
_socketServer = std::make_unique<FileProviderSocketServer>(new FileProviderSocketServer(this));
|
||||||
|
|
||||||
|
if (_socketServer) {
|
||||||
|
qCDebug(lcMacFileProvider) << "Initialised file provider socket server.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileProvider *FileProvider::instance()
|
||||||
|
{
|
||||||
|
if (!fileProviderAvailable()) {
|
||||||
|
qCInfo(lcMacFileProvider) << "File provider system is not available on this version of macOS.";
|
||||||
|
return nullptr;
|
||||||
|
} else if (!ConfigFile().macFileProviderModuleEnabled()) {
|
||||||
|
qCInfo(lcMacFileProvider) << "File provider module is not enabled in application config.";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_instance) {
|
||||||
|
_instance = new FileProvider();
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileProvider::~FileProvider()
|
||||||
|
{
|
||||||
|
_instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileProvider::fileProviderAvailable()
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
} // namespace OCC
|
62
src/gui/macOS/fileproviderdomainmanager.h
Normal file
62
src/gui/macOS/fileproviderdomainmanager.h
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "accountstate.h"
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
class Account;
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
class FileProviderDomainManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileProviderDomainManager(QObject * const parent = nullptr);
|
||||||
|
~FileProviderDomainManager() override;
|
||||||
|
|
||||||
|
static AccountStatePtr accountStateFromFileProviderDomainIdentifier(const QString &domainIdentifier);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void setupFileProviderDomains();
|
||||||
|
|
||||||
|
void addFileProviderDomainForAccount(const OCC::AccountState * const accountState);
|
||||||
|
void removeFileProviderDomainForAccount(const OCC::AccountState * const accountState);
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
|
||||||
|
} // namespace OCC
|
636
src/gui/macOS/fileproviderdomainmanager_mac.mm
Normal file
636
src/gui/macOS/fileproviderdomainmanager_mac.mm
Normal file
|
@ -0,0 +1,636 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "configfile.h"
|
||||||
|
#import <FileProvider/FileProvider.h>
|
||||||
|
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "fileproviderdomainmanager.h"
|
||||||
|
#include "pushnotifications.h"
|
||||||
|
|
||||||
|
#include "gui/accountmanager.h"
|
||||||
|
#include "libsync/account.h"
|
||||||
|
|
||||||
|
// Ensure that conversion to/from domain identifiers and display names
|
||||||
|
// are consistent throughout these classes
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString domainIdentifierForAccount(const OCC::Account * const account)
|
||||||
|
{
|
||||||
|
Q_ASSERT(account);
|
||||||
|
return account->userIdAtHostWithPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString domainIdentifierForAccount(const OCC::AccountPtr account)
|
||||||
|
{
|
||||||
|
return domainIdentifierForAccount(account.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString domainDisplayNameForAccount(const OCC::Account * const account)
|
||||||
|
{
|
||||||
|
Q_ASSERT(account);
|
||||||
|
return account->displayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString domainDisplayNameForAccount(const OCC::AccountPtr account)
|
||||||
|
{
|
||||||
|
return domainDisplayNameForAccount(account.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString accountIdFromDomainId(const QString &domainId)
|
||||||
|
{
|
||||||
|
return domainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString accountIdFromDomainId(NSString * const domainId)
|
||||||
|
{
|
||||||
|
return accountIdFromDomainId(QString::fromNSString(domainId));
|
||||||
|
}
|
||||||
|
|
||||||
|
API_AVAILABLE(macos(11.0))
|
||||||
|
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 {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcMacFileProviderDomainManager, "nextcloud.gui.macfileproviderdomainmanager", QtInfoMsg)
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
class API_AVAILABLE(macos(11.0)) FileProviderDomainManager::MacImplementation {
|
||||||
|
|
||||||
|
public:
|
||||||
|
MacImplementation() = default;
|
||||||
|
~MacImplementation() = default;
|
||||||
|
|
||||||
|
void findExistingFileProviderDomains()
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
// Wait for this to finish
|
||||||
|
dispatch_group_t dispatchGroup = dispatch_group_create();
|
||||||
|
dispatch_group_enter(dispatchGroup);
|
||||||
|
|
||||||
|
[NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray<NSFileProviderDomain *> * const domains, NSError * const error) {
|
||||||
|
if(error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Could not get existing file provider domains: "
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
dispatch_group_leave(dispatchGroup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domains.count == 0) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Found no existing file provider domains";
|
||||||
|
dispatch_group_leave(dispatchGroup);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSFileProviderDomain * const domain in domains) {
|
||||||
|
const auto accountId = accountIdFromDomain(domain);
|
||||||
|
|
||||||
|
if (const auto accountState = AccountManager::instance()->accountFromUserId(accountId);
|
||||||
|
accountState &&
|
||||||
|
accountState->account() &&
|
||||||
|
domainDisplayNameForAccount(accountState->account()) == QString::fromNSString(domain.displayName)) {
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Found existing file provider domain for account:"
|
||||||
|
<< accountState->account()->displayName();
|
||||||
|
[domain retain];
|
||||||
|
_registeredDomains.insert(accountId, domain);
|
||||||
|
|
||||||
|
NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:domain];
|
||||||
|
[fpManager reconnectWithCompletionHandler:^(NSError * const error) {
|
||||||
|
if (error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: "
|
||||||
|
<< domain.displayName
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Successfully reconnected file provider domain: "
|
||||||
|
<< domain.displayName;
|
||||||
|
}];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Found existing file provider domain with no known configured account:"
|
||||||
|
<< domain.displayName;
|
||||||
|
[NSFileProviderManager removeDomain:domain completionHandler:^(NSError * const error) {
|
||||||
|
if(error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error removing file provider domain: "
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch_group_leave(dispatchGroup);
|
||||||
|
}];
|
||||||
|
|
||||||
|
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFileProviderDomain(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
const auto domainDisplayName = domainDisplayNameForAccount(account);
|
||||||
|
const auto domainId = domainIdentifierForAccount(account);
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Adding new file provider domain with id: " << domainId;
|
||||||
|
|
||||||
|
if(_registeredDomains.contains(domainId) && _registeredDomains.value(domainId) != nil) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "File provider domain with id already exists: " << domainId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const fileProviderDomain = [[NSFileProviderDomain alloc] initWithIdentifier:domainId.toNSString()
|
||||||
|
displayName:domainDisplayName.toNSString()];
|
||||||
|
[fileProviderDomain retain];
|
||||||
|
|
||||||
|
[NSFileProviderManager addDomain:fileProviderDomain completionHandler:^(NSError * const error) {
|
||||||
|
if(error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error adding file provider domain: "
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredDomains.insert(domainId, fileProviderDomain);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeFileProviderDomain(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
const auto domainId = domainIdentifierForAccount(account);
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Removing file provider domain with id: " << domainId;
|
||||||
|
|
||||||
|
if(!_registeredDomains.contains(domainId)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const fileProviderDomain = _registeredDomains[domainId];
|
||||||
|
|
||||||
|
[NSFileProviderManager removeDomain:fileProviderDomain completionHandler:^(NSError *error) {
|
||||||
|
if(error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error removing file provider domain: "
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const domain = _registeredDomains.take(domainId);
|
||||||
|
[domain release];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAllFileProviderDomains()
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Removing all file provider domains.";
|
||||||
|
|
||||||
|
[NSFileProviderManager removeAllDomainsWithCompletionHandler:^(NSError * const error) {
|
||||||
|
if(error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error removing all file provider domains: "
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto registeredDomainPtrs = _registeredDomains.values();
|
||||||
|
for (NSFileProviderDomain * const domain : registeredDomainPtrs) {
|
||||||
|
if (domain != nil) {
|
||||||
|
[domain release];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_registeredDomains.clear();
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void wipeAllFileProviderDomains()
|
||||||
|
{
|
||||||
|
if (@available(macOS 12.0, *)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Removing and wiping all file provider domains";
|
||||||
|
|
||||||
|
[NSFileProviderManager getDomainsWithCompletionHandler:^(NSArray<NSFileProviderDomain *> * const domains, NSError * const error) {
|
||||||
|
if (error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error removing and wiping file provider domains: "
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSFileProviderDomain * const domain in domains) {
|
||||||
|
[NSFileProviderManager removeDomain:domain mode:NSFileProviderDomainRemovalModeRemoveAll completionHandler:^(NSURL * const preservedLocation, NSError * const error) {
|
||||||
|
Q_UNUSED(preservedLocation)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error removing and wiping file provider domain: "
|
||||||
|
<< domain.displayName
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const registeredDomainPtr = _registeredDomains.take(QString::fromNSString(domain.identifier));
|
||||||
|
if (registeredDomainPtr != nil) {
|
||||||
|
[domain release];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
} else if (@available(macOS 11.0, *)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Removing all file provider domains, can't specify wipe on macOS 11";
|
||||||
|
removeAllFileProviderDomains();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnectFileProviderDomainForAccount(const AccountState * const accountState, const QString &message)
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
const auto domainId = domainIdentifierForAccount(account);
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Disconnecting file provider domain with id: " << domainId;
|
||||||
|
|
||||||
|
if(!_registeredDomains.contains(domainId)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const fileProviderDomain = _registeredDomains[domainId];
|
||||||
|
Q_ASSERT(fileProviderDomain != nil);
|
||||||
|
|
||||||
|
NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:fileProviderDomain];
|
||||||
|
[fpManager disconnectWithReason:message.toNSString()
|
||||||
|
options:NSFileProviderManagerDisconnectionOptionsTemporary
|
||||||
|
completionHandler:^(NSError * const error) {
|
||||||
|
if (error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error disconnecting file provider domain: "
|
||||||
|
<< fileProviderDomain.displayName
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Successfully disconnected file provider domain: "
|
||||||
|
<< fileProviderDomain.displayName;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconnectFileProviderDomainForAccount(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
const auto domainId = domainIdentifierForAccount(account);
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Reconnecting file provider domain with id: " << domainId;
|
||||||
|
|
||||||
|
if(!_registeredDomains.contains(domainId)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const fileProviderDomain = _registeredDomains[domainId];
|
||||||
|
Q_ASSERT(fileProviderDomain != nil);
|
||||||
|
|
||||||
|
NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:fileProviderDomain];
|
||||||
|
[fpManager reconnectWithCompletionHandler:^(NSError * const error) {
|
||||||
|
if (error) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error reconnecting file provider domain: "
|
||||||
|
<< fileProviderDomain.displayName
|
||||||
|
<< error.code
|
||||||
|
<< error.localizedDescription;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Successfully reconnected file provider domain: "
|
||||||
|
<< fileProviderDomain.displayName;
|
||||||
|
|
||||||
|
signalEnumeratorChanged(account.get());
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void signalEnumeratorChanged(const Account * const account)
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
Q_ASSERT(account);
|
||||||
|
const auto domainId = domainIdentifierForAccount(account);
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Signalling enumerator changed in file provider domain for account with id: " << domainId;
|
||||||
|
|
||||||
|
if(!_registeredDomains.contains(domainId)) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "File provider domain not found for id: " << domainId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSFileProviderDomain * const fileProviderDomain = _registeredDomains[domainId];
|
||||||
|
Q_ASSERT(fileProviderDomain != nil);
|
||||||
|
|
||||||
|
NSFileProviderManager * const fpManager = [NSFileProviderManager managerForDomain:fileProviderDomain];
|
||||||
|
[fpManager signalEnumeratorForContainerItemIdentifier:NSFileProviderWorkingSetContainerItemIdentifier completionHandler:^(NSError * const error) {
|
||||||
|
if (error != nil) {
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Error signalling enumerator changed for working set:"
|
||||||
|
<< error.localizedDescription;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList configuredDomainIds() const {
|
||||||
|
return _registeredDomains.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHash<QString, NSFileProviderDomain*> _registeredDomains;
|
||||||
|
};
|
||||||
|
|
||||||
|
FileProviderDomainManager::FileProviderDomainManager(QObject * const parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
if (@available(macOS 11.0, *)) {
|
||||||
|
d.reset(new FileProviderDomainManager::MacImplementation());
|
||||||
|
|
||||||
|
ConfigFile cfg;
|
||||||
|
std::chrono::milliseconds polltime = cfg.remotePollInterval();
|
||||||
|
_enumeratorSignallingTimer.setInterval(polltime.count());
|
||||||
|
connect(&_enumeratorSignallingTimer, &QTimer::timeout,
|
||||||
|
this, &FileProviderDomainManager::slotEnumeratorSignallingTimerTimeout);
|
||||||
|
_enumeratorSignallingTimer.start();
|
||||||
|
|
||||||
|
setupFileProviderDomains();
|
||||||
|
|
||||||
|
connect(AccountManager::instance(), &AccountManager::accountAdded,
|
||||||
|
this, &FileProviderDomainManager::addFileProviderDomainForAccount);
|
||||||
|
// If an account is deleted from the client, accountSyncConnectionRemoved will be
|
||||||
|
// emitted first. So we treat accountRemoved as only being relevant to client
|
||||||
|
// shutdowns.
|
||||||
|
connect(AccountManager::instance(), &AccountManager::accountSyncConnectionRemoved,
|
||||||
|
this, &FileProviderDomainManager::removeFileProviderDomainForAccount);
|
||||||
|
connect(AccountManager::instance(), &AccountManager::accountRemoved,
|
||||||
|
this, [this](const AccountState * const accountState) {
|
||||||
|
|
||||||
|
const auto trReason = tr("%1 application has been closed. Reopen to reconnect.").arg(APPLICATION_NAME);
|
||||||
|
disconnectFileProviderDomainForAccount(accountState, trReason);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
qCWarning(lcMacFileProviderDomainManager()) << "Trying to run File Provider on system that does not support it.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileProviderDomainManager::~FileProviderDomainManager() = default;
|
||||||
|
|
||||||
|
void FileProviderDomainManager::setupFileProviderDomains()
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->findExistingFileProviderDomains();
|
||||||
|
|
||||||
|
for(auto &accountState : AccountManager::instance()->accounts()) {
|
||||||
|
addFileProviderDomainForAccount(accountState.data());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderDomainManager::addFileProviderDomainForAccount(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
d->addFileProviderDomain(accountState);
|
||||||
|
|
||||||
|
// Disconnect the domain when something changes regarding authentication
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(account);
|
||||||
|
d->signalEnumeratorChanged(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderDomainManager::removeFileProviderDomainForAccount(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->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)
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
d->disconnectFileProviderDomainForAccount(accountState, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderDomainManager::reconnectFileProviderDomainForAccount(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto account = accountState->account();
|
||||||
|
|
||||||
|
d->reconnectFileProviderDomainForAccount(accountState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderDomainManager::slotAccountStateChanged(const AccountState * const accountState)
|
||||||
|
{
|
||||||
|
if (!d) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(accountState);
|
||||||
|
const auto state = accountState->state();
|
||||||
|
|
||||||
|
qCDebug(lcMacFileProviderDomainManager) << "Account state changed for account:"
|
||||||
|
<< accountState->account()->displayName()
|
||||||
|
<< "changing connection status of file provider domain.";
|
||||||
|
|
||||||
|
switch(state) {
|
||||||
|
case AccountState::Disconnected:
|
||||||
|
case AccountState::ConfigurationError:
|
||||||
|
case AccountState::NetworkError:
|
||||||
|
case AccountState::ServiceUnavailable:
|
||||||
|
case AccountState::MaintenanceMode:
|
||||||
|
// Do nothing, File Provider will by itself figure out connection issue
|
||||||
|
break;
|
||||||
|
case AccountState::SignedOut:
|
||||||
|
case AccountState::AskingCredentials:
|
||||||
|
{
|
||||||
|
// Disconnect File Provider domain while unauthenticated
|
||||||
|
const auto trReason = tr("This account is not authenticated. Please check your account state in the %1 application.").arg(APPLICATION_NAME);
|
||||||
|
disconnectFileProviderDomainForAccount(accountState, trReason);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AccountState::Connected:
|
||||||
|
// Provide credentials
|
||||||
|
reconnectFileProviderDomainForAccount(accountState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
qCWarning(lcMacFileProviderDomainManager) << "Cannot return accountstateptr for empty domain identifier";
|
||||||
|
return AccountStatePtr();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto accountUserId = accountIdFromDomainId(domainIdentifier);
|
||||||
|
const auto accountForReceivedDomainIdentifier = AccountManager::instance()->accountFromUserId(accountUserId);
|
||||||
|
if (!accountForReceivedDomainIdentifier) {
|
||||||
|
qCWarning(lcMacFileProviderDomainManager) << "Could not find account matching user id matching file provider domain identifier:"
|
||||||
|
<< domainIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountForReceivedDomainIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
|
||||||
|
} // namespace OCC
|
207
src/gui/macOS/fileprovidersocketcontroller.cpp
Normal file
207
src/gui/macOS/fileprovidersocketcontroller.cpp
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "fileprovidersocketcontroller.h"
|
||||||
|
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
#include "accountmanager.h"
|
||||||
|
#include "fileproviderdomainmanager.h"
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcFileProviderSocketController, "nextcloud.gui.macos.fileprovider.socketcontroller", QtInfoMsg)
|
||||||
|
|
||||||
|
FileProviderSocketController::FileProviderSocketController(QLocalSocket * const socket, QObject * const parent)
|
||||||
|
: QObject{parent}
|
||||||
|
, _socket(socket)
|
||||||
|
{
|
||||||
|
connect(socket, &QLocalSocket::readyRead,
|
||||||
|
this, &FileProviderSocketController::slotReadyRead);
|
||||||
|
connect(socket, &QLocalSocket::disconnected,
|
||||||
|
this, &FileProviderSocketController::slotOnDisconnected);
|
||||||
|
connect(socket, &QLocalSocket::destroyed,
|
||||||
|
this, &FileProviderSocketController::slotSocketDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::slotOnDisconnected()
|
||||||
|
{
|
||||||
|
qCInfo(lcFileProviderSocketController) << "File provider socket disconnected";
|
||||||
|
_socket->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::slotSocketDestroyed(const QObject * const object)
|
||||||
|
{
|
||||||
|
Q_UNUSED(object)
|
||||||
|
qCInfo(lcFileProviderSocketController) << "File provider socket object has been destroyed, destroying controller";
|
||||||
|
Q_EMIT socketDestroyed(_socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::slotReadyRead()
|
||||||
|
{
|
||||||
|
Q_ASSERT(_socket);
|
||||||
|
if (!_socket) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Cannot read data on dead socket";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(_socket->canReadLine()) {
|
||||||
|
const auto line = QString::fromUtf8(_socket->readLine().trimmed()).normalized(QString::NormalizationForm_C);
|
||||||
|
qCDebug(lcFileProviderSocketController) << "Received message in file provider socket:" << line;
|
||||||
|
|
||||||
|
parseReceivedLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::parseReceivedLine(const QString &receivedLine)
|
||||||
|
{
|
||||||
|
if (receivedLine.isEmpty()) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Received empty line, can't parse.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto argPos = receivedLine.indexOf(QLatin1Char(':'));
|
||||||
|
if (argPos == -1) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Received line:"
|
||||||
|
<< receivedLine
|
||||||
|
<< "is incorrectly structured. Can't parse.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto command = receivedLine.mid(0, argPos);
|
||||||
|
const auto argument = receivedLine.mid(argPos + 1);
|
||||||
|
|
||||||
|
if (command == QStringLiteral("FILE_PROVIDER_DOMAIN_IDENTIFIER_REQUEST_REPLY")) {
|
||||||
|
_accountState = FileProviderDomainManager::accountStateFromFileProviderDomainIdentifier(argument);
|
||||||
|
sendAccountDetails();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Unknown command or reply:" << receivedLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::sendMessage(const QString &message) const
|
||||||
|
{
|
||||||
|
if (!_socket) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Not sending message on dead file provider socket:" << message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(lcFileProviderSocketController) << "Sending File Provider socket message:" << message;
|
||||||
|
const auto lineEndChar = '\n';
|
||||||
|
const auto messageToSend = message.endsWith(lineEndChar) ? message : message + lineEndChar;
|
||||||
|
const auto bytesToSend = messageToSend.toUtf8();
|
||||||
|
const auto sent = _socket->write(bytesToSend);
|
||||||
|
|
||||||
|
if (sent != bytesToSend.length()) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Could not send all data on file provider socket for:" << message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void FileProviderSocketController::start()
|
||||||
|
{
|
||||||
|
Q_ASSERT(_socket);
|
||||||
|
if (!_socket) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Cannot start communication on dead socket";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFileProviderDomainInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::requestFileProviderDomainInfo() const
|
||||||
|
{
|
||||||
|
Q_ASSERT(_socket);
|
||||||
|
if (!_socket) {
|
||||||
|
qCWarning(lcFileProviderSocketController) << "Cannot request file provider domain data on dead socket";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto requestMessage = QStringLiteral("SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER");
|
||||||
|
sendMessage(requestMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::slotAccountStateChanged(const AccountState::State state)
|
||||||
|
{
|
||||||
|
switch(state) {
|
||||||
|
case AccountState::Disconnected:
|
||||||
|
case AccountState::ConfigurationError:
|
||||||
|
case AccountState::NetworkError:
|
||||||
|
case AccountState::ServiceUnavailable:
|
||||||
|
case AccountState::MaintenanceMode:
|
||||||
|
// Do nothing, File Provider will by itself figure out connection issue
|
||||||
|
break;
|
||||||
|
case AccountState::SignedOut:
|
||||||
|
case AccountState::AskingCredentials:
|
||||||
|
// Notify File Provider that it should show the not authenticated message
|
||||||
|
sendNotAuthenticated();
|
||||||
|
break;
|
||||||
|
case AccountState::Connected:
|
||||||
|
// Provide credentials
|
||||||
|
sendAccountDetails();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::sendNotAuthenticated() const
|
||||||
|
{
|
||||||
|
Q_ASSERT(_accountState);
|
||||||
|
const auto account = _accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
qCDebug(lcFileProviderSocketController) << "About to send not authenticated message to file provider extension"
|
||||||
|
<< account->displayName();
|
||||||
|
|
||||||
|
const auto message = QString(QStringLiteral("ACCOUNT_NOT_AUTHENTICATED"));
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketController::sendAccountDetails() const
|
||||||
|
{
|
||||||
|
Q_ASSERT(_accountState);
|
||||||
|
const auto account = _accountState->account();
|
||||||
|
Q_ASSERT(account);
|
||||||
|
|
||||||
|
qCDebug(lcFileProviderSocketController) << "About to send account details to file provider extension"
|
||||||
|
<< account->displayName();
|
||||||
|
|
||||||
|
connect(_accountState.data(), &AccountState::stateChanged, this, &FileProviderSocketController::slotAccountStateChanged, Qt::UniqueConnection);
|
||||||
|
|
||||||
|
if (!_accountState->isConnected()) {
|
||||||
|
qCDebug(lcFileProviderSocketController) << "Not sending account details yet as account is not connected"
|
||||||
|
<< account->displayName();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto credentials = account->credentials();
|
||||||
|
Q_ASSERT(credentials);
|
||||||
|
const auto accountUser = credentials->user();
|
||||||
|
const auto accountUrl = account->url().toString();
|
||||||
|
const auto accountPassword = credentials->password();
|
||||||
|
|
||||||
|
// We cannot use colons as separators here due to "https://" in the url
|
||||||
|
const auto message = QString(QStringLiteral("ACCOUNT_DETAILS:") +
|
||||||
|
accountUser + "~" +
|
||||||
|
accountUrl + "~" +
|
||||||
|
accountPassword);
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
src/gui/macOS/fileprovidersocketcontroller.h
Normal file
60
src/gui/macOS/fileprovidersocketcontroller.h
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
#include "accountstate.h"
|
||||||
|
|
||||||
|
class QLocalSocket;
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
class FileProviderSocketController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileProviderSocketController(QLocalSocket * const socket, QObject * const parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void socketDestroyed(const QLocalSocket * const socket);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void sendMessage(const QString &message) const;
|
||||||
|
void start();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void slotOnDisconnected();
|
||||||
|
void slotSocketDestroyed(const QObject * const object);
|
||||||
|
void slotReadyRead();
|
||||||
|
|
||||||
|
void slotAccountStateChanged(const OCC::AccountState::State state);
|
||||||
|
|
||||||
|
void parseReceivedLine(const QString &receivedLine);
|
||||||
|
void requestFileProviderDomainInfo() const;
|
||||||
|
void sendAccountDetails() const;
|
||||||
|
void sendNotAuthenticated() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<QLocalSocket> _socket;
|
||||||
|
AccountStatePtr _accountState;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
|
||||||
|
} // namespace OCC
|
84
src/gui/macOS/fileprovidersocketserver.cpp
Normal file
84
src/gui/macOS/fileprovidersocketserver.cpp
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "fileprovidersocketserver.h"
|
||||||
|
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
#include "fileprovidersocketcontroller.h"
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcFileProviderSocketServer, "nextcloud.gui.macos.fileprovider.socketserver", QtInfoMsg)
|
||||||
|
|
||||||
|
FileProviderSocketServer::FileProviderSocketServer(QObject *parent)
|
||||||
|
: QObject{parent}
|
||||||
|
{
|
||||||
|
_socketPath = fileProviderSocketPath();
|
||||||
|
startListening();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketServer::startListening()
|
||||||
|
{
|
||||||
|
QLocalServer::removeServer(_socketPath);
|
||||||
|
|
||||||
|
const auto serverStarted = _socketServer.listen(_socketPath);
|
||||||
|
if (!serverStarted) {
|
||||||
|
qCWarning(lcFileProviderSocketServer) << "Could not start file provider socket server"
|
||||||
|
<< _socketPath;
|
||||||
|
} else {
|
||||||
|
qCInfo(lcFileProviderSocketServer) << "File provider socket server started, listening"
|
||||||
|
<< _socketPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(&_socketServer, &QLocalServer::newConnection,
|
||||||
|
this, &FileProviderSocketServer::slotNewConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketServer::slotNewConnection()
|
||||||
|
{
|
||||||
|
if (!_socketServer.hasPendingConnections()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCInfo(lcFileProviderSocketServer) << "New connection in file provider socket server";
|
||||||
|
const auto socket = _socketServer.nextPendingConnection();
|
||||||
|
if (!socket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileProviderSocketControllerPtr socketController(new FileProviderSocketController(socket, this));
|
||||||
|
connect(socketController.data(), &FileProviderSocketController::socketDestroyed,
|
||||||
|
this, &FileProviderSocketServer::slotSocketDestroyed);
|
||||||
|
_socketControllers.insert(socket, socketController);
|
||||||
|
|
||||||
|
socketController->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileProviderSocketServer::slotSocketDestroyed(const QLocalSocket * const socket)
|
||||||
|
{
|
||||||
|
const auto socketController = _socketControllers.take(socket);
|
||||||
|
|
||||||
|
if (socketController) {
|
||||||
|
const auto rawSocketControllerPtr = socketController.data();
|
||||||
|
delete rawSocketControllerPtr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
|
||||||
|
} // namespace OCC
|
49
src/gui/macOS/fileprovidersocketserver.h
Normal file
49
src/gui/macOS/fileprovidersocketserver.h
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QLocalServer>
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
namespace Mac {
|
||||||
|
|
||||||
|
class FileProviderSocketController;
|
||||||
|
using FileProviderSocketControllerPtr = QPointer<FileProviderSocketController>;
|
||||||
|
|
||||||
|
QString fileProviderSocketPath();
|
||||||
|
|
||||||
|
class FileProviderSocketServer : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileProviderSocketServer(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void startListening();
|
||||||
|
void slotNewConnection();
|
||||||
|
void slotSocketDestroyed(const QLocalSocket * const socket);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString _socketPath;
|
||||||
|
QLocalServer _socketServer;
|
||||||
|
QHash<const QLocalSocket*, FileProviderSocketControllerPtr> _socketControllers;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
|
||||||
|
} // namespace OCC
|
40
src/gui/macOS/fileprovidersocketserver_mac.mm
Normal file
40
src/gui/macOS/fileprovidersocketserver_mac.mm
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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 <Cocoa/Cocoa.h>
|
||||||
|
#import <QString>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
namespace OCC
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace Mac
|
||||||
|
{
|
||||||
|
|
||||||
|
QString fileProviderSocketPath()
|
||||||
|
{
|
||||||
|
// This must match the code signing Team setting of the extension
|
||||||
|
// Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".fileprovidersocket"
|
||||||
|
// Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".fileprovidersocket"
|
||||||
|
NSString *appGroupId = @SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN;
|
||||||
|
|
||||||
|
NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId];
|
||||||
|
NSURL *socketPath = [container URLByAppendingPathComponent:@".fileprovidersocket" isDirectory:false];
|
||||||
|
return QString::fromNSString(socketPath.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mac
|
||||||
|
|
||||||
|
} // namespace OCC
|
|
@ -106,6 +106,8 @@ static constexpr char certPath[] = "http_certificatePath";
|
||||||
static constexpr char certPasswd[] = "http_certificatePasswd";
|
static constexpr char certPasswd[] = "http_certificatePasswd";
|
||||||
|
|
||||||
static const QSet validUpdateChannels { QStringLiteral("stable"), QStringLiteral("beta") };
|
static const QSet validUpdateChannels { QStringLiteral("stable"), QStringLiteral("beta") };
|
||||||
|
|
||||||
|
static constexpr auto macFileProviderModuleEnabledC = "macFileProviderModuleEnabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace OCC {
|
namespace OCC {
|
||||||
|
@ -1171,4 +1173,16 @@ void ConfigFile::setDiscoveredLegacyConfigPath(const QString &discoveredLegacyCo
|
||||||
_discoveredLegacyConfigPath = discoveredLegacyConfigPath;
|
_discoveredLegacyConfigPath = discoveredLegacyConfigPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ConfigFile::macFileProviderModuleEnabled() const
|
||||||
|
{
|
||||||
|
QSettings settings(configFile(), QSettings::IniFormat);
|
||||||
|
return settings.value(macFileProviderModuleEnabledC, false).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigFile::setMacFileProviderModuleEnabled(const bool moduleEnabled)
|
||||||
|
{
|
||||||
|
QSettings settings(configFile(), QSettings::IniFormat);
|
||||||
|
settings.setValue(QLatin1String(macFileProviderModuleEnabledC), moduleEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,6 +223,9 @@ public:
|
||||||
[[nodiscard]] static QString discoveredLegacyConfigPath();
|
[[nodiscard]] static QString discoveredLegacyConfigPath();
|
||||||
static void setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath);
|
static void setDiscoveredLegacyConfigPath(const QString &discoveredLegacyConfigPath);
|
||||||
|
|
||||||
|
[[nodiscard]] bool macFileProviderModuleEnabled() const;
|
||||||
|
void setMacFileProviderModuleEnabled(const bool moduleEnabled);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
[[nodiscard]] QVariant getPolicySetting(const QString &policy, const QVariant &defaultValue = QVariant()) const;
|
[[nodiscard]] QVariant getPolicySetting(const QString &policy, const QVariant &defaultValue = QVariant()) const;
|
||||||
void storeData(const QString &group, const QString &key, const QVariant &value);
|
void storeData(const QString &group, const QString &key, const QVariant &value);
|
||||||
|
@ -237,7 +240,6 @@ private:
|
||||||
|
|
||||||
[[nodiscard]] QString keychainProxyPasswordKey() const;
|
[[nodiscard]] QString keychainProxyPasswordKey() const;
|
||||||
|
|
||||||
private:
|
|
||||||
using SharedCreds = QSharedPointer<AbstractCredentials>;
|
using SharedCreds = QSharedPointer<AbstractCredentials>;
|
||||||
|
|
||||||
static QString _confDir;
|
static QString _confDir;
|
||||||
|
|
Loading…
Reference in a new issue