diff --git a/Sources/NextcloudKit/Models/NKDownloadLimit.swift b/Sources/NextcloudKit/Models/NKDownloadLimit.swift new file mode 100644 index 00000000..b3722a4c --- /dev/null +++ b/Sources/NextcloudKit/Models/NKDownloadLimit.swift @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +/// +/// Data model for a download limit as returned in the WebDAV response for file properties. +/// +/// Each relates to a share of a file and is optionally provided by the [Files Download Limit](https://github.com/nextcloud/files_downloadlimit) app for Nextcloud server. +/// +public class NKDownloadLimit: NSObject { + /// + /// The number of downloads which already happened. + /// + public let count: Int + + /// + /// Total number of allowed downloas. + /// + public let limit: Int + + /// + /// The token identifying the related share. + /// + public let token: String + + init(count: Int, limit: Int, token: String) { + self.count = count + self.limit = limit + self.token = token + } +} diff --git a/Sources/NextcloudKit/Models/NKFile.swift b/Sources/NextcloudKit/Models/NKFile.swift index 1da7b9d5..dde57ee8 100644 --- a/Sources/NextcloudKit/Models/NKFile.swift +++ b/Sources/NextcloudKit/Models/NKFile.swift @@ -16,6 +16,12 @@ public class NKFile: NSObject { public var date = Date() public var directory: Bool = false public var downloadURL = "" + + /// + /// Download limits for shares of this file. + /// + public var downloadLimits = [NKDownloadLimit]() + public var e2eEncrypted: Bool = false public var etag = "" public var favorite: Bool = false diff --git a/Sources/NextcloudKit/Models/NKProperties.swift b/Sources/NextcloudKit/Models/NKProperties.swift index 929d4473..f5482f58 100644 --- a/Sources/NextcloudKit/Models/NKProperties.swift +++ b/Sources/NextcloudKit/Models/NKProperties.swift @@ -3,9 +3,18 @@ // SPDX-FileCopyrightText: 2023 Claudio Cambra // SPDX-License-Identifier: GPL-3.0-or-later +/// +/// Definition of properties used for decoding in ``NKDataFileXML``. +/// public enum NKProperties: String, CaseIterable { /// DAV case displayname = "" + + /// + /// Download limits for shares of a file as optionally provided by the [Files Download Limit](https://github.com/nextcloud/files_downloadlimit) app for Nextcloud server. + /// + case downloadLimit = "" + case getlastmodified = "" case getetag = "" case getcontenttype = "" diff --git a/Sources/NextcloudKit/Models/NKRecommendedFiles.swift b/Sources/NextcloudKit/Models/NKRecommendedFiles.swift new file mode 100644 index 00000000..186b1d50 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKRecommendedFiles.swift @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import SwiftyXMLParser + +public class NKRecommendation: NSObject { + public var id: String + public var timestamp: Date? + public var name: String + public var directory: String + public var extensionType: String + public var mimeType: String + public var hasPreview: Bool + public var reason: String + + public init(id: String, timestamp: Date?, name: String, directory: String, extensionType: String, mimeType: String, hasPreview: Bool, reason: String) { + self.id = id + self.timestamp = timestamp + self.name = name + self.directory = directory + self.extensionType = extensionType + self.mimeType = mimeType + self.hasPreview = hasPreview + self.reason = reason + } +} + +class XMLToRecommendationParser { + func parse(xml: String) -> [NKRecommendation]? { + guard let data = xml.data(using: .utf8) else { return nil } + let xml = XML.parse(data) + + // Parsing "enabled" + guard let enabledString = xml["ocs", "data", "enabled"].text, + Bool(enabledString == "1") + else { + return nil + } + + // Parsing "recommendations" + var recommendations: [NKRecommendation] = [] + let elements = xml["ocs", "data", "recommendations", "element"] + + for element in elements { + let id = element["id"].text ?? "" + var timestamp: Date? + if let timestampDouble = element["timestamp"].double, timestampDouble > 0 { + timestamp = Date(timeIntervalSince1970: timestampDouble) + } + let name = element["name"].text ?? "" + let directory = element["directory"].text ?? "" + let extensionType = element["extension"].text ?? "" + let mimeType = element["mimeType"].text ?? "" + let hasPreview = element["hasPreview"].text == "1" + let reason = element["reason"].text ?? "" + + let recommendation = NKRecommendation( + id: id, + timestamp: timestamp, + name: name, + directory: directory, + extensionType: extensionType, + mimeType: mimeType, + hasPreview: hasPreview, + reason: reason + ) + recommendations.append(recommendation) + } + + return recommendations + } +} diff --git a/Sources/NextcloudKit/NKDataFileXML.swift b/Sources/NextcloudKit/NKDataFileXML.swift index c01d18e8..eef09d65 100644 --- a/Sources/NextcloudKit/NKDataFileXML.swift +++ b/Sources/NextcloudKit/NKDataFileXML.swift @@ -560,7 +560,23 @@ class NKDataFileXML: NSObject { } file.placePhotos = propstat["d:prop", "nc:metadata-photos-place"].text - + + for downloadLimit in propstat["d:prop", "nc:share-download-limits", "nc:share-download-limit"] { + guard let token = downloadLimit["nc:token"].text else { + continue + } + + guard let limit = downloadLimit["nc:limit"].int else { + continue + } + + guard let count = downloadLimit["nc:count"].int else { + continue + } + + file.downloadLimits.append(NKDownloadLimit(count: count, limit: limit, token: token)) + } + let results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account) file.contentType = results.mimeType diff --git a/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift new file mode 100644 index 00000000..172fb58c --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+RecommendedFiles.swift @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Alamofire +import SwiftyJSON + +public extension NextcloudKit { + func getRecommendedFiles(account: String, + options: NKRequestOptions = NKRequestOptions(), + request: @escaping (DataRequest?) -> Void = { _ in }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ recommendations: [NKRecommendation]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + let endpoint = "ocs/v2.php/apps/recommendations/api/v1/recommendations" + /// + options.contentType = "application/xml" + /// + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let tosRequest = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: nil).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + switch response.result { + case .success(let data): + if let xmlString = String(data: data, encoding: .utf8) { + let parser = XMLToRecommendationParser() + if let recommendations = parser.parse(xml: xmlString) { + options.queue.async { completion(account, recommendations, response, .success) } + } else { + options.queue.async { completion(account, nil, response, .xmlError) } + } + } else { + options.queue.async { completion(account, nil, response, .xmlError) } + } + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + completion(account, nil, response, error) + } + } + } + options.queue.async { request(tosRequest) } + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift new file mode 100644 index 00000000..448e86ec --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+ShareDownloadLimit.swift @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import Alamofire +import Foundation + +public extension NextcloudKit { + private func makeEndpoint(with token: String) -> String { + "ocs/v2.php/apps/files_downloadlimit/api/v1/\(token)/limit" + } + + func removeShareDownloadLimit(account: String, token: String, completion: @escaping (_ error: NKError) -> Void) { + let endpoint = makeEndpoint(with: token) + let options = NKRequestOptions() + + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + completion(.urlError) + } + } + + nkSession + .sessionData + .request(url, method: .delete, parameters: nil, encoding: URLEncoding.default, headers: headers, interceptor: nil) + .validate(statusCode: 200..<300) + .response(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + + options.queue.async { + completion(error) + } + case .success: + options.queue.async { + completion(.success) + } + } + } + } + + func setShareDownloadLimit(account: String, token: String, limit: Int, completion: @escaping (_ error: NKError) -> Void) { + let endpoint = makeEndpoint(with: token) + let options = NKRequestOptions() + options.contentType = "application/json" + + guard let nkSession = nkCommonInstance.getSession(account: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint, options: options), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options), + var urlRequest = try? URLRequest(url: url, method: .put, headers: headers) else { + return options.queue.async { + completion(.urlError) + } + } + + urlRequest.httpBody = try? JSONEncoder().encode([ + "limit": limit + ]) + + nkSession + .sessionData + .request(urlRequest) + .validate(statusCode: 200..<300) + .response(queue: self.nkCommonInstance.backgroundQueue) { response in + if self.nkCommonInstance.levelLog > 0 { + debugPrint(response) + } + + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + + options.queue.async { + completion(error) + } + case .success: + options.queue.async { + completion(.success) + } + } + } + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+Upload.swift b/Sources/NextcloudKit/NextcloudKit+Upload.swift index a8d3ffa7..6438f082 100644 --- a/Sources/NextcloudKit/NextcloudKit+Upload.swift +++ b/Sources/NextcloudKit/NextcloudKit+Upload.swift @@ -96,7 +96,9 @@ public extension NextcloudKit { /// - chunkSizeInMB: Size in MB of chunk func uploadChunk(directory: String, + fileChunksOutputDirectory: String? = nil, fileName: String, + destinationFileName: String? = nil, date: Date?, creationDate: Date?, serverUrl: String, @@ -119,7 +121,7 @@ public extension NextcloudKit { } let fileNameLocalSize = self.nkCommonInstance.getFileSize(filePath: directory + "/" + fileName) let serverUrlChunkFolder = nkSession.urlBase + "/" + nkSession.dav + "/uploads/" + nkSession.userId + "/" + chunkFolder - let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + fileName + let serverUrlFileName = nkSession.urlBase + "/" + nkSession.dav + "/files/" + nkSession.userId + self.nkCommonInstance.returnPathfromServerUrl(serverUrl, urlBase: nkSession.urlBase, userId: nkSession.userId) + "/" + (destinationFileName ?? fileName) if options.customHeader == nil { options.customHeader = [:] } @@ -175,7 +177,8 @@ public extension NextcloudKit { var uploadNKError = NKError() var uploadAFError: AFError? - self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: directory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in + let outputDirectory = fileChunksOutputDirectory ?? directory + self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: outputDirectory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in numChunks(num) } counterChunk: { counter in counterChunk(counter) @@ -190,7 +193,7 @@ public extension NextcloudKit { for fileChunk in filesChunk { let serverUrlFileName = serverUrlChunkFolder + "/" + fileChunk.fileName - let fileNameLocalPath = directory + "/" + fileChunk.fileName + let fileNameLocalPath = outputDirectory + "/" + fileChunk.fileName let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath) if fileSize == 0 { // The file could not be sent