Skip to content
99 changes: 91 additions & 8 deletions templates/swift/Sources/Client.swift.twig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let CRLF = "\r\n"
open class Client {

// MARK: Properties
public static var chunkSize = 5 * 1024 * 1024 // 5MB

open var endPoint = "{{spec.endpoint}}"

Expand All @@ -33,6 +34,8 @@ open class Client {
private static let boundaryChars =
"abcdefghijklmnopqrstuvwxyz1234567890"

private static let boundary = randomBoundary()

private static var eventLoopGroupProvider =
HTTPClient.EventLoopGroupProvider.createNew

Expand Down Expand Up @@ -279,7 +282,7 @@ open class Client {
with params: [String: Any?]
) throws {
if request.headers["content-type"][0] == "multipart/form-data" {
buildMultipart(&request, with: params)
buildMultipart(&request, with: params, chunked: !request.headers["content-range"].isEmpty)
} else {
try buildJSON(&request, with: params)
}
Expand Down Expand Up @@ -342,6 +345,14 @@ open class Client {
default:
var message = ""

if response.body == nil {
completion(.failure({{ spec.title | caseUcfirst }}Error(
message: "Unknown error with status code \(response.status.code)",
code: Int(response.status.code)
)))
return
}

do {
let dict = try JSONSerialization
.jsonObject(with: response.body!) as? [String: Any]
Expand All @@ -352,7 +363,7 @@ open class Client {
message = response.body!.readString(length: response.body!.readableBytes)!
}

let error = AppwriteError(
let error = {{ spec.title | caseUcfirst }}Error(
message: message,
code: Int(response.status.code)
)
Expand All @@ -363,7 +374,77 @@ open class Client {
}
}

private func randomBoundary() -> String {
func chunkedUpload<T>(
path: String,
headers: inout [String: String],
params: inout [String: Any?],
paramName: String,
convert: (([String: Any]) -> T)? = nil,
onProgress: ((Double) -> Void)? = nil,
completion: ((Result<T, {{ spec.title | caseUcfirst }}Error>) -> Void)? = nil
) {
let file = params[paramName] as! File
let size = file.buffer.readableBytes

if size < Client.chunkSize {
call(
method: "POST",
path: path,
headers: headers,
params: params,
convert: convert,
completion: completion
)
return
}

var input = file.buffer
var offset = 0
var result = [String:Any]()
let group = DispatchGroup()

while offset < size {
let slice = input.readSlice(length: Client.chunkSize)
?? input.readSlice(length: size - offset)

params[paramName] = File(
name: file.name,
buffer: slice!
)

headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size))/\(size)"

group.enter()

call(
method: "POST",
path: path,
headers: headers,
params: params,
convert: { return $0 }
) { response in
switch response {
case let .success(map):
result = map
group.leave()
case let .failure(error):
completion?(.failure(error))
return
}
}

group.wait()

offset += Client.chunkSize
headers["x-{{ spec.title | caseLower }}-id"] = result["$id"] as? String
onProgress?(Double(min(offset, size))/Double(size) * 100.0)
}

completion?(.success(convert!(result)))
}


private static func randomBoundary() -> String {
var string = ""
for _ in 0..<16 {
string.append(Client.boundaryChars.randomElement()!)
Expand All @@ -382,11 +463,12 @@ open class Client {

private func buildMultipart(
_ request: inout HTTPClient.Request,
with params: [String: Any?] = [:]
with params: [String: Any?] = [:],
chunked: Bool = false
) {
func addPart(name: String, value: Any) {
bodyBuffer.writeString(DASHDASH)
bodyBuffer.writeString(boundary)
bodyBuffer.writeString(Client.boundary)
bodyBuffer.writeString(CRLF)
bodyBuffer.writeString("Content-Disposition: form-data; name=\"\(name)\"")

Expand All @@ -408,7 +490,6 @@ open class Client {
bodyBuffer.writeString(CRLF)
}

let boundary = randomBoundary()
var bodyBuffer = ByteBuffer()

for (key, value) in params {
Expand All @@ -427,13 +508,15 @@ open class Client {
}

bodyBuffer.writeString(DASHDASH)
bodyBuffer.writeString(boundary)
bodyBuffer.writeString(Client.boundary)
bodyBuffer.writeString(DASHDASH)
bodyBuffer.writeString(CRLF)

request.headers.remove(name: "content-type")
if !chunked {
request.headers.add(name: "Content-Length", value: bodyBuffer.readableBytes.description)
request.headers.add(name: "Content-Type", value: "multipart/form-data;boundary=\"\(boundary)\"")
}
request.headers.add(name: "Content-Type", value: "multipart/form-data;boundary=\"\(Client.boundary)\"")
request.body = .byteBuffer(bodyBuffer)
}

Expand Down
28 changes: 26 additions & 2 deletions templates/swift/Sources/Services/Service.swift.twig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ open class {{ service.name | caseUcfirst }}: Service {
{% for parameter in method.parameters.all %}
{{ parameter.name | caseCamel | escapeKeyword }}: {{ parameter.type | typeName | raw }}{% if not parameter.required %}? = nil{% endif %},
{% endfor %}
{% if 'multipart/form-data' in method.consumes %}
onProgress: ((Double) -> Void)? = nil,
{% endif %}
completion: ((Result<{{ _self.resultType(spec, method) }}, {{ spec.title | caseUcfirst}}Error>) -> Void)? = nil
) {
{% if method.parameters.path %} var{% else %} let{% endif %} path: String = "{{ method.path }}"
Expand All @@ -39,11 +42,12 @@ open class {{ service.name | caseUcfirst }}: Service {
path = path.replacingOccurrences(
of: "{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}",
with: {% if method.parameters.path %}{{ parameter.name | caseCamel | escapeKeyword }}{% else %}""{% endif %}

)

{% endfor %}
{% if method.parameters.query or method.parameters.body or method.parameters.formData or _self.methodNeedsSecurityParameters(method) %}
let params: [String: Any?] = [
{% if 'multipart/form-data' in method.consumes %}var{% else %}let{% endif %} params: [String: Any?] = [
{% else %}
let params: [String: Any?] = [:]
{% endif %}
Expand Down Expand Up @@ -82,7 +86,7 @@ open class {{ service.name | caseUcfirst }}: Service {
completion: completion
)
{% else %}
let headers: [String: String] = [
{% if 'multipart/form-data' in method.consumes %}var{% else %}let{% endif %} headers: [String: String] = [
{{ method.headers|map((header, key) => " \"#{key}\": \"#{header}\"")|join(',\n')|raw }}
]

Expand All @@ -96,6 +100,25 @@ open class {{ service.name | caseUcfirst }}: Service {
}

{% endif %}
{% if 'multipart/form-data' in method.consumes %}
{% for parameter in method.parameters.all %}
{% if parameter.type == 'file' %}
let paramName = "{{ parameter.name }}"

{% endif %}
{% endfor %}
client.chunkedUpload(
path: path,
headers: &headers,
params: &params,
paramName: paramName,
{% if method.responseModel %}
convert: convert,
{% endif %}
onProgress: onProgress,
completion: completion
)
{% else %}
client.call(
method: "{{ method.method | caseUpper }}",
path: path,
Expand All @@ -106,6 +129,7 @@ open class {{ service.name | caseUcfirst }}: Service {
{% endif %}
completion: completion
)
{% endif %}
{% endif %}
}

Expand Down
6 changes: 4 additions & 2 deletions tests/SDKTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,13 @@ class SDKTest extends TestCase
'cp tests/languages/swift-server/Tests.swift tests/sdks/swift-server/Tests/AppwriteTests/Tests.swift',
],
'envs' => [
'swift-5.5' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift-server swift:5.5 swift test',
'swift-5.5' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift-server swiftarm/swift:5.5.2-focal-multi-arch swift test',
],
'expectedOutput' => [
...FOO_RESPONSES,
...BAR_RESPONSES,
...GENERAL_RESPONSES,
'POST:/v1/mock/tests/general/upload:passed', // large file upload
...EXCEPTION_RESPONSES,
],
],
Expand All @@ -180,12 +181,13 @@ class SDKTest extends TestCase
'cp tests/languages/swift-client/Tests.swift tests/sdks/swift-client/Tests/AppwriteTests/Tests.swift',
],
'envs' => [
'swift-5.5' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift-client swift:5.5 swift test',
'swift-5.5' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift-client swiftarm/swift:5.5.2-focal-multi-arch swift test',
],
'expectedOutput' => [
...FOO_RESPONSES,
...BAR_RESPONSES,
...GENERAL_RESPONSES,
'POST:/v1/mock/tests/general/upload:passed', // large file upload
...EXCEPTION_RESPONSES,
...REALTIME_RESPONSES,
],
Expand Down
Loading