Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
23c9dfd
Add RemoteFileChunk type
claucambra Dec 28, 2024
b650cec
Add chunkedUpload method to RemoteInterface protocol
claucambra Dec 28, 2024
ebff7db
Implement chunkedUpload in nextcloudkit extension
claucambra Dec 28, 2024
9640f58
Make the progressHandler for chunked upload match other methods
claucambra Jan 7, 2025
9ea7e3d
Add simple starter test for upload result struct
claucambra Jan 7, 2025
9f29aed
Add basic chunked upload implementation for mock remote interface
claucambra Jan 7, 2025
246b124
Add logging when no valid root item on mock remote interface
claucambra Jan 7, 2025
319e004
Fix remaining file size in chunked upload implementation of MRI
claucambra Jan 7, 2025
5ad26d5
Fix warning about unused response var
claucambra Jan 7, 2025
85a53b4
Add convenience method to get a root mock remote item
claucambra Jan 7, 2025
8cc9751
Use convenience root item method in tests
claucambra Jan 7, 2025
94cd66e
Add convenience method to create root trash item
claucambra Jan 7, 2025
fac3db1
Use convenience method to create root trash item in tests
claucambra Jan 7, 2025
9c0f6cb
Add UploadResult convenience struct
claucambra Jan 7, 2025
fa7437c
Add starter convenience upload function to perform standard/chunked u…
claucambra Jan 7, 2025
6cb61f1
Add basic test to check standard upload in upload convenience func
claucambra Jan 7, 2025
a59bd09
Use correct chunkSize value in uplaod convenience func
claucambra Jan 7, 2025
9dc801f
Add simple test for upload convenience func chunked upload
claucambra Jan 7, 2025
05e446b
Start chunk name from 1 imitating the real server
claucambra Jan 7, 2025
dbe7c19
Make chunk upload id an argument in convenience upload func
claucambra Jan 7, 2025
e7fa1e0
Set chunks at initialisation time in MRI
claucambra Jan 7, 2025
d9f6167
Call chunkUploadStartedHandler in MRI
claucambra Jan 7, 2025
fb4f3d9
Pass all relevant arguments to inner upload during chunk upload in MRI
claucambra Jan 7, 2025
b5b35ae
Call chunkUploadCompleteHandler in MRI
claucambra Jan 7, 2025
1bf5c2b
Allow passing in chunkUploadCompleteHandler
claucambra Jan 7, 2025
e9feb47
Only call chunkUploadCompleteHandler once in MRI
claucambra Jan 7, 2025
9ae04df
Add RemoteFileChunk database object type
claucambra Jan 7, 2025
04e42f3
Remove previous RemoteFileChunk typealias
claucambra Jan 7, 2025
49275d0
Add convenience to get array of remotefilechunk from the nckit file c…
claucambra Jan 7, 2025
8b31c17
Add convenience init for remotefilechunk based on nckit tuple
claucambra Jan 7, 2025
dec57d1
Add convenience converted from remotefilechunk to nckit tuple
claucambra Jan 7, 2025
0f97512
Add convenience converter from array of nckit chunk tuples to remote …
claucambra Jan 7, 2025
029cf9b
Add method to convert array of managed remotefilechunks to unmanaged
claucambra Jan 7, 2025
fbc801f
Register RemoteFileChunk in database init
claucambra Jan 7, 2025
eb3aae0
Fix uses of remotefilechunk in nckit remoteinterface
claucambra Jan 7, 2025
45ba02f
Require remoteChunkStoreFolderName for remote file chunk
claucambra Jan 7, 2025
a21b597
Fix handling of remote file chunks in nckit remote interface, again
claucambra Jan 7, 2025
195d356
Keep track of uploaded and yet to upload chunks in chunked upload
claucambra Jan 7, 2025
f5ecad3
Make methods in RemoteFileChunk public
claucambra Jan 7, 2025
af8bf5b
Rename uses of uploadUuid to new remoteChunkFolder property
claucambra Jan 7, 2025
fc82890
Fix and clean up chunk creation in MRI
claucambra Jan 7, 2025
1504c00
Ensure chunks stay unmanaged in upload procedure
claucambra Jan 7, 2025
d65da18
Pass test database in upload tests into upload func
claucambra Jan 7, 2025
6425143
Remove UploadResult struct as it is not necessary
claucambra Jan 8, 2025
b54d331
Fix remainingFileSize calculation in MRI
claucambra Jan 8, 2025
def94ed
Simulate resumed chunk upload in MRI
claucambra Jan 8, 2025
7307478
Enhance testing of chunked uploads by testing for correct chunk handling
claucambra Jan 8, 2025
8dff7b2
Add test for resuming interrupted chunked uploads
claucambra Jan 8, 2025
9551c7b
Fix expected chunk size in normal chunked upload test
claucambra Jan 8, 2025
488993d
Take fileLocatedAt in upload func as a string
claucambra Jan 8, 2025
090919d
Convert chunk bool property in itemmetadata to a chunkUploadId
claucambra Jan 8, 2025
cd59ca7
Apply appropriate chunkUploadId state when setting upload state in Fi…
claucambra Jan 8, 2025
13a0aeb
Only reset chunkUploadId if old metadata state was upload
claucambra Jan 8, 2025
e456a79
Use new upload function in new item file creation
claucambra Jan 8, 2025
fa91e01
Also use new upload function for bundle inner item creation
claucambra Jan 8, 2025
7edc3a8
Use new upload function when uploading updated item files
claucambra Jan 8, 2025
b6a80ec
Use new upload function to upload inner bundle files
claucambra Jan 8, 2025
f8bbf88
Use URLs to do splicing during chunk upload procedure
claucambra Jan 8, 2025
dbb9275
Actually return ocId post chunk upload
claucambra Jan 8, 2025
d33b75c
Add test for chunked file create upload
claucambra Jan 8, 2025
bf68205
Do not double generate test data for chunked item creation
claucambra Jan 8, 2025
6d5fa1b
Track expected transfer size of completed chunked uploads, not counti…
claucambra Jan 8, 2025
0ea0931
Add test for resumed chunked upload of newly created item
claucambra Jan 8, 2025
1766104
Modify RemoteInterface to directly take a local path and a remote pat…
claucambra Jan 8, 2025
effa38c
Delete unused ItemUploadTests
claucambra Jan 8, 2025
258c998
Add a test for chunked upload of modified item contents
claucambra Jan 8, 2025
7505ee8
Add test for modified item chunked upload, resumed
claucambra Jan 8, 2025
379513a
Properly employ remainingChunks argument in mock remote interface
claucambra Jan 8, 2025
9a0cb4f
Add expected remaining chunks to database in upload tests for resumed…
claucambra Jan 8, 2025
566ac13
Fix state of remaining chunks in database in resumed chunked upload o…
claucambra Jan 8, 2025
bc2e5ea
Fix state of remaining chunks in database in resumed chunked upload o…
claucambra Jan 8, 2025
916b34d
Add test for chunked upload of MRI
claucambra Jan 8, 2025
76bd2e1
Temporarily use nckit branch with chunk destination changes
claucambra Jan 8, 2025
704428a
Add some logging for chunked upload
claucambra Jan 8, 2025
cceffe1
Ensure we remove illegal chars from file provider item id used as chu…
claucambra Jan 8, 2025
e5503e2
Update item create test with chunked upload to check handling of ille…
claucambra Jan 8, 2025
3cc3ac0
Remove period from upload logging
claucambra Jan 8, 2025
c2b6876
Set custom file chunks directory when using nckit chunked upload
claucambra Jan 8, 2025
3e580de
Add additional logging when starting nckit chunked upload
claucambra Jan 8, 2025
73dd1c8
Ensure chunk upload using nckit occurs on non-main queue
claucambra Jan 8, 2025
f2f2d3c
Fix issues with urls used in nckit extension for chunked uploads
claucambra Jan 9, 2025
31cbb27
Remove over-active chunk progress logging
claucambra Jan 9, 2025
c3bd5bf
Simplify returns for upload function
claucambra Jan 9, 2025
a4b2e82
Use setStatusForItemMetadata when setting post-upload metadata state …
claucambra Jan 9, 2025
65d913a
Do not add chunks to database if we have chunkes logged in db
claucambra Jan 9, 2025
07b82ad
Switch back to develop NextcloudKit branch
claucambra Jan 15, 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
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public class FilesDatabaseManager {
}

},
objectTypes: [ItemMetadata.self]
objectTypes: [ItemMetadata.self, RemoteFileChunk.self]
)
self.init(realmConfig: config)
}
Expand Down Expand Up @@ -340,6 +340,9 @@ public class FilesDatabaseManager {
result.downloaded = false
} else if result.isUpload {
result.uploaded = false
result.chunkUploadId = UUID().uuidString
} else if status == .normal, metadata.isUpload {
result.chunkUploadId = ""
}

Self.logger.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@
import Realm
import RealmSwift

extension Results where Element: RemoteFileChunk {
func toUnmanagedResults() -> [RemoteFileChunk] {
return map { RemoteFileChunk(value: $0) }
}
}

extension Results where Element: ItemMetadata {
func toUnmanagedResults() -> [ItemMetadata] {
return map { ItemMetadata(value: $0) }
}
}



Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,130 @@ extension NextcloudKit: RemoteInterface {
}
}

public func chunkedUpload(
localPath: String,
remotePath: String,
remoteChunkStoreFolderName: String = UUID().uuidString,
chunkSize: Int,
remainingChunks: [RemoteFileChunk],
creationDate: Date? = nil,
modificationDate: Date? = nil,
account: Account,
options: NKRequestOptions = .init(),
currentNumChunksUpdateHandler: @escaping (_ num: Int) -> Void = { _ in },
chunkCounter: @escaping (_ counter: Int) -> Void = { _ in },
chunkUploadStartHandler: @escaping (_ filesChunk: [RemoteFileChunk]) -> 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 }
) async -> (
account: String,
fileChunks: [RemoteFileChunk]?,
file: NKFile?,
afError: AFError?,
remoteError: NKError
) {
guard let remoteUrl = URL(string: remotePath) else {
uploadLogger.error("NCKit ext: Could not get url from \(remotePath, privacy: .public)")
return ("", nil, nil, nil, .urlError)
}
let localUrl = URL(fileURLWithPath: localPath)

let fm = FileManager.default
let chunksOutputDirectoryUrl =
fm.temporaryDirectory.appendingPathComponent(remoteChunkStoreFolderName)
do {
try fm.createDirectory(at: chunksOutputDirectoryUrl, withIntermediateDirectories: true)
} catch let error {
uploadLogger.error(
"""
Could not create temporary directory for chunked files: \(error, privacy: .public)
"""
)
return ("", nil, nil, nil, .urlError)
}

var directory = localUrl.deletingLastPathComponent().path
if directory.last == "/" {
directory.removeLast()
}
let fileChunksOutputDirectory = chunksOutputDirectoryUrl.path
let fileName = localUrl.lastPathComponent
let destinationFileName = remoteUrl.lastPathComponent
guard let serverUrl = remoteUrl
.deletingLastPathComponent()
.absoluteString
.removingPercentEncoding
else {
uploadLogger.error(
"NCKit ext: Could not get server url from \(remotePath, privacy: .public)"
)
return ("", nil, nil, nil, .urlError)
}
let fileChunks = remainingChunks.toNcKitChunks()

uploadLogger.info(
"""
Beginning chunked upload of: \(localPath, privacy: .public)
directory: \(directory, privacy: .public)
fileChunksOutputDirectory: \(fileChunksOutputDirectory, privacy: .public)
fileName: \(fileName, privacy: .public)
destinationFileName: \(destinationFileName, privacy: .public)
date: \(modificationDate?.debugDescription ?? "", privacy: .public)
creationDate: \(creationDate?.debugDescription ?? "", privacy: .public)
serverUrl: \(serverUrl, privacy: .public)
chunkFolder: \(remoteChunkStoreFolderName, privacy: .public)
filesChunk: \(fileChunks, privacy: .public)
chunkSize: \(chunkSize, privacy: .public)
"""
)

return await withCheckedContinuation { continuation in
uploadChunk(
directory: directory,
fileChunksOutputDirectory: fileChunksOutputDirectory,
fileName: fileName,
destinationFileName: destinationFileName,
date: modificationDate,
creationDate: creationDate,
serverUrl: serverUrl,
chunkFolder: remoteChunkStoreFolderName,
filesChunk: fileChunks,
chunkSize: chunkSize,
account: account.ncKitAccount,
options: options,
numChunks: currentNumChunksUpdateHandler,
counterChunk: chunkCounter,
start: { processedChunks in
let chunks = RemoteFileChunk.fromNcKitChunks(
processedChunks, remoteChunkStoreFolderName: remoteChunkStoreFolderName
)
chunkUploadStartHandler(chunks)
},
requestHandler: requestHandler,
taskHandler: taskHandler,
progressHandler: { totalBytesExpected, totalBytes, fractionCompleted in
let currentProgress = Progress(totalUnitCount: totalBytesExpected)
currentProgress.completedUnitCount = totalBytes
progressHandler(currentProgress)
},
uploaded: { uploadedChunk in
let chunk = RemoteFileChunk(
ncKitChunk: uploadedChunk,
remoteChunkStoreFolderName: remoteChunkStoreFolderName
)
chunkUploadCompleteHandler(chunk)
}
) { account, receivedChunks, file, afError, error in
let chunks = RemoteFileChunk.fromNcKitChunks(
receivedChunks ?? [], remoteChunkStoreFolderName: remoteChunkStoreFolderName
)
continuation.resume(returning: (account, chunks, file, afError, error))
}
}
}

public func move(
remotePathSource: String,
remotePathDestination: String,
Expand Down
25 changes: 25 additions & 0 deletions Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,31 @@ public protocol RemoteInterface {
remoteError: NKError
)

func chunkedUpload(
localPath: String,
remotePath: String,
remoteChunkStoreFolderName: String,
chunkSize: Int,
remainingChunks: [RemoteFileChunk],
creationDate: Date?,
modificationDate: Date?,
account: Account,
options: NKRequestOptions,
currentNumChunksUpdateHandler: @escaping (_ num: Int) -> Void,
chunkCounter: @escaping (_ counter: Int) -> Void,
chunkUploadStartHandler: @escaping (_ filesChunk: [RemoteFileChunk]) -> Void,
requestHandler: @escaping (_ request: UploadRequest) -> Void,
taskHandler: @escaping (_ task: URLSessionTask) -> Void,
progressHandler: @escaping (Progress) -> Void,
chunkUploadCompleteHandler: @escaping (_ fileChunk: RemoteFileChunk) -> Void
) async -> (
account: String,
fileChunks: [RemoteFileChunk]?,
file: NKFile?,
afError: AFError?,
remoteError: NKError
)

func move(
remotePathSource: String,
remotePathDestination: String,
Expand Down
35 changes: 20 additions & 15 deletions Sources/NextcloudFileProviderKit/Item/Item+Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,17 @@ extension Item {
progress: Progress,
dbManager: FilesDatabaseManager
) async -> (Item?, Error?) {
let (_, ocId, etag, date, size, _, _, error) = await remoteInterface.upload(
remotePath: remotePath,
localPath: localPath,
let chunkUploadId =
itemTemplate.itemIdentifier.rawValue.replacingOccurrences(of: "/", with: "")
let (ocId, _, etag, date, size, _, error) = await upload(
fileLocatedAt: localPath,
toRemotePath: remotePath,
usingRemoteInterface: remoteInterface,
withAccount: account,
usingChunkUploadId: chunkUploadId,
dbManager: dbManager,
creationDate: itemTemplate.creationDate as? Date,
modificationDate: itemTemplate.contentModificationDate as? Date,
account: account,
options: .init(),
requestHandler: { progress.setHandlersFromAfRequest($0) },
taskHandler: { task in
if let domain = domain {
Expand Down Expand Up @@ -147,8 +151,8 @@ extension Item {
filename: \(itemTemplate.filename, privacy: .public)
ocId: \(ocId, privacy: .public)
etag: \(etag ?? "", privacy: .public)
date: \(date ?? NSDate(), privacy: .public)
size: \(size, privacy: .public),
date: \(date ?? Date(), privacy: .public)
size: \(Int(size ?? -1), privacy: .public),
account: \(account.ncKitAccount, privacy: .public)
"""
)
Expand All @@ -157,20 +161,20 @@ extension Item {
Self.logger.warning(
"""
Created item upload reported as successful, but there are differences between
the received file size (\(size, privacy: .public))
the received file size (\(Int(size ?? -1), privacy: .public))
and the original file size (\(itemTemplate.documentSize??.int64Value ?? 0))
"""
)
}

let newMetadata = ItemMetadata()
newMetadata.date = (date ?? NSDate()) as Date
newMetadata.date = date ?? Date()
newMetadata.etag = etag ?? ""
newMetadata.account = account.ncKitAccount
newMetadata.fileName = itemTemplate.filename
newMetadata.fileNameView = itemTemplate.filename
newMetadata.ocId = ocId
newMetadata.size = size
newMetadata.size = size ?? 0
newMetadata.contentType = itemTemplate.contentType?.preferredMIMEType ?? ""
newMetadata.directory = false
newMetadata.serverUrl = parentItemRemotePath
Expand Down Expand Up @@ -302,13 +306,14 @@ extension Item {
Handling child bundle or package file at: \(childUrlPath, privacy: .public)
"""
)
let (_, _, _, _, _, _, _, error) = await remoteInterface.upload(
remotePath: childRemoteUrl,
localPath: childUrlPath,
let (_, _, _, _, _, _, error) = await upload(
fileLocatedAt: childUrlPath,
toRemotePath: childRemoteUrl,
usingRemoteInterface: remoteInterface,
withAccount: account,
dbManager: dbManager,
creationDate: childUrlAttributes.creationDate,
modificationDate: childUrlAttributes.contentModificationDate,
account: account,
options: .init(),
requestHandler: { progress.setHandlersFromAfRequest($0) },
taskHandler: { task in
if let domain {
Expand Down
46 changes: 28 additions & 18 deletions Sources/NextcloudFileProviderKit/Item/Item+Modify.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ public extension Item {
}

let updatedMetadata = await withCheckedContinuation { continuation in
dbManager.setStatusForItemMetadata(
metadata, status: ItemMetadata.Status.uploading
) { continuation.resume(returning: $0) }
dbManager.setStatusForItemMetadata(metadata, status: .uploading) {
continuation.resume(returning: $0)
}
}

if updatedMetadata == nil {
Expand All @@ -132,13 +132,15 @@ public extension Item {
)
}

let (_, _, etag, date, size, _, _, error) = await remoteInterface.upload(
remotePath: remotePath,
localPath: newContents.path,
let (_, _, etag, date, size, _, error) = await upload(
fileLocatedAt: newContents.path,
toRemotePath: remotePath,
usingRemoteInterface: remoteInterface,
withAccount: account,
usingChunkUploadId: metadata.chunkUploadId,
dbManager: dbManager,
creationDate: newCreationDate,
modificationDate: newContentModificationDate,
account: account,
options: .init(),
requestHandler: { progress.setHandlersFromAfRequest($0) },
taskHandler: { task in
if let domain {
Expand Down Expand Up @@ -180,21 +182,28 @@ public extension Item {
Self.logger.warning(
"""
Item content modification upload reported as successful,
but there are differences between the received file size (\(size, privacy: .public))
but there are differences between the received file size (\(size ?? -1, privacy: .public))
and the original file size (\(self.documentSize?.int64Value ?? 0))
"""
)
}

let newMetadata = ItemMetadata(value: metadata)
newMetadata.date = (date ?? NSDate()) as Date
let newMetadata: ItemMetadata = await {
guard let updatedMetadata else { return ItemMetadata(value: metadata) }
return await withCheckedContinuation { continuation in
dbManager.setStatusForItemMetadata(updatedMetadata, status: .normal) { updatedMeta in
continuation.resume(returning: updatedMeta ?? ItemMetadata(value: updatedMetadata))
}
}
}()

newMetadata.date = date ?? Date()
newMetadata.etag = etag ?? metadata.etag
newMetadata.ocId = ocId
newMetadata.size = size
newMetadata.size = size ?? 0
newMetadata.session = ""
newMetadata.sessionError = ""
newMetadata.sessionTaskIdentifier = 0
newMetadata.status = ItemMetadata.Status.normal.rawValue
newMetadata.downloaded = true
newMetadata.uploaded = true

Expand Down Expand Up @@ -388,13 +397,14 @@ public extension Item {
Handling child bundle or package file at: \(childUrlPath, privacy: .public)
"""
)
let (_, _, _, _, _, _, _, error) = await remoteInterface.upload(
remotePath: childRemoteUrl,
localPath: childUrlPath,
let (_, _, _, _, _, _, error) = await upload(
fileLocatedAt: childUrlPath,
toRemotePath: childRemoteUrl,
usingRemoteInterface: remoteInterface,
withAccount: account,
dbManager: dbManager,
creationDate: childUrlAttributes.creationDate,
modificationDate: childUrlAttributes.contentModificationDate,
account: account,
options: .init(),
requestHandler: { progress.setHandlersFromAfRequest($0) },
taskHandler: { task in
if let domain {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class ItemMetadata: Object {
@Persisted public var account = ""
@Persisted public var assetLocalIdentifier = ""
@Persisted public var checksums = ""
@Persisted public var chunk: Bool = false
@Persisted public var chunkUploadId: String = ""
@Persisted public var classFile = ""
@Persisted public var commentsUnread: Bool = false
@Persisted public var contentType = ""
Expand Down
Loading