Skip to content

Commit 9c306ff

Browse files
Merge pull request #167 from nextcloud/chunk
chunk
2 parents 27d9283 + c376da1 commit 9c306ff

2 files changed

Lines changed: 118 additions & 66 deletions

File tree

Sources/NextcloudKit/NKCommon.swift

Lines changed: 85 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -91,27 +91,27 @@ public struct NKCommon: Sendable {
9191
filesChunk: [(fileName: String, size: Int64)],
9292
numChunks: @escaping (_ num: Int) -> Void = { _ in },
9393
counterChunk: @escaping (_ counter: Int) -> Void = { _ in },
94-
completion: @escaping (_ filesChunk: [(fileName: String, size: Int64)]) -> Void = { _ in }) {
95-
// Check if filesChunk is empty
94+
completion: @escaping (_ filesChunk: [(fileName: String, size: Int64)], _ error: Error?) -> Void = { _, _ in }) {
95+
// Return existing chunks immediately
9696
if !filesChunk.isEmpty {
97-
return completion(filesChunk)
97+
return completion(filesChunk, nil)
9898
}
9999

100100
defer {
101101
NotificationCenter.default.removeObserver(self, name: notificationCenterChunkedFileStop, object: nil)
102102
}
103103

104-
let fileManager: FileManager = .default
104+
let fileManager = FileManager.default
105105
var isDirectory: ObjCBool = false
106106
var reader: FileHandle?
107107
var writer: FileHandle?
108-
var chunk: Int = 0
109-
var counter: Int = 1
108+
var chunkWrittenBytes = 0
109+
var counter = 1
110110
var incrementalSize: Int64 = 0
111111
var filesChunk: [(fileName: String, size: Int64)] = []
112112
var chunkSize = chunkSize
113-
let bufferSize = 1000000
114-
var stop: Bool = false
113+
let bufferSize = 1_000_000
114+
var stop = false
115115

116116
NotificationCenter.default.addObserver(forName: notificationCenterChunkedFileStop, object: nil, queue: nil) { _ in
117117
stop = true
@@ -122,78 +122,121 @@ public struct NKCommon: Sendable {
122122
let totalSize = getFileSize(filePath: inputFilePath)
123123
var num: Int = Int(totalSize / Int64(chunkSize))
124124

125-
if num > 10000 {
125+
if num > 10_000 {
126126
chunkSize += 100_000_000
127-
num = Int(totalSize / Int64(chunkSize)) // ricalcolo
127+
num = Int(totalSize / Int64(chunkSize))
128128
}
129129
numChunks(num)
130130

131+
// Create output directory if needed
131132
if !fileManager.fileExists(atPath: outputDirectory, isDirectory: &isDirectory) {
132133
do {
133134
try fileManager.createDirectory(atPath: outputDirectory, withIntermediateDirectories: true, attributes: nil)
134135
} catch {
135-
return completion([])
136+
return completion([], NSError(domain: "chunkedFile", code: -2,userInfo: [NSLocalizedDescriptionKey: "Failed to create the output directory for file chunks."]))
136137
}
137138
}
138139

140+
// Open input file
139141
do {
140142
reader = try .init(forReadingFrom: URL(fileURLWithPath: inputFilePath))
141143
} catch {
142-
return completion([])
144+
return completion([], NSError(domain: "chunkedFile", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to open the input file for reading."]))
143145
}
144146

145-
repeat {
147+
outerLoop: repeat {
146148
if stop {
147-
return completion([])
149+
return completion([], NSError(domain: "chunkedFile", code: -5, userInfo: [NSLocalizedDescriptionKey: "Chunking was stopped by user request or system notification."]))
148150
}
149-
if autoreleasepool(invoking: { () -> Int in
150-
if chunk >= chunkSize {
151-
writer?.closeFile()
152-
writer = nil
153-
chunk = 0
154-
counterChunk(counter)
155-
debugPrint("[DEBUG] Counter: \(counter)")
156-
counter += 1
151+
152+
let result = autoreleasepool(invoking: { () -> Int in
153+
let remaining = chunkSize - chunkWrittenBytes
154+
guard let rawBuffer = reader?.readData(ofLength: min(bufferSize, remaining)) else {
155+
return -1 // Error: read failed
157156
}
158157

159-
let chunkRemaining: Int = chunkSize - chunk
160-
let rawBuffer = reader?.readData(ofLength: min(bufferSize, chunkRemaining))
158+
if rawBuffer.isEmpty {
159+
// Final flush of last chunk
160+
if writer != nil {
161+
writer?.closeFile()
162+
writer = nil
163+
counterChunk(counter)
164+
debugPrint("[DEBUG] Final chunk closed: \(counter)")
165+
counter += 1
166+
}
167+
return 0 // End of file
168+
}
169+
170+
let safeBuffer = Data(rawBuffer)
171+
161172

162173
if writer == nil {
163174
let fileNameChunk = String(counter)
164175
let outputFileName = outputDirectory + "/" + fileNameChunk
165176
fileManager.createFile(atPath: outputFileName, contents: nil, attributes: nil)
166177
do {
167-
writer = try .init(forWritingTo: URL(fileURLWithPath: outputFileName))
178+
writer = try FileHandle(forWritingTo: URL(fileURLWithPath: outputFileName))
168179
} catch {
169-
filesChunk = []
170-
return 0
180+
return -2 // Error: cannot create writer
171181
}
172182
filesChunk.append((fileName: fileNameChunk, size: 0))
173183
}
174184

175-
if let rawBuffer = rawBuffer {
176-
let safeBuffer = Data(rawBuffer) // secure copy
177-
writer?.write(safeBuffer)
178-
chunk = chunk + safeBuffer.count
179-
return safeBuffer.count
185+
// Check free disk space
186+
if let free = try? URL(fileURLWithPath: outputDirectory)
187+
.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
188+
.volumeAvailableCapacityForImportantUsage,
189+
free < Int64(safeBuffer.count * 2) {
190+
return -3 // Not enough disk space
180191
}
181-
filesChunk = []
182-
return 0
183-
}) == 0 { break }
192+
193+
do {
194+
try writer?.write(contentsOf: safeBuffer)
195+
chunkWrittenBytes += safeBuffer.count
196+
if chunkWrittenBytes >= chunkSize {
197+
writer?.closeFile()
198+
writer = nil
199+
chunkWrittenBytes = 0
200+
counterChunk(counter)
201+
debugPrint("[DEBUG] Chunk completed: \(counter)")
202+
counter += 1
203+
}
204+
return 1 // OK
205+
} catch {
206+
return -4 // Write error
207+
}
208+
})
209+
210+
switch result {
211+
case -1:
212+
return completion([], NSError(domain: "chunkedFile", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to read data from the input file."]))
213+
case -2:
214+
return completion([], NSError(domain: "chunkedFile", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to open the output chunk file for writing."]))
215+
case -3:
216+
return completion([], NSError(domain: "chunkedFile", code: -3, userInfo: [NSLocalizedDescriptionKey: "There is not enough available disk space to proceed."]))
217+
case -4:
218+
return completion([], NSError(domain: "chunkedFile", code: -4, userInfo: [NSLocalizedDescriptionKey: "Failed to write data to chunk file."]))
219+
case 0:
220+
break outerLoop
221+
case 1:
222+
continue
223+
default:
224+
break
225+
}
184226
} while true
185227

186228
writer?.closeFile()
187229
reader?.closeFile()
188230

189-
counter = 0
190-
for fileChunk in filesChunk {
191-
let size = getFileSize(filePath: outputDirectory + "/" + fileChunk.fileName)
192-
incrementalSize = incrementalSize + size
193-
filesChunk[counter].size = incrementalSize
194-
counter += 1
231+
// Update incremental chunk sizes
232+
for i in 0..<filesChunk.count {
233+
let path = outputDirectory + "/" + filesChunk[i].fileName
234+
let size = getFileSize(filePath: path)
235+
incrementalSize += size
236+
filesChunk[i].size = incrementalSize
195237
}
196-
return completion(filesChunk)
238+
239+
completion(filesChunk, nil)
197240
}
198241

199242
// MARK: - Server Error GroupDefaults

Sources/NextcloudKit/NextcloudKit+Upload.swift

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ public extension NextcloudKit {
220220
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
221221
progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in },
222222
uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in },
223+
assemble: @escaping () -> Void = { },
223224
completion: @escaping (_ account: String, _ filesChunk: [(fileName: String, size: Int64)]?, _ file: NKFile?, _ error: NKError) -> Void) {
224225
guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account) else {
225226
return completion(account, nil, nil, .urlError)
@@ -233,7 +234,7 @@ public extension NextcloudKit {
233234
options.customHeader?["Destination"] = serverUrlFileName.urlEncoded
234235
options.customHeader?["OC-Total-Length"] = String(fileNameLocalSize)
235236

236-
// check space
237+
// Check available disk space
237238
#if os(macOS)
238239
var fsAttributes: [FileAttributeKey: Any]
239240
do {
@@ -266,13 +267,14 @@ public extension NextcloudKit {
266267
#endif
267268

268269
#if os(visionOS) || os(iOS)
269-
if freeDisk < fileNameLocalSize * 2 {
270+
if freeDisk < fileNameLocalSize * 3 {
270271
// It seems there is not enough space to send the file
271272
return completion(account, nil, nil, .errorChunkNoEnoughMemory)
272273
}
273274
#endif
274275

275-
func createFolder(completion: @escaping (_ errorCode: NKError) -> Void) {
276+
// Ensure upload chunk folder exists
277+
func createFolderIfNeeded(completion: @escaping (_ errorCode: NKError) -> Void) {
276278
readFileOrFolder(serverUrlFileName: serverUrlChunkFolder, depth: "0", account: account, options: options) { _, _, _, error in
277279
if error == .success {
278280
completion(NKError())
@@ -286,33 +288,46 @@ public extension NextcloudKit {
286288
}
287289
}
288290

289-
createFolder { error in
291+
createFolderIfNeeded { error in
290292
guard error == .success else {
291293
return completion(account, nil, nil, .errorChunkCreateFolder)
292294
}
295+
let outputDirectory = fileChunksOutputDirectory ?? directory
293296
var uploadNKError = NKError()
294297

295-
let outputDirectory = fileChunksOutputDirectory ?? directory
296-
self.nkCommonInstance.chunkedFile(inputDirectory: directory, outputDirectory: outputDirectory, fileName: fileName, chunkSize: chunkSize, filesChunk: filesChunk) { num in
298+
299+
self.nkCommonInstance.chunkedFile(inputDirectory: directory,
300+
outputDirectory: outputDirectory,
301+
fileName: fileName,
302+
chunkSize: chunkSize,
303+
filesChunk: filesChunk) { num in
297304
numChunks(num)
298305
} counterChunk: { counter in
299306
counterChunk(counter)
300-
} completion: { filesChunk in
301-
if filesChunk.isEmpty {
302-
// The file for sending could not be created
303-
return completion(account, nil, nil, .errorChunkFilesEmpty)
307+
} completion: { filesChunk, error in
308+
309+
// Check chunking error
310+
if let error {
311+
return completion(account, nil, nil, NKError(error: error))
304312
}
313+
314+
guard !filesChunk.isEmpty else {
315+
return completion(account, nil, nil, NKError(error: NSError(domain: "chunkedFile", code: -5,userInfo: [NSLocalizedDescriptionKey: "Files empty."])))
316+
}
317+
305318
var filesChunkOutput = filesChunk
306319
start(filesChunkOutput)
307320

308321
for fileChunk in filesChunk {
309322
let serverUrlFileName = serverUrlChunkFolder + "/" + fileChunk.fileName
310323
let fileNameLocalPath = outputDirectory + "/" + fileChunk.fileName
311324
let fileSize = self.nkCommonInstance.getFileSize(filePath: fileNameLocalPath)
325+
312326
if fileSize == 0 {
313327
// The file could not be sent
314-
return completion(account, nil, nil, .errorChunkFileNull)
328+
return completion(account, nil, nil, NKError(error: NSError(domain: "chunkedFile", code: -6,userInfo: [NSLocalizedDescriptionKey: "File empty."])))
315329
}
330+
316331
let semaphore = DispatchSemaphore(value: 0)
317332
self.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account, options: options, requestHandler: { request in
318333
requestHandler(request)
@@ -359,6 +374,8 @@ public extension NextcloudKit {
359374
let assembleTimeMax: Double = 30 * 60 // 30 min
360375
options.timeout = max(assembleTimeMin, min(assembleTimePerGB * assembleSizeInGB, assembleTimeMax))
361376

377+
assemble()
378+
362379
self.moveFileOrFolder(serverUrlFileNameSource: serverUrlFileNameSource, serverUrlFileNameDestination: serverUrlFileName, overwrite: true, account: account, options: options) { _, _, error in
363380
guard error == .success else {
364381
return completion(account, filesChunkOutput, nil,.errorChunkMoveFile)
@@ -400,13 +417,9 @@ public extension NextcloudKit {
400417
requestHandler: @escaping (_ request: UploadRequest) -> Void = { _ in },
401418
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in },
402419
progressHandler: @escaping (_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) -> Void = { _, _, _ in },
420+
assemble: @escaping () -> Void = { },
403421
uploaded: @escaping (_ fileChunk: (fileName: String, size: Int64)) -> Void = { _ in }
404-
) async -> (
405-
account: String,
406-
remainingChunks: [(fileName: String, size: Int64)]?,
407-
file: NKFile?,
408-
error: NKError
409-
) {
422+
) async -> (account: String, remainingChunks: [(fileName: String, size: Int64)]?, file: NKFile?, error: NKError) {
410423
await withCheckedContinuation { continuation in
411424
uploadChunk(directory: directory,
412425
fileChunksOutputDirectory: fileChunksOutputDirectory,
@@ -426,13 +439,9 @@ public extension NextcloudKit {
426439
requestHandler: requestHandler,
427440
taskHandler: taskHandler,
428441
progressHandler: progressHandler,
429-
uploaded: uploaded) { account, remaining, file, error in
430-
continuation.resume(returning: (
431-
account: account,
432-
remainingChunks: remaining,
433-
file: file,
434-
error: error
435-
))
442+
uploaded: uploaded,
443+
assemble: assemble) { account, remaining, file, error in
444+
continuation.resume(returning: (account: account, remainingChunks: remaining, file: file, error: error))
436445
}
437446
}
438447
}

0 commit comments

Comments
 (0)