Skip to content

Commit 71b7059

Browse files
authored
Merge pull request #411 from appwrite/feat-resumable-upload-kotlin
Feat resumable upload kotlin
2 parents b872080 + b80ea91 commit 71b7059

File tree

6 files changed

+73
-41
lines changed

6 files changed

+73
-41
lines changed

templates/kotlin/build.gradle.twig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
plugins {
2-
id "org.jetbrains.kotlin.jvm" version '1.5.31'
2+
id "org.jetbrains.kotlin.jvm" version '1.6.10'
33
id "java-library"
44
id "io.github.gradle-nexus.publish-plugin" version "1.1.0"
55
}
@@ -29,12 +29,12 @@ repositories {
2929
}
3030

3131
dependencies {
32-
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
33-
api(platform("com.squareup.okhttp3:okhttp-bom:4.9.0"))
32+
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
33+
api(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
3434
api("com.squareup.okhttp3:okhttp")
3535
implementation("com.squareup.okhttp3:okhttp-urlconnection")
3636
implementation("com.squareup.okhttp3:logging-interceptor")
37-
implementation("com.google.code.gson:gson:2.8.7")
37+
implementation("com.google.code.gson:gson:2.9.0")
3838

3939
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit'
4040
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import okhttp3.logging.HttpLoggingInterceptor
2020
import java.io.BufferedInputStream
2121
import java.io.BufferedReader
2222
import java.io.File
23+
import java.io.RandomAccessFile
2324
import java.io.IOException
2425
import java.security.SecureRandom
2526
import java.security.cert.X509Certificate
@@ -184,7 +185,7 @@ class Client @JvmOverloads constructor(
184185
headers: Map<String, String> = mapOf(),
185186
params: Map<String, Any?> = mapOf(),
186187
responseType: Class<T>,
187-
convert: ((Map<String, Any,>) -> T)? = null
188+
converter: ((Map<String, Any,>) -> T)? = null
188189
): T {
189190
val filteredParams = params.filterValues { it != null }
190191

@@ -220,7 +221,7 @@ class Client @JvmOverloads constructor(
220221
.get()
221222
.build()
222223

223-
return awaitResponse(request, responseType, convert)
224+
return awaitResponse(request, responseType, converter)
224225
}
225226

226227
val body = if (MultipartBody.FORM.toString() == headers["content-type"]) {
@@ -257,7 +258,7 @@ class Client @JvmOverloads constructor(
257258
.method(method, body)
258259
.build()
259260

260-
return awaitResponse(request, responseType, convert)
261+
return awaitResponse(request, responseType, converter)
261262
}
262263

263264
/**
@@ -275,8 +276,9 @@ class Client @JvmOverloads constructor(
275276
headers: MutableMap<String, String>,
276277
params: MutableMap<String, Any?>,
277278
responseType: Class<T>,
278-
convert: ((Map<String, Any,>) -> T),
279+
converter: ((Map<String, Any,>) -> T),
279280
paramName: String,
281+
idParamName: String? = null,
280282
onProgress: ((UploadProgress) -> Unit)? = null,
281283
): T {
282284
val file = params[paramName] as File
@@ -289,74 +291,84 @@ class Client @JvmOverloads constructor(
289291
file.asRequestBody()
290292
)
291293
return call(
292-
"POST",
294+
method = "POST",
293295
path,
294296
headers,
295297
params,
296298
responseType,
297-
convert
299+
converter
298300
)
299301
}
300302

301-
val input = file.inputStream().buffered()
303+
val input = RandomAccessFile(file, "r")
302304
val buffer = ByteArray(CHUNK_SIZE)
303305
var offset = 0L
304306
var result: Map<*, *>? = null
305307

306-
generateSequence {
307-
val readBytes = input.read(buffer)
308-
if (readBytes >= 0) {
309-
buffer.copyOf(readBytes)
310-
} else {
311-
input.close()
312-
null
313-
}
314-
}.forEach {
308+
if (idParamName?.isNotEmpty() == true && params[idParamName] != "unique()") {
309+
// Make a request to check if a file already exists
310+
val current = call(
311+
method = "GET",
312+
path = "$path/${params[idParamName]}",
313+
headers = headers,
314+
params = emptyMap(),
315+
responseType = Map::class.java,
316+
)
317+
val chunksUploaded = current["chunksUploaded"] as Long
318+
offset = (chunksUploaded * CHUNK_SIZE).coerceAtMost(size)
319+
}
320+
321+
while (offset < size) {
322+
input.seek(offset)
323+
input.read(buffer)
324+
315325
params[paramName] = MultipartBody.Part.createFormData(
316326
paramName,
317327
file.name,
318-
it.toRequestBody()
328+
buffer.toRequestBody()
319329
)
320330

321331
headers["Content-Range"] =
322332
"bytes $offset-${((offset + CHUNK_SIZE) - 1).coerceAtMost(size)}/$size"
323333

324334
result = call(
325-
"POST",
335+
method = "POST",
326336
path,
327337
headers,
328338
params,
329-
Map::class.java
339+
responseType = Map::class.java
330340
)
331341

332342
offset += CHUNK_SIZE
333-
headers["x-{{ spec.title | caseLower }}-id"] = result!!["\$id"].toString()
334-
onProgress?.invoke(UploadProgress(
335-
id = result!!["\$id"].toString(),
336-
progress = offset.coerceAtMost(size).toDouble()/size * 100,
337-
sizeUploaded = offset.coerceAtMost(size),
338-
chunksTotal = result!!["chunkTotal"].toString().toInt(),
339-
chunksUploaded = result!!["chunkUploaded"].toString().toInt(),
340-
))
343+
headers["x-appwrite-id"] = result!!["\$id"].toString()
344+
onProgress?.invoke(
345+
UploadProgress(
346+
id = result!!["\$id"].toString(),
347+
progress = offset.coerceAtMost(size).toDouble() / size * 100,
348+
sizeUploaded = offset.coerceAtMost(size),
349+
chunksTotal = result!!["chunksTotal"].toString().toInt(),
350+
chunksUploaded = result!!["chunksUploaded"].toString().toInt(),
351+
)
352+
)
341353
}
342354

343-
return convert(result as Map<String, Any>)
355+
return converter(result as Map<String, Any>)
344356
}
345357

346358
/**
347359
* Await Response
348360
*
349361
* @param request
350362
* @param responseType
351-
* @param convert
363+
* @param converter
352364
*
353365
* @return [T]
354366
*/
355367
@Throws({{ spec.title | caseUcfirst }}Exception::class)
356368
private suspend fun <T> awaitResponse(
357369
request: Request,
358370
responseType: Class<T>,
359-
convert: ((Map<String, Any,>) -> T)? = null
371+
converter: ((Map<String, Any,>) -> T)? = null
360372
) = suspendCancellableCoroutine<T> {
361373
http.newCall(request).enqueue(object : Callback {
362374
override fun onFailure(call: Call, e: IOException) {
@@ -422,7 +434,7 @@ class Client @JvmOverloads constructor(
422434
object : TypeToken<Map<String, Any>>(){}.type
423435
)
424436
it.resume(
425-
convert?.invoke(map) ?: map as T
437+
converter?.invoke(map) ?: map as T
426438
)
427439
}
428440
})

templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) {
112112
{{ method.headers|map((header, key) => " \"#{key}\" to \"#{header}\"")|join(',\n')|raw }}
113113
)
114114
{% if method.responseModel %}
115-
val convert: (Map<String, Any>) -> {{ _self.resultType(sdk.namespace, method) }} = {
115+
val converter: (Map<String, Any>) -> {{ _self.resultType(sdk.namespace, method) }} = {
116116
{% if method.responseModel == 'any' %}
117117
it
118118
{% else %}
@@ -121,6 +121,8 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) {
121121
}
122122
{% endif %}
123123
{% if 'multipart/form-data' in method.consumes %}
124+
val idParamName: String? = {% if method.parameters.all | filter(p => p.isUploadID) | length > 0 %}{% for parameter in method.parameters.all | filter(parameter => parameter.isUploadID) %}"{{ parameter.name }}"{% endfor %}{% else %}null{% endif %}
125+
124126
{% for parameter in method.parameters.all %}
125127
{% if parameter.type == 'file' %}
126128
val paramName = "{{ parameter.name }}"
@@ -132,9 +134,10 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) {
132134
params,
133135
responseType = {{ _self.resultType(sdk.namespace, method) }}::class.java,
134136
{% if method.responseModel %}
135-
convert = convert,
137+
converter,
136138
{% endif %}
137139
paramName,
140+
idParamName,
138141
onProgress,
139142
)
140143
{% else %}
@@ -145,7 +148,7 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) {
145148
params,
146149
responseType = {{ _self.resultType(sdk.namespace, method) }}::class.java,
147150
{% if method.responseModel %}
148-
convert = convert,
151+
converter,
149152
{% endif %}
150153
)
151154
{% endif %}

tests/Base.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ public function testHTTPSuccess(): void
9595
])
9696
->setTest("true");
9797

98+
$dir = __DIR__ . '/sdks/' . $this->language;
99+
100+
$this->rmdir_recursive($dir);
101+
98102
$sdk->generate(__DIR__ . '/sdks/' . $this->language);
99103

100104
/**
@@ -150,6 +154,18 @@ public function testHTTPSuccess(): void
150154
}
151155
}
152156

157+
private function rmdir_recursive($dir) {
158+
if (!\is_dir($dir)) {
159+
return;
160+
}
161+
foreach(\scandir($dir) as $file) {
162+
if ('.' === $file || '..' === $file) continue;
163+
if (\is_dir("$dir/$file")) $this->rmdir_recursive("$dir/$file");
164+
else \unlink("$dir/$file");
165+
}
166+
rmdir($dir);
167+
}
168+
153169
public function getLanguage(): Language
154170
{
155171
return new $this->class();

tests/KotlinTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ class KotlinTest extends Base
1212
'chmod +x tests/sdks/kotlin/gradlew',
1313
];
1414
protected array $envs = [
15-
'java-8' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-alpine sh -c "./gradlew :test -q && cat result.txt"',
16-
'java-11' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin adoptopenjdk/openjdk11:alpine sh -c "./gradlew :test -q && cat result.txt"',
15+
'java-8' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-slim sh -c "./gradlew test -q && cat result.txt"',
16+
'java-11' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:11-jdk-slim sh -c "./gradlew test -q && cat result.txt"',
17+
'java-17' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:17-jdk-slim sh -c "./gradlew test -q && cat result.txt"',
1718
];
1819
protected array $expectedOutput = [
1920
...Base::FOO_RESPONSES,

0 commit comments

Comments
 (0)