diff options
Diffstat (limited to 'libs/local/src/main/java/org')
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) + } +} |
