Skip to content

Commit 96c2b0e

Browse files
authored
Merge pull request #190 from nextcloud/feature/support-lock-type
Extended support for files_lock server app
2 parents 5e08816 + 525bafe commit 96c2b0e

8 files changed

Lines changed: 284 additions & 99 deletions

File tree

Sources/NextcloudKit/Models/NKDataFileXML.swift

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -429,27 +429,8 @@ public class NKDataFileXML: NSObject {
429429
file.richWorkspace = richWorkspace
430430
}
431431

432-
if let lock = propstat["d:prop", "nc:lock"].int {
433-
file.lock = NSNumber(integerLiteral: lock).boolValue
434-
435-
if let lockOwner = propstat["d:prop", "nc:lock-owner"].text {
436-
file.lockOwner = lockOwner
437-
}
438-
if let lockOwnerEditor = propstat["d:prop", "nc:lock-owner-editor"].text {
439-
file.lockOwnerEditor = lockOwnerEditor
440-
}
441-
if let lockOwnerType = propstat["d:prop", "nc:lock-owner-type"].int {
442-
file.lockOwnerType = lockOwnerType
443-
}
444-
if let lockOwnerDisplayName = propstat["d:prop", "nc:lock-owner-displayname"].text {
445-
file.lockOwnerDisplayName = lockOwnerDisplayName
446-
}
447-
if let lockTime = propstat["d:prop", "nc:lock-time"].int {
448-
file.lockTime = Date(timeIntervalSince1970: TimeInterval(lockTime))
449-
}
450-
if let lockTimeOut = propstat["d:prop", "nc:lock-timeout"].int {
451-
file.lockTimeOut = file.lockTime?.addingTimeInterval(TimeInterval(lockTimeOut))
452-
}
432+
if let lock = NKLock(xml: propstat["d:prop"]) {
433+
file.lock = lock
453434
}
454435

455436
let tagsElements = propstat["d:prop", "nc:system-tags"]

Sources/NextcloudKit/Models/NKFile.swift

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import Foundation
77

8+
///
9+
/// A file or directory on the server.
10+
///
811
public struct NKFile: Sendable {
912
public var account: String
1013
public var classFile: String
@@ -35,13 +38,12 @@ public struct NKFile: Sendable {
3538
public var ocId: String
3639
public var ownerId: String
3740
public var ownerDisplayName: String
38-
public var lock: Bool
39-
public var lockOwner: String
40-
public var lockOwnerEditor: String
41-
public var lockOwnerType: Int
42-
public var lockOwnerDisplayName: String
43-
public var lockTime: Date?
44-
public var lockTimeOut: Date?
41+
42+
///
43+
/// An optional lock on this file. `nil` equals the file not being locked.
44+
///
45+
public var lock: NKLock?
46+
4547
public var path: String
4648
public var permissions: String
4749
public var quotaUsedBytes: Int64
@@ -107,13 +109,7 @@ public struct NKFile: Sendable {
107109
ocId: String = "",
108110
ownerId: String = "",
109111
ownerDisplayName: String = "",
110-
lock: Bool = false,
111-
lockOwner: String = "",
112-
lockOwnerEditor: String = "",
113-
lockOwnerType: Int = 0,
114-
lockOwnerDisplayName: String = "",
115-
lockTime: Date? = nil,
116-
lockTimeOut: Date? = nil,
112+
lock: NKLock? = nil,
117113
path: String = "",
118114
permissions: String = "",
119115
quotaUsedBytes: Int64 = 0,
@@ -171,12 +167,6 @@ public struct NKFile: Sendable {
171167
self.ownerId = ownerId
172168
self.ownerDisplayName = ownerDisplayName
173169
self.lock = lock
174-
self.lockOwner = lockOwner
175-
self.lockOwnerEditor = lockOwnerEditor
176-
self.lockOwnerType = lockOwnerType
177-
self.lockOwnerDisplayName = lockOwnerDisplayName
178-
self.lockTime = lockTime
179-
self.lockTimeOut = lockTimeOut
180170
self.path = path
181171
self.permissions = permissions
182172
self.quotaUsedBytes = quotaUsedBytes
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import Foundation
5+
import SwiftyXMLParser
6+
7+
///
8+
/// Description of a file lock.
9+
///
10+
/// This is based on the description of the [`files_lock`](https://github.com/nextcloud/files_lock) server app.
11+
///
12+
public struct NKLock: Equatable, Sendable {
13+
///
14+
/// User id of the owning user.
15+
///
16+
public let owner: String
17+
18+
///
19+
/// App id of an app owned lock to allow clients to suggest joining the collaborative editing session through the web or direct editing.
20+
///
21+
public let ownerEditor: String
22+
23+
///
24+
/// What kind of lock this is.
25+
///
26+
public let ownerType: NKLockType
27+
28+
///
29+
/// Display name of the lock owner.
30+
///
31+
public let ownerDisplayName: String
32+
33+
///
34+
/// Timestamp of the lock creation time.
35+
///
36+
public let time: Date?
37+
38+
///
39+
/// TTL of the lock in seconds staring from the creation time.
40+
/// A value of 0 means the timeout is infinite.
41+
/// Client implementations should properly handle this specific value.
42+
///
43+
public let timeOut: Date?
44+
45+
///
46+
/// Unique lock token (to be preserved on the client side while holding the lock to sent once full webdav locking is implemented).
47+
///
48+
public let token: String?
49+
50+
///
51+
/// Initialize from a SwiftyXML accessor.
52+
///
53+
/// 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.
54+
///
55+
public init?(xml properties: XML.Accessor) {
56+
guard let isLocked = properties["nc:lock"].bool else {
57+
return nil
58+
}
59+
60+
guard let owner = properties["nc:lock-owner"].text else {
61+
return nil
62+
}
63+
64+
guard let ownerDisplayName = properties["nc:lock-owner-displayname"].text else {
65+
return nil
66+
}
67+
68+
guard let ownerEditor = properties["nc:lock-owner-editor"].text else {
69+
return nil
70+
}
71+
72+
guard let rawOwnerTypeValue = properties["nc:lock-owner-type"].int, let lockOwnerType = NKLockType(rawValue: rawOwnerTypeValue) else {
73+
return nil
74+
}
75+
76+
guard let rawTime = properties["nc:lock-time"].double else {
77+
return nil
78+
}
79+
80+
guard let rawTimeOut = properties["nc:lock-timeout"].double else {
81+
return nil
82+
}
83+
84+
let lockToken = properties["nc:lock-token"].text
85+
86+
self.owner = owner
87+
self.ownerEditor = ownerEditor
88+
self.ownerType = lockOwnerType
89+
self.ownerDisplayName = ownerDisplayName
90+
self.time = Date(timeIntervalSince1970: rawTime)
91+
self.timeOut = Date(timeIntervalSince1970: rawTime + rawTimeOut)
92+
self.token = lockToken
93+
}
94+
95+
///
96+
/// Initialize from the response body data of an WebDAV request.
97+
///
98+
public init?(data: Data) {
99+
let properties = XML.parse(data)["d:prop"]
100+
self.init(xml: properties)
101+
}
102+
103+
///
104+
/// Initialize from raw values.
105+
///
106+
public init(owner: String, ownerEditor: String, ownerType: NKLockType, ownerDisplayName: String, time: Date?, timeOut: Date?, token: String?) {
107+
self.owner = owner
108+
self.ownerEditor = ownerEditor
109+
self.ownerType = ownerType
110+
self.ownerDisplayName = ownerDisplayName
111+
self.time = time
112+
self.timeOut = timeOut
113+
self.token = token
114+
}
115+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
///
5+
/// The [`files_lock`](https://github.com/nextcloud/files_lock) server apps distinguishes between different lock types which are represented by this type.
6+
///
7+
public enum NKLockType: Int, Sendable {
8+
///
9+
/// 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.
10+
///
11+
case user = 0
12+
13+
///
14+
/// This lock type is created by collaborative apps like Text or Office to avoid outside changes through WebDAV or other apps.
15+
///
16+
case app = 1
17+
18+
///
19+
/// This lock type will bind the ownership to the provided lock token.
20+
/// 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.
21+
/// This will allow to limit the locking to an individual client.
22+
///
23+
/// 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.
24+
/// 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.
25+
///
26+
case token = 2
27+
}

Sources/NextcloudKit/NextcloudKit+Capabilities.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import Alamofire
1616

1717
public extension NextcloudKit {
1818

19+
///
1920
/// Retrieves the capabilities of the Nextcloud server for the given account.
21+
///
2022
/// - Parameters:
2123
/// - account: The account identifier.
2224
/// - options: Additional request options.
2325
/// - taskHandler: Callback for the underlying URL session task.
2426
/// - completion: Callback returning parsed capabilities or an error.
27+
///
2528
func getCapabilities(account: String,
2629
options: NKRequestOptions = NKRequestOptions(),
2730
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
@@ -269,6 +272,15 @@ public extension NextcloudKit {
269272

270273
struct Files: Codable {
271274
let undelete: Bool?
275+
276+
///
277+
/// Whether different lock types as defined in ``NKLockType`` are supported or not.
278+
///
279+
let lockTypes: Bool?
280+
281+
///
282+
/// The version of the locking API.
283+
///
272284
let locking: String?
273285
let comments: Bool?
274286
let versioning: Bool?
@@ -282,6 +294,7 @@ public extension NextcloudKit {
282294
let forbiddenFileNameExtensions: [String]?
283295

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

391404
capabilities.filesUndelete = json.files?.undelete ?? false
405+
capabilities.filesLockTypes = json.files?.lockTypes ?? false
392406
capabilities.filesLockVersion = json.files?.locking ?? ""
393407
capabilities.filesComments = json.files?.comments ?? false
394408
capabilities.filesBigfilechunking = json.files?.bigfilechunking ?? false
@@ -440,12 +454,17 @@ actor CapabilitiesStore {
440454
}
441455
}
442456

443-
/// Singleton container and public API for accessing and caching capabilities.
457+
///
458+
/// Singleton container and public API for accessing and caching capabilities for user accounts.
459+
///
444460
final public class NKCapabilities: Sendable {
445461
public static let shared = NKCapabilities()
446462

447463
private let store = CapabilitiesStore()
448464

465+
///
466+
/// Flattened set of capabilities after parsing the server response.
467+
///
449468
public class Capabilities: @unchecked Sendable {
450469
public var serverVersionMajor: Int = 0
451470
public var serverVersion: String = ""
@@ -472,6 +491,15 @@ final public class NKCapabilities: Sendable {
472491
public var activity: [String] = []
473492
public var notification: [String] = []
474493
public var filesUndelete: Bool = false
494+
495+
///
496+
/// Whether different lock types as defined in ``NKLockType`` are supported or not.
497+
///
498+
public var filesLockTypes: Bool = false
499+
500+
///
501+
/// The version of the locking API.
502+
///
475503
public var filesLockVersion: String = "" // NC 24
476504
public var filesComments: Bool = false // NC 20
477505
public var filesBigfilechunking: Bool = false
@@ -499,17 +527,37 @@ final public class NKCapabilities: Sendable {
499527

500528
// MARK: - Public API
501529

530+
///
531+
/// Set or overwrite the existing capabilities in the store.
532+
///
533+
/// - Parameters:
534+
/// - account: The account identifier for which the capabilities should be stored for.
535+
/// - capabilities: The actual capabilities which should be stored.
536+
///
502537
public func setCapabilities(for account: String, capabilities: Capabilities) async {
503538
await store.set(account, value: capabilities)
504539
}
505540

541+
///
542+
/// The capabilities by the given account identifier.
543+
///
544+
/// - Parameter account: The account identifier for which the capabilities should be returned.
545+
///
546+
/// - Returns: Either the acquired capabilities or a default object.
547+
///
506548
public func getCapabilities(for account: String?) async -> Capabilities {
507549
guard let account else {
508550
return Capabilities()
509551
}
552+
510553
return await store.get(account) ?? Capabilities()
511554
}
512555

556+
///
557+
/// Remove capabilities stored in the in-memory cache.
558+
///
559+
/// - Parameter account: The account identifier for which the capabilities should be removed.
560+
///
513561
public func removeCapabilities(for account: String) async {
514562
await store.remove(account)
515563
}

0 commit comments

Comments
 (0)