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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

public struct NKGovernanceAvailableLabels: Codable, Sendable, Equatable, Hashable {
public let sensitivity: [NKGovernanceLabel]
public let retention: [NKGovernanceLabel]
public let hold: [NKGovernanceLabel]

public init(sensitivity: [NKGovernanceLabel], retention: [NKGovernanceLabel], hold: [NKGovernanceLabel]) {
self.sensitivity = sensitivity
self.retention = retention
self.hold = hold
}

enum CodingKeys: String, CodingKey {
case sensitivity, retention, hold
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
sensitivity = try container.decodeIfPresent([NKGovernanceLabel].self, forKey: .sensitivity) ?? []
retention = try container.decodeIfPresent([NKGovernanceLabel].self, forKey: .retention) ?? []
hold = try container.decodeIfPresent([NKGovernanceLabel].self, forKey: .hold) ?? []
}
}
40 changes: 40 additions & 0 deletions Sources/NextcloudKit/Models/Governance/NKGovernanceLabel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

public struct NKGovernanceLabel: Codable, Sendable, Equatable, Hashable {
public let id: String
public let name: String
public let priority: Int
public let description: String
public let color: String
public let isAssigned: Bool

public init(id: String, name: String, priority: Int, description: String, color: String, isAssigned: Bool) {
self.id = id
self.name = name
self.priority = priority
self.description = description
self.color = color
self.isAssigned = isAssigned
}

enum CodingKeys: String, CodingKey {
case id, name, priority, description, color, isAssigned
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
priority = try container.decodeIfPresent(Int.self, forKey: .priority) ?? 0
description = try container.decodeIfPresent(String.self, forKey: .description) ?? ""

let rawColor = try container.decodeIfPresent(String.self, forKey: .color) ?? ""
color = rawColor.isEmpty || rawColor.hasPrefix("#") ? rawColor : "#\(rawColor)"

isAssigned = try container.decodeIfPresent(Bool.self, forKey: .isAssigned) ?? false
}
}
11 changes: 11 additions & 0 deletions Sources/NextcloudKit/Models/Governance/NKGovernanceLabelType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

public enum NKGovernanceLabelType: String, Sendable, Equatable, Hashable {
case sensitivity = "SENSITIVITY"
case retention = "RETENTION"
case hold = "HOLD"
}
33 changes: 7 additions & 26 deletions Sources/NextcloudKit/NextcloudKit+Capabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public extension NextcloudKit {
let recommendations: Recommendations?
let termsOfService: TermsOfService?
let clientIntegration: NKClientIntegration?
let governance: Governance?

enum CodingKeys: String, CodingKey {
case downloadLimit = "downloadlimit"
Expand All @@ -147,8 +148,11 @@ public extension NextcloudKit {
case recommendations
case termsOfService = "terms_of_service"
case clientIntegration = "client_integration"
case governance
}

struct Governance: Codable {}

struct DownloadLimit: Codable {
let enabled: Bool?
let defaultLimit: Int?
Expand Down Expand Up @@ -350,32 +354,6 @@ public extension NextcloudKit {
struct Recommendations: Codable {
let enabled: Bool?
}

// struct DeclarativeUI: Codable {
// let contextMenu: [[ContextMenuItem]]
//
// enum CodingKeys: String, CodingKey {
// case contextMenu = "context-menu"
// }
// }

//
// struct DeclarativeUI: Codable {
// let contextMenus: [ContextMenu]
//
// enum CodingKeys: String, CodingKey {
// case contextMenus = "context-menu"
// }
//
// struct ContextMenu: Codable {
// let items
// }
//
// struct ContextMenuItem: Codable {
// let title: String
// let endpoint: String
// }
// }
}
}
}
Expand Down Expand Up @@ -435,6 +413,8 @@ public extension NextcloudKit {
capabilities.activityEnabled = json.activity != nil
capabilities.activity = json.activity?.apiv2 ?? []

capabilities.governanceEnabled = json.governance != nil

capabilities.notification = json.notifications?.ocsendpoints ?? []

capabilities.filesUndelete = json.files?.undelete ?? false
Expand Down Expand Up @@ -548,6 +528,7 @@ final public class NKCapabilities: Sendable {
public var userStatusSupportsBusy: Bool = false
public var externalSites: Bool = false
public var activityEnabled: Bool = false
public var governanceEnabled: Bool = false
public var groupfoldersEnabled: Bool = false // NC27
public var assistantEnabled: Bool = false // NC28
public var isLivePhotoServerAvailable: Bool = false // NC28
Expand Down
117 changes: 117 additions & 0 deletions Sources/NextcloudKit/NextcloudKit+Governance.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import Alamofire

public extension NextcloudKit {
/// Returns all labels the user may apply to an entity, grouped by type, in a single request.
func getGovernanceAvailableLabels(entityType: String = "FILES",
entityId: String,
account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }
) async -> (account: String, labels: NKGovernanceAvailableLabels?, responseData: AFDataResponse<Data>?, error: NKError) {
await withCheckedContinuation { continuation in
let endpoint = governancePath(entityType: entityType, entityId: entityId) + "/available?format=json"
guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account),
let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint),
let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else {
return options.queue.async {
continuation.resume(returning: (account, nil, nil, .urlError))
}
}

nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in
task.taskDescription = options.taskDescription
taskHandler(task)
}.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in
switch response.result {
case .failure(let error):
let error = NKError(error: error, afResponse: response, responseData: response.data)
options.queue.async {
continuation.resume(returning: (account, nil, response, error))
}
case .success(let data):
if let result = try? JSONDecoder().decode(GovernanceOCS<NKGovernanceAvailableLabels>.self, from: data) {
options.queue.async {
continuation.resume(returning: (account, result.ocs.data, response, .success))
}
} else {
options.queue.async {
continuation.resume(returning: (account, nil, response, .invalidData))
}
}
}
}
}
}

/// Applies a label to an entity. Only one label per type may be active (except hold, which allows multiple).
func setGovernanceLabel(entityType: String = "FILES",
entityId: String,
labelType: NKGovernanceLabelType,
labelId: String,
account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }
) async -> (account: String, responseData: AFDataResponse<Data>?, error: NKError) {
await applyGovernanceLabel(method: .post, entityType: entityType, entityId: entityId, labelType: labelType, labelId: labelId, account: account, options: options, taskHandler: taskHandler)
}

/// Removes a label from an entity.
func removeGovernanceLabel(entityType: String = "FILES",
entityId: String,
labelType: NKGovernanceLabelType,
labelId: String,
account: String,
options: NKRequestOptions = NKRequestOptions(),
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }
) async -> (account: String, responseData: AFDataResponse<Data>?, error: NKError) {
await applyGovernanceLabel(method: .delete, entityType: entityType, entityId: entityId, labelType: labelType, labelId: labelId, account: account, options: options, taskHandler: taskHandler)
}

private func governancePath(entityType: String, entityId: String) -> String {
"ocs/v2.php/apps/governance/v1/labels/\(entityType)/\(entityId)"
}

private func applyGovernanceLabel(method: HTTPMethod,
entityType: String,
entityId: String,
labelType: NKGovernanceLabelType,
labelId: String,
account: String,
options: NKRequestOptions,
taskHandler: @escaping (_ task: URLSessionTask) -> Void
) async -> (account: String, responseData: AFDataResponse<Data>?, error: NKError) {
await withCheckedContinuation { continuation in
let endpoint = governancePath(entityType: entityType, entityId: entityId) + "/\(labelType.rawValue)/\(labelId)?format=json"
guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account),
let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint),
let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else {
return options.queue.async {
continuation.resume(returning: (account, nil, .urlError))
}
}

nkSession.sessionData.request(url, method: method, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in
task.taskDescription = options.taskDescription
taskHandler(task)
}.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in
let result = self.evaluateResponse(response)
options.queue.async {
continuation.resume(returning: (account, response, result))
}
}
}
}
}

private struct GovernanceOCS<T: Decodable>: Decodable {
let ocs: Inner

struct Inner: Decodable {
let data: T
}
}
Loading