diff --git a/README.md b/README.md index a09a363f..415e2965 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ dependencies { ## Basic Usage -Use `DockerKotlin.create()` to create a new Docker client instance with the default settings, default settings are based on the +Use `DockerKotlin.create()` to create a new Docker client instance with the default settings, default settings are based on the current platform or environment variables, e.g.: socket path will be set to [`DOCKER_HOST`](https://docs.docker.com/compose/environment-variables/envvars/#docker_host) if present otherwise `unix://var/run/docker.sock` if the current platform is Unix-like. @@ -28,13 +28,25 @@ val client = DockerClient { } ``` -##### Get System Information + +## Resources + +* [System](#system) +* [Containers](#containers) +* [Networks](#networks) +* [Exec](#exec) + +### System + +#### Get Docker Version ```kotlin val version: SystemVersion = client.system.version() ``` -##### Create and start a Container with explicit port bindings +### Containers + +#### Create and start a Container with explicit port bindings ```kotlin val containerId = client.containers.create("busybox:latest") { @@ -51,7 +63,7 @@ val containerId = client.containers.create("busybox:latest") { client.containers.start(containerId) ``` -##### Create and start a Container with auto-assigned port bindings +#### Create and start a Container with auto-assigned port bindings ```kotlin val containerId = client.containers.create("busybox:latest") { @@ -70,13 +82,29 @@ val container = testClient.containers.inspect(id) val ports = container.networkSettings.ports ``` -##### List All Containers +#### List All Containers ```kotlin val containers: List = client.containers.list() ``` -##### Create a new Network +#### Stream Container Logs + +```kotlin +val logs: Flow = client.containers.logs("floral-fury") { + stderr = true + stdout = true +} + +logs.onStart { /* streaming started */ } + .onCompletion { /* streaming finished */ } + .catch { /* something went wrong */ } + .collect { log -> /* do something with each log */ } +``` + +### Networks + +#### Create a new Network ```kotlin val networkId: String = client.networks.create { @@ -85,18 +113,137 @@ val networkId: String = client.networks.create { } ``` -##### Stream Container Logs +#### List all Networks +```kotlin +val networks = client.networks.list() +``` +#### Connect a container to a network ```kotlin -val logs: Flow = client.containers.logs("floral-fury") { - stderr = true - stdout = true +client.networks.connect(networkId, containerId) +``` + +### Exec + +#### Execute a command in a running container +```kotlin +val execId = client.exec.create(containerId) { + command = listOf("echo", "Hello, Docker!") + attachStdout = true } -logs.onStart { /* streaming started */ } - .onCompletion { /* streaming finished */ } - .catch { /* something went wrong */ } - .collect { log -> /* do something with each log */ } +val result = client.exec.start(execId, ExecStartOptions()) +when (result) { + is ExecStartResult.Complete -> println(result.output) + else -> error("Unexpected result") +} +``` + +#### Execute a command with streaming output +```kotlin +val execId = client.exec.create(containerId) { + command = listOf("sh", "-c", "for i in 1 2 3; do echo line \$i; sleep 1; done") + attachStdout = true +} + +val result = client.exec.start(execId) { stream = true } +when (result) { + is ExecStartResult.Stream -> { + result.output.collect { chunk -> + print(chunk) + } + } + else -> error("Unexpected result") +} +``` + +#### Execute a command with separated stdout/stderr +```kotlin +val execId = client.exec.create(containerId) { + command = listOf("sh", "-c", "echo stdout; echo stderr >&2") + attachStdout = true + attachStderr = true +} + +val result = client.exec.start(execId) { demux = true } +when (result) { + is ExecStartResult.CompleteDemuxed -> { + println("STDOUT: ${result.output.stdout}") + println("STDERR: ${result.output.stderr}") + } + else -> error("Unexpected result") +} +``` + +#### Check exec exit code +```kotlin +val execId = client.exec.create(containerId) { + command = listOf("false") +} + +client.exec.start(execId) { detach = true } + +val execInfo = client.exec.inspect(execId) +println("Exit code: ${execInfo.exitCode}") // Exit code: 1 +``` + +### File Operations + +#### Copy a file from container to host +```kotlin +client.containers.copyFileFrom( + containerId, + sourcePath = "/var/log/app.log", + destinationPath = "/tmp/app.log" +) +``` + +##### Copy a file from host to container +```kotlin +client.containers.copyFileTo( + containerId, + sourcePath = "/home/user/config.json", + destinationPath = "/app/config/" +) +``` + +#### Copy a directory from container to host +```kotlin +client.containers.copyDirectoryFrom( + containerId, + sourcePath = "/app/logs", + destinationPath = "/tmp/container-logs" +) +``` + +#### Copy a directory from host to container +```kotlin +client.containers.copyDirectoryTo( + containerId, + sourcePath = "/home/user/configs", + destinationPath = "/app/" +) +``` + +#### Advanced copy with custom options +```kotlin +// Copy with custom options +client.containers.copy.copyTo( + container = containerId, + destinationPath = "/app/data", + tarArchive = myTarArchive +) { + path = "/app/data" + noOverwriteDirNonDir = true // Don't overwrite if types mismatch + copyUIDGID = true // Preserve UID/GID +} + +// Get raw tar archive from container +val result = client.containers.copyFrom(containerId, "/app/config") +val tarData = result.archiveData + +// Archive info including file metadata +val stats = result.stat ``` ## License diff --git a/api/docker-kotlin.api b/api/docker-kotlin.api index 626162dc..1f38ad12 100644 --- a/api/docker-kotlin.api +++ b/api/docker-kotlin.api @@ -78,6 +78,37 @@ public final class me/devnatan/dockerkt/io/SocketUtils { public static final field DefaultDockerUnixSocket Ljava/lang/String; } +public final class me/devnatan/dockerkt/io/TarEntry { + public fun (Ljava/lang/String;JJJZ[B)V + public synthetic fun (Ljava/lang/String;JJJZ[BILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()J + public final fun component5 ()Z + public final fun component6 ()[B + public final fun copy (Ljava/lang/String;JJJZ[B)Lme/devnatan/dockerkt/io/TarEntry; + public static synthetic fun copy$default (Lme/devnatan/dockerkt/io/TarEntry;Ljava/lang/String;JJJZ[BILjava/lang/Object;)Lme/devnatan/dockerkt/io/TarEntry; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()[B + public final fun getMode ()J + public final fun getMtime ()J + public final fun getName ()Ljava/lang/String; + public final fun getSize ()J + public fun hashCode ()I + public final fun isDirectory ()Z + public fun toString ()Ljava/lang/String; +} + +public final class me/devnatan/dockerkt/io/TarOperations { + public static final field INSTANCE Lme/devnatan/dockerkt/io/TarOperations; + public final fun collectDirectoryContents (Lkotlinx/io/files/Path;Ljava/lang/String;Ljava/util/List;)V + public final fun createTarFromDirectory (Lkotlinx/io/files/Path;Ljava/lang/String;)[B + public static synthetic fun createTarFromDirectory$default (Lme/devnatan/dockerkt/io/TarOperations;Lkotlinx/io/files/Path;Ljava/lang/String;ILjava/lang/Object;)[B + public final fun createTarFromFile (Lkotlinx/io/files/Path;)[B + public final fun extractTar ([BLkotlinx/io/files/Path;)V +} + public final class me/devnatan/dockerkt/models/BlkioWeightDevice { public static final field Companion Lme/devnatan/dockerkt/models/BlkioWeightDevice$Companion; public fun (Ljava/lang/String;I)V @@ -1290,19 +1321,19 @@ public final class me/devnatan/dockerkt/models/container/Container$Companion { public final class me/devnatan/dockerkt/models/container/ContainerArchiveInfo { public static final field Companion Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo$Companion; - 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 fun (Ljava/lang/String;JJLjava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;JJLjava/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 component3 ()J 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/dockerkt/models/container/ContainerArchiveInfo; - public static synthetic fun copy$default (Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo;Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo; + public final fun copy (Ljava/lang/String;JJLjava/lang/String;Ljava/lang/String;)Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo; + public static synthetic fun copy$default (Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo;Ljava/lang/String;JJLjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/devnatan/dockerkt/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 getMode ()J + public final fun getModifiedAtMillis ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public final fun getSize ()J public fun hashCode ()I @@ -1405,6 +1436,57 @@ public final class me/devnatan/dockerkt/models/container/ContainerConfig$Compani public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class me/devnatan/dockerkt/models/container/ContainerCopyOptions { + public static final field Companion Lme/devnatan/dockerkt/models/container/ContainerCopyOptions$Companion; + public fun (Ljava/lang/String;ZZZ)V + public synthetic fun (Ljava/lang/String;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Z + public final fun component3 ()Z + public final fun component4 ()Z + public final fun copy (Ljava/lang/String;ZZZ)Lme/devnatan/dockerkt/models/container/ContainerCopyOptions; + public static synthetic fun copy$default (Lme/devnatan/dockerkt/models/container/ContainerCopyOptions;Ljava/lang/String;ZZZILjava/lang/Object;)Lme/devnatan/dockerkt/models/container/ContainerCopyOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getCopyUIDGID ()Z + public final fun getExtractArchive ()Z + public final fun getNoOverwriteDirNonDir ()Z + public final fun getPath ()Ljava/lang/String; + public fun hashCode ()I + public final fun setCopyUIDGID (Z)V + public final fun setExtractArchive (Z)V + public final fun setNoOverwriteDirNonDir (Z)V + public final fun setPath (Ljava/lang/String;)V + public fun toString ()Ljava/lang/String; +} + +public final synthetic class me/devnatan/dockerkt/models/container/ContainerCopyOptions$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lme/devnatan/dockerkt/models/container/ContainerCopyOptions$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/devnatan/dockerkt/models/container/ContainerCopyOptions; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/devnatan/dockerkt/models/container/ContainerCopyOptions;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class me/devnatan/dockerkt/models/container/ContainerCopyOptions$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class me/devnatan/dockerkt/models/container/ContainerCopyResult { + public fun ([BLme/devnatan/dockerkt/models/container/ContainerArchiveInfo;)V + public final fun component1 ()[B + public final fun component2 ()Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo; + public final fun copy ([BLme/devnatan/dockerkt/models/container/ContainerArchiveInfo;)Lme/devnatan/dockerkt/models/container/ContainerCopyResult; + public static synthetic fun copy$default (Lme/devnatan/dockerkt/models/container/ContainerCopyResult;[BLme/devnatan/dockerkt/models/container/ContainerArchiveInfo;ILjava/lang/Object;)Lme/devnatan/dockerkt/models/container/ContainerCopyResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getArchiveData ()[B + public final fun getStat ()Lme/devnatan/dockerkt/models/container/ContainerArchiveInfo; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class me/devnatan/dockerkt/models/container/ContainerCreateOptions { public static final field Companion Lme/devnatan/dockerkt/models/container/ContainerCreateOptions$Companion; public fun ()V @@ -3858,6 +3940,11 @@ public final class me/devnatan/dockerkt/resource/ResourcePaths { public static final field INSTANCE Lme/devnatan/dockerkt/resource/ResourcePaths; } +public final class me/devnatan/dockerkt/resource/container/ArchiveNotFoundException : me/devnatan/dockerkt/resource/container/ContainerException { + public final fun getContainerId ()Ljava/lang/String; + public final fun getSourcePath ()Ljava/lang/String; +} + public final class me/devnatan/dockerkt/resource/container/ContainerAlreadyExistsException : me/devnatan/dockerkt/resource/container/ContainerException { public final fun getName ()Ljava/lang/String; } @@ -3892,12 +3979,18 @@ public final class me/devnatan/dockerkt/resource/container/ContainerRenameConfli public final class me/devnatan/dockerkt/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/dockerkt/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 fun copyDirectoryFrom (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun copyDirectoryTo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/devnatan/dockerkt/models/container/ContainerCopyOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun copyDirectoryTo$default (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/devnatan/dockerkt/models/container/ContainerCopyOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun copyFileFrom (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun copyFileTo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/devnatan/dockerkt/models/container/ContainerCopyOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun copyFileTo$default (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/devnatan/dockerkt/models/container/ContainerCopyOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun copyFrom (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun copyTo (Ljava/lang/String;Ljava/lang/String;[BLme/devnatan/dockerkt/models/container/ContainerCopyOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun copyTo$default (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;[BLme/devnatan/dockerkt/models/container/ContainerCopyOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final synthetic fun create (Lme/devnatan/dockerkt/models/container/ContainerCreateOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun createAsync (Lme/devnatan/dockerkt/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 inspect (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun inspect$default (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun inspectAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; @@ -3948,7 +4041,6 @@ public final class me/devnatan/dockerkt/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/dockerkt/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; @@ -3957,6 +4049,9 @@ public final class me/devnatan/dockerkt/resource/container/ContainerResource { } public final class me/devnatan/dockerkt/resource/container/ContainerResourceExtKt { + public static final fun copyDirectoryTo (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun copyFileTo (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun copyTo (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;[BLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun create (Lme/devnatan/dockerkt/resource/container/ContainerResource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun list (Lme/devnatan/dockerkt/resource/container/ContainerResource;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun logs (Lme/devnatan/dockerkt/resource/container/ContainerResource;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; diff --git a/api/docker-kotlin.klib.api b/api/docker-kotlin.klib.api index 319d8345..85229952 100644 --- a/api/docker-kotlin.klib.api +++ b/api/docker-kotlin.klib.api @@ -122,6 +122,34 @@ abstract interface me.devnatan.dockerkt/Closeable { // me.devnatan.dockerkt/Clos abstract fun close() // me.devnatan.dockerkt/Closeable.close|close(){}[0] } +final class me.devnatan.dockerkt.io/TarEntry { // me.devnatan.dockerkt.io/TarEntry|null[0] + constructor (kotlin/String, kotlin/Long, kotlin/Long, kotlin/Long, kotlin/Boolean, kotlin/ByteArray? = ...) // me.devnatan.dockerkt.io/TarEntry.|(kotlin.String;kotlin.Long;kotlin.Long;kotlin.Long;kotlin.Boolean;kotlin.ByteArray?){}[0] + + final val data // me.devnatan.dockerkt.io/TarEntry.data|{}data[0] + final fun (): kotlin/ByteArray? // me.devnatan.dockerkt.io/TarEntry.data.|(){}[0] + final val isDirectory // me.devnatan.dockerkt.io/TarEntry.isDirectory|{}isDirectory[0] + final fun (): kotlin/Boolean // me.devnatan.dockerkt.io/TarEntry.isDirectory.|(){}[0] + final val mode // me.devnatan.dockerkt.io/TarEntry.mode|{}mode[0] + final fun (): kotlin/Long // me.devnatan.dockerkt.io/TarEntry.mode.|(){}[0] + final val mtime // me.devnatan.dockerkt.io/TarEntry.mtime|{}mtime[0] + final fun (): kotlin/Long // me.devnatan.dockerkt.io/TarEntry.mtime.|(){}[0] + final val name // me.devnatan.dockerkt.io/TarEntry.name|{}name[0] + final fun (): kotlin/String // me.devnatan.dockerkt.io/TarEntry.name.|(){}[0] + final val size // me.devnatan.dockerkt.io/TarEntry.size|{}size[0] + final fun (): kotlin/Long // me.devnatan.dockerkt.io/TarEntry.size.|(){}[0] + + final fun component1(): kotlin/String // me.devnatan.dockerkt.io/TarEntry.component1|component1(){}[0] + final fun component2(): kotlin/Long // me.devnatan.dockerkt.io/TarEntry.component2|component2(){}[0] + final fun component3(): kotlin/Long // me.devnatan.dockerkt.io/TarEntry.component3|component3(){}[0] + final fun component4(): kotlin/Long // me.devnatan.dockerkt.io/TarEntry.component4|component4(){}[0] + final fun component5(): kotlin/Boolean // me.devnatan.dockerkt.io/TarEntry.component5|component5(){}[0] + final fun component6(): kotlin/ByteArray? // me.devnatan.dockerkt.io/TarEntry.component6|component6(){}[0] + final fun copy(kotlin/String = ..., kotlin/Long = ..., kotlin/Long = ..., kotlin/Long = ..., kotlin/Boolean = ..., kotlin/ByteArray? = ...): me.devnatan.dockerkt.io/TarEntry // me.devnatan.dockerkt.io/TarEntry.copy|copy(kotlin.String;kotlin.Long;kotlin.Long;kotlin.Long;kotlin.Boolean;kotlin.ByteArray?){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // me.devnatan.dockerkt.io/TarEntry.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // me.devnatan.dockerkt.io/TarEntry.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // me.devnatan.dockerkt.io/TarEntry.toString|toString(){}[0] +} + final class me.devnatan.dockerkt.models.container/Container { // me.devnatan.dockerkt.models.container/Container|null[0] final val appArmorProfile // me.devnatan.dockerkt.models.container/Container.appArmorProfile|{}appArmorProfile[0] final fun (): kotlin/String? // me.devnatan.dockerkt.models.container/Container.appArmorProfile.|(){}[0] @@ -215,14 +243,14 @@ final class me.devnatan.dockerkt.models.container/Container { // me.devnatan.doc } final class me.devnatan.dockerkt.models.container/ContainerArchiveInfo { // me.devnatan.dockerkt.models.container/ContainerArchiveInfo|null[0] - constructor (kotlin/String, kotlin/Long, kotlin/Int, kotlin/String, kotlin/String = ...) // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.|(kotlin.String;kotlin.Long;kotlin.Int;kotlin.String;kotlin.String){}[0] + constructor (kotlin/String, kotlin/Long, kotlin/Long, kotlin/String, kotlin/String = ...) // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.|(kotlin.String;kotlin.Long;kotlin.Long;kotlin.String;kotlin.String){}[0] final val linkTarget // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.linkTarget|{}linkTarget[0] final fun (): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.linkTarget.|(){}[0] final val mode // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.mode|{}mode[0] - final fun (): kotlin/Int // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.mode.|(){}[0] - final val modifiedAtRaw // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.modifiedAtRaw|{}modifiedAtRaw[0] - final fun (): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.modifiedAtRaw.|(){}[0] + final fun (): kotlin/Long // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.mode.|(){}[0] + final val modifiedAtMillis // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.modifiedAtMillis|{}modifiedAtMillis[0] + final fun (): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.modifiedAtMillis.|(){}[0] final val name // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.name|{}name[0] final fun (): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.name.|(){}[0] final val size // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.size|{}size[0] @@ -230,10 +258,10 @@ final class me.devnatan.dockerkt.models.container/ContainerArchiveInfo { // me.d final fun component1(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.component1|component1(){}[0] final fun component2(): kotlin/Long // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.component2|component2(){}[0] - final fun component3(): kotlin/Int // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.component3|component3(){}[0] + final fun component3(): kotlin/Long // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.component3|component3(){}[0] final fun component4(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.component4|component4(){}[0] final fun component5(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.component5|component5(){}[0] - final fun copy(kotlin/String = ..., kotlin/Long = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/String = ...): me.devnatan.dockerkt.models.container/ContainerArchiveInfo // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.copy|copy(kotlin.String;kotlin.Long;kotlin.Int;kotlin.String;kotlin.String){}[0] + final fun copy(kotlin/String = ..., kotlin/Long = ..., kotlin/Long = ..., kotlin/String = ..., kotlin/String = ...): me.devnatan.dockerkt.models.container/ContainerArchiveInfo // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.copy|copy(kotlin.String;kotlin.Long;kotlin.Long;kotlin.String;kotlin.String){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerArchiveInfo.toString|toString(){}[0] @@ -352,6 +380,61 @@ final class me.devnatan.dockerkt.models.container/ContainerConfig { // me.devnat } } +final class me.devnatan.dockerkt.models.container/ContainerCopyOptions { // me.devnatan.dockerkt.models.container/ContainerCopyOptions|null[0] + constructor (kotlin/String, kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // me.devnatan.dockerkt.models.container/ContainerCopyOptions.|(kotlin.String;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0] + + final var copyUIDGID // me.devnatan.dockerkt.models.container/ContainerCopyOptions.copyUIDGID|{}copyUIDGID[0] + final fun (): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.copyUIDGID.|(){}[0] + final fun (kotlin/Boolean) // me.devnatan.dockerkt.models.container/ContainerCopyOptions.copyUIDGID.|(kotlin.Boolean){}[0] + final var extractArchive // me.devnatan.dockerkt.models.container/ContainerCopyOptions.extractArchive|{}extractArchive[0] + final fun (): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.extractArchive.|(){}[0] + final fun (kotlin/Boolean) // me.devnatan.dockerkt.models.container/ContainerCopyOptions.extractArchive.|(kotlin.Boolean){}[0] + final var noOverwriteDirNonDir // me.devnatan.dockerkt.models.container/ContainerCopyOptions.noOverwriteDirNonDir|{}noOverwriteDirNonDir[0] + final fun (): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.noOverwriteDirNonDir.|(){}[0] + final fun (kotlin/Boolean) // me.devnatan.dockerkt.models.container/ContainerCopyOptions.noOverwriteDirNonDir.|(kotlin.Boolean){}[0] + final var path // me.devnatan.dockerkt.models.container/ContainerCopyOptions.path|{}path[0] + final fun (): kotlin/String // me.devnatan.dockerkt.models.container/ContainerCopyOptions.path.|(){}[0] + final fun (kotlin/String) // me.devnatan.dockerkt.models.container/ContainerCopyOptions.path.|(kotlin.String){}[0] + + final fun component1(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerCopyOptions.component1|component1(){}[0] + final fun component2(): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.component2|component2(){}[0] + final fun component3(): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.component3|component3(){}[0] + final fun component4(): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.component4|component4(){}[0] + final fun copy(kotlin/String = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...): me.devnatan.dockerkt.models.container/ContainerCopyOptions // me.devnatan.dockerkt.models.container/ContainerCopyOptions.copy|copy(kotlin.String;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyOptions.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // me.devnatan.dockerkt.models.container/ContainerCopyOptions.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerCopyOptions.toString|toString(){}[0] + + final object $serializer : kotlinx.serialization.internal/GeneratedSerializer { // me.devnatan.dockerkt.models.container/ContainerCopyOptions.$serializer|null[0] + final val descriptor // me.devnatan.dockerkt.models.container/ContainerCopyOptions.$serializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // me.devnatan.dockerkt.models.container/ContainerCopyOptions.$serializer.descriptor.|(){}[0] + + final fun childSerializers(): kotlin/Array> // me.devnatan.dockerkt.models.container/ContainerCopyOptions.$serializer.childSerializers|childSerializers(){}[0] + final fun deserialize(kotlinx.serialization.encoding/Decoder): me.devnatan.dockerkt.models.container/ContainerCopyOptions // me.devnatan.dockerkt.models.container/ContainerCopyOptions.$serializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, me.devnatan.dockerkt.models.container/ContainerCopyOptions) // me.devnatan.dockerkt.models.container/ContainerCopyOptions.$serializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;me.devnatan.dockerkt.models.container.ContainerCopyOptions){}[0] + } + + final object Companion { // me.devnatan.dockerkt.models.container/ContainerCopyOptions.Companion|null[0] + final fun serializer(): kotlinx.serialization/KSerializer // me.devnatan.dockerkt.models.container/ContainerCopyOptions.Companion.serializer|serializer(){}[0] + } +} + +final class me.devnatan.dockerkt.models.container/ContainerCopyResult { // me.devnatan.dockerkt.models.container/ContainerCopyResult|null[0] + constructor (kotlin/ByteArray, me.devnatan.dockerkt.models.container/ContainerArchiveInfo?) // me.devnatan.dockerkt.models.container/ContainerCopyResult.|(kotlin.ByteArray;me.devnatan.dockerkt.models.container.ContainerArchiveInfo?){}[0] + + final val archiveData // me.devnatan.dockerkt.models.container/ContainerCopyResult.archiveData|{}archiveData[0] + final fun (): kotlin/ByteArray // me.devnatan.dockerkt.models.container/ContainerCopyResult.archiveData.|(){}[0] + final val stat // me.devnatan.dockerkt.models.container/ContainerCopyResult.stat|{}stat[0] + final fun (): me.devnatan.dockerkt.models.container/ContainerArchiveInfo? // me.devnatan.dockerkt.models.container/ContainerCopyResult.stat.|(){}[0] + + final fun component1(): kotlin/ByteArray // me.devnatan.dockerkt.models.container/ContainerCopyResult.component1|component1(){}[0] + final fun component2(): me.devnatan.dockerkt.models.container/ContainerArchiveInfo? // me.devnatan.dockerkt.models.container/ContainerCopyResult.component2|component2(){}[0] + final fun copy(kotlin/ByteArray = ..., me.devnatan.dockerkt.models.container/ContainerArchiveInfo? = ...): me.devnatan.dockerkt.models.container/ContainerCopyResult // me.devnatan.dockerkt.models.container/ContainerCopyResult.copy|copy(kotlin.ByteArray;me.devnatan.dockerkt.models.container.ContainerArchiveInfo?){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // me.devnatan.dockerkt.models.container/ContainerCopyResult.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // me.devnatan.dockerkt.models.container/ContainerCopyResult.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // me.devnatan.dockerkt.models.container/ContainerCopyResult.toString|toString(){}[0] +} + final class me.devnatan.dockerkt.models.container/ContainerCreateOptions { // me.devnatan.dockerkt.models.container/ContainerCreateOptions|null[0] constructor (kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/Boolean? = ..., kotlin.collections/List? = ..., kotlin.collections/List? = ..., kotlin.collections/List? = ..., me.devnatan.dockerkt.models/HealthConfig? = ..., kotlin/Boolean? = ..., kotlin/String? = ..., kotlin.collections/List? = ..., kotlin/String? = ..., kotlin.collections/List? = ..., kotlin/Boolean? = ..., kotlin/String? = ..., kotlin.collections/List? = ..., kotlin.collections/Map? = ..., kotlin/String? = ..., kotlin/Int? = ..., kotlin.collections/List? = ..., me.devnatan.dockerkt.models/HostConfig? = ..., me.devnatan.dockerkt.models.network/NetworkingConfig? = ..., kotlin/Boolean? = ...) // me.devnatan.dockerkt.models.container/ContainerCreateOptions.|(kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.Boolean?;kotlin.collections.List?;kotlin.collections.List?;kotlin.collections.List?;me.devnatan.dockerkt.models.HealthConfig?;kotlin.Boolean?;kotlin.String?;kotlin.collections.List?;kotlin.String?;kotlin.collections.List?;kotlin.Boolean?;kotlin.String?;kotlin.collections.List?;kotlin.collections.Map?;kotlin.String?;kotlin.Int?;kotlin.collections.List?;me.devnatan.dockerkt.models.HostConfig?;me.devnatan.dockerkt.models.network.NetworkingConfig?;kotlin.Boolean?){}[0] @@ -3901,6 +3984,13 @@ final class me.devnatan.dockerkt.models/ThrottleDevice { // me.devnatan.dockerkt } } +final class me.devnatan.dockerkt.resource.container/ArchiveNotFoundException : me.devnatan.dockerkt.resource.container/ContainerException { // me.devnatan.dockerkt.resource.container/ArchiveNotFoundException|null[0] + final val containerId // me.devnatan.dockerkt.resource.container/ArchiveNotFoundException.containerId|{}containerId[0] + final fun (): kotlin/String // me.devnatan.dockerkt.resource.container/ArchiveNotFoundException.containerId.|(){}[0] + final val sourcePath // me.devnatan.dockerkt.resource.container/ArchiveNotFoundException.sourcePath|{}sourcePath[0] + final fun (): kotlin/String // me.devnatan.dockerkt.resource.container/ArchiveNotFoundException.sourcePath.|(){}[0] +} + final class me.devnatan.dockerkt.resource.container/ContainerAlreadyExistsException : me.devnatan.dockerkt.resource.container/ContainerException { // me.devnatan.dockerkt.resource.container/ContainerAlreadyExistsException|null[0] final val name // me.devnatan.dockerkt.resource.container/ContainerAlreadyExistsException.name|{}name[0] final fun (): kotlin/String // me.devnatan.dockerkt.resource.container/ContainerAlreadyExistsException.name.|(){}[0] @@ -3943,9 +4033,13 @@ final class me.devnatan.dockerkt.resource.container/ContainerResource { // me.de final fun attach(kotlin/String): kotlinx.coroutines.flow/Flow // me.devnatan.dockerkt.resource.container/ContainerResource.attach|attach(kotlin.String){}[0] final fun logs(kotlin/String, me.devnatan.dockerkt.models.container/ContainerLogsOptions): kotlinx.coroutines.flow/Flow // me.devnatan.dockerkt.resource.container/ContainerResource.logs|logs(kotlin.String;me.devnatan.dockerkt.models.container.ContainerLogsOptions){}[0] - final suspend fun archive(kotlin/String, kotlin/String = ...): me.devnatan.dockerkt.models.container/ContainerArchiveInfo // me.devnatan.dockerkt.resource.container/ContainerResource.archive|archive(kotlin.String;kotlin.String){}[0] + final suspend fun copyDirectoryFrom(kotlin/String, kotlin/String, kotlin/String) // me.devnatan.dockerkt.resource.container/ContainerResource.copyDirectoryFrom|copyDirectoryFrom(kotlin.String;kotlin.String;kotlin.String){}[0] + final suspend fun copyDirectoryTo(kotlin/String, kotlin/String, kotlin/String, me.devnatan.dockerkt.models.container/ContainerCopyOptions = ...) // me.devnatan.dockerkt.resource.container/ContainerResource.copyDirectoryTo|copyDirectoryTo(kotlin.String;kotlin.String;kotlin.String;me.devnatan.dockerkt.models.container.ContainerCopyOptions){}[0] + final suspend fun copyFileFrom(kotlin/String, kotlin/String, kotlin/String) // me.devnatan.dockerkt.resource.container/ContainerResource.copyFileFrom|copyFileFrom(kotlin.String;kotlin.String;kotlin.String){}[0] + final suspend fun copyFileTo(kotlin/String, kotlin/String, kotlin/String, me.devnatan.dockerkt.models.container/ContainerCopyOptions = ...) // me.devnatan.dockerkt.resource.container/ContainerResource.copyFileTo|copyFileTo(kotlin.String;kotlin.String;kotlin.String;me.devnatan.dockerkt.models.container.ContainerCopyOptions){}[0] + final suspend fun copyFrom(kotlin/String, kotlin/String): me.devnatan.dockerkt.models.container/ContainerCopyResult // me.devnatan.dockerkt.resource.container/ContainerResource.copyFrom|copyFrom(kotlin.String;kotlin.String){}[0] + final suspend fun copyTo(kotlin/String, kotlin/String, kotlin/ByteArray, me.devnatan.dockerkt.models.container/ContainerCopyOptions = ...) // me.devnatan.dockerkt.resource.container/ContainerResource.copyTo|copyTo(kotlin.String;kotlin.String;kotlin.ByteArray;me.devnatan.dockerkt.models.container.ContainerCopyOptions){}[0] final suspend fun create(me.devnatan.dockerkt.models.container/ContainerCreateOptions): kotlin/String // me.devnatan.dockerkt.resource.container/ContainerResource.create|create(me.devnatan.dockerkt.models.container.ContainerCreateOptions){}[0] - final suspend fun downloadArchive(kotlin/String, kotlin/String): kotlinx.io/RawSource // me.devnatan.dockerkt.resource.container/ContainerResource.downloadArchive|downloadArchive(kotlin.String;kotlin.String){}[0] final suspend fun inspect(kotlin/String, kotlin/Boolean = ...): me.devnatan.dockerkt.models.container/Container // me.devnatan.dockerkt.resource.container/ContainerResource.inspect|inspect(kotlin.String;kotlin.Boolean){}[0] final suspend fun kill(kotlin/String, kotlin/String? = ...) // me.devnatan.dockerkt.resource.container/ContainerResource.kill|kill(kotlin.String;kotlin.String?){}[0] final suspend fun list(me.devnatan.dockerkt.models.container/ContainerListOptions = ...): kotlin.collections/List // me.devnatan.dockerkt.resource.container/ContainerResource.list|list(me.devnatan.dockerkt.models.container.ContainerListOptions){}[0] @@ -3958,7 +4052,6 @@ final class me.devnatan.dockerkt.resource.container/ContainerResource { // me.de final suspend fun start(kotlin/String, kotlin/String? = ...) // me.devnatan.dockerkt.resource.container/ContainerResource.start|start(kotlin.String;kotlin.String?){}[0] final suspend fun stop(kotlin/String, kotlin.time/Duration? = ...) // me.devnatan.dockerkt.resource.container/ContainerResource.stop|stop(kotlin.String;kotlin.time.Duration?){}[0] final suspend fun unpause(kotlin/String) // me.devnatan.dockerkt.resource.container/ContainerResource.unpause|unpause(kotlin.String){}[0] - final suspend fun uploadArchive(kotlin/String, kotlin/String, kotlin/String) // me.devnatan.dockerkt.resource.container/ContainerResource.uploadArchive|uploadArchive(kotlin.String;kotlin.String;kotlin.String){}[0] final suspend fun wait(kotlin/String, kotlin/String? = ...): me.devnatan.dockerkt.models.container/ContainerWaitResult // me.devnatan.dockerkt.resource.container/ContainerResource.wait|wait(kotlin.String;kotlin.String?){}[0] } @@ -4195,6 +4288,13 @@ sealed class me.devnatan.dockerkt.models.exec/ExecStartResult { // me.devnatan.d final object Detached : me.devnatan.dockerkt.models.exec/ExecStartResult // me.devnatan.dockerkt.models.exec/ExecStartResult.Detached|null[0] } +final object me.devnatan.dockerkt.io/TarOperations { // me.devnatan.dockerkt.io/TarOperations|null[0] + final fun collectDirectoryContents(kotlinx.io.files/Path, kotlin/String, kotlin.collections/MutableList) // me.devnatan.dockerkt.io/TarOperations.collectDirectoryContents|collectDirectoryContents(kotlinx.io.files.Path;kotlin.String;kotlin.collections.MutableList){}[0] + final fun createTarFromDirectory(kotlinx.io.files/Path, kotlin/String = ...): kotlin/ByteArray // me.devnatan.dockerkt.io/TarOperations.createTarFromDirectory|createTarFromDirectory(kotlinx.io.files.Path;kotlin.String){}[0] + final fun createTarFromFile(kotlinx.io.files/Path): kotlin/ByteArray // me.devnatan.dockerkt.io/TarOperations.createTarFromFile|createTarFromFile(kotlinx.io.files.Path){}[0] + final fun extractTar(kotlin/ByteArray, kotlinx.io.files/Path) // me.devnatan.dockerkt.io/TarOperations.extractTar|extractTar(kotlin.ByteArray;kotlinx.io.files.Path){}[0] +} + final object me.devnatan.dockerkt.resource/ResourcePaths { // me.devnatan.dockerkt.resource/ResourcePaths|null[0] final const val CONTAINERS // me.devnatan.dockerkt.resource/ResourcePaths.CONTAINERS|{}CONTAINERS[0] final fun (): kotlin/String // me.devnatan.dockerkt.resource/ResourcePaths.CONTAINERS.|(){}[0] @@ -4280,6 +4380,9 @@ final inline fun (me.devnatan.dockerkt.models.container/ContainerListOptions).me final inline fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/logs(kotlin/String, kotlin/Function1): kotlinx.coroutines.flow/Flow // me.devnatan.dockerkt.resource.container/logs|logs@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.String;kotlin.Function1){}[0] final inline fun (me.devnatan.dockerkt.resource.system/SystemResource).me.devnatan.dockerkt.resource.system/events(kotlin/Function1): kotlinx.coroutines.flow/Flow // me.devnatan.dockerkt.resource.system/events|events@me.devnatan.dockerkt.resource.system.SystemResource(kotlin.Function1){}[0] final inline fun me.devnatan.dockerkt/DockerClient(crossinline kotlin/Function1): me.devnatan.dockerkt/DockerClient // me.devnatan.dockerkt/DockerClient|DockerClient(kotlin.Function1){}[0] +final suspend fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/copyDirectoryTo(kotlin/String, kotlin/String, kotlin/String, kotlin/Function1) // me.devnatan.dockerkt.resource.container/copyDirectoryTo|copyDirectoryTo@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.String;kotlin.String;kotlin.String;kotlin.Function1){}[0] +final suspend fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/copyFileTo(kotlin/String, kotlin/String, kotlin/String, kotlin/Function1) // me.devnatan.dockerkt.resource.container/copyFileTo|copyFileTo@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.String;kotlin.String;kotlin.String;kotlin.Function1){}[0] +final suspend fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/copyTo(kotlin/String, kotlin/String, kotlin/ByteArray, kotlin/Function1) // me.devnatan.dockerkt.resource.container/copyTo|copyTo@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.String;kotlin.String;kotlin.ByteArray;kotlin.Function1){}[0] final suspend inline fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/create(kotlin/Function1): kotlin/String // me.devnatan.dockerkt.resource.container/create|create@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.Function1){}[0] final suspend inline fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/list(kotlin/Function1): kotlin.collections/List // me.devnatan.dockerkt.resource.container/list|list@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.Function1){}[0] final suspend inline fun (me.devnatan.dockerkt.resource.container/ContainerResource).me.devnatan.dockerkt.resource.container/prune(kotlin/Function1): me.devnatan.dockerkt.models.container/ContainerPruneResult // me.devnatan.dockerkt.resource.container/prune|prune@me.devnatan.dockerkt.resource.container.ContainerResource(kotlin.Function1){}[0] diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/io/FileSystemUtils.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/io/FileSystemUtils.kt new file mode 100644 index 00000000..8309079e --- /dev/null +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/io/FileSystemUtils.kt @@ -0,0 +1,84 @@ +package me.devnatan.dockerkt.io + +import kotlinx.io.buffered +import kotlinx.io.files.FileMetadata +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.files.SystemTemporaryDirectory +import kotlinx.io.readByteArray +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +/** Multiplatform file operations wrapper. */ +internal object FileSystemUtils { + private val fs = SystemFileSystem + + fun readFile(path: Path): ByteArray = + fs.source(path).buffered().use { source -> + source.readByteArray() + } + + fun writeFile( + path: Path, + data: ByteArray, + ) = fs.sink(path).buffered().use { sink -> + sink.write(data) + } + + fun createDirectories(path: Path) { + fs.createDirectories(path) + } + + fun exists(path: Path): Boolean = fs.exists(path) + + fun isDirectory(path: Path): Boolean = fs.metadataOrNull(path)?.isDirectory ?: false + + fun listDirectory(path: Path): List = fs.list(path).toList() + + fun getMetadata(path: Path): FileMetadata? = fs.metadataOrNull(path) + + @OptIn(ExperimentalTime::class) + fun currentTimeSeconds(): Long = Clock.System.now().epochSeconds + + fun delete(path: Path) { + fs.delete(path) + } + + fun deleteRecursively(path: Path) { + if (!exists(path)) { + return + } + + if (isDirectory(path)) { + val children = listDirectory(path) + children.forEach { child -> + deleteRecursively(child) + } + } + + delete(path) + } + + fun createTempDirectory(prefix: String = "docker-kotlin-"): Path { + val timestamp = currentTimeSeconds() + val random = kotlin.random.Random.nextInt(10000) + val dirName = "$prefix$timestamp-$random" + val path = Path(SystemTemporaryDirectory, dirName) + + createDirectories(path) + return path + } + + fun createTempFile( + prefix: String = "docker-kotlin-", + suffix: String = ".tmp", + ): Path { + val timestamp = currentTimeSeconds() + val random = kotlin.random.Random.nextInt(10000) + val fileName = "$prefix$timestamp-$random$suffix" + val path = Path(SystemTemporaryDirectory, fileName) + + writeFile(path, ByteArray(0)) + return path + } +} diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/io/TarFile.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/io/TarFile.kt index 11afa381..5b79cbb7 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/io/TarFile.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/io/TarFile.kt @@ -1,7 +1,379 @@ package me.devnatan.dockerkt.io +import kotlinx.io.Buffer import kotlinx.io.RawSource +import kotlinx.io.files.Path +import kotlinx.io.readByteArray +import kotlinx.io.readTo +import kotlin.math.min -internal expect fun readTarFile(input: RawSource): RawSource +public data class TarEntry( + val name: String, + val size: Long, + val mode: Long, + val mtime: Long, + val isDirectory: Boolean, + val data: ByteArray? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false -internal expect fun writeTarFile(filePath: String): RawSource + other as TarEntry + + if (name != other.name) return false + if (size != other.size) return false + if (mode != other.mode) return false + if (mtime != other.mtime) return false + if (isDirectory != other.isDirectory) return false + if (data != null) { + if (other.data == null) return false + if (!data.contentEquals(other.data)) return false + } else if (other.data != null) { + return false + } + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + mode.hashCode() + result = 31 * result + mtime.hashCode() + result = 31 * result + isDirectory.hashCode() + result = 31 * result + (data?.contentHashCode() ?: 0) + return result + } +} + +internal object TarUtils { + private const val BLOCK_SIZE = 512 + private const val NAME_SIZE = 100 + private const val MODE_SIZE = 8 + private const val UID_SIZE = 8 + private const val GID_SIZE = 8 + private const val SIZE_SIZE = 12 + private const val MTIME_SIZE = 12 + private const val CHECKSUM_SIZE = 8 + + private const val TYPE_REGULAR = '0'.code.toByte() + private const val TYPE_DIRECTORY = '5'.code.toByte() + + fun createTarArchive(entries: List): ByteArray { + val buffer = Buffer() + + entries.forEach { entry -> + writeEntry(buffer, entry) + } + + // Write two empty blocks to mark end of archive + buffer.write(ByteArray(BLOCK_SIZE * 2)) + + return buffer.readByteArray() + } + + private fun writeEntry( + buffer: Buffer, + entry: TarEntry, + ) { + val header = ByteArray(BLOCK_SIZE) + + writeString(header, 0, entry.name, NAME_SIZE) // Name (100 bytes) + writeOctal(header, 100, entry.mode, MODE_SIZE) // Mode (8 bytes) - octal + writeOctal(header, 108, 0, UID_SIZE) // UID (8 bytes) - octal + writeOctal(header, 116, 0, GID_SIZE) // GID (8 bytes) - octal + writeOctal(header, 124, entry.size, SIZE_SIZE) // Size (12 bytes) - octal + writeOctal(header, 136, entry.mtime, MTIME_SIZE) // Mtime (12 bytes) - octal + + // Checksum (8 bytes) - initially fill with spaces + repeat(CHECKSUM_SIZE) { header[148 + it] = ' '.code.toByte() } + + // Type flag (1 byte) + header[156] = if (entry.isDirectory) TYPE_DIRECTORY else TYPE_REGULAR + + // Calculate and write checksum + val checksum = header.sumOf { it.toInt() and 0xFF } + writeOctal(header, 148, checksum.toLong(), CHECKSUM_SIZE - 1) + header[148 + CHECKSUM_SIZE - 1] = 0 // Null terminator for checksum + + // Write header + buffer.write(header) + + // Write data (if not a directory) + if (!entry.isDirectory && entry.data != null) { + buffer.write(entry.data) + + // Pad to block size + val padding = BLOCK_SIZE - (entry.data.size % BLOCK_SIZE) + if (padding < BLOCK_SIZE) { + buffer.write(ByteArray(padding)) + } + } + } + + fun extractTarArchive(tarData: ByteArray): List { + val buffer = Buffer() + buffer.write(tarData) + + val entries = mutableListOf() + + while (buffer.size >= BLOCK_SIZE) { + val header = ByteArray(BLOCK_SIZE) + buffer.readTo(header) + + // Check if this is an empty block (end of archive) + if (header.all { it == 0.toByte() }) { + break + } + + val entry = parseEntry(buffer, header) + entries.add(entry) + } + + return entries + } + + /** Parses a TAR entry from header and buffer. */ + private fun parseEntry( + buffer: Buffer, + header: ByteArray, + ): TarEntry { + val name = readString(header, 0, NAME_SIZE) + val size = readOctal(header, 124, SIZE_SIZE) + + val mode = readOctal(header, 100, MODE_SIZE) + val mtime = readOctal(header, 136, MTIME_SIZE) + val typeFlag = header[156] + + val isDirectory = typeFlag == TYPE_DIRECTORY + + if (size == 0L) { + return TarEntry( + name = name, + size = size, + mode = mode, + mtime = mtime, + isDirectory = isDirectory, + data = byteArrayOf(), + ) + } + + val data = + if (!isDirectory && size > 0) { + val fileData = ByteArray(size.toInt()) + buffer.readTo(fileData) + + // Skip padding + val padding = BLOCK_SIZE - (size % BLOCK_SIZE).toInt() + if (padding < BLOCK_SIZE) { + buffer.skip(padding.toLong()) + } + + fileData + } else { + null + } + + return TarEntry( + name = name, + size = size, + mode = mode, + mtime = mtime, + isDirectory = isDirectory, + data = data, + ) + } + + private fun writeString( + dest: ByteArray, + offset: Int, + value: String, + maxLength: Int, + ) { + val bytes = value.encodeToByteArray() + val length = min(bytes.size, maxLength - 1) // Leave room for null terminator + bytes.copyInto(dest, offset, 0, length) + dest[offset + length] = 0 // Null terminator + } + + /** Writes an octal number to a byte array at the given offset.*/ + private fun writeOctal( + dest: ByteArray, + offset: Int, + value: Long, + maxLength: Int, + ) { + val octal = value.toString(8) + val length = min(octal.length, maxLength - 1) + octal.takeLast(length).forEachIndexed { index, char -> + dest[offset + index] = char.code.toByte() + } + dest[offset + length] = 0 // Null terminator + } + + /** Reads a null-terminated string from a byte array. */ + private fun readString( + source: ByteArray, + offset: Int, + maxLength: Int, + ): String { + val end = (offset until offset + maxLength).firstOrNull { source[it] == 0.toByte() } ?: (offset + maxLength) + return source.decodeToString(offset, end) + } + + private fun readOctal( + source: ByteArray, + offset: Int, + maxLength: Int, + ): Long { + val str = readString(source, offset, maxLength).trim() + return if (str.isEmpty()) 0 else str.toLongOrNull(8) ?: 0 + } +} + +public object TarOperations { + /** + * Creates a tar archive from a single file. + */ + public fun createTarFromFile(filePath: Path): ByteArray { + val data = FileSystemUtils.readFile(filePath) + val metadata = FileSystemUtils.getMetadata(filePath) + val fileName = filePath.name + + val entry = + TarEntry( + name = fileName, + size = data.size.toLong(), + mode = 644, // Default file permissions + mtime = FileSystemUtils.currentTimeSeconds(), + isDirectory = false, + data = data, + ) + + return TarUtils.createTarArchive(listOf(entry)) + } + + /** + * Creates a tar archive from a directory recursively. + */ + public fun createTarFromDirectory( + dirPath: Path, + basePath: String = "", + ): ByteArray { + val entries = mutableListOf() + collectEntriesFromDirectory(dirPath, basePath, entries) + return TarUtils.createTarArchive(entries) + } + + public fun collectDirectoryContents( + dirPath: Path, + basePath: String, + entries: MutableList, + ) { + val files = FileSystemUtils.listDirectory(dirPath) + + files.forEach { file -> + val fileName = file.name + val entryName = if (basePath.isEmpty()) fileName else "$basePath/$fileName" + + if (FileSystemUtils.isDirectory(file)) { + // Add directory entry + entries.add( + TarEntry( + name = "$entryName/", + size = 0, + mode = 755, + mtime = FileSystemUtils.currentTimeSeconds(), + isDirectory = true, + data = null, + ), + ) + + // Recursively add contents + collectDirectoryContents(file, entryName, entries) + } else { + // Add file entry + val data = FileSystemUtils.readFile(file) + entries.add( + TarEntry( + name = entryName, + size = data.size.toLong(), + mode = 644, + mtime = FileSystemUtils.currentTimeSeconds(), + isDirectory = false, + data = data, + ), + ) + } + } + } + + private fun collectEntriesFromDirectory( + dirPath: Path, + basePath: String, + entries: MutableList, + ) { + val files = FileSystemUtils.listDirectory(dirPath) + + files.forEach { file -> + val fileName = file.name + val entryName = if (basePath.isEmpty()) fileName else "$basePath/$fileName" + + if (FileSystemUtils.isDirectory(file)) { + // Add directory entry + entries.add( + TarEntry( + name = "$entryName/", + size = 0, + mode = 755, // Default directory permissions + mtime = FileSystemUtils.currentTimeSeconds(), + isDirectory = true, + data = null, + ), + ) + + // Recursively add contents + collectEntriesFromDirectory(file, entryName, entries) + } else { + // Add file entry + val data = FileSystemUtils.readFile(file) + entries.add( + TarEntry( + name = entryName, + size = data.size.toLong(), + mode = 644, // Default file permissions + mtime = FileSystemUtils.currentTimeSeconds(), + isDirectory = false, + data = data, + ), + ) + } + } + } + + /** + * Extracts a tar archive to the local filesystem. + */ + public fun extractTar( + tarData: ByteArray, + destinationPath: Path, + ) { + val entries = TarUtils.extractTarArchive(tarData) + + entries.forEach { entry -> + destinationPath + val entryPath = Path(destinationPath, entry.name) + + if (entry.isDirectory) { + FileSystemUtils.createDirectories(entryPath) + } else { + // Create parent directories + entryPath.parent?.let { FileSystemUtils.createDirectories(it) } + + // Write file + entry.data?.let { FileSystemUtils.writeFile(entryPath, it) } + } + } + } +} diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerArchiveInfo.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerArchiveInfo.kt index 737cb215..6a3c52fe 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerArchiveInfo.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerArchiveInfo.kt @@ -7,13 +7,18 @@ import kotlin.time.Instant @Serializable public data class ContainerArchiveInfo( + @SerialName("name") val name: String, + @SerialName("size") val size: Long, - val mode: Int, - @SerialName("mtime") val modifiedAtRaw: String, + @SerialName("mode") + val mode: Long, + @SerialName("mtime") + val modifiedAtMillis: String, + @SerialName("linkTarget") val linkTarget: String = "", ) @OptIn(ExperimentalTime::class) public val ContainerArchiveInfo.modifiedAt: Instant - get() = Instant.parse(modifiedAtRaw) + get() = Instant.parse(modifiedAtMillis) diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerCopyOptions.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerCopyOptions.kt new file mode 100644 index 00000000..5226668b --- /dev/null +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerCopyOptions.kt @@ -0,0 +1,31 @@ +package me.devnatan.dockerkt.models.container + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +public data class ContainerCopyOptions( + /** Path inside the container where files will be copied to. */ + @SerialName("Path") + var path: String, + /** + * If true, extract the tar archive in the destination directory. + * If false, copy the tar archive itself. + * Default: true + */ + @Transient + var extractArchive: Boolean = true, + /** + * If true, do not overwrite existing files/directories. + * Default: false + */ + @Transient + var noOverwriteDirNonDir: Boolean = false, + /** + * If true, copy UID/GID maps for the files. + * Default: false + */ + @Transient + var copyUIDGID: Boolean = false, +) diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerCopyResult.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerCopyResult.kt new file mode 100644 index 00000000..410da739 --- /dev/null +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/models/container/ContainerCopyResult.kt @@ -0,0 +1,24 @@ +package me.devnatan.dockerkt.models.container + +public data class ContainerCopyResult( + val archiveData: ByteArray, + val stat: ContainerArchiveInfo?, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ContainerCopyResult + + if (!archiveData.contentEquals(other.archiveData)) return false + if (stat != other.stat) return false + + return true + } + + override fun hashCode(): Int { + var result = archiveData.contentHashCode() + result = 31 * result + (stat?.hashCode() ?: 0) + return result + } +} diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerException.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerException.kt index 58426e7f..7182ba5d 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerException.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerException.kt @@ -41,3 +41,9 @@ public class ContainerNotRunningException internal constructor( cause: Throwable?, public val containerId: String?, ) : ContainerException(cause) + +public class ArchiveNotFoundException internal constructor( + cause: Throwable?, + public val containerId: String, + public val sourcePath: String, +) : ContainerException(cause) diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.kt index 6ac78d4f..8a137e50 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.kt @@ -1,12 +1,12 @@ package me.devnatan.dockerkt.resource.container import kotlinx.coroutines.flow.Flow -import kotlinx.io.RawSource import me.devnatan.dockerkt.DockerResponseException import me.devnatan.dockerkt.models.Frame import me.devnatan.dockerkt.models.ResizeTTYOptions import me.devnatan.dockerkt.models.container.Container -import me.devnatan.dockerkt.models.container.ContainerArchiveInfo +import me.devnatan.dockerkt.models.container.ContainerCopyOptions +import me.devnatan.dockerkt.models.container.ContainerCopyResult import me.devnatan.dockerkt.models.container.ContainerCreateOptions import me.devnatan.dockerkt.models.container.ContainerListOptions import me.devnatan.dockerkt.models.container.ContainerLogsOptions @@ -16,11 +16,8 @@ import me.devnatan.dockerkt.models.container.ContainerRemoveOptions import me.devnatan.dockerkt.models.container.ContainerSummary import me.devnatan.dockerkt.models.container.ContainerWaitResult import me.devnatan.dockerkt.resource.image.ImageNotFoundException -import kotlin.jvm.JvmOverloads import kotlin.time.Duration -internal const val FileSystemRoot = "/" - public expect class ContainerResource { /** * Returns a list of all containers. @@ -160,44 +157,115 @@ public expect class ContainerResource { // TODO documentation public suspend fun prune(filters: ContainerPruneFilters = ContainerPruneFilters()): ContainerPruneResult + public fun logs( + container: String, + options: ContainerLogsOptions, + ): Flow + /** - * Retrieves information about files of a container file system. + * Copy files or folders from a container to the local filesystem. + * + * This method retrieves files from a container as a tar archive. + * The archive is then extracted to the local filesystem. * - * @param container The container id. - * @param path The path to the file or directory inside the container file system. + * @param container Container id or name. + * @param sourcePath Path to the file or folder inside the container. + * @return [ContainerCopyResult] containing the tar archive and path statistics. + * @throws ContainerNotFoundException If the container is not found. + * @throws ArchiveNotFoundException If the path does not exist in the container. */ - @JvmOverloads - public suspend fun archive( + public suspend fun copyFrom( container: String, - path: String = FileSystemRoot, - ): ContainerArchiveInfo + sourcePath: String, + ): ContainerCopyResult /** - * Downloads files from a container file system. + * Copy files or folders from the local filesystem to a container. + * + * This method uploads a tar archive to a container and extracts it + * at the specified destination path. * - * @param container The container id. - * @param remotePath The path to the file or directory inside the container file system. + * @param container Container id or name. + * @param destinationPath Path inside the container where files will be extracted. + * @param tarArchive The tar archive containing files to copy. + * @param options Additional options for the copy operation. + * @throws ContainerNotFoundException If the container is not found. + * @throws IllegalArgumentException If the destination path is invalid. */ - public suspend fun downloadArchive( + public suspend fun copyTo( container: String, - remotePath: String, - ): RawSource + destinationPath: String, + tarArchive: ByteArray, + options: ContainerCopyOptions = ContainerCopyOptions(path = destinationPath), + ) /** - * Uploads files into a container file system. + * Copy a single file from the local filesystem to a container. + * + * This is a convenience method that creates a tar archive from a single file + * and uploads it to the container. * - * @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. + * @param container Container id or name. + * @param sourcePath Path to the file on the local filesystem. + * @param destinationPath Path inside the container where the file will be copied. + * @param options Additional options for the copy operation. + * @throws ArchiveNotFoundException If the source file does not exist. + * @throws ContainerNotFoundException If the container is not found. */ - public suspend fun uploadArchive( + public suspend fun copyFileTo( container: String, - inputPath: String, - remotePath: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions = ContainerCopyOptions(path = destinationPath), ) - public fun logs( + /** + * Copy a file from a container to the local filesystem. + * + * This is a convenience method that retrieves a tar archive from the container + * and extracts a single file from it. + * + * @param container Container id or name. + * @param sourcePath Path to the file inside the container. + * @param destinationPath Path on the local filesystem where the file will be saved. + * @throws ContainerNotFoundException If the container is not found. + * @throws ArchiveNotFoundException If the source path does not exist in the container. + */ + public suspend fun copyFileFrom( container: String, - options: ContainerLogsOptions, - ): Flow + sourcePath: String, + destinationPath: String, + ) + + /** + * Copy a directory from a container to the local filesystem. + * + * @param container Container id or name. + * @param sourcePath Path to the directory inside the container. + * @param destinationPath Path on the local filesystem where files will be extracted. + * @throws ContainerNotFoundException If the container is not found. + * @throws ArchiveNotFoundException If the source path does not exist in the container. + */ + public suspend fun copyDirectoryFrom( + container: String, + sourcePath: String, + destinationPath: String, + ) + + /** + * Copy a directory from the local filesystem to a container. + * + * @param container Container ID or name. + * @param sourcePath Path to the directory on the local filesystem. + * @param destinationPath Path inside the container where files will be copied. + * @param options Additional options for the copy operation. + * @throws kotlinx.io.files.FileNotFoundException If the source directory does not exist. + * @throws ContainerNotFoundException If the container is not found. + */ + public suspend fun copyDirectoryTo( + container: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions = ContainerCopyOptions(path = destinationPath), + ) } diff --git a/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResourceExt.kt b/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResourceExt.kt index a6508dd0..de9b7915 100644 --- a/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResourceExt.kt +++ b/src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResourceExt.kt @@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.Flow import me.devnatan.dockerkt.DockerResponseException import me.devnatan.dockerkt.models.Frame import me.devnatan.dockerkt.models.ResizeTTYOptions +import me.devnatan.dockerkt.models.container.ContainerCopyOptions +import me.devnatan.dockerkt.models.container.ContainerCopyResult import me.devnatan.dockerkt.models.container.ContainerCreateOptions import me.devnatan.dockerkt.models.container.ContainerListOptions import me.devnatan.dockerkt.models.container.ContainerLogsOptions @@ -77,3 +79,78 @@ public fun ContainerResource.logs(container: String): Flow = stdout = true, ), ) + +/** + * Copy files or folders from the local filesystem to a container. + * + * This method uploads a tar archive to a container and extracts it + * at the specified destination path. + * + * @param container Container id or name. + * @param destinationPath Path inside the container where files will be extracted. + * @param tarArchive The tar archive containing files to copy. + * @param options Additional options for the copy operation. + * @throws ContainerNotFoundException If the container is not found. + * @throws IllegalArgumentException If the destination path is invalid. + */ +public suspend fun ContainerResource.copyTo( + container: String, + destinationPath: String, + tarArchive: ByteArray, + options: ContainerCopyOptions.() -> Unit, +): Unit = + copyTo( + container = container, + destinationPath = destinationPath, + tarArchive = tarArchive, + options = ContainerCopyOptions(path = destinationPath).apply(options), + ) + +/** + * Copy a single file from the local filesystem to a container. + * + * This is a convenience method that creates a tar archive from a single file + * and uploads it to the container. + * + * @param container Container id or name. + * @param sourcePath Path to the file on the local filesystem. + * @param destinationPath Path inside the container where the file will be copied. + * @param options Additional options for the copy operation. + * @throws ArchiveNotFoundException If the source file does not exist. + * @throws ContainerNotFoundException If the container is not found. + */ +public suspend fun ContainerResource.copyFileTo( + container: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions.() -> Unit, +): Unit = + copyFileTo( + container = container, + sourcePath = sourcePath, + destinationPath = destinationPath, + options = ContainerCopyOptions(path = destinationPath).apply(options), + ) + +/** + * Copy a directory from the local filesystem to a container. + * + * @param container Container ID or name. + * @param sourcePath Path to the directory on the local filesystem. + * @param destinationPath Path inside the container where files will be copied. + * @param options Additional options for the copy operation. + * @throws kotlinx.io.files.FileNotFoundException If the source directory does not exist. + * @throws ContainerNotFoundException If the container is not found. + */ +public suspend fun ContainerResource.copyDirectoryTo( + container: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions.() -> Unit, +): Unit = + copyDirectoryTo( + container = container, + sourcePath = sourcePath, + destinationPath = destinationPath, + options = ContainerCopyOptions(path = destinationPath).apply(options), + ) diff --git a/src/commonTest/kotlin/me/devnatan/dockerkt/io/TarUtilsTest.kt b/src/commonTest/kotlin/me/devnatan/dockerkt/io/TarUtilsTest.kt new file mode 100644 index 00000000..ead44596 --- /dev/null +++ b/src/commonTest/kotlin/me/devnatan/dockerkt/io/TarUtilsTest.kt @@ -0,0 +1,554 @@ +package me.devnatan.dockerkt.io + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class TarUtilsTest { + @Test + fun `create and extract single file tar archive`() { + val fileName = "test.txt" + val content = "Hello, World!".encodeToByteArray() + + val entry = + TarEntry( + name = fileName, + size = content.size.toLong(), + isDirectory = false, + data = content, + mode = 644, + mtime = 1234567890, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + val extractedEntry = extracted[0] + + assertEquals(fileName, extractedEntry.name) + assertEquals(content.size.toLong(), extractedEntry.size) + assertEquals(644L, extractedEntry.mode) + assertEquals(1234567890L, extractedEntry.mtime) + assertFalse(extractedEntry.isDirectory) + assertNotNull(extractedEntry.data) + assertContentEquals(content, extractedEntry.data) + } + + @Test + fun `create and extract multiple files tar archive`() { + val entries = + listOf( + TarEntry( + name = "file1.txt", + size = 5, + mode = 644, + mtime = 1000000, + isDirectory = false, + data = "file1".encodeToByteArray(), + ), + TarEntry( + name = "file2.txt", + size = 5, + mode = 644, + mtime = 1000001, + isDirectory = false, + data = "file2".encodeToByteArray(), + ), + TarEntry( + name = "file3.txt", + size = 5, + mode = 755, + mtime = 1000002, + isDirectory = false, + data = "file3".encodeToByteArray(), + ), + ) + + val tarData = TarUtils.createTarArchive(entries) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(3, extracted.size) + + assertEquals("file1.txt", extracted[0].name) + assertEquals("file1", extracted[0].data?.decodeToString()) + + assertEquals("file2.txt", extracted[1].name) + assertEquals("file2", extracted[1].data?.decodeToString()) + + assertEquals("file3.txt", extracted[2].name) + assertEquals("file3", extracted[2].data?.decodeToString()) + assertEquals(755L, extracted[2].mode) + } + + @Test + fun `create and extract directory entry`() { + val entry = + TarEntry( + name = "testdir/", + size = 0, + mode = 755, + mtime = 1234567890, + isDirectory = true, + data = null, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + val extractedEntry = extracted[0] + + assertEquals("testdir/", extractedEntry.name) + assertEquals(0L, extractedEntry.size) + assertEquals(755L, extractedEntry.mode) + assertTrue(extractedEntry.isDirectory) + assertNotNull(extractedEntry.data) + assertTrue(extractedEntry.data.isEmpty()) + } + + @Test + fun `create and extract nested directory structure`() { + val entries = + listOf( + TarEntry( + name = "parent/", + size = 0, + mode = 755, + mtime = 1000000, + isDirectory = true, + data = null, + ), + TarEntry( + name = "parent/child/", + size = 0, + mode = 755, + mtime = 1000001, + isDirectory = true, + data = null, + ), + TarEntry( + name = "parent/child/file.txt", + size = 7, + mode = 644, + mtime = 1000002, + isDirectory = false, + data = "content".encodeToByteArray(), + ), + ) + + val tarData = TarUtils.createTarArchive(entries) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(3, extracted.size) + + assertTrue(extracted[0].isDirectory) + assertEquals("parent/", extracted[0].name) + + assertTrue(extracted[1].isDirectory) + assertEquals("parent/child/", extracted[1].name) + + assertFalse(extracted[2].isDirectory) + assertEquals("parent/child/file.txt", extracted[2].name) + assertEquals("content", extracted[2].data?.decodeToString()) + } + + @Test + fun `create and extract empty file`() { + val entry = + TarEntry( + name = "empty.txt", + size = 0, + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = ByteArray(0), + ) + + val tarData = TarUtils.createTarArchive(entries = listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + assertEquals("empty.txt", extracted[0].name) + assertEquals(0L, extracted[0].size) + assertNotNull(extracted[0].data) + assertEquals(0, extracted[0].data!!.size) + } + + @Test + fun `create and extract large file`() { + val largeContent = "X".repeat(10000).encodeToByteArray() + + val entry = + TarEntry( + name = "large.txt", + size = largeContent.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = largeContent, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + assertEquals("large.txt", extracted[0].name) + assertEquals(largeContent.size.toLong(), extracted[0].size) + assertContentEquals(largeContent, extracted[0].data) + } + + @Test + fun `create and extract file with special characters in name`() { + val entries = + listOf( + TarEntry( + name = "file-with-dash.txt", + size = 4, + mode = 644, + mtime = 1000000, + isDirectory = false, + data = "test".encodeToByteArray(), + ), + TarEntry( + name = "file_with_underscore.txt", + size = 4, + mode = 644, + mtime = 1000001, + isDirectory = false, + data = "test".encodeToByteArray(), + ), + TarEntry( + name = "file.with.dots.txt", + size = 4, + mode = 644, + mtime = 1000002, + isDirectory = false, + data = "test".encodeToByteArray(), + ), + ) + + val tarData = TarUtils.createTarArchive(entries) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(3, extracted.size) + assertEquals("file-with-dash.txt", extracted[0].name) + assertEquals("file_with_underscore.txt", extracted[1].name) + assertEquals("file.with.dots.txt", extracted[2].name) + } + + @Test + fun `create and extract file with long name`() { + // TAR standard supports names up to 100 characters + val longName = "a".repeat(99) + ".txt" + val content = "test content".encodeToByteArray() + + val entry = + TarEntry( + name = longName, + size = content.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = content, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + // Name should be truncated to fit in TAR header + assertTrue(extracted[0].name.length <= 100) + assertTrue(extracted[0].name.startsWith("aaa")) + } + + @Test + fun `create and extract binary file`() { + val binaryContent = + byteArrayOf( + 0x00, + 0x01, + 0x02, + 0x03, + 0xFF.toByte(), + 0xFE.toByte(), + 0xFD.toByte(), + 0xFC.toByte(), + 0x89.toByte(), + 0x50, + 0x4E, + 0x47, // PNG header + ) + + val entry = + TarEntry( + name = "binary.bin", + size = binaryContent.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = binaryContent, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + assertContentEquals(binaryContent, extracted[0].data) + } + + @Test + fun `create and extract file with different permissions`() { + val entries = + listOf( + TarEntry( + name = "readonly.txt", + size = 4, + mode = 444, // Read-only + mtime = 1000000, + isDirectory = false, + data = "test".encodeToByteArray(), + ), + TarEntry( + name = "writable.txt", + size = 4, + mode = 644, // Read-write + mtime = 1000001, + isDirectory = false, + data = "test".encodeToByteArray(), + ), + TarEntry( + name = "executable.sh", + size = 4, + mode = 755, // Executable + mtime = 1000002, + isDirectory = false, + data = "test".encodeToByteArray(), + ), + ) + + val tarData = TarUtils.createTarArchive(entries) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(3, extracted.size) + assertEquals(444L, extracted[0].mode) + assertEquals(644L, extracted[1].mode) + assertEquals(755L, extracted[2].mode) + } + + @Test + fun `create and extract empty archive`() { + val tarData = TarUtils.createTarArchive(emptyList()) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(0, extracted.size) + } + + @Test + fun `create and extract mixed files and directories`() { + val entries = + listOf( + TarEntry( + name = "dir1/", + size = 0, + mode = 755, + mtime = 1000000, + isDirectory = true, + data = null, + ), + TarEntry( + name = "dir1/file1.txt", + size = 6, + mode = 644, + mtime = 1000001, + isDirectory = false, + data = "file1\n".encodeToByteArray(), + ), + TarEntry( + name = "dir2/", + size = 0, + mode = 755, + mtime = 1000002, + isDirectory = true, + data = null, + ), + TarEntry( + name = "dir2/subdir/", + size = 0, + mode = 755, + mtime = 1000003, + isDirectory = true, + data = null, + ), + TarEntry( + name = "dir2/subdir/file2.txt", + size = 6, + mode = 644, + mtime = 1000004, + isDirectory = false, + data = "file2\n".encodeToByteArray(), + ), + TarEntry( + name = "root.txt", + size = 5, + mode = 644, + mtime = 1000005, + isDirectory = false, + data = "root\n".encodeToByteArray(), + ), + ) + + val tarData = TarUtils.createTarArchive(entries) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(6, extracted.size) + + // Verify structure + assertTrue(extracted[0].isDirectory) + assertEquals("dir1/", extracted[0].name) + + assertFalse(extracted[1].isDirectory) + assertEquals("dir1/file1.txt", extracted[1].name) + assertEquals("file1\n", extracted[1].data?.decodeToString()) + + assertTrue(extracted[2].isDirectory) + assertEquals("dir2/", extracted[2].name) + + assertTrue(extracted[3].isDirectory) + assertEquals("dir2/subdir/", extracted[3].name) + + assertFalse(extracted[4].isDirectory) + assertEquals("dir2/subdir/file2.txt", extracted[4].name) + assertEquals("file2\n", extracted[4].data?.decodeToString()) + + assertFalse(extracted[5].isDirectory) + assertEquals("root.txt", extracted[5].name) + assertEquals("root\n", extracted[5].data?.decodeToString()) + } + + @Test + fun `tar archive has correct structure`() { + val content = "test".encodeToByteArray() + val entry = + TarEntry( + name = "test.txt", + size = content.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = content, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + + // TAR format: + // - 512 bytes header + // - Data (padded to 512 byte blocks) + // - 1024 bytes (2 empty blocks) at end + + val expectedSize = + 512 + // Header + 512 + // Data block (4 bytes + padding) + 1024 // End markers + + assertEquals(expectedSize, tarData.size) + } + + @Test + fun `extract handles padding correctly`() { + // File size not multiple of 512 should be padded + val content = "A".repeat(100).encodeToByteArray() // 100 bytes + + val entry = + TarEntry( + name = "padded.txt", + size = content.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = content, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + assertEquals(100L, extracted[0].size) + assertContentEquals(content, extracted[0].data) + } + + @Test + fun `roundtrip preserves all metadata`() { + val originalEntry = + TarEntry( + name = "metadata-test.txt", + size = 13, + mode = 755, + mtime = 1234567890, + isDirectory = false, + data = "test content!".encodeToByteArray(), + ) + + val tarData = TarUtils.createTarArchive(listOf(originalEntry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + val roundtripEntry = extracted[0] + + assertEquals(originalEntry.name, roundtripEntry.name) + assertEquals(originalEntry.size, roundtripEntry.size) + assertEquals(originalEntry.mode, roundtripEntry.mode) + assertEquals(originalEntry.mtime, roundtripEntry.mtime) + assertEquals(originalEntry.isDirectory, roundtripEntry.isDirectory) + assertContentEquals(originalEntry.data, roundtripEntry.data) + } + + @Test + fun `file with exact block size boundary`() { + // 512 bytes exactly (one block) + val content = "X".repeat(512).encodeToByteArray() + + val entry = + TarEntry( + name = "block-size.txt", + size = content.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = content, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + assertEquals(512L, extracted[0].size) + assertContentEquals(content, extracted[0].data) + } + + @Test + fun `file with size just over block boundary`() { + // 513 bytes (needs 2 blocks) + val content = "X".repeat(513).encodeToByteArray() + + val entry = + TarEntry( + name = "over-block.txt", + size = content.size.toLong(), + mode = 644, + mtime = 1234567890, + isDirectory = false, + data = content, + ) + + val tarData = TarUtils.createTarArchive(listOf(entry)) + val extracted = TarUtils.extractTarArchive(tarData) + + assertEquals(1, extracted.size) + assertEquals(513L, extracted[0].size) + assertContentEquals(content, extracted[0].data) + } +} diff --git a/src/commonTest/kotlin/me/devnatan/dockerkt/resource/container/CopyContainerArchivesIT.kt b/src/commonTest/kotlin/me/devnatan/dockerkt/resource/container/CopyContainerArchivesIT.kt new file mode 100644 index 00000000..3fea58bd --- /dev/null +++ b/src/commonTest/kotlin/me/devnatan/dockerkt/resource/container/CopyContainerArchivesIT.kt @@ -0,0 +1,225 @@ +package me.devnatan.dockerkt.resource.container + +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import kotlinx.io.files.Path +import me.devnatan.dockerkt.io.FileSystemUtils +import me.devnatan.dockerkt.models.exec.ExecStartOptions +import me.devnatan.dockerkt.models.exec.ExecStartResult +import me.devnatan.dockerkt.resource.ResourceIT +import me.devnatan.dockerkt.resource.exec.create +import me.devnatan.dockerkt.sleepForever +import me.devnatan.dockerkt.withContainer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class CopyContainerArchivesIT : ResourceIT() { + private val testImage = "alpine:latest" + + @Test + fun `copy file from container`() = + runTest { + testClient.withContainer( + testImage, + { + command = listOf("sh", "-c", "echo 'test content' > /tmp/test.txt && sleep infinity") + }, + ) { id -> + testClient.containers.start(id) + + // Wait for file to be created + delay(500) + + val tempDir = FileSystemUtils.createTempDirectory() + try { + testClient.containers.copyFileFrom( + id, + "/tmp/test.txt", + tempDir.toString(), + ) + + val copiedFile = Path(tempDir, "test.txt") + assertTrue(FileSystemUtils.exists(copiedFile)) + + val content = FileSystemUtils.readFile(copiedFile).decodeToString() + assertEquals( + expected = "test content\n", + actual = content, + ) + } finally { + FileSystemUtils.deleteRecursively(tempDir) + testClient.containers.stop(id) + } + } + } + + @Test + fun `copy file to container`() = + runTest { + testClient.withContainer( + testImage, + { + sleepForever() + }, + ) { id -> + testClient.containers.start(id) + + val tempFile = FileSystemUtils.createTempFile() + try { + FileSystemUtils.writeFile(tempFile, "hello from host".encodeToByteArray()) + + testClient.containers.copyFileTo( + id, + tempFile.toString(), + "/tmp/", + ) + + val execId = + testClient.exec.create(id) { + command = listOf("cat", "/tmp/${tempFile.name}") + attachStdout = true + } + + val result = testClient.exec.start(execId, ExecStartOptions()) + assertTrue(result is ExecStartResult.Complete) + assertTrue(result.output.contains("hello from host")) + } finally { + FileSystemUtils.delete(tempFile) + testClient.containers.stop(id) + } + } + } + + @Test + fun `copy directory from container`() = + runTest { + testClient.withContainer( + testImage, + { + command = + listOf( + "sh", + "-c", + "mkdir -p /tmp/testdir && echo 'file1' > /tmp/testdir/file1.txt && echo 'file2' > /tmp/testdir/file2.txt && sleep infinity", + ) + }, + ) { id -> + testClient.containers.start(id) + + delay(500) + + val tempDir = FileSystemUtils.createTempDirectory() + try { + testClient.containers.copyDirectoryFrom( + id, + "/tmp/testdir", + tempDir.toString(), + ) + + val file1 = Path(tempDir, "testdir/file1.txt") + val file2 = Path(tempDir, "testdir/file2.txt") + + assertTrue(FileSystemUtils.exists(file1)) + assertTrue(FileSystemUtils.exists(file2)) + + assertEquals( + expected = "file1\n", + actual = FileSystemUtils.readFile(file1).decodeToString(), + ) + assertEquals( + expected = "file2\n", + actual = FileSystemUtils.readFile(file2).decodeToString(), + ) + } finally { + FileSystemUtils.deleteRecursively(tempDir) + testClient.containers.stop(id) + } + } + } + + @Test + fun `copy directory to container`() = + runTest { + testClient.withContainer( + testImage, + { + sleepForever() + }, + ) { id -> + testClient.containers.start(id) + + val tempDir = FileSystemUtils.createTempDirectory() + try { + // Create test files + val file1 = Path(tempDir, "file1.txt") + val file2 = Path(tempDir, "file2.txt") + + FileSystemUtils.writeFile(file1, "content1".encodeToByteArray()) + FileSystemUtils.writeFile(file2, "content2".encodeToByteArray()) + + testClient.containers.copyDirectoryTo( + id, + tempDir.toString(), + "/tmp/", + ) + + val execId = + testClient.exec.create(id) { + command = listOf("sh", "-c", "cat /tmp/file1.txt && cat /tmp/file2.txt") + attachStdout = true + } + + val result = testClient.exec.start(execId, ExecStartOptions()) + assertTrue(result is ExecStartResult.Complete) + + val output = result.output + assertTrue( + actual = output.contains("content1"), + message = "Expected 'content1' in output, but got: $output", + ) + assertTrue( + actual = output.contains("content2"), + message = "Expected 'content2' in output, but got: $output", + ) + } finally { + FileSystemUtils.deleteRecursively(tempDir) + testClient.containers.stop(id) + } + } + } + + @Test + fun `copy fails when container not found`() = + runTest { + assertFailsWith { + testClient.containers.copyFrom( + "nonexistent_container", + "/tmp/test.txt", + ) + } + } + + @Test + fun `copy fails when path not found in container`() = + runTest { + testClient.withContainer( + testImage, + { + sleepForever() + }, + ) { id -> + testClient.containers.start(id) + + assertFailsWith { + testClient.containers.copyFrom( + id, + "/nonexistent/path.txt", + ) + } + + testClient.containers.stop(id) + } + } +} diff --git a/src/jvmMain/kotlin/me/devnatan/dockerkt/io/TarFile.jvm.kt b/src/jvmMain/kotlin/me/devnatan/dockerkt/io/TarFile.jvm.kt deleted file mode 100644 index 2fcc3694..00000000 --- a/src/jvmMain/kotlin/me/devnatan/dockerkt/io/TarFile.jvm.kt +++ /dev/null @@ -1,26 +0,0 @@ -package me.devnatan.dockerkt.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 = - TarArchiveInputStream(input.buffered().asInputStream()) - .apply { - nextTarEntry - }.asSource() - -internal actual fun writeTarFile(filePath: String): RawSource { - val output = Files.createTempFile("dockerkt", ".tar.gz") - CompressArchiveUtil.tar( - inputPath = Paths.get(filePath), - outputPath = output, - childrenOnly = false, - ) - return output.inputStream().asSource() -} diff --git a/src/jvmMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.jvm.kt b/src/jvmMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.jvm.kt index 98eb6027..194da382 100644 --- a/src/jvmMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.jvm.kt @@ -2,19 +2,20 @@ package me.devnatan.dockerkt.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.prepareGet import io.ktor.client.request.preparePost import io.ktor.client.request.put import io.ktor.client.request.setBody +import io.ktor.client.statement.readRawBytes import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType import io.ktor.util.cio.toByteReadChannel +import io.ktor.util.decodeBase64Bytes import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.CancellationException import io.ktor.utils.io.availableForRead @@ -27,20 +28,21 @@ 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.io.files.Path import kotlinx.serialization.json.Json import me.devnatan.dockerkt.DockerResponseException -import me.devnatan.dockerkt.io.readTarFile +import me.devnatan.dockerkt.io.FileSystemUtils +import me.devnatan.dockerkt.io.TarEntry +import me.devnatan.dockerkt.io.TarOperations +import me.devnatan.dockerkt.io.TarUtils import me.devnatan.dockerkt.io.requestCatching -import me.devnatan.dockerkt.io.writeTarFile import me.devnatan.dockerkt.models.Frame import me.devnatan.dockerkt.models.ResizeTTYOptions import me.devnatan.dockerkt.models.Stream import me.devnatan.dockerkt.models.container.Container import me.devnatan.dockerkt.models.container.ContainerArchiveInfo +import me.devnatan.dockerkt.models.container.ContainerCopyOptions +import me.devnatan.dockerkt.models.container.ContainerCopyResult import me.devnatan.dockerkt.models.container.ContainerCreateOptions import me.devnatan.dockerkt.models.container.ContainerCreateResult import me.devnatan.dockerkt.models.container.ContainerListOptions @@ -52,10 +54,7 @@ import me.devnatan.dockerkt.models.container.ContainerSummary import me.devnatan.dockerkt.models.container.ContainerWaitResult import me.devnatan.dockerkt.resource.ResourcePaths.CONTAINERS import me.devnatan.dockerkt.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 @@ -536,70 +535,117 @@ public actual class ContainerResource( public fun pruneAsync(filters: ContainerPruneFilters = ContainerPruneFilters()): CompletableFuture = coroutineScope.async { prune(filters) }.asCompletableFuture() - /** - * 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. - */ - @OptIn(ExperimentalEncodingApi::class) - public actual suspend fun archive( + public actual suspend fun copyFrom( container: String, - path: String, - ): ContainerArchiveInfo = - requestCatching { + sourcePath: String, + ): ContainerCopyResult = + requestCatching( + HttpStatusCode.NotFound to { exception -> + if (exception.message?.contains("file") == true) { + ArchiveNotFoundException(exception, container, sourcePath) + } else { + ContainerNotFoundException(exception, container) + } + }, + ) { val response = - httpClient.head("$CONTAINERS/$container/archive") { - parameter("path", path) + httpClient.get("$CONTAINERS/$container/archive") { + parameter("path", sourcePath) } - val pathStat = response.headers["X-Docker-Container-Path-Stat"] ?: error("Missing path stat header") - val decoded = Base64.decode(pathStat).decodeToString() - return json.decodeFromString(decoded) - } + val archiveData = response.readRawBytes() - /** - * 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, - remotePath: String, - ): RawSource { - val contents = - requestCatching { - httpClient.get("$CONTAINERS/$container/archive") { - accept(ContentType.parse("application/x-tar")) - parameter("path", remotePath) + val statHeader = response.headers["X-Docker-Container-Path-Stat"] + val stat = + statHeader?.let { header -> + val decoded = header.decodeBase64Bytes() + json.decodeFromString(decoded.decodeToString()) } - }.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( + ContainerCopyResult(archiveData, stat) + } + + public actual suspend fun copyTo( container: String, - inputPath: String, - remotePath: String, + destinationPath: String, + tarArchive: ByteArray, + options: ContainerCopyOptions, ): Unit = - requestCatching { - val archive = writeTarFile(inputPath) - + requestCatching( + HttpStatusCode.NotFound to { exception -> ContainerNotFoundException(exception, container) }, + HttpStatusCode.BadRequest to { exception -> + IllegalArgumentException("Invalid destination path: $destinationPath", exception) + }, + ) { httpClient.put("$CONTAINERS/$container/archive") { - parameter("path", remotePath.ifEmpty { FileSystemRoot }) - parameter("noOverwriteDirNonDir", false) - setBody(archive.buffered().asInputStream().toByteReadChannel()) + parameter("path", destinationPath) + parameter("noOverwriteDirNonDir", options.noOverwriteDirNonDir.toString()) + parameter("copyUIDGID", options.copyUIDGID.toString()) + + setBody(tarArchive) + contentType(ContentType.Application.OctetStream) } } + public actual suspend fun copyFileFrom( + container: String, + sourcePath: String, + destinationPath: String, + ) { + val result = copyFrom(container, sourcePath) + TarOperations.extractTar(result.archiveData, Path(destinationPath)) + } + + public actual suspend fun copyFileTo( + container: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions, + ) { + val path = Path(sourcePath) + if (!FileSystemUtils.exists(path)) { + throw IllegalArgumentException("Source file not found: $sourcePath") + } + + if (FileSystemUtils.isDirectory(path)) { + throw IllegalArgumentException("Source is a directory, use copyDirectoryTo instead: $sourcePath") + } + + val tarArchive = TarOperations.createTarFromFile(path) + copyTo(container, destinationPath, tarArchive, options) + } + + public actual suspend fun copyDirectoryFrom( + container: String, + sourcePath: String, + destinationPath: String, + ) { + val result = copyFrom(container, sourcePath) + TarOperations.extractTar(result.archiveData, Path(destinationPath)) + } + + public actual suspend fun copyDirectoryTo( + container: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions, + ) { + val path = Path(sourcePath) + if (!FileSystemUtils.exists(path) || !FileSystemUtils.isDirectory(path)) { + throw IllegalArgumentException("Source directory not found: $sourcePath") + } + + // Create tar with directory contents only (not including the root directory name) + val tarArchive = createTarFromDirectoryContents(path) + copyTo(container, destinationPath, tarArchive, options) + } + + private fun createTarFromDirectoryContents(dirPath: Path): ByteArray { + val entries = mutableListOf() + TarOperations.collectDirectoryContents(dirPath, "", entries) + return TarUtils.createTarArchive(entries) + } + public actual fun logs( container: String, options: ContainerLogsOptions, diff --git a/src/nativeMain/kotlin/me/devnatan/dockerkt/io/TarFile.native.kt b/src/nativeMain/kotlin/me/devnatan/dockerkt/io/TarFile.native.kt deleted file mode 100644 index b0566cd4..00000000 --- a/src/nativeMain/kotlin/me/devnatan/dockerkt/io/TarFile.native.kt +++ /dev/null @@ -1,7 +0,0 @@ -package me.devnatan.dockerkt.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/dockerkt/resource/container/ContainerResource.native.kt b/src/nativeMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.native.kt index f528d38c..8e5199ee 100644 --- a/src/nativeMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.native.kt +++ b/src/nativeMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.native.kt @@ -1,11 +1,11 @@ package me.devnatan.dockerkt.resource.container import kotlinx.coroutines.flow.Flow -import kotlinx.io.RawSource import me.devnatan.dockerkt.models.Frame import me.devnatan.dockerkt.models.ResizeTTYOptions import me.devnatan.dockerkt.models.container.Container -import me.devnatan.dockerkt.models.container.ContainerArchiveInfo +import me.devnatan.dockerkt.models.container.ContainerCopyOptions +import me.devnatan.dockerkt.models.container.ContainerCopyResult import me.devnatan.dockerkt.models.container.ContainerCreateOptions import me.devnatan.dockerkt.models.container.ContainerListOptions import me.devnatan.dockerkt.models.container.ContainerLogsOptions @@ -182,51 +182,44 @@ 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( + public actual fun logs( container: String, - path: String, - ): ContainerArchiveInfo { + options: ContainerLogsOptions, + ): Flow { 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, - remotePath: String, - ): RawSource { + public actual suspend fun copyFrom(container: String, sourcePath: String): ContainerCopyResult { 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( + public actual suspend fun copyTo( container: String, - inputPath: String, - remotePath: String, + destinationPath: String, + tarArchive: ByteArray, + options: ContainerCopyOptions ) { - TODO("Not yet implemented") } - public actual fun logs( + public actual suspend fun copyFileTo( container: String, - options: ContainerLogsOptions, - ): Flow { - TODO("Not yet implemented") + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions + ) { + } + + public actual suspend fun copyFileFrom(container: String, sourcePath: String, destinationPath: String) { + } + + public actual suspend fun copyDirectoryFrom(container: String, sourcePath: String, destinationPath: String) { + } + + public actual suspend fun copyDirectoryTo( + container: String, + sourcePath: String, + destinationPath: String, + options: ContainerCopyOptions + ) { } }