Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/macos vfs locking #6960

Merged
merged 26 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1d0e3bc
Move all sharing components for FileProviderUIExt into a Sharing folder
claucambra Jul 30, 2024
73674bf
Add basic LockViewController components
claucambra Jul 30, 2024
501c1c4
Add lock/unlock actions in FileProviderUIExt info plist
claucambra Jul 30, 2024
ec66bcd
Handle new locking action identifiers in document action view control…
claucambra Jul 30, 2024
e3b6cd9
Add basic properties to lock view controller
claucambra Jul 30, 2024
10f37d3
Design lock view in file provider ui
claucambra Jul 30, 2024
4edbb4b
Add close action to lock view controller
claucambra Jul 30, 2024
624b72b
Move service connection fetcher method into a utils file
claucambra Jul 30, 2024
81566ee
Move item metadata fetch into util file in FileProviderUIExt
claucambra Jul 30, 2024
05e5793
Add convenience method to present and log errors in lock view controller
claucambra Jul 30, 2024
fdbc52b
Add method to simply provide file details in locking view
claucambra Jul 30, 2024
3b33b26
Process target item on init of lock view controller
claucambra Jul 30, 2024
c7d2ed3
Perform lock/unlock procedure in lock view controller
claucambra Jul 30, 2024
65e4297
Add convenience function to stop/hide loading indicator in lock view …
claucambra Jul 31, 2024
516fc7c
Stop loading indicator when presenting error
claucambra Jul 31, 2024
d312d00
Pull out error handling from completionHandler for nckit lockunlock
claucambra Jul 31, 2024
7ae9f83
Once finished locking/unlocking, stop loading indicator
claucambra Jul 31, 2024
f3e341c
Make sure not to check for kit before we should in shares table view …
claucambra Jul 31, 2024
4dbfbc3
Fix file lock server url in lockviewcontroller
claucambra Jul 31, 2024
616ac2e
Fix XIB initialisation in LockViewController
claucambra Jul 31, 2024
4348869
Fix internal layouting of lock view by simply using a nsview rather t…
claucambra Jul 31, 2024
19c9c19
Add warn image to locking view if there is an error
claucambra Aug 1, 2024
419035c
Fix NSExtensionFileProviderActionActivationRules for locking menu items
claucambra Aug 1, 2024
7413cf6
Display checkmark when file lock completed
claucambra Aug 1, 2024
4012d43
Do not provide items if account is not set up correctly
claucambra Aug 1, 2024
94a783c
Signal enumeration of locked/unlocked file
claucambra Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,15 @@ import OSLog
request _: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress {
if let item = Item.storedItem(identifier: identifier, remoteInterface: ncKit) {
if ncAccount == nil {
Logger.fileProviderExtension.error(
"""
Not fetching item for identifier: \(identifier.rawValue, privacy: .public)
as account not set up yet.
"""
)
completionHandler(nil, NSFileProviderError(.notAuthenticated))
} else if let item = Item.storedItem(identifier: identifier, remoteInterface: ncKit) {
completionHandler(item, nil)
} else {
completionHandler(nil, NSFileProviderError(.noSuchItem))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,21 @@ class DocumentActionViewController: FPUIActionExtensionViewController {
) {
Logger.actionViewController.info("Preparing action: \(actionIdentifier, privacy: .public)")

if actionIdentifier == "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction" {
switch (actionIdentifier) {
case "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction":
prepare(childViewController: ShareViewController(itemIdentifiers))
case "com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction":
prepare(childViewController: LockViewController(itemIdentifiers, locking: true))
case "com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction":
prepare(childViewController: LockViewController(itemIdentifiers, locking: false))
default:
return
}

}

override func prepare(forError error: Error) {
Logger.actionViewController.info(
"""
Preparing for error: \(error.localizedDescription, privacy: .public)
"""
"Preparing for error: \(error.localizedDescription, privacy: .public)"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier!

static let actionViewController = Logger(subsystem: subsystem, category: "actionViewController")
static let lockViewController = Logger(subsystem: subsystem, category: "lockViewController")
static let metadataProvider = Logger(subsystem: subsystem, category: "metadataProvider")
static let shareCapabilities = Logger(subsystem: subsystem, category: "shareCapabilities")
static let shareController = Logger(subsystem: subsystem, category: "shareController")
static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// FileProviderCommunication.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 30/7/24.
//

import FileProvider

enum FileProviderCommunicationError: Error {
case serviceNotFound
case remoteProxyObjectInvalid
}

func serviceConnection(
url: URL, interruptionHandler: @escaping () -> Void
) async throws -> FPUIExtensionService {
let services = try await FileManager().fileProviderServicesForItem(at: url)
guard let service = services[fpUiExtensionServiceName] else {
throw FileProviderCommunicationError.serviceNotFound
}
let connection: NSXPCConnection
connection = try await service.fileProviderConnection()
connection.remoteObjectInterface = NSXPCInterface(with: FPUIExtensionService.self)
connection.interruptionHandler = interruptionHandler
connection.resume()
guard let proxy = connection.remoteObjectProxy as? FPUIExtensionService else {
throw FileProviderCommunicationError.remoteProxyObjectInvalid
}
return proxy
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,32 @@
<!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 UI Extension</string>
<key>CFBundleIdentifier</key>
<string>$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleDisplayName</key>
<string>$(OC_APPLICATION_NAME) File Provider UI Extension</string>
<key>CFBundleIdentifier</key>
<string>$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionFileProviderActions</key>
<array>
<dict>
<key>NSExtensionFileProviderActionIdentifier</key>
<string>com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction</string>
<key>NSExtensionFileProviderActionName</key>
<string>Unlock file</string>
<key>NSExtensionFileProviderActionActivationRule</key>
<string>SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked != nil &amp;&amp; !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO &quot;public.folder&quot;) ).@count &gt; 0</string>
</dict>
<dict>
<key>NSExtensionFileProviderActionActivationRule</key>
<string>SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked == nil &amp;&amp; !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO &quot;public.folder&quot;) ).@count &gt; 0</string>
<key>NSExtensionFileProviderActionName</key>
<string>Lock file</string>
<key>NSExtensionFileProviderActionIdentifier</key>
<string>com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction</string>
</dict>
<dict>
<key>NSExtensionFileProviderActionActivationRule</key>
<string>TRUEPREDICATE</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
//
// LockViewController.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 30/7/24.
//

import AppKit
import FileProvider
import NextcloudFileProviderKit
import NextcloudKit
import OSLog
import QuickLookThumbnailing

class LockViewController: NSViewController {
let itemIdentifiers: [NSFileProviderItemIdentifier]
let locking: Bool

@IBOutlet weak var fileNameIcon: NSImageView!
@IBOutlet weak var fileNameLabel: NSTextField!
@IBOutlet weak var descriptionLabel: NSTextField!
@IBOutlet weak var closeButton: NSButton!
@IBOutlet weak var loadingIndicator: NSProgressIndicator!
@IBOutlet weak var warnImage: NSImageView!

public override var nibName: NSNib.Name? {
return NSNib.Name(self.className)
}

var actionViewController: DocumentActionViewController! {
return parent as? DocumentActionViewController
}

init(_ itemIdentifiers: [NSFileProviderItemIdentifier], locking: Bool) {
self.itemIdentifiers = itemIdentifiers
self.locking = locking
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
guard let firstItem = itemIdentifiers.first else {
Logger.shareViewController.error("called without items")
closeAction(self)
return
}

Logger.lockViewController.info(
"""
Locking \(self.locking ? "enabled" : "disabled", privacy: .public) for items:
\(firstItem.rawValue, privacy: .public)
"""
)

Task {
await processItemIdentifier(firstItem)
}
}

@IBAction func closeAction(_ sender: Any) {
actionViewController.extensionContext.completeRequest()
}

private func stopIndicatingLoading() {
loadingIndicator.stopAnimation(self)
loadingIndicator.isHidden = true
warnImage.isHidden = false
}

private func presentError(_ error: String) {
Logger.lockViewController.error("Error: \(error, privacy: .public)")
descriptionLabel.stringValue = "Error: \(error)"
stopIndicatingLoading()
}

private func processItemIdentifier(_ itemIdentifier: NSFileProviderItemIdentifier) async {
guard let manager = NSFileProviderManager(for: actionViewController.domain) else {
fatalError("NSFileProviderManager isn't expected to fail")
}

do {
let itemUrl = try await manager.getUserVisibleURL(for: itemIdentifier)
guard itemUrl.startAccessingSecurityScopedResource() else {
Logger.lockViewController.error("Could not access scoped resource for item url!")
return
}
await updateFileDetailsDisplay(itemUrl: itemUrl)
itemUrl.stopAccessingSecurityScopedResource()
await lockOrUnlockFile(localItemUrl: itemUrl)
} catch let error {
let errorString = "Error processing item: \(error)"
Logger.lockViewController.error("\(errorString, privacy: .public)")
fileNameLabel.stringValue = "Could not lock unknown item…"
descriptionLabel.stringValue = errorString
}
}

private func updateFileDetailsDisplay(itemUrl: URL) async {
let lockAction = locking ? "Locking" : "Unlocking"
fileNameLabel.stringValue = "\(lockAction) file \(itemUrl.lastPathComponent)…"

let request = QLThumbnailGenerator.Request(
fileAt: itemUrl,
size: CGSize(width: 48, height: 48),
scale: 1.0,
representationTypes: .icon
)
let generator = QLThumbnailGenerator.shared
let fileThumbnail = await withCheckedContinuation { continuation in
generator.generateRepresentations(for: request) { thumbnail, type, error in
if thumbnail == nil || error != nil {
Logger.lockViewController.error(
"Could not get thumbnail: \(error, privacy: .public)"
)
}
continuation.resume(returning: thumbnail)
}
}

fileNameIcon.image =
fileThumbnail?.nsImage ??
NSImage(systemSymbolName: "doc", accessibilityDescription: "doc")
}

private func lockOrUnlockFile(localItemUrl: URL) async {
descriptionLabel.stringValue = "Fetching file details…"

guard let itemIdentifier = await withCheckedContinuation({
(continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
NSFileProviderManager.getIdentifierForUserVisibleFile(
at: localItemUrl
) { identifier, domainIdentifier, error in
defer { continuation.resume(returning: identifier) }
guard error == nil else {
self.presentError("No item with identifier: \(error.debugDescription)")
return
}
}
}) else {
presentError("Could not get identifier for item, no shares can be acquired.")
return
}

do {
let connection = try await serviceConnection(url: localItemUrl, interruptionHandler: {
Logger.lockViewController.error("Service connection interrupted")
})
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
let credentials = await connection.credentials() as? Dictionary<String, String>,
let account = Account(dictionary: credentials),
!account.password.isEmpty
else {
presentError("Failed to get details from File Provider Extension.")
return
}
let serverPathString = serverPath as String
let kit = NextcloudKit()
kit.setup(
user: account.username,
userId: account.username,
password: account.password,
urlBase: account.serverUrl
)
// guard let capabilities = await fetchCapabilities() else {
guard let itemMetadata = await fetchItemMetadata(
itemRelativePath: serverPathString, kit: kit
) else {
presentError("Could not get item metadata.")
return
}

// Run lock state checks
if locking {
guard !itemMetadata.lock else {
presentError("File is already locked.")
return
}
} else {
guard itemMetadata.lock else {
presentError("File is already unlocked.")
return
}
}

descriptionLabel.stringValue =
"Communicating with server, \(locking ? "locking" : "unlocking") file…"

let serverUrlFileName = itemMetadata.serverUrl + "/" + itemMetadata.fileName
Logger.lockViewController.info(
"""
Locking file: \(serverUrlFileName, privacy: .public)
\(self.locking ? "locking" : "unlocking", privacy: .public)
"""
)

let error = await withCheckedContinuation { continuation in
kit.lockUnlockFile(
serverUrlFileName: serverUrlFileName,
shouldLock: locking,
completion: { _, error in
continuation.resume(returning: error)
}
)
}
if error == .success {
descriptionLabel.stringValue = "File \(self.locking ? "locked" : "unlocked")!"
warnImage.image = NSImage(
systemSymbolName: "checkmark.circle.fill",
accessibilityDescription: "checkmark.circle.fill"
)
stopIndicatingLoading()
if let manager = NSFileProviderManager(for: actionViewController.domain) {
do {
try await manager.signalEnumerator(for: itemIdentifier)
} catch let error {
presentError(
"""
Could not signal lock state change in virtual file.
Changes may take a while to be reflected on your Mac.
Error: \(error.localizedDescription)
""")
}
}
} else {
presentError("Could not lock file: \(error.errorDescription).")
}
} catch let error {
presentError("Could not lock file: \(error).")
}
}
}
Loading
Loading