Merge pull request #7110 from nextcloud/backport/6960/stable-3.14

[stable-3.14] Feature/macos vfs locking
This commit is contained in:
Matthieu Gallien 2024-09-12 10:16:05 +02:00 committed by GitHub
commit 78a7256036
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 531 additions and 81 deletions

View file

@ -104,7 +104,15 @@ import OSLog
request _: NSFileProviderRequest, request _: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress { ) -> 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) completionHandler(item, nil)
} else { } else {
completionHandler(nil, NSFileProviderError(.noSuchItem)) completionHandler(nil, NSFileProviderError(.noSuchItem))

View file

@ -36,17 +36,21 @@ class DocumentActionViewController: FPUIActionExtensionViewController {
) { ) {
Logger.actionViewController.info("Preparing action: \(actionIdentifier, privacy: .public)") 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)) 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) { override func prepare(forError error: Error) {
Logger.actionViewController.info( Logger.actionViewController.info(
""" "Preparing for error: \(error.localizedDescription, privacy: .public)"
Preparing for error: \(error.localizedDescription, privacy: .public)
"""
) )
} }

View file

@ -11,6 +11,8 @@ extension Logger {
private static var subsystem = Bundle.main.bundleIdentifier! private static var subsystem = Bundle.main.bundleIdentifier!
static let actionViewController = Logger(subsystem: subsystem, category: "actionViewController") 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 shareCapabilities = Logger(subsystem: subsystem, category: "shareCapabilities")
static let shareController = Logger(subsystem: subsystem, category: "shareController") static let shareController = Logger(subsystem: subsystem, category: "shareController")
static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource") static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource")

View file

@ -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
}

View file

@ -12,6 +12,22 @@
<dict> <dict>
<key>NSExtensionFileProviderActions</key> <key>NSExtensionFileProviderActions</key>
<array> <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> <dict>
<key>NSExtensionFileProviderActionActivationRule</key> <key>NSExtensionFileProviderActionActivationRule</key>
<string>TRUEPREDICATE</string> <string>TRUEPREDICATE</string>

View file

@ -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).")
}
}
}

View file

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="LockViewController" customModule="FileProviderUIExt" customModuleProvider="target">
<connections>
<outlet property="closeButton" destination="7qN-mr-hAh" id="NkY-2f-Ltx"/>
<outlet property="descriptionLabel" destination="DG3-ti-eu7" id="rez-CW-FUS"/>
<outlet property="fileNameIcon" destination="KlP-OW-SKo" id="Dey-vA-qIG"/>
<outlet property="fileNameLabel" destination="LDe-7m-hvL" id="AzB-UH-ndO"/>
<outlet property="loadingIndicator" destination="UWQ-uR-PJA" id="Swv-It-LT9"/>
<outlet property="view" destination="MSC-7J-Z1I" id="6cA-xC-NDA"/>
<outlet property="warnImage" destination="zlo-mM-ZNd" id="5gS-Ab-nDE"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="MSC-7J-Z1I">
<rect key="frame" x="0.0" y="0.0" width="500" height="87"/>
<subviews>
<progressIndicator horizontalHuggingPriority="1000" verticalHuggingPriority="1000" maxValue="100" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="UWQ-uR-PJA">
<rect key="frame" x="10" y="28" width="32" height="32"/>
<constraints>
<constraint firstAttribute="width" constant="32" id="ShG-G9-AMJ"/>
<constraint firstAttribute="width" secondItem="UWQ-uR-PJA" secondAttribute="height" multiplier="1:1" id="Tcq-dQ-mHX"/>
</constraints>
</progressIndicator>
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zlo-mM-ZNd">
<rect key="frame" x="10" y="25" width="32" height="38"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="Yym-fE-Cdh"/>
</imageView>
<imageView horizontalHuggingPriority="1000" verticalHuggingPriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="KlP-OW-SKo">
<rect key="frame" x="52" y="16.5" width="48" height="55"/>
<constraints>
<constraint firstAttribute="width" secondItem="KlP-OW-SKo" secondAttribute="height" multiplier="1:1" id="u2z-cf-k5P"/>
<constraint firstAttribute="width" constant="48" id="ucZ-Lb-C49"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="doc" catalog="system" id="sA8-Dn-b0s"/>
</imageView>
<stackView distribution="fillEqually" orientation="vertical" alignment="leading" spacing="10" horizontalStackHuggingPriority="250" verticalStackHuggingPriority="250" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dpJ-oy-su9">
<rect key="frame" x="110" y="21" width="338" height="45"/>
<subviews>
<textField focusRingType="none" verticalHuggingPriority="750" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="LDe-7m-hvL">
<rect key="frame" x="-2" y="26" width="342" height="19"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Locking file filename.txt…" id="Zld-Ku-SeH">
<font key="font" metaFont="systemBold" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="749" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="DG3-ti-eu7">
<rect key="frame" x="-2" y="0.0" width="342" height="16"/>
<textFieldCell key="cell" selectable="YES" title="Communicating with server..." id="tz0-OE-Too">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="DG3-ti-eu7" secondAttribute="trailing" id="8TF-VE-4z7"/>
<constraint firstAttribute="trailing" secondItem="LDe-7m-hvL" secondAttribute="trailing" id="BAx-hY-Nrb"/>
<constraint firstItem="LDe-7m-hvL" firstAttribute="leading" secondItem="dpJ-oy-su9" secondAttribute="leading" id="NUj-8a-xvp"/>
<constraint firstItem="DG3-ti-eu7" firstAttribute="leading" secondItem="dpJ-oy-su9" secondAttribute="leading" id="eV4-Uv-5xX"/>
</constraints>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<button horizontalHuggingPriority="1000" verticalHuggingPriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="7qN-mr-hAh">
<rect key="frame" x="458" y="23" width="32" height="43"/>
<buttonCell key="cell" type="bevel" title="Close" bezelStyle="rounded" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="7Oc-Xd-RzM">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<imageReference key="image" image="xmark.circle.fill" catalog="system" symbolScale="large"/>
</buttonCell>
<constraints>
<constraint firstAttribute="width" secondItem="7qN-mr-hAh" secondAttribute="height" multiplier="1:1" id="K00-Bi-dEy"/>
<constraint firstAttribute="width" constant="32" id="zVJ-h1-QXJ"/>
</constraints>
<connections>
<action selector="closeAction:" target="-2" id="E6h-U9-2eB"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="KlP-OW-SKo" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="MSC-7J-Z1I" secondAttribute="leading" constant="10" id="42d-pC-0Lq"/>
<constraint firstItem="zlo-mM-ZNd" firstAttribute="bottom" secondItem="UWQ-uR-PJA" secondAttribute="bottom" id="4kj-Yy-erm"/>
<constraint firstItem="UWQ-uR-PJA" firstAttribute="top" relation="greaterThanOrEqual" secondItem="MSC-7J-Z1I" secondAttribute="top" constant="10" id="86t-6h-ezO"/>
<constraint firstItem="dpJ-oy-su9" firstAttribute="leading" secondItem="KlP-OW-SKo" secondAttribute="trailing" constant="10" id="CkI-Rn-Ens"/>
<constraint firstItem="7qN-mr-hAh" firstAttribute="leading" secondItem="dpJ-oy-su9" secondAttribute="trailing" constant="10" id="D0y-zd-Mkx"/>
<constraint firstItem="UWQ-uR-PJA" firstAttribute="leading" secondItem="MSC-7J-Z1I" secondAttribute="leading" constant="10" id="DOR-jC-JYh"/>
<constraint firstItem="dpJ-oy-su9" firstAttribute="top" relation="greaterThanOrEqual" secondItem="MSC-7J-Z1I" secondAttribute="top" constant="10" id="EnF-gg-RXe"/>
<constraint firstItem="UWQ-uR-PJA" firstAttribute="centerY" secondItem="MSC-7J-Z1I" secondAttribute="centerY" id="GAc-W2-Bue"/>
<constraint firstItem="zlo-mM-ZNd" firstAttribute="trailing" secondItem="UWQ-uR-PJA" secondAttribute="trailing" id="Hn0-5r-n4p"/>
<constraint firstItem="KlP-OW-SKo" firstAttribute="top" relation="greaterThanOrEqual" secondItem="MSC-7J-Z1I" secondAttribute="top" constant="10" id="JEs-Z1-i2x"/>
<constraint firstItem="KlP-OW-SKo" firstAttribute="leading" secondItem="UWQ-uR-PJA" secondAttribute="trailing" constant="10" id="YHI-Z6-DqR"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="KlP-OW-SKo" secondAttribute="bottom" constant="10" id="g7d-i6-GFB"/>
<constraint firstAttribute="trailing" secondItem="7qN-mr-hAh" secondAttribute="trailing" constant="10" id="jyt-D2-tiy"/>
<constraint firstItem="zlo-mM-ZNd" firstAttribute="leading" secondItem="UWQ-uR-PJA" secondAttribute="leading" id="ljA-uc-fNi"/>
<constraint firstItem="7qN-mr-hAh" firstAttribute="centerY" secondItem="MSC-7J-Z1I" secondAttribute="centerY" id="tuo-AL-2Xr"/>
<constraint firstItem="KlP-OW-SKo" firstAttribute="centerY" secondItem="MSC-7J-Z1I" secondAttribute="centerY" id="upa-4o-uiG"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="dpJ-oy-su9" secondAttribute="bottom" constant="10" id="xOO-EK-GWO"/>
<constraint firstItem="zlo-mM-ZNd" firstAttribute="top" secondItem="UWQ-uR-PJA" secondAttribute="top" id="xeA-Ec-B32"/>
<constraint firstItem="dpJ-oy-su9" firstAttribute="centerY" secondItem="MSC-7J-Z1I" secondAttribute="centerY" id="yKD-Bd-jC9"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="UWQ-uR-PJA" secondAttribute="bottom" constant="10" id="zgg-6J-3Zy"/>
</constraints>
<point key="canvasLocation" x="-266" y="-126.5"/>
</customView>
</objects>
<resources>
<image name="doc" catalog="system" width="14" height="16"/>
<image name="exclamationmark.circle.fill" catalog="system" width="15" height="15"/>
<image name="xmark.circle.fill" catalog="system" width="20" height="20"/>
</resources>
</document>

View file

@ -0,0 +1,45 @@
//
// MetadataProvider.swift
// FileProviderUIExt
//
// Created by Claudio Cambra on 30/7/24.
//
import Foundation
import NextcloudKit
import OSLog
func fetchItemMetadata(itemRelativePath: String, kit: NextcloudKit) async -> NKFile? {
func slashlessPath(_ string: String) -> String {
var strCopy = string
if strCopy.hasPrefix("/") {
strCopy.removeFirst()
}
if strCopy.hasSuffix("/") {
strCopy.removeLast()
}
return strCopy
}
let nkCommon = kit.nkCommonInstance
let urlBase = slashlessPath(nkCommon.urlBase)
let davSuffix = slashlessPath(nkCommon.dav)
let userId = nkCommon.userId
let itemRelPath = slashlessPath(itemRelativePath)
let itemFullServerPath = "\(urlBase)/\(davSuffix)/files/\(userId)/\(itemRelPath)"
return await withCheckedContinuation { continuation in
kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0") {
account, files, data, error in
guard error == .success else {
Logger.metadataProvider.error(
"Error getting item metadata: \(error.errorDescription)"
)
continuation.resume(returning: nil)
return
}
Logger.metadataProvider.info("Successfully retrieved item metadata")
continuation.resume(returning: files.first)
}
}
}

View file

@ -66,7 +66,10 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
} }
func reload() async { func reload() async {
guard let itemURL = itemURL else { return } guard let itemURL else {
presentError("No item URL, cannot reload data!")
return
}
guard let itemIdentifier = await withCheckedContinuation({ guard let itemIdentifier = await withCheckedContinuation({
(continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
NSFileProviderManager.getIdentifierForUserVisibleFile( NSFileProviderManager.getIdentifierForUserVisibleFile(
@ -84,7 +87,9 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
} }
do { do {
let connection = try await serviceConnection(url: itemURL) let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
Logger.sharesDataSource.error("Service connection interrupted")
})
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier), guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
let credentials = await connection.credentials() as? Dictionary<String, String>, let credentials = await connection.credentials() as? Dictionary<String, String>,
let convertedAccount = Account(dictionary: credentials), let convertedAccount = Account(dictionary: credentials),
@ -104,7 +109,11 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
presentError("Server does not support shares.") presentError("Server does not support shares.")
return return
} }
itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString) guard let kit else {
presentError("NextcloudKit instance is unavailable, cannot reload data!")
return
}
itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString, kit: kit)
guard itemMetadata?.permissions.contains("R") == true else { guard itemMetadata?.permissions.contains("R") == true else {
presentError("This file cannot be shared.") presentError("This file cannot be shared.")
return return
@ -118,25 +127,6 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
} }
} }
private func serviceConnection(url: URL) async throws -> FPUIExtensionService {
let services = try await FileManager().fileProviderServicesForItem(at: url)
guard let service = services[fpUiExtensionServiceName] else {
Logger.sharesDataSource.error("Couldn't get service, required service not present")
throw NSFileProviderError(.providerNotFound)
}
let connection: NSXPCConnection
connection = try await service.fileProviderConnection()
connection.remoteObjectInterface = NSXPCInterface(with: FPUIExtensionService.self)
connection.interruptionHandler = {
Logger.sharesDataSource.error("Service connection interrupted")
}
connection.resume()
guard let proxy = connection.remoteObjectProxy as? FPUIExtensionService else {
throw NSFileProviderError(.serverUnreachable)
}
return proxy
}
private func fetch( private func fetch(
itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
) async -> [NKShare] { ) async -> [NKShare] {
@ -180,44 +170,6 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
} }
} }
private func fetchItemMetadata(itemRelativePath: String) async -> NKFile? {
guard let kit = kit else {
presentError("Could not fetch item metadata as NextcloudKit instance is unavailable")
return nil
}
func slashlessPath(_ string: String) -> String {
var strCopy = string
if strCopy.hasPrefix("/") {
strCopy.removeFirst()
}
if strCopy.hasSuffix("/") {
strCopy.removeLast()
}
return strCopy
}
let nkCommon = kit.nkCommonInstance
let urlBase = slashlessPath(nkCommon.urlBase)
let davSuffix = slashlessPath(nkCommon.dav)
let userId = nkCommon.userId
let itemRelPath = slashlessPath(itemRelativePath)
let itemFullServerPath = "\(urlBase)/\(davSuffix)/files/\(userId)/\(itemRelPath)"
return await withCheckedContinuation { continuation in
kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0") {
account, files, data, error in
guard error == .success else {
self.presentError("Error getting item metadata: \(error.errorDescription)")
continuation.resume(returning: nil)
return
}
Logger.sharesDataSource.info("Successfully retrieved item metadata")
continuation.resume(returning: files.first)
}
}
}
private func presentError(_ errorString: String) { private func presentError(_ errorString: String) {
Logger.sharesDataSource.error("\(errorString, privacy: .public)") Logger.sharesDataSource.error("\(errorString, privacy: .public)")
Task { @MainActor in self.uiDelegate?.showError(errorString) } Task { @MainActor in self.uiDelegate?.showError(errorString) }

View file

@ -25,6 +25,10 @@
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; }; 537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; };
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; }; 537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; }; 537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
537BD67A2C58D67800446ED0 /* LockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537BD6792C58D67800446ED0 /* LockViewController.swift */; };
537BD67C2C58D7B700446ED0 /* LockViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 537BD67B2C58D7B700446ED0 /* LockViewController.xib */; };
537BD6802C58F01B00446ED0 /* FileProviderCommunication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537BD67F2C58F01B00446ED0 /* FileProviderCommunication.swift */; };
537BD6822C58F72E00446ED0 /* MetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537BD6812C58F72E00446ED0 /* MetadataProvider.swift */; };
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */; }; 538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */; };
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */; }; 538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */; };
538E397627F4765000FA63D5 /* FileProviderExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 538E396727F4765000FA63D5 /* FileProviderExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 538E397627F4765000FA63D5 /* FileProviderExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 538E396727F4765000FA63D5 /* FileProviderExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -163,6 +167,10 @@
537630922B85F4B00026BFAB /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; }; 537630922B85F4B00026BFAB /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionServiceSource.swift; sourceTree = "<group>"; }; 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionServiceSource.swift; sourceTree = "<group>"; };
537630962B860D920026BFAB /* FPUIExtensionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionService.swift; sourceTree = "<group>"; }; 537630962B860D920026BFAB /* FPUIExtensionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionService.swift; sourceTree = "<group>"; };
537BD6792C58D67800446ED0 /* LockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockViewController.swift; sourceTree = "<group>"; };
537BD67B2C58D7B700446ED0 /* LockViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LockViewController.xib; sourceTree = "<group>"; };
537BD67F2C58F01B00446ED0 /* FileProviderCommunication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCommunication.swift; sourceTree = "<group>"; };
537BD6812C58F72E00446ED0 /* MetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataProvider.swift; sourceTree = "<group>"; };
538E396727F4765000FA63D5 /* FileProviderExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 538E396727F4765000FA63D5 /* FileProviderExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = "<group>"; }; 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = "<group>"; };
@ -280,6 +288,31 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
537BD6772C58D0C400446ED0 /* Sharing */ = {
isa = PBXGroup;
children = (
5374FD432B95EE1400C78D54 /* ShareController.swift */,
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */,
53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */,
53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */,
531522812B8E01C6002E31BE /* ShareTableItemView.xib */,
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */,
537630922B85F4B00026BFAB /* ShareViewController.swift */,
537630902B85F4980026BFAB /* ShareViewController.xib */,
53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */,
);
path = Sharing;
sourceTree = "<group>";
};
537BD6782C58D0FC00446ED0 /* Locking */ = {
isa = PBXGroup;
children = (
537BD6792C58D67800446ED0 /* LockViewController.swift */,
537BD67B2C58D7B700446ED0 /* LockViewController.xib */,
);
path = Locking;
sourceTree = "<group>";
};
538E396827F4765000FA63D5 /* Frameworks */ = { 538E396827F4765000FA63D5 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -321,16 +354,11 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5376307B2B85E2E00026BFAB /* Extensions */, 5376307B2B85E2E00026BFAB /* Extensions */,
537BD6782C58D0FC00446ED0 /* Locking */,
537BD6772C58D0C400446ED0 /* Sharing */,
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */, 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */,
5374FD432B95EE1400C78D54 /* ShareController.swift */, 537BD67F2C58F01B00446ED0 /* FileProviderCommunication.swift */,
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */, 537BD6812C58F72E00446ED0 /* MetadataProvider.swift */,
53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */,
53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */,
531522812B8E01C6002E31BE /* ShareTableItemView.xib */,
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */,
537630922B85F4B00026BFAB /* ShareViewController.swift */,
537630902B85F4980026BFAB /* ShareViewController.xib */,
53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */,
53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */, 53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */,
53B979852B84C81F002DA742 /* Info.plist */, 53B979852B84C81F002DA742 /* Info.plist */,
); );
@ -617,6 +645,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
537BD67C2C58D7B700446ED0 /* LockViewController.xib in Resources */,
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */, 531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */,
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */, 537630912B85F4980026BFAB /* ShareViewController.xib in Resources */,
); );
@ -691,9 +720,11 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
537BD6822C58F72E00446ED0 /* MetadataProvider.swift in Sources */,
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */, 537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */,
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */, 53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */,
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */, 53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */,
537BD67A2C58D67800446ED0 /* LockViewController.swift in Sources */,
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */, 53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */,
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */, 53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */,
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */, 5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */,
@ -701,6 +732,7 @@
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */, 53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */,
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */, 5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */,
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */, 53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */,
537BD6802C58F01B00446ED0 /* FileProviderCommunication.swift in Sources */,
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */, 537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;