From 42564c71cfb70c28831c662a3b6bf4084e079353 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Mon, 15 Jul 2024 23:52:28 +0200 Subject: Break out io code in libs Preparing for adding more io implementations. Really tried writing the convention plugins in kotlin dsl but could not find the exact right hacks to get it to work. --- .../the_jk/cleversync/io/local/PathDirectory.kt | 188 +++++++++++++++++++++ .../org/the_jk/cleversync/io/local/PathFile.kt | 66 ++++++++ .../org/the_jk/cleversync/io/local/PathLink.kt | 62 +++++++ .../org/the_jk/cleversync/io/local/PathTree.kt | 10 ++ .../org/the_jk/cleversync/io/local/PathWatcher.kt | 82 +++++++++ .../the_jk/cleversync/local/LocalTreeFactory.kt | 16 ++ libs/local/src/main/res/values/strings.xml | 3 + 7 files changed, 427 insertions(+) create mode 100644 libs/local/src/main/java/org/the_jk/cleversync/io/local/PathDirectory.kt create mode 100644 libs/local/src/main/java/org/the_jk/cleversync/io/local/PathFile.kt create mode 100644 libs/local/src/main/java/org/the_jk/cleversync/io/local/PathLink.kt create mode 100644 libs/local/src/main/java/org/the_jk/cleversync/io/local/PathTree.kt create mode 100644 libs/local/src/main/java/org/the_jk/cleversync/io/local/PathWatcher.kt create mode 100644 libs/local/src/main/java/org/the_jk/cleversync/local/LocalTreeFactory.kt create mode 100644 libs/local/src/main/res/values/strings.xml (limited to 'libs/local/src/main') diff --git a/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathDirectory.kt b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathDirectory.kt new file mode 100644 index 0000000..9899f02 --- /dev/null +++ b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathDirectory.kt @@ -0,0 +1,188 @@ +package org.the_jk.cleversync.io.local + +import androidx.annotation.AnyThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import org.the_jk.cleversync.io.Directory +import org.the_jk.cleversync.io.File +import org.the_jk.cleversync.io.ModifiableDirectory +import org.the_jk.cleversync.io.ModifiableFile +import org.the_jk.cleversync.io.ModifiableLink +import java.nio.file.LinkOption +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createDirectory +import kotlin.io.path.createSymbolicLinkPointingTo +import kotlin.io.path.deleteIfExists +import kotlin.io.path.deleteRecursively +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.isSymbolicLink +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.readSymbolicLink + +@OptIn(ExperimentalPathApi::class) +internal open class PathDirectory( + internal val path: Path, + private val pathWatcher: PathWatcher, +) : ModifiableDirectory { + private val watcher: DirectoryWatcher by lazy { + DirectoryWatcher() + } + + private val modifiableLiveContent: LiveData by lazy { + watcher.content + } + + private val liveContent: LiveData by lazy { + modifiableLiveContent.map { + Directory.Content( + it.directories, + it.files, + it.links, + ) + } + } + + override fun modifiableOpenDir(name: String): ModifiableDirectory? { + val path = path.resolve(name) + if (path.isDirectory(LinkOption.NOFOLLOW_LINKS)) return PathDirectory(path, pathWatcher) + if (path.isSymbolicLink()) { + val target = path.readSymbolicLink() + if (target.isDirectory()) return PathDirectory(target.toRealPath(), pathWatcher) + } + return null + } + + override fun modifiableOpenFile(name: String): ModifiableFile? { + val path = path.resolve(name) + if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS)) return PathFile(path) + if (path.isSymbolicLink()) { + val target = path.readSymbolicLink() + if (target.isRegularFile()) return PathFile(target.toRealPath()) + } + return null + } + + override fun modifiableOpenLink(name: String): ModifiableLink? { + val path = path.resolve(name) + return if (path.isSymbolicLink()) PathLink(path, pathWatcher) else null + } + + override fun modifiableList() = makeContent(path.listDirectoryEntries()) + override fun modifiableLiveList() = modifiableLiveContent + + override fun createDirectory(name: String): ModifiableDirectory { + val path = path.resolve(name) + return PathDirectory(path.createDirectory(), pathWatcher) + } + + override fun createFile(name: String): ModifiableFile { + val path = path.resolve(name) + return PathFile(path) + } + + override fun createLink(name: String, target: Directory): ModifiableLink { + val path = path.resolve(name) + return PathLink(path.createSymbolicLinkPointingTo((target as PathDirectory).path), pathWatcher) + } + + override fun createLink(name: String, target: File): ModifiableLink { + val path = path.resolve(name) + return PathLink(path.createSymbolicLinkPointingTo((target as PathFile).path), pathWatcher) + } + + override fun createLink(name: String, target: String): ModifiableLink { + val targetPath = path.resolve(target) + val path = path.resolve(name) + return PathLink(path.createSymbolicLinkPointingTo(targetPath), pathWatcher) + } + + override fun removeDirectory(name: String): Boolean { + val path = path.resolve(name) + return if (path.isDirectory(LinkOption.NOFOLLOW_LINKS)) { + path.deleteRecursively() + true + } else false + } + + override fun removeFile(name: String): Boolean { + val path = path.resolve(name) + return path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && path.deleteIfExists() + } + + override fun removeLink(name: String): Boolean { + val path = path.resolve(name) + return path.isSymbolicLink() && path.deleteIfExists() + } + + override val name: String + get() = path.name + + override fun list(): Directory.Content { + val modifiable = modifiableList() + return Directory.Content(modifiable.directories, modifiable.files, modifiable.links) + } + + override fun liveList() = liveContent + + override fun openDir(name: String) = modifiableOpenDir(name) + override fun openFile(name: String) = modifiableOpenFile(name) + override fun openLink(name: String) = modifiableOpenLink(name) + + override fun equals(other: Any?) = other is PathDirectory && other.path == path + override fun hashCode() = path.hashCode() + override fun toString() = path.toString() + + private inner class DirectoryWatcher : PathWatcher.Delegate { + val content: LiveData + get() = _content + + @AnyThread + override fun update(added: List, removed: List) { + try { + _content.postValue(makeContent(path.listDirectoryEntries())) + } catch (ignored: NoSuchFileException) { + } + } + + private val _content = object : MutableLiveData() { + override fun onActive() { + setup() + } + + override fun onInactive() { + clear() + } + } + + private fun setup() { + val entries = path.listDirectoryEntries() + pathWatcher.add(path, this) + _content.value = makeContent(entries) + } + + private fun clear() { + pathWatcher.remove(path) + } + } + + private fun makeContent(entries: List): ModifiableDirectory.Content { + val directories = mutableListOf() + val files = mutableListOf() + val links = mutableListOf() + entries.forEach { + if (it.isDirectory(LinkOption.NOFOLLOW_LINKS)) { + directories.add(PathDirectory(it, pathWatcher)) + } else if (it.isRegularFile(LinkOption.NOFOLLOW_LINKS)) { + files.add(PathFile(it)) + } else if (it.isSymbolicLink()) { + links.add(PathLink(it, pathWatcher)) + } + } + return ModifiableDirectory.Content(directories, files, links) + } +} diff --git a/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathFile.kt b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathFile.kt new file mode 100644 index 0000000..6aeb895 --- /dev/null +++ b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathFile.kt @@ -0,0 +1,66 @@ +package org.the_jk.cleversync.io.local + +import org.the_jk.cleversync.io.ModifiableFile +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.nio.file.StandardOpenOption +import java.time.Instant +import kotlin.io.path.exists +import kotlin.io.path.fileSize +import kotlin.io.path.getLastModifiedTime +import kotlin.io.path.inputStream +import kotlin.io.path.name +import kotlin.io.path.outputStream + +internal class PathFile(internal val path: Path) : ModifiableFile { + override fun write(): OutputStream { + // If file doesn't exist, write to it directly. + if (!path.exists(LinkOption.NOFOLLOW_LINKS)) + return path.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + + // Otherwise, write to temp file, only overwriting when done. + val tmp = path.parent.resolve(".#" + path.name) + val os = tmp.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + return object : OutputStream() { + override fun write(value: Int) { + os.write(value) + } + + override fun write(b: ByteArray) { + os.write(b) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + os.write(b, off, len) + } + + override fun flush() { + os.flush() + } + + override fun close() { + os.close() + Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE) + } + } + } + + override val name: String + get() = path.name + override val size: ULong + get() = path.fileSize().toULong() + override val lastModified: Instant + get() = path.getLastModifiedTime().toInstant() + + override fun read(): InputStream { + return path.inputStream(StandardOpenOption.READ) + } + + override fun equals(other: Any?) = other is PathFile && other.path == path + override fun hashCode() = path.hashCode() + override fun toString() = path.toString() +} diff --git a/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathLink.kt b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathLink.kt new file mode 100644 index 0000000..97cd117 --- /dev/null +++ b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathLink.kt @@ -0,0 +1,62 @@ +package org.the_jk.cleversync.io.local + +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.nio.file.Path +import kotlin.io.path.createSymbolicLinkPointingTo +import kotlin.io.path.deleteIfExists +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.name +import kotlin.io.path.readSymbolicLink + +internal class PathLink( + private val path: Path, + private val pathWatcher: PathWatcher, +) : ModifiableLink { + override fun modifiableResolve(): ModifiableLink.ModifiableLinkTarget { + val target = path.readSymbolicLink() + return if (target.isDirectory()) { + ModifiableLink.ModifiableDirectoryTarget(PathDirectory(target.toRealPath(), pathWatcher)) + } else if (target.isRegularFile()) { + ModifiableLink.ModifiableFileTarget(PathFile(target.toRealPath())) + } else { + ModifiableLink.NoTarget + } + } + + override fun target(directory: Directory) { + path.deleteIfExists() + path.createSymbolicLinkPointingTo((directory as PathDirectory).path) + } + + override fun target(file: File) { + path.deleteIfExists() + path.createSymbolicLinkPointingTo((file as PathFile).path) + } + + override fun target(name: String) { + path.deleteIfExists() + path.createSymbolicLinkPointingTo(path.parent.resolve(name)) + } + + override val name: String + get() = path.name + + override fun equals(other: Any?) = other is PathLink && other.path == path + override fun hashCode() = path.hashCode() + override fun toString() = path.toString() + + override fun resolve(): Link.LinkTarget { + val target = path.readSymbolicLink() + return if (target.isDirectory()) { + Link.DirectoryTarget(PathDirectory(target.toRealPath(), pathWatcher)) + } else if (target.isRegularFile()) { + Link.FileTarget(PathFile(target.toRealPath())) + } else { + Link.NoTarget + } + } +} diff --git a/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathTree.kt b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathTree.kt new file mode 100644 index 0000000..23442a1 --- /dev/null +++ b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathTree.kt @@ -0,0 +1,10 @@ +package org.the_jk.cleversync.io.local + +import android.content.res.Resources +import org.the_jk.cleversync.io.ModifiableTree +import org.the_jk.cleversync.local.R +import java.nio.file.Path + +internal class PathTree(root: Path) : PathDirectory(root, PathWatcher()), ModifiableTree { + override fun description(resources: Resources) = resources.getString(R.string.local_directory) +} diff --git a/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathWatcher.kt b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathWatcher.kt new file mode 100644 index 0000000..0fa9f03 --- /dev/null +++ b/libs/local/src/main/java/org/the_jk/cleversync/io/local/PathWatcher.kt @@ -0,0 +1,82 @@ +package org.the_jk.cleversync.io.local + +import androidx.annotation.GuardedBy +import androidx.annotation.WorkerThread +import java.nio.file.ClosedWatchServiceException +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchKey +import java.nio.file.WatchService +import java.util.concurrent.Executors + +internal class PathWatcher { + private val executor = Executors.newSingleThreadExecutor() + private var service: WatchService? = null + private val keys = mutableMapOf() + private val lock = Object() + @GuardedBy("lock") + private val delegates = mutableMapOf() + + fun add(path: Path, delegate: Delegate) { + if (keys.isEmpty()) { + val service = FileSystems.getDefault().newWatchService() + executor.execute{ runner(service) } + this.service = service + } + val key = path.register(service!!, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE) + delegates[key] = delegate + keys[path] = key + } + + fun remove(path: Path) { + val key = keys.remove(path) ?: return + key.cancel() + synchronized(lock) { + delegates.remove(key) + } + if (keys.isEmpty()) { + service?.close() + service = null + } + } + + interface Delegate { + fun update(added: List, removed: List) + } + + @WorkerThread + private fun runner(service: WatchService) { + while (true) { + val key: WatchKey + try { + key = service.take() + } catch (ignored: InterruptedException) { + return + } catch (ignored: ClosedWatchServiceException) { + return + } + + val added = mutableListOf() + val removed = mutableListOf() + var overflow = false + + for (event in key.pollEvents()) { + when (event.kind()) { + StandardWatchEventKinds.OVERFLOW -> overflow = true + StandardWatchEventKinds.ENTRY_CREATE -> added.add(event.context() as Path) + StandardWatchEventKinds.ENTRY_DELETE -> removed.add(event.context() as Path) + } + } + + if (overflow || added.isNotEmpty() || removed.isNotEmpty()) { + val delegate = synchronized(lock) { + delegates[key] + } + delegate?.update(added, removed) + } + + key.reset() + } + } +} diff --git a/libs/local/src/main/java/org/the_jk/cleversync/local/LocalTreeFactory.kt b/libs/local/src/main/java/org/the_jk/cleversync/local/LocalTreeFactory.kt new file mode 100644 index 0000000..3990416 --- /dev/null +++ b/libs/local/src/main/java/org/the_jk/cleversync/local/LocalTreeFactory.kt @@ -0,0 +1,16 @@ +package org.the_jk.cleversync.local + +import org.the_jk.cleversync.io.ModifiableTree +import org.the_jk.cleversync.io.Tree +import org.the_jk.cleversync.io.local.PathTree +import java.nio.file.Path + +object LocalTreeFactory { + fun tree(path: Path): Tree { + return PathTree(path) + } + + fun modifiableTree(path: Path): ModifiableTree { + return PathTree(path) + } +} diff --git a/libs/local/src/main/res/values/strings.xml b/libs/local/src/main/res/values/strings.xml new file mode 100644 index 0000000..9fef6ef --- /dev/null +++ b/libs/local/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Local directory + -- cgit v1.2.3-70-g09d2