diff --git a/Sources/NextcloudKit/Models/NKLock.swift b/Sources/NextcloudKit/Models/NKLock.swift
index 182a14ca..487d3d36 100644
--- a/Sources/NextcloudKit/Models/NKLock.swift
+++ b/Sources/NextcloudKit/Models/NKLock.swift
@@ -47,6 +47,11 @@ public struct NKLock: Equatable, Sendable {
///
public let token: String?
+ ///
+ /// Normalized resource ETag returned by the LOCK response.
+ ///
+ public let etag: String?
+
///
/// Initialize from a SwiftyXML accessor.
///
@@ -90,6 +95,8 @@ public struct NKLock: Equatable, Sendable {
self.time = Date(timeIntervalSince1970: rawTime)
self.timeOut = Date(timeIntervalSince1970: rawTime + rawTimeOut)
self.token = lockToken
+ self.etag = properties["d:getetag"].text?
+ .trimmingCharacters(in: CharacterSet(charactersIn: "\""))
}
///
@@ -103,7 +110,7 @@ public struct NKLock: Equatable, Sendable {
///
/// Initialize from raw values.
///
- public init(owner: String, ownerEditor: String, ownerType: NKLockType, ownerDisplayName: String, time: Date?, timeOut: Date?, token: String?) {
+ public init(owner: String, ownerEditor: String, ownerType: NKLockType, ownerDisplayName: String, time: Date?, timeOut: Date?, token: String?, etag: String? = nil) {
self.owner = owner
self.ownerEditor = ownerEditor
self.ownerType = ownerType
@@ -111,5 +118,6 @@ public struct NKLock: Equatable, Sendable {
self.time = time
self.timeOut = timeOut
self.token = token
+ self.etag = etag
}
}
diff --git a/Tests/NextcloudKitUnitTests/NKLockUnitTests.swift b/Tests/NextcloudKitUnitTests/NKLockUnitTests.swift
new file mode 100644
index 00000000..7e1d4257
--- /dev/null
+++ b/Tests/NextcloudKitUnitTests/NKLockUnitTests.swift
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import Foundation
+import Testing
+@testable import NextcloudKit
+
+@Suite("NKLock parsing")
+struct NKLockUnitTests {
+ private func makeLockData(etagElement: String = "") -> Data {
+ let xml = """
+
+
+ 1
+ user-id
+ text
+ \(NKLockType.token.rawValue)
+ User Name
+ 1
+ 60
+ files_lock/test-token
+ \(etagElement)
+
+ """
+
+ return Data(xml.utf8)
+ }
+
+ @Test("Parses quoted ETag from LOCK response")
+ func parsesQuotedETag() {
+ let lock = NKLock(data: makeLockData(etagElement: "\"etag-after-lock\""))
+
+ #expect(lock != nil)
+ #expect(lock?.token == "files_lock/test-token")
+ #expect(lock?.etag == "etag-after-lock")
+ }
+
+ @Test("Missing ETag keeps lock parsing valid")
+ func missingETagDoesNotFailParsing() {
+ let lock = NKLock(data: makeLockData())
+
+ #expect(lock != nil)
+ #expect(lock?.etag == nil)
+ }
+
+ @Test("Unquoted ETag is preserved")
+ func preservesUnquotedETag() {
+ let lock = NKLock(data: makeLockData(etagElement: "etag-after-lock"))
+
+ #expect(lock != nil)
+ #expect(lock?.etag == "etag-after-lock")
+ }
+}