Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 2 additions & 21 deletions Sources/NextcloudKit/Models/NKDataFileXML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,27 +429,8 @@ public class NKDataFileXML: NSObject {
file.richWorkspace = richWorkspace
}

if let lock = propstat["d:prop", "nc:lock"].int {
file.lock = NSNumber(integerLiteral: lock).boolValue

if let lockOwner = propstat["d:prop", "nc:lock-owner"].text {
file.lockOwner = lockOwner
}
if let lockOwnerEditor = propstat["d:prop", "nc:lock-owner-editor"].text {
file.lockOwnerEditor = lockOwnerEditor
}
if let lockOwnerType = propstat["d:prop", "nc:lock-owner-type"].int {
file.lockOwnerType = lockOwnerType
}
if let lockOwnerDisplayName = propstat["d:prop", "nc:lock-owner-displayname"].text {
file.lockOwnerDisplayName = lockOwnerDisplayName
}
if let lockTime = propstat["d:prop", "nc:lock-time"].int {
file.lockTime = Date(timeIntervalSince1970: TimeInterval(lockTime))
}
if let lockTimeOut = propstat["d:prop", "nc:lock-timeout"].int {
file.lockTimeOut = file.lockTime?.addingTimeInterval(TimeInterval(lockTimeOut))
}
if let lock = NKLock(xml: propstat["d:prop"]) {
file.lock = lock
}

let tagsElements = propstat["d:prop", "nc:system-tags"]
Expand Down
30 changes: 10 additions & 20 deletions Sources/NextcloudKit/Models/NKFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import Foundation

///
/// A file or directory on the server.
///
public struct NKFile: Sendable {
public var account: String
public var classFile: String
Expand Down Expand Up @@ -35,13 +38,12 @@ public struct NKFile: Sendable {
public var ocId: String
public var ownerId: String
public var ownerDisplayName: String
public var lock: Bool
public var lockOwner: String
public var lockOwnerEditor: String
public var lockOwnerType: Int
public var lockOwnerDisplayName: String
public var lockTime: Date?
public var lockTimeOut: Date?

///
/// An optional lock on this file. `nil` equals the file not being locked.
///
public var lock: NKLock?

public var path: String
public var permissions: String
public var quotaUsedBytes: Int64
Expand Down Expand Up @@ -107,13 +109,7 @@ public struct NKFile: Sendable {
ocId: String = "",
ownerId: String = "",
ownerDisplayName: String = "",
lock: Bool = false,
lockOwner: String = "",
lockOwnerEditor: String = "",
lockOwnerType: Int = 0,
lockOwnerDisplayName: String = "",
lockTime: Date? = nil,
lockTimeOut: Date? = nil,
lock: NKLock? = nil,
path: String = "",
permissions: String = "",
quotaUsedBytes: Int64 = 0,
Expand Down Expand Up @@ -171,12 +167,6 @@ public struct NKFile: Sendable {
self.ownerId = ownerId
self.ownerDisplayName = ownerDisplayName
self.lock = lock
self.lockOwner = lockOwner
self.lockOwnerEditor = lockOwnerEditor
self.lockOwnerType = lockOwnerType
self.lockOwnerDisplayName = lockOwnerDisplayName
self.lockTime = lockTime
self.lockTimeOut = lockTimeOut
self.path = path
self.permissions = permissions
self.quotaUsedBytes = quotaUsedBytes
Expand Down
115 changes: 115 additions & 0 deletions Sources/NextcloudKit/Models/NKLock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import SwiftyXMLParser

///
/// Description of a file lock.
///
/// This is based on the description of the [`files_lock`](https://github.com/nextcloud/files_lock) server app.
///
public struct NKLock: Equatable, Sendable {
///
/// User id of the owning user.
///
public let owner: String

///
/// App id of an app owned lock to allow clients to suggest joining the collaborative editing session through the web or direct editing.
///
public let ownerEditor: String

///
/// What kind of lock this is.
///
public let ownerType: NKLockType

///
/// Display name of the lock owner.
///
public let ownerDisplayName: String

///
/// Timestamp of the lock creation time.
///
public let time: Date?

///
/// TTL of the lock in seconds staring from the creation time.
/// A value of 0 means the timeout is infinite.
/// Client implementations should properly handle this specific value.
///
public let timeOut: Date?

///
/// Unique lock token (to be preserved on the client side while holding the lock to sent once full webdav locking is implemented).
///
public let token: String?

///
/// Initialize from a SwiftyXML accessor.
///
/// This is intended for creating an instance based on a superset of required properties returned by a `PROPFIND` request to the server about an item.
///
public init?(xml properties: XML.Accessor) {
guard let isLocked = properties["nc:lock"].bool else {
return nil
}

guard let owner = properties["nc:lock-owner"].text else {
return nil
}

guard let ownerDisplayName = properties["nc:lock-owner-displayname"].text else {
return nil
}

guard let ownerEditor = properties["nc:lock-owner-editor"].text else {
return nil
}

guard let rawOwnerTypeValue = properties["nc:lock-owner-type"].int, let lockOwnerType = NKLockType(rawValue: rawOwnerTypeValue) else {
return nil
}

guard let rawTime = properties["nc:lock-time"].double else {
return nil
}

guard let rawTimeOut = properties["nc:lock-timeout"].double else {
return nil
}

let lockToken = properties["nc:lock-token"].text

self.owner = owner
self.ownerEditor = ownerEditor
self.ownerType = lockOwnerType
self.ownerDisplayName = ownerDisplayName
self.time = Date(timeIntervalSince1970: rawTime)
self.timeOut = Date(timeIntervalSince1970: rawTime + rawTimeOut)
self.token = lockToken
}

///
/// Initialize from the response body data of an WebDAV request.
///
public init?(data: Data) {
let properties = XML.parse(data)["d:prop"]
self.init(xml: properties)
}

///
/// Initialize from raw values.
///
public init(owner: String, ownerEditor: String, ownerType: NKLockType, ownerDisplayName: String, time: Date?, timeOut: Date?, token: String?) {
self.owner = owner
self.ownerEditor = ownerEditor
self.ownerType = ownerType
self.ownerDisplayName = ownerDisplayName
self.time = time
self.timeOut = timeOut
self.token = token
}
}
27 changes: 27 additions & 0 deletions Sources/NextcloudKit/NKLockType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later

///
/// The [`files_lock`](https://github.com/nextcloud/files_lock) server apps distinguishes between different lock types which are represented by this type.
///
public enum NKLockType: Int, Sendable {
///
/// This lock type is initiated by a user manually through the WebUI or Clients and will limit editing capabilities on the file to the lock owning user.
///
case user = 0

///
/// This lock type is created by collaborative apps like Text or Office to avoid outside changes through WebDAV or other apps.
///
case app = 1

///
/// This lock type will bind the ownership to the provided lock token.
/// Any request that aims to modify the file will be required to sent the token, the user itself is not able to write to files without the token.
/// This will allow to limit the locking to an individual client.
///
/// This is mostly used for automatic client locking, e.g. when a file is opened in a client or with WebDAV clients that support native WebDAV locking.
/// The lock token can be skipped on follow up requests using the OCS API or the X-User-Lock header for WebDAV requests, but in that case the server will not be able to validate the lock ownership when unlocking the file from the client.
///
case token = 2
}
50 changes: 49 additions & 1 deletion Sources/NextcloudKit/NextcloudKit+Capabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import Alamofire

public extension NextcloudKit {

///
/// Retrieves the capabilities of the Nextcloud server for the given account.
///
/// - Parameters:
/// - account: The account identifier.
/// - options: Additional request options.
/// - taskHandler: Callback for the underlying URL session task.
/// - completion: Callback returning parsed capabilities or an error.
///
func getCapabilities(account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
Expand Down Expand Up @@ -269,6 +272,15 @@ public extension NextcloudKit {

struct Files: Codable {
let undelete: Bool?

///
/// Whether different lock types as defined in ``NKLockType`` are supported or not.
///
let lockTypes: Bool?

///
/// The version of the locking API.
///
let locking: String?
let comments: Bool?
let versioning: Bool?
Expand All @@ -282,6 +294,7 @@ public extension NextcloudKit {
let forbiddenFileNameExtensions: [String]?

enum CodingKeys: String, CodingKey {
case lockTypes = "api-feature-lock-type"
case undelete, locking, comments, versioning, directEditing, bigfilechunking
case versiondeletion = "version_deletion"
case versionlabeling = "version_labeling"
Expand Down Expand Up @@ -389,6 +402,7 @@ public extension NextcloudKit {
capabilities.notification = json.notifications?.ocsendpoints ?? []

capabilities.filesUndelete = json.files?.undelete ?? false
capabilities.filesLockTypes = json.files?.lockTypes ?? false
capabilities.filesLockVersion = json.files?.locking ?? ""
capabilities.filesComments = json.files?.comments ?? false
capabilities.filesBigfilechunking = json.files?.bigfilechunking ?? false
Expand Down Expand Up @@ -440,12 +454,17 @@ actor CapabilitiesStore {
}
}

/// Singleton container and public API for accessing and caching capabilities.
///
/// Singleton container and public API for accessing and caching capabilities for user accounts.
///
final public class NKCapabilities: Sendable {
public static let shared = NKCapabilities()

private let store = CapabilitiesStore()

///
/// Flattened set of capabilities after parsing the server response.
///
public class Capabilities: @unchecked Sendable {
public var serverVersionMajor: Int = 0
public var serverVersion: String = ""
Expand All @@ -472,6 +491,15 @@ final public class NKCapabilities: Sendable {
public var activity: [String] = []
public var notification: [String] = []
public var filesUndelete: Bool = false

///
/// Whether different lock types as defined in ``NKLockType`` are supported or not.
///
public var filesLockTypes: Bool = false

///
/// The version of the locking API.
///
public var filesLockVersion: String = "" // NC 24
public var filesComments: Bool = false // NC 20
public var filesBigfilechunking: Bool = false
Expand Down Expand Up @@ -499,17 +527,37 @@ final public class NKCapabilities: Sendable {

// MARK: - Public API

///
/// Set or overwrite the existing capabilities in the store.
///
/// - Parameters:
/// - account: The account identifier for which the capabilities should be stored for.
/// - capabilities: The actual capabilities which should be stored.
///
public func setCapabilities(for account: String, capabilities: Capabilities) async {
await store.set(account, value: capabilities)
}

///
/// The capabilities by the given account identifier.
///
/// - Parameter account: The account identifier for which the capabilities should be returned.
///
/// - Returns: Either the acquired capabilities or a default object.
///
public func getCapabilities(for account: String?) async -> Capabilities {
guard let account else {
return Capabilities()
}

return await store.get(account) ?? Capabilities()
}

///
/// Remove capabilities stored in the in-memory cache.
///
/// - Parameter account: The account identifier for which the capabilities should be removed.
///
public func removeCapabilities(for account: String) async {
await store.remove(account)
}
Expand Down
Loading
Loading