Skip to content

Commit

Permalink
Merge pull request #5527 from nextcloud/feature/file-provider-try-2
Browse files Browse the repository at this point in the history
Implement File Provider file synchronisation engine for macOS
  • Loading branch information
claucambra committed May 12, 2023
2 parents 9b77da6 + 23855a1 commit c4d1211
Show file tree
Hide file tree
Showing 59 changed files with 5,772 additions and 750 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dlldata.c
# macOS specific
xcuserdata/
**/.DS_Store
**/Carthage/

# Visual C++ cache files
ipch/
Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ else()
unset(CMAKE_CXX_CLANG_TIDY)
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
# 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.
Expand Down
71 changes: 50 additions & 21 deletions shell_integration/MacOSX/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
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")

# 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.
# Pass the info through the xcodebuild command line and make sure that the project uses
# those user-defined settings to build the plist.
add_custom_target( mac_overlayplugin ALL
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj
-target FinderSyncExt -configuration Release "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
COMMENT building Mac Overlay icons
VERBATIM)
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
if (CMAKE_BUILD_TYPE MATCHES "Debug" OR CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo")
set(XCODE_TARGET_CONFIGURATION "Debug")
else()
set(XCODE_TARGET_CONFIGURATION "Release")
endif()

if (BUILD_OWNCLOUD_OSX_BUNDLE)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
USE_SOURCE_PERMISSIONS)
endif()
# 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.
# Pass the info through the xcodebuild command line and make sure that the project uses
# those user-defined settings to build the plist.
add_custom_target( mac_overlayplugin ALL
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj
-target FinderSyncExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
COMMENT building Mac Overlay icons
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
endif()

if (BUILD_OWNCLOUD_OSX_BUNDLE)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
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()

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 comments on commit c4d1211

Please sign in to comment.