2024-02-27 15:27:04 +03:00
|
|
|
//
|
|
|
|
// ShareTableViewDataSource.swift
|
|
|
|
// FileProviderUIExt
|
|
|
|
//
|
|
|
|
// Created by Claudio Cambra on 27/2/24.
|
|
|
|
//
|
|
|
|
|
2024-02-27 15:28:09 +03:00
|
|
|
import AppKit
|
|
|
|
import FileProvider
|
|
|
|
import NextcloudKit
|
2024-02-27 15:28:55 +03:00
|
|
|
import OSLog
|
|
|
|
|
2024-02-27 18:56:08 +03:00
|
|
|
class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
|
|
|
|
private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
|
2024-02-27 18:56:18 +03:00
|
|
|
private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
|
|
|
|
|
2024-02-28 16:16:34 +03:00
|
|
|
var uiDelegate: ShareViewDataSourceUIDelegate?
|
2024-02-27 15:28:09 +03:00
|
|
|
var sharesTableView: NSTableView? {
|
|
|
|
didSet {
|
2024-02-27 18:56:18 +03:00
|
|
|
sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
|
2024-02-27 19:21:01 +03:00
|
|
|
sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB
|
2024-02-27 15:28:09 +03:00
|
|
|
sharesTableView?.dataSource = self
|
2024-02-27 19:19:37 +03:00
|
|
|
sharesTableView?.delegate = self
|
2024-02-27 15:28:09 +03:00
|
|
|
sharesTableView?.reloadData()
|
|
|
|
}
|
|
|
|
}
|
2024-03-18 14:16:29 +03:00
|
|
|
var shareCapabilities = ShareCapabilities()
|
2024-03-19 13:57:52 +03:00
|
|
|
var itemMetadata: NKFile?
|
2024-02-28 16:16:34 +03:00
|
|
|
|
2024-03-04 16:42:06 +03:00
|
|
|
private(set) var kit: NextcloudKit?
|
2024-03-05 14:15:05 +03:00
|
|
|
private(set) var itemURL: URL?
|
|
|
|
private(set) var itemServerRelativePath: String?
|
2024-02-27 15:28:09 +03:00
|
|
|
private var shares: [NKShare] = [] {
|
2024-03-04 19:05:10 +03:00
|
|
|
didSet { Task { @MainActor in sharesTableView?.reloadData() } }
|
2024-02-27 15:28:09 +03:00
|
|
|
}
|
2024-02-27 18:56:55 +03:00
|
|
|
private var account: NextcloudAccount? {
|
|
|
|
didSet {
|
|
|
|
guard let account = account else { return }
|
|
|
|
kit = NextcloudKit()
|
|
|
|
kit?.setup(
|
|
|
|
user: account.username,
|
|
|
|
userId: account.username,
|
|
|
|
password: account.password,
|
|
|
|
urlBase: account.serverUrl
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2024-02-27 15:28:27 +03:00
|
|
|
|
2024-02-27 18:56:55 +03:00
|
|
|
func loadItem(url: URL) {
|
2024-03-05 14:24:16 +03:00
|
|
|
itemServerRelativePath = nil
|
2024-02-27 15:28:55 +03:00
|
|
|
itemURL = url
|
|
|
|
Task {
|
|
|
|
await reload()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-04 16:51:37 +03:00
|
|
|
func reload() async {
|
2024-02-27 18:56:55 +03:00
|
|
|
guard let itemURL = itemURL else { return }
|
|
|
|
guard let itemIdentifier = await withCheckedContinuation({
|
|
|
|
(continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
|
|
|
|
NSFileProviderManager.getIdentifierForUserVisibleFile(
|
|
|
|
at: itemURL
|
|
|
|
) { identifier, domainIdentifier, error in
|
|
|
|
defer { continuation.resume(returning: identifier) }
|
|
|
|
guard error == nil else {
|
|
|
|
Logger.sharesDataSource.error("No identifier: \(error, privacy: .public)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}) else {
|
|
|
|
Logger.sharesDataSource.error("Could not get identifier for item, no shares.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-27 15:28:55 +03:00
|
|
|
do {
|
|
|
|
let connection = try await serviceConnection(url: itemURL)
|
2024-02-27 18:56:55 +03:00
|
|
|
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
|
|
|
|
let credentials = await connection.credentials() as? Dictionary<String, String>,
|
|
|
|
let convertedAccount = NextcloudAccount(dictionary: credentials) else {
|
|
|
|
Logger.sharesDataSource.error("Failed to get details from FileProviderExt")
|
|
|
|
return
|
|
|
|
}
|
2024-03-19 13:57:52 +03:00
|
|
|
let serverPathString = serverPath as String
|
|
|
|
itemServerRelativePath = serverPathString
|
2024-02-27 18:56:55 +03:00
|
|
|
account = convertedAccount
|
2024-03-04 19:05:25 +03:00
|
|
|
await sharesTableView?.deselectAll(self)
|
2024-03-18 13:06:26 +03:00
|
|
|
shareCapabilities = await fetchCapabilities()
|
2024-03-18 13:06:56 +03:00
|
|
|
guard shareCapabilities.apiEnabled else {
|
|
|
|
let errorMsg = "Server does not support shares."
|
|
|
|
Logger.sharesDataSource.info("\(errorMsg)")
|
|
|
|
uiDelegate?.showError(errorMsg)
|
|
|
|
return
|
|
|
|
}
|
2024-03-19 13:57:52 +03:00
|
|
|
itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString)
|
2024-03-19 14:38:56 +03:00
|
|
|
guard itemMetadata?.permissions.contains("R") == true else {
|
2024-03-19 13:57:52 +03:00
|
|
|
let errorMsg = "This file cannot be shared."
|
|
|
|
Logger.sharesDataSource.warning("\(errorMsg)")
|
|
|
|
uiDelegate?.showError(errorMsg)
|
|
|
|
return
|
|
|
|
}
|
2024-02-27 18:56:55 +03:00
|
|
|
shares = await fetch(
|
2024-03-19 13:57:52 +03:00
|
|
|
itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
|
2024-02-27 18:56:55 +03:00
|
|
|
)
|
2024-02-27 15:28:55 +03:00
|
|
|
} catch let error {
|
2024-02-27 18:56:55 +03:00
|
|
|
Logger.sharesDataSource.error("Could not reload data: \(error, privacy: .public)")
|
2024-02-27 15:28:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-27 15:28:27 +03:00
|
|
|
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
|
|
|
|
}
|
2024-02-27 18:55:26 +03:00
|
|
|
|
2024-02-27 18:56:55 +03:00
|
|
|
private func fetch(
|
|
|
|
itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
|
|
|
|
) async -> [NKShare] {
|
2024-02-28 16:57:08 +03:00
|
|
|
Task { @MainActor in uiDelegate?.fetchStarted() }
|
|
|
|
defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
|
|
|
|
|
2024-02-27 18:56:55 +03:00
|
|
|
let rawIdentifier = itemIdentifier.rawValue
|
|
|
|
Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
|
|
|
|
|
|
|
|
guard let kit = kit else {
|
|
|
|
Logger.sharesDataSource.error("NextcloudKit instance is nil")
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
let parameter = NKShareParameter(path: itemRelativePath)
|
|
|
|
|
|
|
|
return await withCheckedContinuation { continuation in
|
|
|
|
kit.readShares(parameters: parameter) { account, shares, data, error in
|
|
|
|
let shareCount = shares?.count ?? 0
|
|
|
|
Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
|
|
|
|
defer { continuation.resume(returning: shares ?? []) }
|
|
|
|
guard error == .success else {
|
2024-03-04 20:33:47 +03:00
|
|
|
let errorString = "Error fetching shares: \(error.errorDescription)"
|
|
|
|
Logger.sharesDataSource.error("\(errorString)")
|
|
|
|
Task { @MainActor in self.uiDelegate?.showError(errorString) }
|
2024-02-27 18:56:55 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-18 13:06:26 +03:00
|
|
|
private func fetchCapabilities() async -> ShareCapabilities {
|
|
|
|
return await withCheckedContinuation { continuation in
|
|
|
|
kit?.getCapabilities { account, capabilitiesJson, error in
|
|
|
|
guard error == .success, let capabilitiesJson = capabilitiesJson else {
|
|
|
|
let errorString = "Error getting server capabilities: \(error.errorDescription)"
|
2024-03-19 14:05:54 +03:00
|
|
|
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
|
2024-03-18 13:06:26 +03:00
|
|
|
Task { @MainActor in self.uiDelegate?.showError(errorString) }
|
|
|
|
continuation.resume(returning: ShareCapabilities())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
|
|
|
|
continuation.resume(returning: ShareCapabilities(json: capabilitiesJson))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-19 13:57:28 +03:00
|
|
|
private func fetchItemMetadata(itemRelativePath: String) async -> NKFile? {
|
2024-03-19 14:38:41 +03:00
|
|
|
guard let kit = kit else {
|
|
|
|
let errorString = "Could not fetch item metadata as nckit unavailable"
|
|
|
|
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
|
|
|
|
Task { @MainActor in self.uiDelegate?.showError(errorString) }
|
|
|
|
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)"
|
2024-03-19 13:57:28 +03:00
|
|
|
return await withCheckedContinuation { continuation in
|
2024-03-19 14:38:41 +03:00
|
|
|
kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0") {
|
2024-03-19 13:57:28 +03:00
|
|
|
account, files, data, error in
|
|
|
|
guard error == .success else {
|
|
|
|
let errorString = "Error getting item metadata: \(error.errorDescription)"
|
2024-03-19 14:05:54 +03:00
|
|
|
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
|
2024-03-19 13:57:28 +03:00
|
|
|
Task { @MainActor in self.uiDelegate?.showError(errorString) }
|
|
|
|
continuation.resume(returning: nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
Logger.sharesDataSource.info("Successfully retrieved item metadata")
|
|
|
|
continuation.resume(returning: files.first)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-27 18:55:26 +03:00
|
|
|
// MARK: - NSTableViewDataSource protocol methods
|
|
|
|
|
|
|
|
@objc func numberOfRows(in tableView: NSTableView) -> Int {
|
|
|
|
shares.count
|
|
|
|
}
|
2024-02-27 18:56:08 +03:00
|
|
|
|
|
|
|
// MARK: - NSTableViewDelegate protocol methods
|
|
|
|
|
|
|
|
@objc func tableView(
|
|
|
|
_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
|
|
|
|
) -> NSView? {
|
2024-02-27 19:20:43 +03:00
|
|
|
let share = shares[row]
|
|
|
|
guard let view = tableView.makeView(
|
|
|
|
withIdentifier: shareItemViewIdentifier, owner: self
|
|
|
|
) as? ShareTableItemView else {
|
|
|
|
Logger.sharesDataSource.error("Acquired item view from table is not a Share item view!")
|
|
|
|
return nil
|
|
|
|
}
|
2024-02-28 11:01:01 +03:00
|
|
|
view.share = share
|
2024-02-27 18:56:08 +03:00
|
|
|
return view
|
|
|
|
}
|
2024-02-28 16:16:34 +03:00
|
|
|
|
|
|
|
@objc func tableViewSelectionDidChange(_ notification: Notification) {
|
|
|
|
guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
|
2024-03-19 15:08:14 +03:00
|
|
|
Task { @MainActor in uiDelegate?.hideOptions(self) }
|
2024-02-28 16:16:34 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
let share = shares[selectedRow]
|
2024-02-28 16:57:23 +03:00
|
|
|
Task { @MainActor in uiDelegate?.showOptions(share: share) }
|
2024-02-28 16:16:34 +03:00
|
|
|
}
|
2024-02-27 15:28:09 +03:00
|
|
|
}
|