Skip to content

Commit 40836a2

Browse files
Merge pull request #64 from appwrite/concurrent-chunk-uploads-1-9-x-minimal
feat: support concurrent chunk uploads
2 parents 29a63f4 + ac9ddcf commit 40836a2

1 file changed

Lines changed: 93 additions & 15 deletions

File tree

Sources/Appwrite/Client.swift

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ open class Client {
1616

1717
// MARK: Properties
1818
public static var chunkSize = 5 * 1024 * 1024 // 5MB
19+
public static var maxConcurrentUploads = 8
1920

2021
open var endPoint = "https://cloud.appwrite.io/v1"
2122

@@ -583,6 +584,7 @@ open class Client {
583584

584585
var offset = 0
585586
var result = [String:Any]()
587+
var uploadId = idParamName != nil ? params[idParamName!] as? String : nil
586588

587589
if idParamName != nil {
588590
// Make a request to check if a file already exists
@@ -596,37 +598,113 @@ open class Client {
596598
)
597599
let chunksUploaded = map["chunksUploaded"] as! Int
598600
offset = chunksUploaded * Client.chunkSize
601+
result = map
599602
} catch {
600603
// File does not exist yet, swallow exception
601604
}
602605
}
603606

604-
while offset < size {
605-
let slice = (input.data as! ByteBuffer).getSlice(at: offset, length: Client.chunkSize)
606-
?? (input.data as! ByteBuffer).getSlice(at: offset, length: Int(size - offset))
607+
let totalChunks = Int(ceil(Double(size) / Double(Client.chunkSize)))
608+
var nextChunk = offset / Client.chunkSize
609+
var completedChunks = nextChunk
610+
var uploadedBytes = min(offset, size)
611+
var completedResponse: [String: Any]? = nil
612+
var lastChunkResponse: [String: Any]? = nil
613+
let baseParams = params
614+
let baseHeaders = headers
615+
616+
func isUploadComplete(_ response: [String: Any]) -> Bool {
617+
guard let chunksUploaded = response["chunksUploaded"] as? Int else {
618+
return false
619+
}
620+
621+
let chunksTotal = response["chunksTotal"] as? Int ?? totalChunks
622+
return chunksUploaded >= chunksTotal
623+
}
607624

608-
params[paramName] = InputFile.fromBuffer(slice!, filename: input.filename, mimeType: input.mimeType)
609-
headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size - 1))/\(size)"
625+
func uploadChunk(index: Int, uploadId: String?) async throws -> (Int, Int, [String: Any]) {
626+
let chunkOffset = index * Client.chunkSize
627+
let chunkLength = min(Client.chunkSize, size - chunkOffset)
628+
guard let slice = (input.data as! ByteBuffer).getSlice(at: chunkOffset, length: chunkLength) else {
629+
throw AppwriteError(message: "Failed to read upload chunk")
630+
}
631+
var chunkParams = baseParams
632+
var chunkHeaders = baseHeaders
633+
chunkParams[paramName] = InputFile.fromBuffer(slice, filename: input.filename, mimeType: input.mimeType)
634+
chunkHeaders["content-range"] = "bytes \(chunkOffset)-\(chunkOffset + chunkLength - 1)/\(size)"
635+
if let uploadId = uploadId {
636+
chunkHeaders["x-appwrite-id"] = uploadId
637+
}
610638

611-
result = try await call(
639+
let chunkResult = try await call(
612640
method: "POST",
613641
path: path,
614-
headers: headers,
615-
params: params,
642+
headers: chunkHeaders,
643+
params: chunkParams,
616644
converter: { return $0 as! [String: Any] }
617645
)
618646

619-
offset += Client.chunkSize
620-
headers["x-appwrite-id"] = result["$id"] as? String
647+
return (index, chunkLength, chunkResult)
648+
}
649+
650+
if nextChunk == 0 {
651+
let first = try await uploadChunk(index: 0, uploadId: uploadId)
652+
result = first.2
653+
uploadId = result["$id"] as? String
654+
nextChunk = 1
655+
completedChunks = 1
656+
uploadedBytes = first.1
621657
onProgress?(UploadProgress(
622-
id: result["$id"] as? String ?? "",
623-
progress: Double(min(offset, size))/Double(size) * 100.0,
624-
sizeUploaded: min(offset, size),
625-
chunksTotal: result["chunksTotal"] as? Int ?? -1,
626-
chunksUploaded: result["chunksUploaded"] as? Int ?? -1
658+
id: uploadId ?? "",
659+
progress: Double(uploadedBytes)/Double(size) * 100.0,
660+
sizeUploaded: uploadedBytes,
661+
chunksTotal: result["chunksTotal"] as? Int ?? totalChunks,
662+
chunksUploaded: result["chunksUploaded"] as? Int ?? completedChunks
627663
))
628664
}
629665

666+
let maxConcurrency = Client.maxConcurrentUploads
667+
668+
try await withThrowingTaskGroup(of: (Int, Int, [String: Any]).self) { group in
669+
var inFlight = 0
670+
671+
while inFlight < maxConcurrency && nextChunk < totalChunks {
672+
let index = nextChunk
673+
let currentUploadId = uploadId
674+
group.addTask { try await uploadChunk(index: index, uploadId: currentUploadId) }
675+
nextChunk += 1
676+
inFlight += 1
677+
}
678+
679+
while let chunk = try await group.next() {
680+
inFlight -= 1
681+
completedChunks += 1
682+
uploadedBytes += chunk.1
683+
lastChunkResponse = chunk.2
684+
if isUploadComplete(chunk.2) {
685+
completedResponse = chunk.2
686+
}
687+
688+
onProgress?(UploadProgress(
689+
id: uploadId ?? "",
690+
progress: Double(min(uploadedBytes, size))/Double(size) * 100.0,
691+
sizeUploaded: min(uploadedBytes, size),
692+
chunksTotal: chunk.2["chunksTotal"] as? Int ?? totalChunks,
693+
chunksUploaded: chunk.2["chunksUploaded"] as? Int ?? completedChunks
694+
))
695+
696+
while inFlight < maxConcurrency && nextChunk < totalChunks {
697+
let index = nextChunk
698+
let currentUploadId = uploadId
699+
group.addTask { try await uploadChunk(index: index, uploadId: currentUploadId) }
700+
nextChunk += 1
701+
inFlight += 1
702+
}
703+
}
704+
}
705+
706+
result = completedResponse ?? lastChunkResponse ?? result
707+
630708
return try converter!(result)
631709
}
632710

0 commit comments

Comments
 (0)