From eaf135e8aae74b9e0042e90f3822d46bc085917f Mon Sep 17 00:00:00 2001 From: Natan Vieira do Nascimento Date: Sun, 13 Aug 2023 13:52:39 -0300 Subject: [PATCH 1/4] Create downloadArchive and uploadArchive --- .../resource/container/ContainerResource.kt | 14 ++++++++++++++ .../container/ContainerResource.jvm.kt | 18 ++++++++++++++++++ .../container/ContainerResource.native.kt | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt index 704147b..e79a34c 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt @@ -136,4 +136,18 @@ public expect class ContainerResource { // TODO documentation public suspend fun prune(filters: ContainerPruneFilters = ContainerPruneFilters()): ContainerPruneResult + + /** + * Downloads files from a container file system. + * + * @param container The container id. + */ + public suspend fun downloadArchive(container: String) + + /** + * Uploads files into a container file system. + * + * @param container The container id. + */ + public suspend fun uploadArchive(container: String) } diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt b/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt index da32704..f203a31 100644 --- a/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt @@ -477,4 +477,22 @@ public actual class ContainerResource( @JvmOverloads public fun pruneAsync(filters: ContainerPruneFilters = ContainerPruneFilters()): CompletableFuture = coroutineScope.async { prune(filters) }.asCompletableFuture() + + /** + * Downloads files from a container file system. + * + * @param container The container id. + */ + public actual suspend fun downloadArchive(container: String) { + } + + /** + * Uploads files into a container file system. + * + * @param container The container id. + */ + public actual suspend fun uploadArchive(container: String) { + } + + } diff --git a/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt b/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt index 6b2630d..8a0f018 100644 --- a/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt +++ b/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt @@ -166,4 +166,20 @@ public actual class ContainerResource { public actual suspend fun prune(filters: ContainerPruneFilters): ContainerPruneResult { TODO("Not yet implemented") } + + /** + * Downloads files from a container file system. + * + * @param container The container id. + */ + public actual suspend fun downloadArchive(container: String) { + } + + /** + * Uploads files into a container file system. + * + * @param container The container id. + */ + public actual suspend fun uploadArchive(container: String) { + } } From ac327e4d9e979c7e3ec056caebb671ca18b0d9aa Mon Sep 17 00:00:00 2001 From: Natan Vieira do Nascimento Date: Thu, 17 Aug 2023 19:14:11 -0300 Subject: [PATCH 2/4] Untar downloaded archive Replace okio by kotlinx-io --- THIRDPARTY.md | 2 +- api/yoki.api | 51 ++++++++++ build.gradle.kts | 3 +- gradle/libs.versions.toml | 15 ++- .../kotlin/me/devnatan/yoki/io/Http.kt | 11 ++- .../kotlin/me/devnatan/yoki/io/TarFile.kt | 7 ++ .../models/container/ContainerArchiveInfo.kt | 18 ++++ .../resource/container/ContainerResource.kt | 21 +++- .../kotlin/me/devnatan/yoki/Yoki.jvm.kt | 1 + .../devnatan/yoki/io/CompressArchiveUtil.kt | 95 +++++++++++++++++++ .../kotlin/me/devnatan/yoki/io/Sockets.jvm.kt | 6 +- .../kotlin/me/devnatan/yoki/io/TarFile.jvm.kt | 24 +++++ .../container/ContainerResource.jvm.kt | 76 +++++++++++++-- .../me/devnatan/yoki/io/TarFile.native.kt | 11 +++ .../container/ContainerResource.native.kt | 27 +++++- 15 files changed, 341 insertions(+), 27 deletions(-) create mode 100644 src/commonMain/kotlin/me/devnatan/yoki/io/TarFile.kt create mode 100644 src/commonMain/kotlin/me/devnatan/yoki/models/container/ContainerArchiveInfo.kt create mode 100644 src/jvmMain/kotlin/me/devnatan/yoki/io/CompressArchiveUtil.kt create mode 100644 src/jvmMain/kotlin/me/devnatan/yoki/io/TarFile.jvm.kt create mode 100644 src/nativeMain/kotlin/me/devnatan/yoki/io/TarFile.native.kt diff --git a/THIRDPARTY.md b/THIRDPARTY.md index 7448b84..e82b105 100644 --- a/THIRDPARTY.md +++ b/THIRDPARTY.md @@ -7,8 +7,8 @@ This document contains a list of third-party libraries that Yoki uses. * [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines) * [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) * [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) +* [kotlinx-io](https://github.com/Kotlin/kotlinx-io) * [JetBrains Annotations](https://github.com/JetBrains/java-annotations) -* [Okio](https://github.com/square/okio) * [publish-on-central](https://github.com/DanySK/publish-on-central) * [kotlinter](https://github.com/jeremymailen/kotlinter-gradle) * [detekt](https://github.com/detekt/detekt) diff --git a/api/yoki.api b/api/yoki.api index f3c4d04..457e0ec 100644 --- a/api/yoki.api +++ b/api/yoki.api @@ -1,3 +1,8 @@ +public final class me/devnatan/yoki/MainKt { + public static final fun main (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun main ([Ljava/lang/String;)V +} + public final class me/devnatan/yoki/Yoki : kotlinx/coroutines/CoroutineScope { public fun ()V public fun (Lme/devnatan/yoki/YokiConfig;)V @@ -1283,6 +1288,48 @@ public final class me/devnatan/yoki/models/container/Container$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class me/devnatan/yoki/models/container/ContainerArchiveInfo { + public static final field Companion Lme/devnatan/yoki/models/container/ContainerArchiveInfo$Companion; + public synthetic fun (ILjava/lang/String;JILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()J + public final fun component3 ()I + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;)Lme/devnatan/yoki/models/container/ContainerArchiveInfo; + public static synthetic fun copy$default (Lme/devnatan/yoki/models/container/ContainerArchiveInfo;Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/devnatan/yoki/models/container/ContainerArchiveInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getLinkTarget ()Ljava/lang/String; + public final fun getMode ()I + public final fun getModifiedAtRaw ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getSize ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public static final synthetic fun write$Self (Lme/devnatan/yoki/models/container/ContainerArchiveInfo;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V +} + +public final class me/devnatan/yoki/models/container/ContainerArchiveInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lme/devnatan/yoki/models/container/ContainerArchiveInfo$$serializer; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/devnatan/yoki/models/container/ContainerArchiveInfo; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/devnatan/yoki/models/container/ContainerArchiveInfo;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class me/devnatan/yoki/models/container/ContainerArchiveInfo$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class me/devnatan/yoki/models/container/ContainerArchiveInfoKt { + public static final fun getModifiedAt (Lme/devnatan/yoki/models/container/ContainerArchiveInfo;)Lkotlinx/datetime/Instant; +} + public final class me/devnatan/yoki/models/container/ContainerConfig { public static final field Companion Lme/devnatan/yoki/models/container/ContainerConfig$Companion; public fun ()V @@ -3667,9 +3714,12 @@ public final class me/devnatan/yoki/resource/container/ContainerRenameConflictEx public final class me/devnatan/yoki/resource/container/ContainerResource { public fun (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/serialization/json/Json;Lio/ktor/client/HttpClient;)V + public final fun archive (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun archive$default (Lme/devnatan/yoki/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final synthetic fun attach (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; public final synthetic fun create (Lme/devnatan/yoki/models/container/ContainerCreateOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun createAsync (Lme/devnatan/yoki/models/container/ContainerCreateOptions;)Ljava/util/concurrent/CompletableFuture; + public final fun downloadArchive (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final synthetic fun exec (Ljava/lang/String;Lme/devnatan/yoki/models/exec/ExecCreateOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun exec$default (Lme/devnatan/yoki/resource/container/ContainerResource;Ljava/lang/String;Lme/devnatan/yoki/models/exec/ExecCreateOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun execAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; @@ -3724,6 +3774,7 @@ public final class me/devnatan/yoki/resource/container/ContainerResource { public final fun stopAsync (Ljava/lang/String;I)Ljava/util/concurrent/CompletableFuture; public final synthetic fun unpause (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun unpauseAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; + public final fun uploadArchive (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final synthetic fun wait (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun wait$default (Lme/devnatan/yoki/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun waitAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; diff --git a/build.gradle.kts b/build.gradle.kts index 45c7712..8b033c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ kotlin { implementation(libs.ktx.datetime) implementation(libs.bundles.ktor) implementation(libs.bundles.ktx) - implementation(libs.okio) + implementation(libs.kotlinx.io.core) } } @@ -71,6 +71,7 @@ kotlin { implementation(libs.junixsocket.common) implementation(libs.ktor.client.engine.okhttp) implementation(libs.slf4j.api) + implementation(libs.apache.compress) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1c0162..bc94bfb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,9 @@ ktor = "2.3.3" junixsocket = "2.6.1" ktx-datetime = "0.4.0" junit = "5.10.0" -okio = "3.5.0" slf4j = "2.0.7" +apache-compress = "1.23.0" +kotlinx-io = "0.2.1" plugin-kotlinter = "3.16.0" plugin-publishOnCentral = "5.0.11" plugin-binaryCompatibilityValidator = "0.13.2" @@ -82,14 +83,18 @@ version.ref = "ktx-datetime" module = "org.junit.jupiter:junit-jupiter-engine" version.ref = "junit" -[libraries.okio] -module = "com.squareup.okio:okio" -version.ref = "okio" - [libraries.slf4j-api] module = "org.slf4j:slf4j-api" version.ref = "slf4j" +[libraries.apache-compress] +module = "org.apache.commons:commons-compress" +version.ref = "apache-compress" + +[libraries.kotlinx-io-core] +module = "org.jetbrains.kotlinx:kotlinx-io-core" +version.ref = "kotlinx-io" + [bundles] ktor = ["ktor-client-core", "ktor-client-serialization", "ktor-client-json", "ktor-client-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-network"] ktx = ["ktx-coroutines-core", "ktx-serialization-core", "ktx-serialization-json"] diff --git a/src/commonMain/kotlin/me/devnatan/yoki/io/Http.kt b/src/commonMain/kotlin/me/devnatan/yoki/io/Http.kt index cba7bc7..c5dce04 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/io/Http.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/io/Http.kt @@ -22,7 +22,6 @@ import kotlinx.serialization.json.Json import me.devnatan.yoki.GenericDockerErrorResponse import me.devnatan.yoki.Yoki import me.devnatan.yoki.YokiResponseException -import okio.ByteString.Companion.encodeUtf8 internal expect fun HttpClientConfig.configureHttpClient(client: Yoki) @@ -45,11 +44,14 @@ internal fun createHttpClient(client: Yoki): HttpClient { handleResponseExceptionWithRequest { exception, _ -> val responseException = exception as? ResponseException ?: return@handleResponseExceptionWithRequest val exceptionResponse = responseException.response + println("exceptionResponse = ${exceptionResponse.body()}") - val error = exceptionResponse.body() + val errorMessage = runCatching { + exceptionResponse.body() + }.getOrNull()?.message throw YokiResponseException( cause = exception, - message = error.message, + message = errorMessage, statusCode = exceptionResponse.status, ) } @@ -70,11 +72,12 @@ internal fun createHttpClient(client: Yoki): HttpClient { } } +@OptIn(ExperimentalStdlibApi::class) private fun createUrlBuilder(socketPath: String): URLBuilder = if (isUnixSocket(socketPath)) { URLBuilder( protocol = URLProtocol.HTTP, port = DOCKER_SOCKET_PORT, - host = socketPath.substringAfter(UNIX_SOCKET_PREFIX).encodeUtf8().hex() + ENCODED_HOSTNAME_SUFFIX, + host = socketPath.substringAfter(UNIX_SOCKET_PREFIX).toInt().toHexString() + ENCODED_HOSTNAME_SUFFIX, ) } else { val url = Url(socketPath) diff --git a/src/commonMain/kotlin/me/devnatan/yoki/io/TarFile.kt b/src/commonMain/kotlin/me/devnatan/yoki/io/TarFile.kt new file mode 100644 index 0000000..94390e2 --- /dev/null +++ b/src/commonMain/kotlin/me/devnatan/yoki/io/TarFile.kt @@ -0,0 +1,7 @@ +package me.devnatan.yoki.io + +import kotlinx.io.RawSource + +internal expect fun readTarFile(input: RawSource): RawSource + +internal expect fun writeTarFile(filePath: String): RawSource diff --git a/src/commonMain/kotlin/me/devnatan/yoki/models/container/ContainerArchiveInfo.kt b/src/commonMain/kotlin/me/devnatan/yoki/models/container/ContainerArchiveInfo.kt new file mode 100644 index 0000000..4902d4e --- /dev/null +++ b/src/commonMain/kotlin/me/devnatan/yoki/models/container/ContainerArchiveInfo.kt @@ -0,0 +1,18 @@ +package me.devnatan.yoki.models.container + +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class ContainerArchiveInfo( + val name: String, + val size: Long, + val mode: Int, + @SerialName("mtime") val modifiedAtRaw: String, + val linkTarget: String = "", +) + +public val ContainerArchiveInfo.modifiedAt: Instant + get() = modifiedAtRaw.toInstant() diff --git a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt index e79a34c..15971fb 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt @@ -1,10 +1,12 @@ package me.devnatan.yoki.resource.container import kotlinx.coroutines.flow.Flow +import kotlinx.io.RawSource import me.devnatan.yoki.YokiResponseException import me.devnatan.yoki.models.Frame import me.devnatan.yoki.models.ResizeTTYOptions import me.devnatan.yoki.models.container.Container +import me.devnatan.yoki.models.container.ContainerArchiveInfo import me.devnatan.yoki.models.container.ContainerCreateOptions import me.devnatan.yoki.models.container.ContainerListOptions import me.devnatan.yoki.models.container.ContainerPruneFilters @@ -14,8 +16,11 @@ import me.devnatan.yoki.models.container.ContainerSummary import me.devnatan.yoki.models.container.ContainerWaitResult import me.devnatan.yoki.models.exec.ExecCreateOptions import me.devnatan.yoki.resource.image.ImageNotFoundException +import kotlin.jvm.JvmOverloads import kotlin.time.Duration +internal const val FS_ROOT = "/" + public expect class ContainerResource { /** @@ -137,17 +142,29 @@ public expect class ContainerResource { // TODO documentation public suspend fun prune(filters: ContainerPruneFilters = ContainerPruneFilters()): ContainerPruneResult + /** + * Retrieves information about files of a container file system. + * + * @param container The container id. + * @param path The path to the file or directory inside the container file system. + */ + @JvmOverloads + public suspend fun archive(container: String, path: String = FS_ROOT): ContainerArchiveInfo + /** * Downloads files from a container file system. * * @param container The container id. + * @param remotePath The path to the file or directory inside the container file system. */ - public suspend fun downloadArchive(container: String) + public suspend fun downloadArchive(container: String, remotePath: String): RawSource /** * Uploads files into a container file system. * * @param container The container id. + * @param inputPath Path to the file that will be uploaded. + * @param remotePath Path to the file or directory inside the container file system. */ - public suspend fun uploadArchive(container: String) + public suspend fun uploadArchive(container: String, inputPath: String, remotePath: String) } diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/Yoki.jvm.kt b/src/jvmMain/kotlin/me/devnatan/yoki/Yoki.jvm.kt index e05c33e..43801d2 100644 --- a/src/jvmMain/kotlin/me/devnatan/yoki/Yoki.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/yoki/Yoki.jvm.kt @@ -47,5 +47,6 @@ public actual class Yoki public actual constructor(public actual val config: Yok public actual fun close() { cancel() + httpClient.close() } } diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/io/CompressArchiveUtil.kt b/src/jvmMain/kotlin/me/devnatan/yoki/io/CompressArchiveUtil.kt new file mode 100644 index 0000000..4b70c63 --- /dev/null +++ b/src/jvmMain/kotlin/me/devnatan/yoki/io/CompressArchiveUtil.kt @@ -0,0 +1,95 @@ +package me.devnatan.yoki.io + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import kotlin.io.path.pathString +import kotlin.io.path.relativeTo + +internal object CompressArchiveUtil { + + @Throws(IOException::class) + fun tar(inputPath: Path, outputPath: Path, childrenOnly: Boolean) { + buildTarStream(outputPath).use { tarArchiveOutputStream -> + if (!Files.isDirectory(inputPath)) { + addFileToTar(tarArchiveOutputStream, inputPath, inputPath.fileName.toString()) + } else { + var sourcePath: Path = inputPath + if (!childrenOnly) { + // In order to have the dossier as the root entry + sourcePath = inputPath.parent + } + Files.walkFileTree(inputPath, TarDirWalker(sourcePath, tarArchiveOutputStream)) + } + tarArchiveOutputStream.flush() + } + } + + @Throws(IOException::class) + fun addFileToTar(tarArchiveOutputStream: TarArchiveOutputStream, file: Path, entryName: String?) { + if (Files.isSymbolicLink(file)) { + tarArchiveOutputStream.putArchiveEntry( + TarArchiveEntry(entryName, TarArchiveEntry.LF_SYMLINK).apply { + linkName = Files.readSymbolicLink(file).toString() + }, + ) + } else { + val tarArchiveEntry = tarArchiveOutputStream.createArchiveEntry( + file.toFile(), + entryName, + ) as TarArchiveEntry + if (file.toFile().canExecute()) { + tarArchiveEntry.mode = tarArchiveEntry.mode or 493 + } + tarArchiveOutputStream.putArchiveEntry(tarArchiveEntry) + if (file.toFile().isFile()) { + BufferedInputStream(Files.newInputStream(file)).use { input -> + input.copyTo(tarArchiveOutputStream) + } + } + } + tarArchiveOutputStream.closeArchiveEntry() + } + + @Throws(IOException::class) + private fun buildTarStream(outputPath: Path) = + TarArchiveOutputStream(GzipCompressorOutputStream(BufferedOutputStream(Files.newOutputStream(outputPath)))) + .apply { + setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) + setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX) + } + + class TarDirWalker(private val basePath: Path, private val tarArchiveOutputStream: TarArchiveOutputStream) : + SimpleFileVisitor() { + @Throws(IOException::class) + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + if (dir != basePath) { + tarArchiveOutputStream.putArchiveEntry( + TarArchiveEntry(dir.relativeTo(basePath)), + ) + tarArchiveOutputStream.closeArchiveEntry() + } + return FileVisitResult.CONTINUE + } + + @Throws(IOException::class) + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + addFileToTar(tarArchiveOutputStream, file, file.relativize(basePath).pathString) + return FileVisitResult.CONTINUE + } + + @Throws(IOException::class) + override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult { + tarArchiveOutputStream.close() + throw exc + } + } +} diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/io/Sockets.jvm.kt b/src/jvmMain/kotlin/me/devnatan/yoki/io/Sockets.jvm.kt index 8c6a370..5b8a585 100644 --- a/src/jvmMain/kotlin/me/devnatan/yoki/io/Sockets.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/yoki/io/Sockets.jvm.kt @@ -1,7 +1,6 @@ package me.devnatan.yoki.io import okhttp3.Dns -import okio.ByteString.Companion.decodeHex import org.newsclub.net.unix.AFUNIXSocketAddress import org.newsclub.net.unix.AFUNIXSocketFactory import java.net.InetAddress @@ -25,11 +24,12 @@ internal class SocketDns(private val isUnixSocket: Boolean) : Dns { } internal class UnixSocketFactory : AFUNIXSocketFactory() { + @OptIn(ExperimentalStdlibApi::class) private fun decodeHostname(hostname: String): String { return hostname .substring(0, hostname.indexOf(ENCODED_HOSTNAME_SUFFIX)) - .decodeHex() - .utf8() + .hexToByteArray() + .decodeToString() } override fun addressFromHost(host: String, port: Int): AFUNIXSocketAddress { diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/io/TarFile.jvm.kt b/src/jvmMain/kotlin/me/devnatan/yoki/io/TarFile.jvm.kt new file mode 100644 index 0000000..f05ef96 --- /dev/null +++ b/src/jvmMain/kotlin/me/devnatan/yoki/io/TarFile.jvm.kt @@ -0,0 +1,24 @@ +package me.devnatan.yoki.io + +import kotlinx.io.RawSource +import kotlinx.io.asInputStream +import kotlinx.io.asSource +import kotlinx.io.buffered +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.inputStream + +internal actual fun readTarFile(input: RawSource): RawSource { + return TarArchiveInputStream(input.buffered().asInputStream()).apply { nextTarEntry }.asSource() +} + +internal actual fun writeTarFile(filePath: String): RawSource { + val output = Files.createTempFile("yoki", ".tar.gz") + CompressArchiveUtil.tar( + inputPath = Paths.get(filePath), + outputPath = output, + childrenOnly = false, + ) + return output.inputStream().asSource() +} diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt b/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt index f203a31..d2d67e5 100644 --- a/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt @@ -2,29 +2,41 @@ package me.devnatan.yoki.resource.container import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.request.accept import io.ktor.client.request.delete import io.ktor.client.request.get +import io.ktor.client.request.head import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.request.preparePost +import io.ktor.client.request.put import io.ktor.client.request.setBody +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.jvm.javaio.toByteReadChannel import io.ktor.utils.io.readUTF8Line import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.io.RawSource +import kotlinx.io.asInputStream +import kotlinx.io.asSource +import kotlinx.io.buffered import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import me.devnatan.yoki.YokiResponseException +import me.devnatan.yoki.io.readTarFile import me.devnatan.yoki.io.requestCatching +import me.devnatan.yoki.io.writeTarFile import me.devnatan.yoki.models.Frame import me.devnatan.yoki.models.IdOnlyResponse import me.devnatan.yoki.models.ResizeTTYOptions import me.devnatan.yoki.models.Stream import me.devnatan.yoki.models.container.Container +import me.devnatan.yoki.models.container.ContainerArchiveInfo import me.devnatan.yoki.models.container.ContainerCreateOptions import me.devnatan.yoki.models.container.ContainerCreateResult import me.devnatan.yoki.models.container.ContainerListOptions @@ -36,7 +48,10 @@ import me.devnatan.yoki.models.container.ContainerWaitResult import me.devnatan.yoki.models.exec.ExecCreateOptions import me.devnatan.yoki.resource.ResourcePaths.CONTAINERS import me.devnatan.yoki.resource.image.ImageNotFoundException +import java.io.InputStream import java.util.concurrent.CompletableFuture +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -68,7 +83,8 @@ public actual class ContainerResource( * @param options Options to customize the listing result. */ @JvmOverloads - public fun listAsync(options: ContainerListOptions = ContainerListOptions(all = true)): CompletableFuture> = coroutineScope.async { list(options) }.asCompletableFuture() + public fun listAsync(options: ContainerListOptions = ContainerListOptions(all = true)): CompletableFuture> = + coroutineScope.async { list(options) }.asCompletableFuture() /** * Runs a command inside a running container. @@ -77,7 +93,10 @@ public actual class ContainerResource( * @param options Exec instance command options. */ @JvmOverloads - public fun execAsync(container: String, options: ExecCreateOptions = ExecCreateOptions()): CompletableFuture = + public fun execAsync( + container: String, + options: ExecCreateOptions = ExecCreateOptions(), + ): CompletableFuture = coroutineScope.async { exec(container, options) }.asCompletableFuture() /** @@ -151,7 +170,10 @@ public actual class ContainerResource( * @throws ContainerRemoveConflictException When trying to remove an active container without the `force` option. */ @JvmOverloads - public fun removeAsync(container: String, options: ContainerRemoveOptions = ContainerRemoveOptions()): CompletableFuture = + public fun removeAsync( + container: String, + options: ContainerRemoveOptions = ContainerRemoveOptions(), + ): CompletableFuture = coroutineScope.async { remove(container, options) }.asCompletableFuture() /** @@ -410,7 +432,10 @@ public actual class ContainerResource( * @throws YokiResponseException If the container cannot be resized or if an error occurs in the request. */ @JvmOverloads - public fun resizeTTYAsync(container: String, options: ResizeTTYOptions = ResizeTTYOptions()): CompletableFuture = + public fun resizeTTYAsync( + container: String, + options: ResizeTTYOptions = ResizeTTYOptions(), + ): CompletableFuture = coroutineScope.async { resizeTTY(container, options) }.asCompletableFuture() /** @@ -479,20 +504,53 @@ public actual class ContainerResource( coroutineScope.async { prune(filters) }.asCompletableFuture() /** - * Downloads files from a container file system. + * Retrieves information about files of a container file system. * * @param container The container id. + * @param path The path to the file or directory inside the container file system. */ - public actual suspend fun downloadArchive(container: String) { + @OptIn(ExperimentalEncodingApi::class) + public actual suspend fun archive(container: String, path: String): ContainerArchiveInfo = requestCatching { + val response = httpClient.head("$CONTAINERS/$container/archive") { + parameter("path", path) + } + + val pathStat = response.headers["X-Docker-Container-Path-Stat"] ?: error("Missing path stat header") + val decoded = Base64.decode(pathStat).decodeToString() + return json.decodeFromString(decoded) } /** - * Uploads files into a container file system. + * Downloads files from a container file system. * * @param container The container id. + * @param remotePath The path to the file or directory inside the container file system. */ - public actual suspend fun uploadArchive(container: String) { + public actual suspend fun downloadArchive(container: String, remotePath: String): RawSource { + val contents = requestCatching { + httpClient.get("$CONTAINERS/$container/archive") { + accept(ContentType.parse("application/x-tar")) + parameter("path", remotePath) + } + }.body() + return readTarFile(contents.asSource()) } - + /** + * Uploads files into a container file system. + * + * @param container The container id. + * @param inputPath Path to the file that will be uploaded. + * @param remotePath Path to the file or directory inside the container file system. + */ + public actual suspend fun uploadArchive(container: String, inputPath: String, remotePath: String): Unit = + requestCatching { + val archive = writeTarFile(inputPath) + + httpClient.put("$CONTAINERS/$container/archive") { + parameter("path", remotePath.ifEmpty { FS_ROOT }) + parameter("noOverwriteDirNonDir", false) + setBody(archive.buffered().asInputStream().toByteReadChannel()) + } + } } diff --git a/src/nativeMain/kotlin/me/devnatan/yoki/io/TarFile.native.kt b/src/nativeMain/kotlin/me/devnatan/yoki/io/TarFile.native.kt new file mode 100644 index 0000000..b22ed9e --- /dev/null +++ b/src/nativeMain/kotlin/me/devnatan/yoki/io/TarFile.native.kt @@ -0,0 +1,11 @@ +package me.devnatan.yoki.io + +import kotlinx.io.RawSource + +internal actual fun readTarFile(input: RawSource): RawSource { + throw NotImplementedError() +} + +internal actual fun writeTarFile(filePath: String): RawSource { + throw NotImplementedError() +} diff --git a/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt b/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt index 8a0f018..390211e 100644 --- a/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt +++ b/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt @@ -1,9 +1,11 @@ package me.devnatan.yoki.resource.container import kotlinx.coroutines.flow.Flow +import kotlinx.io.RawSource import me.devnatan.yoki.models.Frame import me.devnatan.yoki.models.ResizeTTYOptions import me.devnatan.yoki.models.container.Container +import me.devnatan.yoki.models.container.ContainerArchiveInfo import me.devnatan.yoki.models.container.ContainerCreateOptions import me.devnatan.yoki.models.container.ContainerListOptions import me.devnatan.yoki.models.container.ContainerPruneFilters @@ -167,19 +169,40 @@ public actual class ContainerResource { TODO("Not yet implemented") } + /** + * Retrieves information about files of a container file system. + * + * @param container The container id. + * @param path The path to the file or directory inside the container file system. + */ + public actual suspend fun archive( + container: String, + path: String, + ): ContainerArchiveInfo { + TODO("Not yet implemented") + } + /** * Downloads files from a container file system. * * @param container The container id. + * @param remotePath The path to the file or directory inside the container file system. */ - public actual suspend fun downloadArchive(container: String) { + public actual suspend fun downloadArchive(container: String, remotePath: String): RawSource { + TODO("Not yet implemented") } /** * Uploads files into a container file system. * * @param container The container id. + * @param inputPath Path to the file that will be uploaded. + * @param remotePath Path to the file or directory inside the container file system. */ - public actual suspend fun uploadArchive(container: String) { + public actual suspend fun uploadArchive( + container: String, + inputPath: String, + remotePath: String, + ) { } } From 75b87e86c5bc1696d25abaa7a1e60fdf0ef5ce77 Mon Sep 17 00:00:00 2001 From: Natan Vieira do Nascimento Date: Thu, 17 Aug 2023 19:15:18 -0300 Subject: [PATCH 3/4] Update SUPPORTED_ENDPOINTS.md --- SUPPORTED_ENDPOINTS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SUPPORTED_ENDPOINTS.md b/SUPPORTED_ENDPOINTS.md index 6c4a818..19a5b70 100644 --- a/SUPPORTED_ENDPOINTS.md +++ b/SUPPORTED_ENDPOINTS.md @@ -1,6 +1,6 @@ # Yoki supported Docker API endpoints -Supports 43 of 106 endpoints +Supports 46 of 106 endpoints ### Containers (15/25) * [x] List containers - GET **/containers/json** @@ -24,9 +24,9 @@ Supports 43 of 106 endpoints * [ ] Attach to a container via a websocket - **POST /containers/:id/attach/ws** * [x] Wait for a container - **POST /containers/:id/wait** * [x] Remove a container - **DELETE /containers/:id** -* [ ] Get information about files in a container - **HEAD /containers/:id/archive** -* [ ] Get an archive of a filesystem resource in a container - **GET /containers/:id/archive** -* [ ] Extract an archive of files or folders to a directory in a container - **PUT /containers/:id/archive** +* [x] Get information about files in a container - **HEAD /containers/:id/archive** +* [x] Get an archive of a filesystem resource in a container - **GET /containers/:id/archive** +* [x] Extract an archive of files or folders to a directory in a container - **PUT /containers/:id/archive** * [x] Delete stopped containers - **PUT /containers/prune** ### Images (3/15) From e1cce0f9004000ed361bb3171090a6f9e52ec704 Mon Sep 17 00:00:00 2001 From: Natan Vieira do Nascimento Date: Thu, 17 Aug 2023 19:20:26 -0300 Subject: [PATCH 4/4] API dump --- api/yoki.api | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/yoki.api b/api/yoki.api index 457e0ec..1a27de5 100644 --- a/api/yoki.api +++ b/api/yoki.api @@ -1,8 +1,3 @@ -public final class me/devnatan/yoki/MainKt { - public static final fun main (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun main ([Ljava/lang/String;)V -} - public final class me/devnatan/yoki/Yoki : kotlinx/coroutines/CoroutineScope { public fun ()V public fun (Lme/devnatan/yoki/YokiConfig;)V