Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
20c8a53
Define NKDownloadLimit as a sendable struct
claucambra Jan 22, 2025
17b6db7
Define NKFile as a sendable struct
claucambra Jan 22, 2025
d9be8df
Define NKSession as a sendable struct
claucambra Jan 22, 2025
8021bd7
Define UTTypeConformsToServer as a sendable struct
claucambra Jan 22, 2025
14e92d5
Define NKError as a sendable, equatable struct
claucambra Jan 22, 2025
77ce436
Convert ThreadSafeArray into a sendable struct
claucambra Jan 22, 2025
b510247
Make FileAutoRenamer Sendable compliant
claucambra Jan 22, 2025
41d681d
Make FileNameValidator Sendable compliant
claucambra Jan 22, 2025
13dc02b
Fix copyright header in NSLock extension
claucambra Jan 22, 2025
8d0b1a1
Use self createFolder rather than going for shared instance
claucambra Jan 22, 2025
78ec1da
Make NextcloudKitDelegate protocol sendable
claucambra Jan 22, 2025
c96853a
Make NKBackground a final class
claucambra Jan 22, 2025
d1dd28d
Make NextcloudKitSessionDelegate conform to sendable
claucambra Jan 22, 2025
7e6bb08
Use the appropriate nkCommonInstance in NKSession initialiser
claucambra Jan 22, 2025
a026b49
Make nkCommonInstance mutable in NextcloudKit
claucambra Jan 22, 2025
2454e0e
Only provide shared NextcloudKit instance on swift <6
claucambra Jan 22, 2025
39f9dc3
When using swift 6, use a task to retrieve screen scale on iOS
claucambra Jan 22, 2025
22c2d40
Make NKFileProperty a sendable struct
claucambra Jan 22, 2025
1f2c2f2
Convert NKCommon into a sendable struct
claucambra Jan 22, 2025
c15bdac
Fix whitespace handling in FileAutoRenamer
claucambra Jan 22, 2025
0d3f5bb
Remove leading dot for hidden file filenames in autorenamer
claucambra Jan 22, 2025
0a90663
Merge branch 'develop' into feature/sendable-struct-types
mpivchev Jan 30, 2025
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
14 changes: 14 additions & 0 deletions Sources/NextcloudKit/Extensions/NSLock+Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2025 Claudio Cambra <[email protected]>
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation

extension NSLock {
@discardableResult
func perform<T>(_ block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
2 changes: 1 addition & 1 deletion Sources/NextcloudKit/Models/NKDownloadLimit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
///
/// 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 {
public struct NKDownloadLimit: Sendable {
///
/// The number of downloads which already happened.
///
Expand All @@ -25,7 +25,7 @@
///
public let token: String

init(count: Int, limit: Int, token: String) {

Check warning on line 28 in Sources/NextcloudKit/Models/NKDownloadLimit.swift

View workflow job for this annotation

GitHub Actions / Lint

Unneeded Synthesized Initializer Violation: This memberwise initializer would be synthesized automatically - you do not need to define it (unneeded_synthesized_initializer)
self.count = count
self.limit = limit
self.token = token
Expand Down
2 changes: 1 addition & 1 deletion Sources/NextcloudKit/Models/NKFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import Foundation

public class NKFile: NSObject {
public struct NKFile: Sendable {
public var account = ""
public var classFile = ""
public var commentsUnread: Bool = false
Expand Down
9 changes: 9 additions & 0 deletions Sources/NextcloudKit/Models/NKFileProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@

import Foundation

#if swift(<6.0)
public class NKFileProperty: NSObject {
public var classFile: String = ""
public var iconName: String = ""
public var name: String = ""
public var ext: String = ""
}
#else
public struct NKFileProperty: Sendable {
public var classFile: String = ""
public var iconName: String = ""
public var name: String = ""
public var ext: String = ""
}
#endif
62 changes: 49 additions & 13 deletions Sources/NextcloudKit/NKCommon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import CoreServices
#endif

public protocol NextcloudKitDelegate {
public protocol NextcloudKitDelegate: Sendable {

Check warning on line 15 in Sources/NextcloudKit/NKCommon.swift

View workflow job for this annotation

GitHub Actions / Lint

Class Delegate Protocol Violation: Delegate protocols should be class-only so they can be weakly referenced (class_delegate_protocol)
func authenticationChallenge(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession)

Expand All @@ -29,7 +29,7 @@
func request<Value>(_ request: DataRequest, didParseResponse response: AFDataResponse<Value>)
}

public class NKCommon: NSObject {
public struct NKCommon: Sendable {
public var nksessions = ThreadSafeArray<NKSession>()
public var delegate: NextcloudKitDelegate?

Expand Down Expand Up @@ -82,7 +82,7 @@
case xls = "xls"
}

public struct UTTypeConformsToServer {
public struct UTTypeConformsToServer: Sendable {
var typeIdentifier: String
var classFile: String
var editor: String
Expand All @@ -91,9 +91,15 @@
var account: String
}

#if swift(<6.0)
internal var utiCache = NSCache<NSString, CFString>()
internal var mimeTypeCache = NSCache<CFString, NSString>()
internal var filePropertiesCache = NSCache<CFString, NKFileProperty>()
#else
internal var utiCache = [String: String]()
internal var mimeTypeCache = [String: String]()
internal var filePropertiesCache = [String: NKFileProperty]()
#endif
internal var internalTypeIdentifiers = ThreadSafeArray<UTTypeConformsToServer>()

public var filenamePathLog: String = ""
Expand Down Expand Up @@ -133,56 +139,77 @@

// MARK: - Init

override init() {
super.init()

init() {
filenamePathLog = internalPathLog + "/" + internalFilenameLog
}

// MARK: - Type Identifier

public func clearInternalTypeIdentifier(account: String) {
mutating public func clearInternalTypeIdentifier(account: String) {
internalTypeIdentifiers = internalTypeIdentifiers.filter({ $0.account != account })
}

public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String, account: String) {
mutating public func addInternalTypeIdentifier(typeIdentifier: String, classFile: String, editor: String, iconName: String, name: String, account: String) {
if !internalTypeIdentifiers.contains(where: { $0.typeIdentifier == typeIdentifier && $0.editor == editor && $0.account == account}) {
let newUTI = UTTypeConformsToServer(typeIdentifier: typeIdentifier, classFile: classFile, editor: editor, iconName: iconName, name: name, account: account)
internalTypeIdentifiers.append(newUTI)
}
}

public func getInternalType(fileName: String, mimeType: String, directory: Bool, account: String) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) {
mutating public func getInternalType(fileName: String, mimeType: String, directory: Bool, account: String) -> (mimeType: String, classFile: String, iconName: String, typeIdentifier: String, fileNameWithoutExt: String, ext: String) {
var ext = (fileName as NSString).pathExtension.lowercased()
var mimeType = mimeType
var classFile = "", iconName = "", typeIdentifier = "", fileNameWithoutExt = ""
var inUTI: CFString?

#if swift(<6.0)
if let cachedUTI = utiCache.object(forKey: ext as NSString) {
inUTI = cachedUTI
} else {
}
#else
if let cachedUTI = utiCache[ext] {
inUTI = cachedUTI as CFString
}
#endif
if inUTI == nil {
if let unmanagedFileUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext as CFString, nil) {
inUTI = unmanagedFileUTI.takeRetainedValue()
if let inUTI {
#if swift(<6.0)
utiCache.setObject(inUTI, forKey: ext as NSString)
#else
utiCache[ext] = inUTI as String
#endif
}
}
}

if let inUTI = inUTI {
if let inUTI {
typeIdentifier = inUTI as String
fileNameWithoutExt = (fileName as NSString).deletingPathExtension

// contentType detect
if mimeType.isEmpty {
#if swift(<6.0)
if let cachedMimeUTI = mimeTypeCache.object(forKey: inUTI) {
mimeType = cachedMimeUTI as String
} else {
}
#else
if let cachedMimeUTI = mimeTypeCache[inUTI as String] {
mimeType = cachedMimeUTI
}
#endif

if mimeType.isEmpty {
if let mimeUTI = UTTypeCopyPreferredTagWithClass(inUTI, kUTTagClassMIMEType) {
let mimeUTIString = mimeUTI.takeRetainedValue() as String

mimeType = mimeUTIString
#if swift(<6.0)
mimeTypeCache.setObject(mimeUTIString as NSString, forKey: inUTI)
#else
mimeTypeCache[inUTI as String] = mimeUTIString as String
#endif
}
}
}
Expand All @@ -197,12 +224,21 @@
} else {
var fileProperties: NKFileProperty

#if swift(<6.0)
if let cachedFileProperties = filePropertiesCache.object(forKey: inUTI) {
fileProperties = cachedFileProperties
} else {
fileProperties = getFileProperties(inUTI: inUTI)
filePropertiesCache.setObject(fileProperties, forKey: inUTI)
}
#else
if let cachedFileProperties = filePropertiesCache[inUTI as String] {
fileProperties = cachedFileProperties
} else {
fileProperties = getFileProperties(inUTI: inUTI)
filePropertiesCache[inUTI as String] = fileProperties
}
#endif

classFile = fileProperties.classFile
iconName = fileProperties.iconName
Expand All @@ -212,7 +248,7 @@
}

public func getFileProperties(inUTI: CFString) -> NKFileProperty {
let fileProperty = NKFileProperty()
var fileProperty = NKFileProperty()
let typeIdentifier: String = inUTI as String

if let fileExtension = UTTypeCopyPreferredTagWithClass(inUTI as CFString, kUTTagClassFilenameExtension) {
Expand Down
6 changes: 3 additions & 3 deletions Sources/NextcloudKit/NKDataFileXML.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import SwiftyXMLParser

class NKDataFileXML: NSObject {
let nkCommonInstance: NKCommon
var nkCommonInstance: NKCommon
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change to vars here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claucambra could you please elaborate on this :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for missing this!

This is a var because NKCommon is now a struct (to make it more easily Sendable) and we use functions that mutate the struct instance (mainly getInternalType, which modifies the cache)

let requestBodyComments =
"""
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
Expand Down Expand Up @@ -297,7 +297,7 @@
return xml["ocs", "data", "apppassword"].text
}

func convertDataFile(xmlData: Data, nkSession: NKSession, showHiddenFiles: Bool, includeHiddenFiles: [String]) -> [NKFile] {

Check warning on line 300 in Sources/NextcloudKit/NKDataFileXML.swift

View workflow job for this annotation

GitHub Actions / Lint

Function Body Length Violation: Function body should span 200 lines or less excluding comments and whitespace: currently spans 245 lines (function_body_length)
var files: [NKFile] = []
let rootFiles = "/" + nkSession.dav + "/files/"
guard let baseUrl = self.nkCommonInstance.getHostName(urlString: nkSession.urlBase) else {
Expand All @@ -307,7 +307,7 @@
let elements = xml["d:multistatus", "d:response"]

for element in elements {
let file = NKFile()
var file = NKFile()
if let href = element["d:href"].text {
var fileNamePath = href
if href.last == "/" {
Expand Down Expand Up @@ -577,7 +577,7 @@
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)
var results = self.nkCommonInstance.getInternalType(fileName: file.fileName, mimeType: file.contentType, directory: file.directory, account: nkSession.account)

file.contentType = results.mimeType
file.iconName = results.iconName
Expand Down
16 changes: 10 additions & 6 deletions Sources/NextcloudKit/NKError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
static var ocsXMLMsg: Self { ["d:error", "s:message"] }
}

public class NKError: NSObject {
public struct NKError: Sendable, Equatable {
static let internalError = -9999
// Chunk error
public static let chunkNoEnoughMemory = -9998
Expand Down Expand Up @@ -159,7 +159,7 @@
self.responseData = responseData
}

convenience init(httpResponse: HTTPURLResponse) {
init(httpResponse: HTTPURLResponse) {
self.init(statusCode: httpResponse.statusCode, fallbackDescription: httpResponse.description)
}

Expand All @@ -181,7 +181,7 @@
self.error = NSError(domain: NSCocoaErrorDomain, code: self.errorCode, userInfo: [NSLocalizedDescriptionKey: self.errorDescription])
}

public convenience init<T: AFResponse>(error: AFError?, afResponse: T, responseData: Data? = nil) {
public init<T: AFResponse>(error: AFError?, afResponse: T, responseData: Data? = nil) {
if let errorCode = afResponse.response?.statusCode {
guard let dataResponse = afResponse as? Alamofire.DataResponse<T.Success, T.Failure>,
let errorData = dataResponse.data
Expand Down Expand Up @@ -216,9 +216,13 @@
}
}

public override func isEqual(_ object: Any?) -> Bool {
if let object = object as? NKError {
return self.errorCode == object.errorCode && self.errorDescription == object.errorDescription
public static func == (lhs: NKError, rhs: NKError) -> Bool {
return lhs.errorCode == rhs.errorCode && lhs.errorDescription == rhs.errorDescription
}

public static func == (lhs: NKError, rhs: NKError?) -> Bool {
if let rhs {
return lhs == rhs;

Check warning on line 225 in Sources/NextcloudKit/NKError.swift

View workflow job for this annotation

GitHub Actions / Lint

Trailing Semicolon Violation: Lines should not have trailing semicolons (trailing_semicolon)
}
return false
}
Expand Down
19 changes: 10 additions & 9 deletions Sources/NextcloudKit/NKSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import Alamofire
@preconcurrency import Alamofire

public class NKSession {
public struct NKSession: Sendable {
public var urlBase: String
public var user: String
public var userId: String
Expand All @@ -26,7 +26,8 @@ public class NKSession {
public let sessionUploadBackgroundWWan: URLSession
public let sessionUploadBackgroundExt: URLSession

init(urlBase: String,
init(nkCommonInstance: NKCommon,
urlBase: String,
user: String,
userId: String,
password: String,
Expand All @@ -51,7 +52,7 @@ public class NKSession {
self.httpMaximumConnectionsPerHostInUpload = httpMaximumConnectionsPerHostInUpload
self.requestCachePolicy = requestCachePolicy

let backgroundSessionDelegate = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)
let backgroundSessionDelegate = NKBackground(nkCommonInstance: nkCommonInstance)
/// Strange but works ?!?!
let sharedCookieStorage = user + "@" + urlBase

Expand All @@ -66,11 +67,11 @@ public class NKSession {

configuration.httpCookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: sharedCookieStorage)
sessionData = Alamofire.Session(configuration: configuration,
delegate: NextcloudKitSessionDelegate(nkCommonInstance: NextcloudKit.shared.nkCommonInstance),
rootQueue: NextcloudKit.shared.nkCommonInstance.rootQueue,
requestQueue: NextcloudKit.shared.nkCommonInstance.requestQueue,
serializationQueue: NextcloudKit.shared.nkCommonInstance.serializationQueue,
eventMonitors: [NKLogger(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)])
delegate: NextcloudKitSessionDelegate(nkCommonInstance: nkCommonInstance),
rootQueue: nkCommonInstance.rootQueue,
requestQueue: nkCommonInstance.requestQueue,
serializationQueue: nkCommonInstance.serializationQueue,
eventMonitors: [NKLogger(nkCommonInstance: nkCommonInstance)])

/// Session Download Background
let configurationDownloadBackground = URLSessionConfiguration.background(withIdentifier: NKCommon().identifierSessionDownloadBackground)
Expand Down
14 changes: 13 additions & 1 deletion Sources/NextcloudKit/NextcloudKit+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,19 @@ public extension NextcloudKit {
#else

#if os(iOS)
let screenScale = UIScreen.main.scale
var screenScale = 1.0
if #available(iOS 13.0, *) {
let semaphore = DispatchSemaphore(value: 0)
Task {
screenScale = await UIScreen.main.scale
semaphore.signal()
}
semaphore.wait()
} else {
#if swift(<6.0)
screenScale = UIScreen.main.scale
#endif
}
#else
let screenScale = 1.0
#endif
Expand Down
2 changes: 1 addition & 1 deletion Sources/NextcloudKit/NextcloudKit+Upload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public extension NextcloudKit {
if error == .success {
completion(NKError())
} else if error.errorCode == 404 {
NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, _, error in
self.createFolder(serverUrlFileName: serverUrlChunkFolder, account: account, options: options) { _, _, _, _, error in
completion(error)
}
} else {
Expand Down
Loading
Loading