From d372cdcea3b3a0ba4b49180695c4e6b0e2d074a5 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Mon, 19 Aug 2024 00:34:03 +0200 Subject: Increase the samba implemetation With the exception of openDir, largely untested. --- .../org/the_jk/cleversync/io/samba/NativeSamba.kt | 68 +++++++++++++++++++ .../the_jk/cleversync/io/samba/SambaConnection.kt | 25 ++++--- .../the_jk/cleversync/io/samba/SambaDirectory.kt | 79 +++++++++++++++------- .../org/the_jk/cleversync/io/samba/SambaFile.kt | 40 +++++++++++ .../org/the_jk/cleversync/io/samba/SambaLink.kt | 57 ++++++++++++++++ .../org/the_jk/cleversync/io/samba/SambaTree.kt | 2 +- .../the_jk/cleversync/samba/SambaTreeFactory.kt | 3 +- 7 files changed, 235 insertions(+), 39 deletions(-) create mode 100644 libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaFile.kt create mode 100644 libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaLink.kt (limited to 'libs/samba/src/main/java') diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/NativeSamba.kt b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/NativeSamba.kt index e48268d..1863e22 100644 --- a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/NativeSamba.kt +++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/NativeSamba.kt @@ -4,6 +4,7 @@ package org.the_jk.cleversync.io.samba import androidx.annotation.Keep +import java.time.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -21,14 +22,35 @@ internal object NativeSamba { fun connect(url: Url, credentials: SambaCredentials): Boolean fun lastError(): String fun openDir(path: String): Dir? + fun entry(path: String): DirEntry? + fun makeDir(path: String): Boolean + fun removeDir(path: String): Boolean + fun unlink(path: String): Boolean + fun readLink(path: String): String? } interface Url : Object { fun path(): String } + enum class DirEntryType { + DIR, + FILE, + LINK, + } + + @Keep + data class DirEntry( + val name: String, + val type: DirEntryType, + val size: ULong, + val lastModified: Instant, + ) + interface Dir : Object { val path: String + + fun list(): Array } private class NativeContext(private var ptr: Long): Context { @@ -55,6 +77,26 @@ internal object NativeSamba { val dir = nativeContextOpenDir(ptr, path) return if (dir != 0L) NativeDir(path, dir) else null } + + override fun entry(path: String): DirEntry? { + return nativeContextEntry(ptr, path) + } + + override fun makeDir(path: String): Boolean { + return nativeContextMakeDir(ptr, path) + } + + override fun removeDir(path: String): Boolean { + return nativeContextRemoveDir(ptr, path) + } + + override fun unlink(path: String): Boolean { + return nativeContextUnlink(ptr, path) + } + + override fun readLink(path: String): String? { + return nativeContextReadLink(ptr, path) + } } private class NativeUrl(private var ptr: Long): Url { @@ -75,21 +117,47 @@ internal object NativeSamba { nativeDirDestroy(ptr) ptr = 0L } + + override fun list(): Array { + return nativeDirList(ptr) + } } init { System.loadLibrary("samba") } + @JvmStatic + @Keep + @Suppress("UnusedPrivateMember") + private fun createDirEntry(name: String, type: Int, size: Long, lastModified: Long) = + DirEntry( + name = name, + when (type) { + 0 -> DirEntryType.DIR + 1 -> DirEntryType.FILE + 2 -> DirEntryType.LINK + else -> throw IllegalArgumentException("Unknown type: $type") + }, + size = size.toULong(), + lastModified = Instant.ofEpochMilli(lastModified), + ) + private external fun nativeContextNew(timeoutSeconds: Int): Long private external fun nativeContextDestroy(ptr: Long) private external fun nativeContextParseUrl(ptr: Long, url: String): Long private external fun nativeContextConnect(ptr: Long, url: Long, username: String?, password: String?): Boolean private external fun nativeContextGetError(ptr: Long): String private external fun nativeContextOpenDir(ptr: Long, path: String): Long + private external fun nativeContextEntry(ptr: Long, path: String): DirEntry? + private external fun nativeContextMakeDir(ptr: Long, path: String): Boolean + private external fun nativeContextRemoveDir(ptr: Long, path: String): Boolean + private external fun nativeContextUnlink(ptr: Long, path: String): Boolean + private external fun nativeContextReadLink(otr: Long, path: String): String? private external fun nativeUrlDestroy(ptr: Long) private external fun nativeUrlPath(ptr: Long): String private external fun nativeDirDestroy(ptr: Long) + private external fun nativeDirList(ptr: Long): Array } diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaConnection.kt b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaConnection.kt index 05cc831..04aff44 100644 --- a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaConnection.kt +++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaConnection.kt @@ -12,21 +12,26 @@ internal class SambaConnection(uri: String, credentials: SambaCredentials) { fun openDir(path: String): NativeSamba.Dir? = if (connected) context.openDir(join(url!!.path(), path)) else null + fun entry(path: String): NativeSamba.DirEntry? = + if (connected) context.entry(join(url!!.path(), path)) else null + + fun makeDir(path: String): Boolean = + connected && context.makeDir(join(url!!.path(), path)) + + fun removeDir(path: String): Boolean = + connected && context.removeDir(join(url!!.path(), path)) + + fun unlink(path: String): Boolean = + connected && context.unlink(join(url!!.path(), path)) + + fun readLink(path: String): String? = + if (connected) context.readLink(join(url!!.path(), path)) else null + companion object { fun join(a: String, b: String): String { if (a.isEmpty() || b.startsWith("/")) return b if (b.isEmpty()) return a return if (a.endsWith("/")) a + b else "${a}/${b}" } - - fun dirname(path: String): String { - val last = path.lastIndexOf('/') - return if (last == -1) path else path.substring(last + 1) - } - - fun basename(path: String): String { - val last = path.lastIndexOf('/') - return if (last < 1) "" else path.substring(0, last - 1) - } } } diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaDirectory.kt b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaDirectory.kt index b82e745..d9ec6aa 100644 --- a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaDirectory.kt +++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaDirectory.kt @@ -3,30 +3,61 @@ package org.the_jk.cleversync.io.samba import androidx.lifecycle.LiveData import org.the_jk.cleversync.io.Directory import org.the_jk.cleversync.io.File -import org.the_jk.cleversync.io.Link import org.the_jk.cleversync.io.ModifiableDirectory import org.the_jk.cleversync.io.ModifiableFile import org.the_jk.cleversync.io.ModifiableLink +import java.io.IOException +import java.time.Instant internal open class SambaDirectory( private val conn: SambaConnection, - private val dir: NativeSamba.Dir, + private val path: String, + override val name: String, ) : ModifiableDirectory { override fun modifiableOpenDir(name: String): ModifiableDirectory? { - val dir = conn.openDir(SambaConnection.join(dir.path, name)) ?: return null - return SambaDirectory(conn, dir) + val newPath = SambaConnection.join(path, name) + val entry = conn.entry(newPath) ?: return null + if (entry.type != NativeSamba.DirEntryType.DIR) return null + return SambaDirectory(conn, newPath, name) } override fun modifiableOpenFile(name: String): ModifiableFile? { - TODO("Not yet implemented") + val newPath = SambaConnection.join(path, name) + val entry = conn.entry(newPath) ?: return null + if (entry.type != NativeSamba.DirEntryType.FILE) return null + return SambaFile(conn, newPath, name, entry.size, entry.lastModified) } override fun modifiableOpenLink(name: String): ModifiableLink? { - TODO("Not yet implemented") + val newPath = SambaConnection.join(path, name) + val entry = conn.entry(newPath) ?: return null + if (entry.type != NativeSamba.DirEntryType.LINK) return null + return SambaLink(conn, newPath, name) } override fun modifiableList(): ModifiableDirectory.Content { - TODO("Not yet implemented") + val directories = mutableListOf() + val files = mutableListOf() + val links = mutableListOf() + val dir = conn.openDir(path) + if (dir != null) { + dir.list().forEach { entry -> + val entryPath = SambaConnection.join(path, entry.name) + when (entry.type) { + NativeSamba.DirEntryType.DIR -> { + directories.add(SambaDirectory(conn, entryPath, entry.name)) + } + NativeSamba.DirEntryType.FILE -> { + files.add(SambaFile(conn, entryPath, entry.name, entry.size, entry.lastModified)) + } + NativeSamba.DirEntryType.LINK -> { + links.add(SambaLink(conn, entryPath, entry.name)) + } + } + } + dir.destroy() + } + return ModifiableDirectory.Content(directories, files, links) } override fun modifiableLiveList(): LiveData { @@ -34,52 +65,48 @@ internal open class SambaDirectory( } override fun createDirectory(name: String): ModifiableDirectory { - TODO("Not yet implemented") + val newPath = SambaConnection.join(path, name) + if (!conn.makeDir(newPath)) throw IOException(conn.error) + return SambaDirectory(conn, newPath, name) } override fun createFile(name: String): ModifiableFile { - TODO("Not yet implemented") + val newPath = SambaConnection.join(path, name) + return SambaFile(conn, newPath, name, 0UL, Instant.EPOCH, Instant.EPOCH) } override fun createLink(name: String, target: Directory): ModifiableLink { - TODO("Not yet implemented") + throw IOException("Unsupported operation") } override fun createLink(name: String, target: File): ModifiableLink { - TODO("Not yet implemented") + throw IOException("Unsupported operation") } override fun createLink(name: String, target: String): ModifiableLink { - TODO("Not yet implemented") + throw IOException("Unsupported operation") } override fun removeDirectory(name: String): Boolean { - TODO("Not yet implemented") + return conn.removeDir(SambaConnection.join(path, name)) } override fun removeFile(name: String): Boolean { - TODO("Not yet implemented") + return conn.unlink(SambaConnection.join(path, name)) } override fun removeLink(name: String): Boolean { - TODO("Not yet implemented") + return conn.unlink(SambaConnection.join(path, name)) } - override val name: String - get() = SambaConnection.dirname(dir.path) - override fun openDir(name: String) = modifiableOpenDir(name) - override fun openFile(name: String): File? { - TODO("Not yet implemented") - } + override fun openFile(name: String) = modifiableOpenFile(name) - override fun openLink(name: String): Link? { - TODO("Not yet implemented") - } + override fun openLink(name: String) = modifiableOpenLink(name) - override fun list(): Directory.Content { - TODO("Not yet implemented") + override fun list() = with(modifiableList()) { + Directory.Content(directories, files, links) } override fun liveList(): LiveData { diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaFile.kt b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaFile.kt new file mode 100644 index 0000000..817c1bf --- /dev/null +++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaFile.kt @@ -0,0 +1,40 @@ +package org.the_jk.cleversync.io.samba + +import org.the_jk.cleversync.io.ModifiableFile +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.time.Instant + +internal class SambaFile( + private val conn: SambaConnection, + private val path: String, + override val name: String, + private val cachedSize: ULong, + private val cachedLastModified: Instant, + private var cacheEndOfLife: Instant = Instant.now().plusSeconds(60), +) : ModifiableFile { + override fun write(): OutputStream { + TODO("Not yet implemented") + } + + override fun read(): InputStream { + TODO("Not yet implemented") + } + + override val size: ULong get() { + if (useCached()) return cachedSize + val entry = conn.entry(path) ?: throw IOException(conn.error) + return entry.size + } + + override val lastModified: Instant get() { + if (useCached()) return cachedLastModified + val entry = conn.entry(path) ?: throw IOException(conn.error) + return entry.lastModified + } + + private fun useCached(): Boolean { + return Instant.now().isBefore(cacheEndOfLife) + } +} diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaLink.kt b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaLink.kt new file mode 100644 index 0000000..9d9fd1d --- /dev/null +++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaLink.kt @@ -0,0 +1,57 @@ +package org.the_jk.cleversync.io.samba + +import org.the_jk.cleversync.io.Directory +import org.the_jk.cleversync.io.File +import org.the_jk.cleversync.io.Link +import org.the_jk.cleversync.io.ModifiableLink +import java.io.IOException + +internal class SambaLink( + private val conn: SambaConnection, + private val path: String, + override val name: String, +) : ModifiableLink { + override fun modifiableResolve(): ModifiableLink.ModifiableLinkTarget { + val (newPath, entry) = doResolve() + if (entry == null) return ModifiableLink.NoTarget + return when (entry.type) { + NativeSamba.DirEntryType.DIR -> ModifiableLink.ModifiableDirectoryTarget(SambaDirectory(conn, newPath, entry.name)) + NativeSamba.DirEntryType.FILE -> ModifiableLink.ModifiableFileTarget(SambaFile(conn, newPath, entry.name, entry.size, entry.lastModified)) + NativeSamba.DirEntryType.LINK -> ModifiableLink.NoTarget + } + } + + override fun target(directory: Directory) { + throw IOException("Unsupported operation") + } + + override fun target(file: File) { + throw IOException("Unsupported operation") + } + + override fun target(name: String) { + throw IOException("Unsupported operation") + } + + override fun resolve(): Link.LinkTarget { + val (newPath, entry) = doResolve() + if (entry == null) return Link.NoTarget + return when (entry.type) { + NativeSamba.DirEntryType.DIR -> Link.DirectoryTarget(SambaDirectory(conn, newPath, entry.name)) + NativeSamba.DirEntryType.FILE -> Link.FileTarget(SambaFile(conn, newPath, entry.name, entry.size, entry.lastModified)) + NativeSamba.DirEntryType.LINK -> Link.NoTarget + } + } + + private fun doResolve(): Pair { + var linkPath = path + var entry: NativeSamba.DirEntry? = null + while (true) { + val target = conn.readLink(linkPath) ?: break + linkPath = SambaConnection.join(linkPath, target) + entry = conn.entry(linkPath) ?: break + if (entry.type != NativeSamba.DirEntryType.LINK) break + } + return linkPath to entry + } +} diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaTree.kt b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaTree.kt index 8b4a86d..762a61f 100644 --- a/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaTree.kt +++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaTree.kt @@ -3,7 +3,7 @@ package org.the_jk.cleversync.io.samba import android.content.res.Resources import org.the_jk.cleversync.io.ModifiableTree -internal class SambaTree(conn: SambaConnection, root: NativeSamba.Dir) : SambaDirectory(conn, root), ModifiableTree { +internal class SambaTree(conn: SambaConnection, root: String) : SambaDirectory(conn, root, ""), ModifiableTree { override fun description(resources: Resources): CharSequence { TODO("Not yet implemented") } diff --git a/libs/samba/src/main/java/org/the_jk/cleversync/samba/SambaTreeFactory.kt b/libs/samba/src/main/java/org/the_jk/cleversync/samba/SambaTreeFactory.kt index 5c9bc4f..ebf47d2 100644 --- a/libs/samba/src/main/java/org/the_jk/cleversync/samba/SambaTreeFactory.kt +++ b/libs/samba/src/main/java/org/the_jk/cleversync/samba/SambaTreeFactory.kt @@ -12,7 +12,6 @@ object SambaTreeFactory { fun modifiableTree(uri: String, credentials: SambaCredentials): Result { val connection = SambaConnection(uri, credentials) if (!connection.connected) return Result.failure(Exception(connection.error)) - val root = connection.openDir("") ?: return Result.failure(Exception(connection.error)) - return Result.success(SambaTree(connection, root)) + return Result.success(SambaTree(connection, "")) } } -- cgit v1.2.3-70-g09d2