summaryrefslogtreecommitdiff
path: root/libs/samba/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/samba/src')
-rw-r--r--libs/samba/src/main/cpp/jni.cpp100
-rw-r--r--libs/samba/src/main/cpp/jni.hpp142
-rw-r--r--libs/samba/src/main/cpp/samba.cpp155
-rw-r--r--libs/samba/src/main/java/org/the_jk/cleversync/io/samba/NativeSamba.kt92
-rw-r--r--libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaConnection.kt32
-rw-r--r--libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaDirectory.kt88
-rw-r--r--libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaTree.kt10
-rw-r--r--libs/samba/src/main/java/org/the_jk/cleversync/samba/SambaTreeFactory.kt17
8 files changed, 636 insertions, 0 deletions
diff --git a/libs/samba/src/main/cpp/jni.cpp b/libs/samba/src/main/cpp/jni.cpp
new file mode 100644
index 0000000..be13df7
--- /dev/null
+++ b/libs/samba/src/main/cpp/jni.cpp
@@ -0,0 +1,100 @@
+#include "jni.hpp"
+
+#include <android/log.h>
+
+namespace {
+
+JavaVM *g_vm;
+
+const char *_jni_error(jint err) {
+ switch (err) {
+ case JNI_OK:
+ return "OK";
+ case JNI_ERR:
+ return "Unknown error";
+ case JNI_EDETACHED:
+ return "Thread detached from the VM";
+ case JNI_EVERSION:
+ return "JNI version error";
+ case JNI_ENOMEM:
+ return "Not enough memory";
+ case JNI_EEXIST:
+ return "VM already created";
+ case JNI_EINVAL:
+ return "Invalid arguments";
+ default:
+ return "Unexpected error";
+ }
+}
+
+} // namespace
+
+namespace jni {
+
+namespace internal {
+
+void _abort_if_not_ok(const char *file, int line, jint ret) {
+ if (ret == JNI_OK) [[likely]] return;
+ __android_log_assert(nullptr, "jni", "JNI error: %s", _jni_error(ret));
+}
+
+void _abort_with_exception(const char* file, int line, JNIEnv* env) {
+ auto throwable = jni::ExceptionOccurred(env);
+ env->ExceptionClear();
+ if (throwable) {
+ auto throwable_class = jni::FindClass(env, "java/lang/Throwable");
+ if (throwable_class) {
+ auto throwable_toString = env->GetMethodID(throwable_class.get(),
+ "toString",
+ "()Ljava/lang/String;");
+ if (throwable_toString) {
+ auto description = jni::CallObjectMethod<jstring>(env, throwable,
+ throwable_toString);
+ auto str = jni::StringToUTF8(env, description);
+ __android_log_assert(nullptr, "jni", "JNI error: %s", str.c_str());
+ }
+ }
+ env->ExceptionClear();
+ __android_log_assert(nullptr, "jni",
+ "Unexpected NULL but no exception");
+ }
+}
+
+} // namespace internal
+
+JNIEnv* AttachCurrentThread() {
+ JNIEnv* env;
+ auto ret = g_vm->AttachCurrentThread(&env, nullptr);
+ ABORT_IF_NOT_OK(ret);
+ return env;
+}
+
+JNIEnv* OnLoad(JavaVM* vm) {
+ void* v_env;
+ auto ret = vm->GetEnv(&v_env, JNI_VERSION);
+ ABORT_IF_NOT_OK(ret);
+ return reinterpret_cast<JNIEnv*>(v_env);
+}
+
+LocalRef<jclass> FindClass(JNIEnv *env, const char *name) {
+ return {env, env->FindClass(name)};
+}
+
+LocalRef<jthrowable> ExceptionOccurred(JNIEnv* env) {
+ return {env, env->ExceptionOccurred()};
+}
+
+std::string StringToUTF8(JNIEnv* env, const Ref<jstring>& str) {
+ if (!str) return "null";
+ auto len = env->GetStringUTFLength(str.get());
+ std::string ret(len, ' ');
+ env->GetStringUTFRegion(str.get(), 0, len, ret.data());
+ // This returns modified UTF-8 encoding, don't care.
+ return ret;
+}
+
+LocalRef<jstring> UTF8ToString(JNIEnv* env, const std::string& str) {
+ return {env, env->NewStringUTF(str.c_str())};
+}
+
+} // namespace jni
diff --git a/libs/samba/src/main/cpp/jni.hpp b/libs/samba/src/main/cpp/jni.hpp
new file mode 100644
index 0000000..90ca011
--- /dev/null
+++ b/libs/samba/src/main/cpp/jni.hpp
@@ -0,0 +1,142 @@
+#ifndef CLEVERSYNC_JNI_HPP
+#define CLEVERSYNC_JNI_HPP
+
+#include <jni.h>
+#include <string>
+
+#define ABORT_IF_NOT_OK(x) (::jni::internal::_abort_if_not_ok(__FILE__, __LINE__, (x)))
+#define ABORT_IF_NULL(env, x) (::jni::internal::_abort_if_null(__FILE__, __LINE__, (env), (x)))
+
+namespace jni {
+
+namespace internal {
+
+void _abort_if_not_ok(const char *file, int line, jint ret);
+void _abort_with_exception(const char* file, int line, JNIEnv* env);
+
+} // namespace internal
+
+template<class T>
+class Ref {
+ public:
+ constexpr Ref() : env_(nullptr), ptr_(0) {}
+ Ref(const Ref<T>&) = delete;
+ Ref<T>& operator=(const Ref<T>&) = delete;
+
+ [[nodiscard]] T get() const { return ptr_; }
+ [[nodiscard]] T release() {
+ auto ret = release_to_local();
+ ptr_ = 0;
+ return ret;
+ }
+
+ void reset() {
+ del();
+ ptr_ = 0;
+ }
+
+ explicit operator bool() const { return ptr_ != 0; }
+
+ protected:
+ Ref(JNIEnv* env, T ptr): env_(env), ptr_(ptr) {}
+ virtual ~Ref() = default;
+
+ virtual T release_to_local() = 0;
+ virtual void del() = 0;
+
+ JNIEnv* const env_;
+ T ptr_;
+};
+
+template<class T>
+class LocalRef : public Ref<T> {
+ public:
+ LocalRef(JNIEnv* env, T ptr): Ref<T>(env, ptr) {}
+ ~LocalRef() override { free(); }
+
+ protected:
+ T release_to_local() override { return this->ptr_; }
+ void del() override { free(); }
+
+ private:
+ void free() {
+ if (this->ptr_)
+ this->env_->DeleteLocalRef(this->ptr_);
+ }
+};
+
+template<class T>
+class ParamRef : public Ref<T> {
+ public:
+ ParamRef(JNIEnv* env, T ptr) : Ref<T>(env, ptr) {}
+ ~ParamRef() override = default;
+
+ protected:
+ T release_to_local() override {
+ if (this->ptr_)
+ return static_cast<T>(this->env_->NewLocalRef(static_cast<jobject>(this->ptr_)));
+ return 0;
+ }
+ void del() override {}
+};
+
+template<class T>
+class GlobalRef : public Ref<T> {
+ public:
+ GlobalRef(JNIEnv* env, T ptr) : Ref<T>(env, ptr ? env->NewGlobalRef(ptr) : 0) {}
+ explicit GlobalRef(const Ref<T>& other) : Ref<T>(other.env_, other ? other.env_->NewGlobalRef(other.ptr_) : 0) {}
+
+ ~GlobalRef() override { free(); }
+
+ protected:
+ T release_to_local() override {
+ if (this->ptr_) {
+ auto ret = static_cast<T>(this->env_->NewLocalRef(
+ static_cast<jobject>(this->ptr_)));
+ free();
+ return ret;
+ }
+ return 0;
+ }
+ void del() override { free(); }
+
+ private:
+ void free() {
+ if (this->ptr_)
+ this->env_->DeleteGlobalRef(this->ptr_);
+ }
+};
+
+constexpr jint JNI_VERSION = JNI_VERSION_1_2;
+
+JNIEnv* AttachCurrentThread();
+
+JNIEnv* OnLoad(JavaVM* vm);
+
+LocalRef<jclass> FindClass(JNIEnv *env, const char *name);
+
+LocalRef<jthrowable> ExceptionOccurred(JNIEnv* env);
+
+template<typename Out, typename In>
+LocalRef<Out> CallObjectMethod(JNIEnv* env, const Ref<In>& object, jmethodID method) {
+ return {env, static_cast<Out>(env->CallObjectMethod(object.get(), method))};
+}
+
+std::string StringToUTF8(JNIEnv* env, const Ref<jstring>& str);
+
+LocalRef<jstring> UTF8ToString(JNIEnv* env, const std::string& str);
+
+namespace internal {
+
+template<typename T>
+void _abort_if_null(const char* file, int line, JNIEnv* env, const jni::Ref<T>& ref) {
+ if (ref) [[likely]] return;
+
+ _abort_with_exception(file, line, env);
+}
+
+} // namespace internal
+
+} // namespace jni
+
+#endif // CLEVERSYNC_JNI_HPP
diff --git a/libs/samba/src/main/cpp/samba.cpp b/libs/samba/src/main/cpp/samba.cpp
new file mode 100644
index 0000000..5eafacc
--- /dev/null
+++ b/libs/samba/src/main/cpp/samba.cpp
@@ -0,0 +1,155 @@
+#include <cassert>
+#include <jni.h>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "jni.hpp"
+#include "libsmb2.h"
+
+namespace {
+
+class Dir {
+ public:
+ Dir(std::shared_ptr<smb2_context> context, smb2dir* dir) : context_(std::move(context)), dir_(dir) {
+ assert(context_ && dir_);
+ }
+
+ ~Dir() {
+ smb2_closedir(context_.get(), dir_);
+ }
+
+ Dir(const Dir&) = delete;
+ Dir& operator=(const Dir&) = delete;
+
+ private:
+ std::shared_ptr<smb2_context> context_;
+ smb2dir* const dir_;
+};
+
+class Url {
+ public:
+ explicit Url(smb2_url* url) : url_(url) {
+ assert(url_);
+ }
+
+ ~Url() {
+ smb2_destroy_url(url_);
+ }
+
+ Url(const Url&) = delete;
+ Url& operator=(const Url&) = delete;
+
+ [[nodiscard]] const char* path() const { return url_->path; }
+ [[nodiscard]] const char* server() const { return url_->server; }
+ [[nodiscard]] const char* share() const { return url_->share; }
+ [[nodiscard]] const char* user() const { return url_->user; }
+
+ private:
+ smb2_url* url_;
+};
+
+class Context {
+ public:
+ Context() : context_(smb2_init_context(), ContextDeleter{}) {}
+ ~Context() = default;
+
+ Context(const Context&) = delete;
+ Context& operator=(const Context&) = delete;
+
+ [[nodiscard]] std::unique_ptr<Url> ParseUrl(const std::string& url) {
+ auto* ptr = smb2_parse_url(context_.get(), url.c_str());
+ return ptr ? std::make_unique<Url>(ptr): nullptr;
+ }
+
+ bool Connect(const Url& url) {
+ return smb2_connect_share(context_.get(), url.server(), url.share(), url.user()) == 0;
+ }
+
+ [[nodiscard]] std::string_view GetError() {
+ return smb2_get_error(context_.get());
+ }
+
+ [[nodiscard]] std::unique_ptr<Dir> OpenDir(const std::string& path) {
+ auto* ptr = smb2_opendir(context_.get(), path.c_str());
+ return ptr ? std::make_unique<Dir>(context_, ptr) : nullptr;
+ }
+
+ private:
+ struct ContextDeleter {
+ void operator()(smb2_context* context) {
+ smb2_destroy_context(context);
+ }
+ };
+
+ std::shared_ptr<smb2_context> context_;
+};
+
+jlong nativeContextNew(JNIEnv* env, jclass clazz) {
+ return reinterpret_cast<jlong>(new Context());
+}
+
+void nativeContextDestroy(JNIEnv* env, jclass clazz, jlong ptr) {
+ delete reinterpret_cast<Context*>(ptr);
+}
+
+jlong nativeContextParseUrl(JNIEnv* env, jclass clazz, jlong ptr, jstring url) {
+ return reinterpret_cast<jlong>(reinterpret_cast<Context*>(ptr)->ParseUrl(jni::StringToUTF8(env, jni::ParamRef(env, url))).release());
+}
+
+jboolean nativeContextConnect(JNIEnv* env, jclass clazz, jlong context_ptr, jlong url_ptr) {
+ auto* url = reinterpret_cast<Url*>(url_ptr);
+ if (!url) return JNI_FALSE;
+ return reinterpret_cast<Context*>(context_ptr)->Connect(*url) ? JNI_TRUE : JNI_FALSE;
+}
+
+jstring nativeContextGetError(JNIEnv* env, jclass clazz, jlong ptr) {
+ return jni::UTF8ToString(env, std::string(reinterpret_cast<Context*>(ptr)->GetError())).release();
+}
+
+jlong nativeContextOpenDir(JNIEnv* env, jclass clazz, jlong ptr, jstring path) {
+ return reinterpret_cast<jlong>(reinterpret_cast<Context*>(ptr)->OpenDir(jni::StringToUTF8(env, jni::ParamRef<jstring>(env, path))).release());
+}
+
+void nativeUrlDestroy(JNIEnv* env, jclass clazz, jlong ptr) {
+ delete reinterpret_cast<Url*>(ptr);
+}
+
+jstring nativeUrlPath(JNIEnv* env, jclass clazz, jlong ptr) {
+ return jni::UTF8ToString(env, std::string(reinterpret_cast<Url*>(ptr)->path())).release();
+}
+
+void nativeDirDestroy(JNIEnv* env, jclass clazz, jlong ptr) {
+ delete reinterpret_cast<Dir*>(ptr);
+}
+
+void RegisterSamba(JNIEnv* env) {
+ auto clazz = jni::FindClass(env, "org/the_jk/cleversync/io/samba/NativeSamba");
+ ABORT_IF_NULL(env, clazz);
+ static const JNINativeMethod methods[] = {
+ { "nativeContextNew", "()J", reinterpret_cast<void*>(&nativeContextNew) },
+ { "nativeContextDestroy", "(J)V", reinterpret_cast<void*>(&nativeContextDestroy) },
+ { "nativeContextParseUrl", "(JLjava/lang/String;)J", reinterpret_cast<void*>(&nativeContextParseUrl) },
+ { "nativeContextConnect", "(JJ)Z", reinterpret_cast<void*>(&nativeContextConnect) },
+ { "nativeContextGetError", "(J)Ljava/lang/String;", reinterpret_cast<void*>(&nativeContextGetError) },
+ { "nativeContextOpenDir", "(JLjava/lang/String;)J", reinterpret_cast<void*>(&nativeContextOpenDir) },
+
+ { "nativeUrlDestroy", "(J)V", reinterpret_cast<void*>(&nativeUrlDestroy) },
+ { "nativeUrlPath", "(J)Ljava/lang/String;", reinterpret_cast<void*>(&nativeUrlPath) },
+
+ { "nativeDirDestroy", "(J)V", reinterpret_cast<void*>(&nativeDirDestroy) },
+ };
+ auto ret = env->RegisterNatives(clazz.get(), methods, sizeof(methods) / sizeof(methods[0]));
+ ABORT_IF_NOT_OK(ret);
+}
+
+} // namespace
+
+jint JNI_OnLoad(JavaVM *vm, void *reserved) {
+ auto* env = jni::OnLoad(vm);
+
+ RegisterSamba(env);
+
+ return jni::JNI_VERSION;
+}
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
new file mode 100644
index 0000000..360aaa9
--- /dev/null
+++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/NativeSamba.kt
@@ -0,0 +1,92 @@
+// Using RegisterNatives
+@file:Suppress("KotlinJniMissingFunction")
+
+package org.the_jk.cleversync.io.samba
+
+import androidx.annotation.Keep
+
+@Keep
+internal object NativeSamba {
+ fun newContext(): Context = NativeContext(nativeContextNew())
+
+ interface Object {
+ fun destroy()
+ }
+
+ interface Context : Object {
+ fun parseUrl(url: String): Url?
+ fun connect(url: Url): Boolean
+ fun lastError(): String
+ fun openDir(path: String): Dir?
+ }
+
+ interface Url : Object {
+ fun path(): String
+ }
+
+ interface Dir : Object {
+ val path: String
+ }
+
+ private class NativeContext(private var ptr: Long): Context {
+ override fun destroy() {
+ if (ptr == 0L) return
+ nativeContextDestroy(ptr)
+ ptr = 0L
+ }
+
+ override fun parseUrl(url: String): Url? {
+ val ptr = nativeContextParseUrl(ptr, url)
+ return if (ptr != 0L) NativeUrl(ptr) else null
+ }
+
+ override fun connect(url: Url): Boolean {
+ return nativeContextConnect(ptr, (url as NativeUrl).get())
+ }
+
+ override fun lastError(): String {
+ return nativeContextGetError(ptr)
+ }
+
+ override fun openDir(path: String): Dir? {
+ val dir = nativeContextOpenDir(ptr, path)
+ return if (dir != 0L) NativeDir(path, dir) else null
+ }
+ }
+
+ private class NativeUrl(private var ptr: Long): Url {
+ override fun destroy() {
+ if (ptr == 0L) return
+ nativeUrlDestroy(ptr)
+ ptr = 0L
+ }
+
+ override fun path(): String = nativeUrlPath(ptr)
+
+ fun get(): Long = ptr
+ }
+
+ private class NativeDir(override val path: String, private var ptr: Long): Dir {
+ override fun destroy() {
+ if (ptr == 0L) return
+ nativeDirDestroy(ptr)
+ ptr = 0L
+ }
+ }
+
+ init {
+ System.loadLibrary("samba")
+ }
+
+ private external fun nativeContextNew(): Long
+ private external fun nativeContextDestroy(ptr: Long)
+ private external fun nativeContextParseUrl(ptr: Long, url: String): Long
+ private external fun nativeContextConnect(ptr: Long, url: Long): Boolean
+ private external fun nativeContextGetError(ptr: Long): String
+ private external fun nativeContextOpenDir(ptr: Long, path: String): Long
+
+ private external fun nativeUrlDestroy(ptr: Long)
+ private external fun nativeUrlPath(ptr: Long): String
+
+ private external fun nativeDirDestroy(ptr: Long)
+}
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
new file mode 100644
index 0000000..03d3a9e
--- /dev/null
+++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaConnection.kt
@@ -0,0 +1,32 @@
+package org.the_jk.cleversync.io.samba
+
+internal class SambaConnection(uri: String) {
+ private val context = NativeSamba.newContext()
+ private val url = context.parseUrl(uri)
+
+ val connected = if (url != null) { context.connect(url) } else false
+
+ val error: String
+ get() = context.lastError()
+
+ fun openDir(path: String): NativeSamba.Dir? =
+ if (connected) context.openDir(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
new file mode 100644
index 0000000..b82e745
--- /dev/null
+++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaDirectory.kt
@@ -0,0 +1,88 @@
+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
+
+internal open class SambaDirectory(
+ private val conn: SambaConnection,
+ private val dir: NativeSamba.Dir,
+) : ModifiableDirectory {
+ override fun modifiableOpenDir(name: String): ModifiableDirectory? {
+ val dir = conn.openDir(SambaConnection.join(dir.path, name)) ?: return null
+ return SambaDirectory(conn, dir)
+ }
+
+ override fun modifiableOpenFile(name: String): ModifiableFile? {
+ TODO("Not yet implemented")
+ }
+
+ override fun modifiableOpenLink(name: String): ModifiableLink? {
+ TODO("Not yet implemented")
+ }
+
+ override fun modifiableList(): ModifiableDirectory.Content {
+ TODO("Not yet implemented")
+ }
+
+ override fun modifiableLiveList(): LiveData<ModifiableDirectory.Content> {
+ TODO("Not yet implemented")
+ }
+
+ override fun createDirectory(name: String): ModifiableDirectory {
+ TODO("Not yet implemented")
+ }
+
+ override fun createFile(name: String): ModifiableFile {
+ TODO("Not yet implemented")
+ }
+
+ override fun createLink(name: String, target: Directory): ModifiableLink {
+ TODO("Not yet implemented")
+ }
+
+ override fun createLink(name: String, target: File): ModifiableLink {
+ TODO("Not yet implemented")
+ }
+
+ override fun createLink(name: String, target: String): ModifiableLink {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeDirectory(name: String): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeFile(name: String): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ override fun removeLink(name: String): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ 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 openLink(name: String): Link? {
+ TODO("Not yet implemented")
+ }
+
+ override fun list(): Directory.Content {
+ TODO("Not yet implemented")
+ }
+
+ override fun liveList(): LiveData<Directory.Content> {
+ TODO("Not yet implemented")
+ }
+}
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
new file mode 100644
index 0000000..8b4a86d
--- /dev/null
+++ b/libs/samba/src/main/java/org/the_jk/cleversync/io/samba/SambaTree.kt
@@ -0,0 +1,10 @@
+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 {
+ 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
new file mode 100644
index 0000000..3455de8
--- /dev/null
+++ b/libs/samba/src/main/java/org/the_jk/cleversync/samba/SambaTreeFactory.kt
@@ -0,0 +1,17 @@
+package org.the_jk.cleversync.samba
+
+import org.the_jk.cleversync.io.ModifiableTree
+import org.the_jk.cleversync.io.Tree
+import org.the_jk.cleversync.io.samba.SambaConnection
+import org.the_jk.cleversync.io.samba.SambaTree
+
+object SambaTreeFactory {
+ fun tree(uri: String): Result<Tree> = modifiableTree(uri)
+
+ fun modifiableTree(uri: String): Result<ModifiableTree> {
+ val connection = SambaConnection(uri)
+ 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))
+ }
+}