mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-22 04:55:48 +03:00
Merge pull request #6960 from nextcloud/feature/macos-vfs-locking
Feature/macos vfs locking
This commit is contained in:
commit
804cb1d4d6
18 changed files with 531 additions and 81 deletions
|
@ -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))
|
||||
|
|
|
@ -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)"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -12,6 +12,22 @@
|
|||
<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 && !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO "public.folder") ).@count > 0</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderActionActivationRule</key>
|
||||
<string>SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked == nil && !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO "public.folder") ).@count > 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>
|
||||
|
|
|
@ -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).")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,7 +66,10 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
|
|||
}
|
||||
|
||||
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({
|
||||
(continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
|
||||
NSFileProviderManager.getIdentifierForUserVisibleFile(
|
||||
|
@ -84,7 +87,9 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
|
|||
}
|
||||
|
||||
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),
|
||||
let credentials = await connection.credentials() as? Dictionary<String, String>,
|
||||
let convertedAccount = Account(dictionary: credentials),
|
||||
|
@ -104,7 +109,11 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele
|
|||
presentError("Server does not support shares.")
|
||||
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 {
|
||||
presentError("This file cannot be shared.")
|
||||
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(
|
||||
itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
|
||||
) 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) {
|
||||
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
|
||||
Task { @MainActor in self.uiDelegate?.showError(errorString) }
|
|
@ -25,6 +25,10 @@
|
|||
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; };
|
||||
537630972B860D920026BFAB /* 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 */; };
|
||||
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, ); }; };
|
||||
|
@ -163,6 +167,10 @@
|
|||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
@ -280,6 +288,31 @@
|
|||
path = Extensions;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -321,16 +354,11 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
5376307B2B85E2E00026BFAB /* Extensions */,
|
||||
537BD6782C58D0FC00446ED0 /* Locking */,
|
||||
537BD6772C58D0C400446ED0 /* Sharing */,
|
||||
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */,
|
||||
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 */,
|
||||
537BD67F2C58F01B00446ED0 /* FileProviderCommunication.swift */,
|
||||
537BD6812C58F72E00446ED0 /* MetadataProvider.swift */,
|
||||
53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */,
|
||||
53B979852B84C81F002DA742 /* Info.plist */,
|
||||
);
|
||||
|
@ -617,6 +645,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
537BD67C2C58D7B700446ED0 /* LockViewController.xib in Resources */,
|
||||
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */,
|
||||
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */,
|
||||
);
|
||||
|
@ -691,9 +720,11 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
537BD6822C58F72E00446ED0 /* MetadataProvider.swift in Sources */,
|
||||
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */,
|
||||
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */,
|
||||
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */,
|
||||
537BD67A2C58D67800446ED0 /* LockViewController.swift in Sources */,
|
||||
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */,
|
||||
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */,
|
||||
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */,
|
||||
|
@ -701,6 +732,7 @@
|
|||
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */,
|
||||
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */,
|
||||
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */,
|
||||
537BD6802C58F01B00446ED0 /* FileProviderCommunication.swift in Sources */,
|
||||
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
Loading…
Reference in a new issue