summaryrefslogtreecommitdiff
path: root/libs/local/src/main/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'libs/local/src/main/java/org')
-rw-r--r--libs/local/src/main/java/org/the_jk/cleversync/io/local/PathDirectory.kt188
-rw-r--r--libs/local/src/main/java/org/the_jk/cleversync/io/local/PathFile.kt66
-rw-r--r--libs/local/src/main/java/org/the_jk/cleversync/io/local/PathLink.kt62
-rw-r--r--libs/local/src/main/java/org/the_jk/cleversync/io/local/PathTree.kt10
-rw-r--r--libs/local/src/main/java/org/the_jk/cleversync/io/local/PathWatcher.kt82
-rw-r--r--libs/local/src/main/java/org/the_jk/cleversync/local/LocalTreeFactory.kt16
6 files changed, 424 insertions, 0 deletions
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<ModifiableDirectory.Content> by lazy {
+ watcher.content
+ }
+
+ private val liveContent: LiveData<Directory.Content> 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<ModifiableDirectory.Content>
+ get() = _content
+
+ @AnyThread
+ override fun update(added: List<Path>, removed: List<Path>) {
+ try {
+ _content.postValue(makeContent(path.listDirectoryEntries()))
+ } catch (ignored: NoSuchFileException) {
+ }
+ }
+
+ private val _content = object : MutableLiveData<ModifiableDirectory.Content>() {
+ 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<Path>): ModifiableDirectory.Content {
+ val directories = mutableListOf<ModifiableDirectory>()
+ val files = mutableListOf<ModifiableFile>()
+ val links = mutableListOf<ModifiableLink>()
+ 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<Path, WatchKey>()
+ private val lock = Object()
+ @GuardedBy("lock")
+ private val delegates = mutableMapOf<WatchKey, Delegate>()
+
+ 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<Path>, removed: List<Path>)
+ }
+
+ @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<Path>()
+ val removed = mutableListOf<Path>()
+ 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)
+ }
+}