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
Expand Up @@ -74,7 +74,20 @@ public extension FilesDatabaseManager {
$0.account == directoryAccount && $0.serverUrl.starts(with: directoryUrlPath)
}

// Protect items whose local data is not yet on the server from deletion when their
// parent is removed. Otherwise a moved or renamed parent would wipe unsynced children.
// TODO: the parent directory itself is still deleted here, so a protected child is
// orphaned once its upload completes. Follow up by deferring parent deletion or
// reparenting the child after the upload finishes.
for result in results {
if result.status >= Status.inUpload.rawValue {
logger.info("Skipping deletion of child with pending upload.", [.item: result.ocId])
continue
}
if result.isLockFileOfLocalOrigin {
logger.info("Skipping deletion of local origin lock file during directory delete.", [.item: result.ocId, .name: result.fileName])
continue
}
let inactiveItemMetadata = SendableItemMetadata(value: result)
do {
try database.write { result.deleted = true }
Expand Down
11 changes: 11 additions & 0 deletions Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,17 @@ public final class Enumerator: NSObject, NSFileProviderEnumerator, Sendable {
allDeletedMetadatas = deletedMetadatas
}

// Protect items with pending uploads: their local data is not yet on the server.
allDeletedMetadatas.removeAll { deletedMetadata in
guard deletedMetadata.status >= Status.inUpload.rawValue else { return false }
logger.info("Skipping deletion of item with pending upload.", [.item: deletedMetadata.ocId])
return true
}

// Report parents before children so macOS creates a moved directory before placing its
// contents, preventing both the old and new folder name from appearing during a rename.
allUpdatedMetadatas.sort { $0.remotePath().count < $1.remotePath().count }

let allFpItemDeletionsIdentifiers = Array(
allDeletedMetadatas.map { NSFileProviderItemIdentifier($0.ocId) })
if !allFpItemDeletionsIdentifiers.isEmpty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extension NKFile {
sharePermissionsCloudMesh: sharePermissionsCloudMesh,
shareType: shareType,
size: size,
tags: tags,
tags: tags.map(\.name),
uploaded: uploaded,
trashbinFileName: trashbinFileName,
trashbinOriginalLocation: trashbinOriginalLocation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,17 @@ extension NextcloudKit: RemoteInterface {
requestHandler: requestHandler,
taskHandler: taskHandler,
progressHandler: progressHandler
) { account, ocId, etag, date, size, response, nkError in
) { account, response, nkError in
let allHeaderFields = response?.response?.allHeaderFields
let ocId = self.nkCommonInstance.findHeader("oc-fileid", allHeaderFields: allHeaderFields)
let etag = self.nkCommonInstance.normalizedETag(self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: allHeaderFields))
let date = self.nkCommonInstance.findHeader("date", allHeaderFields: allHeaderFields)?.parsedDate(using: "EEE, dd MMM y HH:mm:ss zzz")
var size: Int64 = 0

if let value = allHeaderFields?["Content-Length"] as? String {
size = Int64(value) ?? 0
}

continuation.resume(returning: (
account,
ocId,
Expand Down Expand Up @@ -88,7 +98,7 @@ extension NextcloudKit: RemoteInterface {
chunkCounter: @escaping (_ counter: Int) -> Void = { _ in },
log: any FileProviderLogging,
chunkUploadStartHandler: @escaping (_ filesChunk: [RemoteFileChunk]) -> Void = { _ in },
requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in },
requestHandler _: @escaping (_ request: UploadRequest) -> Void = { _ in },
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
progressHandler: @escaping (Progress) -> Void = { _ in },
chunkUploadCompleteHandler: @escaping (_ fileChunk: RemoteFileChunk) -> Void = { _ in }
Expand Down Expand Up @@ -154,8 +164,10 @@ extension NextcloudKit: RemoteInterface {
"""
)

return await withCheckedContinuation { continuation in
uploadChunk(
var startedChunks: [RemoteFileChunk] = []

do {
let (uploadAccount, file) = try await uploadChunkAsync(
directory: directory,
fileChunksOutputDirectory: fileChunksOutputDirectory,
fileName: fileName,
Expand All @@ -168,17 +180,19 @@ extension NextcloudKit: RemoteInterface {
chunkSize: chunkSize,
account: account.ncKitAccount,
options: options,
numChunks: currentNumChunksUpdateHandler,
counterChunk: chunkCounter,
start: { processedChunks in
chunkProgressHandler: { total, counter in
currentNumChunksUpdateHandler(total)
chunkCounter(counter)
},
uploadStart: { processedChunks in
let chunks = RemoteFileChunk.fromNcKitChunks(
processedChunks, remoteChunkStoreFolderName: remoteChunkStoreFolderName
)
startedChunks = chunks
chunkUploadStartHandler(chunks)
},
requestHandler: requestHandler,
taskHandler: taskHandler,
progressHandler: { totalBytesExpected, totalBytes, _ in
uploadTaskHandler: taskHandler,
uploadProgressHandler: { totalBytesExpected, totalBytes, _ in
let currentProgress = Progress(totalUnitCount: totalBytesExpected)
currentProgress.completedUnitCount = totalBytes
progressHandler(currentProgress)
Expand All @@ -190,11 +204,64 @@ extension NextcloudKit: RemoteInterface {
)
chunkUploadCompleteHandler(chunk)
}
) { account, receivedChunks, file, error in
let chunks = RemoteFileChunk.fromNcKitChunks(
receivedChunks ?? [], remoteChunkStoreFolderName: remoteChunkStoreFolderName
)
continuation.resume(returning: (account, chunks, file, error))
)

return (uploadAccount, startedChunks, file, .success)
} catch let nkError as NKError {
return (account.ncKitAccount, nil, nil, nkError)
} catch {
return (account.ncKitAccount, nil, nil, .urlError)
}
}

/// NextcloudKit 7.3.x changed its own downloadAsync to return the raw AFDownloadResponse,
/// which no longer matches RemoteInterface. Provide a conforming wrapper that parses the
/// metadata the callers expect from the response headers.
public func downloadAsync(
serverUrlFileName: Any,
fileNameLocalPath: String,
account: String,
options: NKRequestOptions = .init(),
requestHandler: @escaping (_ request: DownloadRequest) -> Void = { _ in },
taskHandler: @Sendable @escaping (_ task: URLSessionTask) -> Void = { _ in },
progressHandler: @escaping (_ progress: Progress) -> Void = { _ in }
) async -> (
account: String,
etag: String?,
date: Date?,
length: Int64,
headers: [AnyHashable: any Sendable]?,
afError: AFError?,
nkError: NKError
) {
await withCheckedContinuation { continuation in
download(
serverUrlFileName: serverUrlFileName,
fileNameLocalPath: fileNameLocalPath,
account: account,
options: options,
requestHandler: requestHandler,
taskHandler: taskHandler,
progressHandler: progressHandler
) { account, response, nkError in
let allHeaderFields = response?.response?.allHeaderFields
let etag = self.nkCommonInstance.normalizedETag(self.nkCommonInstance.findHeader("oc-etag", allHeaderFields: allHeaderFields))
let date = self.nkCommonInstance.findHeader("date", allHeaderFields: allHeaderFields)?.parsedDate(using: "EEE, dd MMM y HH:mm:ss zzz")
var length: Int64 = 0

if let value = allHeaderFields?["Content-Length"] as? String {
length = Int64(value) ?? 0
}

continuation.resume(returning: (
account,
etag,
date,
length,
allHeaderFields as? [AnyHashable: any Sendable],
response?.error,
nkError
))
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/NextcloudFileProviderKit/Item/Item+Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public extension Item {
) async -> (Item?, Error?) {
let logger = FileProviderLogger(category: "Item", log: log)

// Note: when a parent folder is renamed on another client and an editor has a file open
// inside it, the editor may recreate the old folder here, producing a server side duplicate.
// NSFilePresenter callbacks are only delivered when the writer uses NSFileCoordinator.
// It is not confirmed whether the File Provider daemon already does this internally when
// processing didUpdate renames. If it does not, wrapping rename propagation in an
// NSFileCoordinator coordinated write would notify registered presenters.
let (_, _, _, createError) = await remoteInterface.createFolder(
remotePath: remotePath, account: account, options: .init(), taskHandler: { task in
if let domain, let itemTemplate {
Expand Down
Loading
Loading