diff --git a/src/main/kotlin/com/beust/kobalt/maven/Http.kt b/src/main/kotlin/com/beust/kobalt/maven/Http.kt index a39537a9..b05f33a7 100644 --- a/src/main/kotlin/com/beust/kobalt/maven/Http.kt +++ b/src/main/kotlin/com/beust/kobalt/maven/Http.kt @@ -1,8 +1,9 @@ package com.beust.kobalt.maven +import com.beust.kobalt.misc.CountingFileRequestBody import com.beust.kobalt.misc.log import com.squareup.okhttp.* -import java.io.File +import retrofit.mime.TypedFile import java.io.IOException import java.io.InputStream import javax.inject.Singleton @@ -47,15 +48,34 @@ public class Http { return result } - public fun uploadFile(user: String?, password: String?, url: String, file: File, - success: (Response) -> Unit, - error: (Response) -> Unit) { - val request = builder(user, password) - .url(url) - .put(RequestBody.create(MEDIA_TYPE_BINARY, file)) - .build() + fun percentProgressCallback(totalSize: Long) : (Long) -> Unit { + return { num: Long -> + val progress = num * 100 / totalSize + log(1, "\rUploaded: $progress%", newLine = false) + } + } - log(2, "Uploading $file to $url") + val DEFAULT_ERROR_RESPONSE = { r: Response -> + error("Couldn't upload file: " + r.message()) + } + + public fun uploadFile(user: String? = null, password: String? = null, url: String, file: TypedFile, + progressCallback: (Long) -> Unit = {}, + headers: Headers = Headers.of(), + success: (Response) -> Unit = {}, + error: (Response) -> Unit = DEFAULT_ERROR_RESPONSE) { + + val fullHeaders = Headers.Builder() + fullHeaders.set("Content-Type", file.mimeType()) + headers.names().forEach { fullHeaders.set(it, headers.get(it)) } + + val request = builder(user, password) + .headers(fullHeaders.build()) + .url(url) + .post(CountingFileRequestBody(file.file(), file.mimeType(), progressCallback)) + .build() + + log(1, "Uploading $file to $url") val response = OkHttpClient().newCall(request).execute() if (! response.isSuccessful) { error(response) @@ -63,17 +83,6 @@ public class Http { success(response) } } - -// private val JSON = MediaType.parse("application/json; charset=utf-8") -// -// fun post(user: String?, password: String?, url: String, payload: String) : String { -// val request = builder(user, password) -// .url(url) -// .post(RequestBody.create(JSON, payload)) -// .build() -// val response = OkHttpClient().newCall(request).execute() -// return response.body().string() -// } } class KobaltException(s: String? = null, ex: Throwable? = null) : RuntimeException(s, ex) { diff --git a/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt b/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt new file mode 100644 index 00000000..fd90564c --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/misc/CountingFileRequestBody.kt @@ -0,0 +1,65 @@ +package com.beust.kobalt.misc + +import com.squareup.okhttp.MediaType +import com.squareup.okhttp.RequestBody +import okio.BufferedSink +import okio.Okio +import java.io.File + +/** + * An OkHttp RequestBody subclass that counts the outgoing bytes and offers progress callbacks. + */ +class CountingFileRequestBody(val file: File, val contentType: String, + val listenerCallback: (Long) -> Unit) : RequestBody() { + + val SEGMENT_SIZE = 4096L + + override fun contentLength() = file.length() + + override fun contentType() = MediaType.parse(contentType) + + override fun writeTo(sink: BufferedSink) { + Okio.source(file).use { source -> + var total = 0L + var read: Long = source.read(sink.buffer(), SEGMENT_SIZE) + + while (read != -1L) { + total += read + sink.flush(); + listenerCallback(total) + read = source.read(sink.buffer(), SEGMENT_SIZE) + } + } + } + +// companion object { +// private val MEDIA_TYPE_BINARY = MediaType.parse("application/octet-stream") +// +// fun progressUpload(file: File, url: String) { +// val totalSize = file.length() +// +// val progressListener = object : ProgressListener { +// override fun transferred(num: Long) { +// val progress: Float = (num.toFloat() * 100) / totalSize +// print("\rProgress: $progress") +// } +// } +// +// val request = Request.Builder() +// .url(url) +// // .post(RequestBody.create(MEDIA_TYPE_BINARY, file)) +// .put(CountingFileRequestBody(file, "application/octet-stream", progressListener)) +// // .post(requestBody) +// .build(); +// +// val response = OkHttpClient().newCall(request).execute() +// if (! response.isSuccessful) { +// println("ERROR") +// } else { +// println("SUCCESS") +// } +// } +// } +} + + diff --git a/src/main/kotlin/com/beust/kobalt/misc/GithubApi.kt b/src/main/kotlin/com/beust/kobalt/misc/GithubApi.kt index 129ca103..7447631d 100644 --- a/src/main/kotlin/com/beust/kobalt/misc/GithubApi.kt +++ b/src/main/kotlin/com/beust/kobalt/misc/GithubApi.kt @@ -1,18 +1,21 @@ package com.beust.kobalt.misc +import com.beust.kobalt.maven.Http import com.beust.kobalt.maven.KobaltException import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.gson.annotations.SerializedName +import com.squareup.okhttp.Headers import com.squareup.okhttp.OkHttpClient import retrofit.RestAdapter import retrofit.RetrofitError import retrofit.client.OkClient -import retrofit.client.Response -import retrofit.http.* -import retrofit.mime.MimeUtil +import retrofit.http.Body +import retrofit.http.POST +import retrofit.http.Path +import retrofit.http.Query import retrofit.mime.TypedByteArray import retrofit.mime.TypedFile import rx.Observable @@ -29,7 +32,7 @@ import javax.inject.Inject * Retrieve Kobalt's latest release version from github. */ public class GithubApi @Inject constructor(val executors: KobaltExecutors, - val localProperties: LocalProperties) { + val localProperties: LocalProperties, val http: Http) { companion object { const val RELEASES_URL = "https://api.github.com/repos/cbeust/kobalt/releases" const val PROPERTY_ACCESS_TOKEN = "github.accessToken" @@ -41,24 +44,24 @@ public class GithubApi @Inject constructor(val executors: KobaltExecutors, private fun parseRetrofitError(e: Throwable) : RetrofitErrorsResponse { val re = e as RetrofitError - val body = e.body val json = String((re.response.body as TypedByteArray).bytes) return Gson().fromJson(json, RetrofitErrorsResponse::class.java) } fun uploadRelease(packageName: String, tagName: String, zipFile: File) { log(1, "Uploading release ${zipFile.name}") + val username = localProperties.get(PROPERTY_USERNAME) val accessToken = localProperties.get(PROPERTY_ACCESS_TOKEN) try { service.createRelease(username, accessToken, packageName, CreateRelease(tagName)) .flatMap { response -> - uploadService.uploadAsset(username, accessToken, - packageName, response.id!!, zipFile.name, TypedFile("application/zip", zipFile)) + uploadAsset(accessToken, response.uploadUrl!!, TypedFile("application/zip", zipFile), + tagName) } .toBlocking() .forEach { action -> - log(1, "Release successfully uploaded ${zipFile.name}") + log(1, "\nRelease successfully uploaded ${zipFile.name}") } } catch(e: RetrofitError) { val error = parseRetrofitError(e) @@ -67,6 +70,18 @@ public class GithubApi @Inject constructor(val executors: KobaltExecutors, } } + private fun uploadAsset(token: String, uploadUrl: String, typedFile: TypedFile, tagName: String) + : Observable { + val strippedUrl = uploadUrl.substring(0, uploadUrl.indexOf("{")) + val url = "$strippedUrl?name=$tagName&label=$tagName" + val headers = Headers.of("Authorization", "token $token") + val totalSize = typedFile.file().length() + http.uploadFile(url = url, file = typedFile, headers = headers, + progressCallback = http.percentProgressCallback(totalSize)) + + return Observable.just(UploadAssetResponse(tagName, tagName)) + } + // // Read only Api // @@ -78,57 +93,20 @@ public class GithubApi @Inject constructor(val executors: KobaltExecutors, .build() .create(Api::class.java) - class Release { - var name: String? = null - var prerelease: Boolean? = null - } - + // + // JSON mapped classes that get sent up and down + // class CreateRelease(@SerializedName("tag_name") var tagName: String? = null, var name: String? = tagName) - class CreateReleaseResponse(var id: String? = null) - class GetReleaseResponse(var id: String? = null, - @SerializedName("upload_url") var uploadUrl: String? = null) + class CreateReleaseResponse(var id: String? = null, @SerializedName("upload_url") var uploadUrl: String?) + class UploadAssetResponse(var id: String? = null, val name: String? = null) interface Api { - - @GET("/repos/{owner}/{repo}/releases/tags/{tag}") - fun getReleaseByTagName(@Path("owner") owner: String, @Path("repo") repo: String, - @Path("tag") tagName: String): GetReleaseResponse - - @GET("/repos/{owner}/{repo}/releases") - fun releases(@Path("owner") owner: String, @Path("repo") repo: String): List - @POST("/repos/{owner}/{repo}/releases") fun createRelease(@Path("owner") owner: String, @Query("access_token") accessToken: String, @Path("repo") repo: String, - @Body createRelease: CreateRelease - ): Observable - } - - // - // Upload Api - // - - val uploadService = RestAdapter.Builder() - .setEndpoint("https://uploads.github.com/") -// .setLogLevel(RestAdapter.LogLevel.FULL) - .setClient(OkClient(OkHttpClient())) - .build() - .create(UploadApi::class.java) - - class UploadReleaseResponse(var id: String? = null, val name: String? = null) - - interface UploadApi { - @POST("/repos/{owner}/{repo}/releases/{id}/assets") - fun uploadAsset(@Path("owner") owner: String, - @Query("access_token") accessToken: String, - @Path("repo") repo: String, - @Path("id") id: String, - @Query("name") name: String, - @Body file: TypedFile) - // @Query("Content-Type") contentType: String = "text/plain")//"application/zip") - : Observable + @Body createRelease: CreateRelease): Observable } val latestKobaltVersion: Future @@ -140,7 +118,6 @@ public class GithubApi @Inject constructor(val executors: KobaltExecutors, @Suppress("UNCHECKED_CAST") val reader = BufferedReader(InputStreamReader(ins)) val jo = JsonParser().parse(reader) as JsonArray - // val jo = Parser().parse(ins) as JsonArray if (jo.size() > 0) { var versionName = (jo.get(0) as JsonObject).get("name").asString if (Strings.isEmpty(versionName)) { @@ -158,12 +135,3 @@ public class GithubApi @Inject constructor(val executors: KobaltExecutors, return executors.miscExecutor.submit(callable) } } - -fun Response.bodyContent() : String { - val bodyBytes = (body as TypedByteArray).bytes - val bodyMime = body.mimeType() - val bodyCharset = MimeUtil.parseCharset(bodyMime, "utf-8") - val result = String(bodyBytes, bodyCharset) - return result - // return new Gson().fromJson(data, type); -} diff --git a/src/main/kotlin/com/beust/kobalt/plugin/publish/JCenterApi.kt b/src/main/kotlin/com/beust/kobalt/plugin/publish/JCenterApi.kt index 72e00b5a..30e524c6 100644 --- a/src/main/kotlin/com/beust/kobalt/plugin/publish/JCenterApi.kt +++ b/src/main/kotlin/com/beust/kobalt/plugin/publish/JCenterApi.kt @@ -10,11 +10,13 @@ import com.beust.kobalt.misc.KobaltExecutors import com.beust.kobalt.misc.error import com.beust.kobalt.misc.log import com.beust.kobalt.misc.warn +import com.google.common.net.MediaType import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.inject.assistedinject.Assisted import com.squareup.okhttp.Response import org.jetbrains.annotations.Nullable +import retrofit.mime.TypedFile import java.io.File import javax.inject.Inject @@ -147,9 +149,10 @@ public class JCenterApi @Inject constructor (@Nullable @Assisted("username") val val results = arrayListOf() filesToUpload.forEach { file -> - http.uploadFile(username, password, fileToPath(file) + optionPath, file, - { r: Response -> results.add(true)}, - { r: Response -> + http.uploadFile(username, password, fileToPath(file) + optionPath, + TypedFile(MediaType.ANY_APPLICATION_TYPE.toString(), file), + success = { r: Response -> results.add(true) }, + error = { r: Response -> results.add(false) val jo = parseResponse(r.body().string()) errorMessages.add(jo.get("message").asString ?: "No message found")