From 5ac1ae8525181ba86ac6c17ef2192a5f7b17a86c Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Thu, 11 Jul 2024 23:28:01 +0200 Subject: Initial commit Local (Path based) implementation of Tree, Directory, File and Link. --- .../java/org/the_jk/cleversync/FirstFragment.kt | 35 +++++ .../java/org/the_jk/cleversync/LiveDataUtils.kt | 14 ++ .../java/org/the_jk/cleversync/MainActivity.kt | 60 ++++++++ .../java/org/the_jk/cleversync/SecondFragment.kt | 35 +++++ .../java/org/the_jk/cleversync/io/Directory.kt | 15 ++ app/src/main/java/org/the_jk/cleversync/io/File.kt | 12 ++ app/src/main/java/org/the_jk/cleversync/io/Link.kt | 13 ++ .../the_jk/cleversync/io/ModifiableDirectory.kt | 21 +++ .../org/the_jk/cleversync/io/ModifiableFile.kt | 7 + .../org/the_jk/cleversync/io/ModifiableLink.kt | 15 ++ .../org/the_jk/cleversync/io/ModifiableTree.kt | 3 + app/src/main/java/org/the_jk/cleversync/io/Tree.kt | 7 + .../java/org/the_jk/cleversync/io/TreeFactory.kt | 12 ++ .../org/the_jk/cleversync/io/impl/PathDirectory.kt | 161 +++++++++++++++++++++ .../java/org/the_jk/cleversync/io/impl/PathFile.kt | 65 +++++++++ .../java/org/the_jk/cleversync/io/impl/PathLink.kt | 49 +++++++ .../java/org/the_jk/cleversync/io/impl/PathTree.kt | 10 ++ .../org/the_jk/cleversync/io/impl/PathWatcher.kt | 82 +++++++++++ 18 files changed, 616 insertions(+) create mode 100644 app/src/main/java/org/the_jk/cleversync/FirstFragment.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/MainActivity.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/SecondFragment.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/Directory.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/File.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/Link.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/Tree.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt create mode 100644 app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt (limited to 'app/src/main/java/org') diff --git a/app/src/main/java/org/the_jk/cleversync/FirstFragment.kt b/app/src/main/java/org/the_jk/cleversync/FirstFragment.kt new file mode 100644 index 0000000..fa73bf6 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/FirstFragment.kt @@ -0,0 +1,35 @@ +package org.the_jk.cleversync + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import org.the_jk.cleversync.databinding.FragmentFirstBinding + +class FirstFragment : Fragment() { + private var _binding: FragmentFirstBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonFirst.setOnClickListener { + findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt b/app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt new file mode 100644 index 0000000..7f6ab1f --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt @@ -0,0 +1,14 @@ +package org.the_jk.cleversync + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +fun LiveData.safeValue(): T? { + if (this.hasActiveObservers()) + return value + var ret: T? = null + val observer = Observer { value -> ret = value } + this.observeForever(observer) + this.removeObserver(observer) + return ret +} diff --git a/app/src/main/java/org/the_jk/cleversync/MainActivity.kt b/app/src/main/java/org/the_jk/cleversync/MainActivity.kt new file mode 100644 index 0000000..96fd167 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/MainActivity.kt @@ -0,0 +1,60 @@ +package org.the_jk.cleversync + +import android.os.Bundle +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import android.view.Menu +import android.view.MenuItem +import org.the_jk.cleversync.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navController = + findNavController(R.id.nav_host_fragment_content_main) + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + + binding.fab.setOnClickListener { view -> + Snackbar.make( + view, + "Replace with your own action", + Snackbar.LENGTH_LONG + ) + .setAction("Action", null) + .setAnchorView(R.id.fab).show() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = + findNavController(R.id.nav_host_fragment_content_main) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/SecondFragment.kt b/app/src/main/java/org/the_jk/cleversync/SecondFragment.kt new file mode 100644 index 0000000..b105450 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/SecondFragment.kt @@ -0,0 +1,35 @@ +package org.the_jk.cleversync + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import org.the_jk.cleversync.databinding.FragmentSecondBinding + +class SecondFragment : Fragment() { + private var _binding: FragmentSecondBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSecondBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonSecond.setOnClickListener { + findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/Directory.kt b/app/src/main/java/org/the_jk/cleversync/io/Directory.kt new file mode 100644 index 0000000..2273f3e --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/Directory.kt @@ -0,0 +1,15 @@ +package org.the_jk.cleversync.io + +import androidx.lifecycle.LiveData + +interface Directory { + val name: String + + fun list(): Content + + data class Content( + val directories: LiveData>, + val files: LiveData>, + val links: LiveData>, + ) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/File.kt b/app/src/main/java/org/the_jk/cleversync/io/File.kt new file mode 100644 index 0000000..b5333eb --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/File.kt @@ -0,0 +1,12 @@ +package org.the_jk.cleversync.io + +import java.io.InputStream +import java.time.Instant + +interface File { + val name: String + val size: ULong + val lastModified: Instant + + fun open(): InputStream +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/Link.kt b/app/src/main/java/org/the_jk/cleversync/io/Link.kt new file mode 100644 index 0000000..3ecf5e6 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/Link.kt @@ -0,0 +1,13 @@ +package org.the_jk.cleversync.io + +interface Link { + val name: String + + fun resolve(): LinkTarget + + sealed class LinkTarget + + class DirectoryTarget(val directory: Directory): LinkTarget() + class FileTarget(val file: File): LinkTarget() + data object NoTarget: LinkTarget() +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt new file mode 100644 index 0000000..43efa8f --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt @@ -0,0 +1,21 @@ +package org.the_jk.cleversync.io + +import androidx.lifecycle.LiveData + +interface ModifiableDirectory : Directory { + fun modifiableList(): Content + + fun createDirectory(name: String): ModifiableDirectory + fun createFile(name: String): ModifiableFile + fun createLink(name: String, target: String): ModifiableLink + + fun removeDirectory(name: String): Boolean + fun removeFile(name: String): Boolean + fun removeLink(name: String): Boolean + + data class Content( + val directories: LiveData>, + val files: LiveData>, + val links: LiveData>, + ) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt new file mode 100644 index 0000000..8675dae --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt @@ -0,0 +1,7 @@ +package org.the_jk.cleversync.io + +import java.io.OutputStream + +interface ModifiableFile : File { + fun write(): OutputStream +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt new file mode 100644 index 0000000..a20bb6a --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt @@ -0,0 +1,15 @@ +package org.the_jk.cleversync.io + +interface ModifiableLink : Link { + fun modifiableResolve(): ModifiableLinkTarget + + fun target(directory: Directory) = target(directory.name) + fun target(file: File) = target(file.name) + fun target(name: String) + + sealed class ModifiableLinkTarget + + class ModifiableDirectoryTarget(val directory: ModifiableDirectory): ModifiableLinkTarget() + class ModifiableFileTarget(val file: ModifiableFile): ModifiableLinkTarget() + data object ModifiableNoTarget: ModifiableLinkTarget() +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt new file mode 100644 index 0000000..383360d --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt @@ -0,0 +1,3 @@ +package org.the_jk.cleversync.io + +interface ModifiableTree : Tree, ModifiableDirectory diff --git a/app/src/main/java/org/the_jk/cleversync/io/Tree.kt b/app/src/main/java/org/the_jk/cleversync/io/Tree.kt new file mode 100644 index 0000000..b6f2d54 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/Tree.kt @@ -0,0 +1,7 @@ +package org.the_jk.cleversync.io + +import android.content.res.Resources + +interface Tree : Directory { + fun description(resources: Resources): CharSequence +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt b/app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt new file mode 100644 index 0000000..d7c22f5 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt @@ -0,0 +1,12 @@ +package org.the_jk.cleversync.io + +import org.the_jk.cleversync.io.impl.PathTree +import java.nio.file.Path + +object TreeFactory { + fun localModifiableTree(root: Path): ModifiableTree { + return PathTree(root) + } + + fun localTree(root: Path): Tree = localModifiableTree(root) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt new file mode 100644 index 0000000..fab4dcc --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt @@ -0,0 +1,161 @@ +package org.the_jk.cleversync.io.impl + +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.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 + +@OptIn(ExperimentalPathApi::class) +internal open class PathDirectory( + private val path: Path, + private val pathWatcher: PathWatcher, +) : ModifiableDirectory { + private val watcher: DirectoryWatcher by lazy { + DirectoryWatcher() + } + + private val modifiableContent: ModifiableDirectory.Content by lazy { + val base = watcher.content + ModifiableDirectory.Content( + base.map { entries -> + entries.filterIsInstance().map { entry -> + PathDirectory(entry.path, pathWatcher) + } + }, + base.map { entries -> + entries.filterIsInstance().map { entry -> + PathFile(entry.path) + } + }, + base.map { entries -> + entries.filterIsInstance().map { entry -> + PathLink(entry.path, pathWatcher) + } + }, + ) + } + + private val content: Directory.Content by lazy { + val base = modifiableContent + Directory.Content( + base.directories.map { it }, + base.files.map { it }, + base.links.map { it }, + ) + } + + override fun modifiableList() = modifiableContent + + 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: String): ModifiableLink { + val path = path.resolve(name) + return PathLink(path.createSymbolicLinkPointingTo(path.resolve(target)), 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() = content + + override fun equals(other: Any?) = other is PathDirectory && other.path == path + override fun hashCode() = path.hashCode() + + private sealed class Entry(val path: Path) { + class Directory(path: Path) : Entry(path) + class File(path: Path) : Entry(path) + class Link(path: Path) : Entry(path) + } + + private inner class DirectoryWatcher : PathWatcher.Delegate { + val content: LiveData> + get() = _content + + @AnyThread + override fun update(added: List, removed: List) { + try { + _content.postValue(mapEntries(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 = mapEntries(entries) + } + + private fun clear() { + pathWatcher.remove(path) + } + } + + companion object { + private fun mapEntries(entries: List): List { + return entries.mapNotNull { + if (it.isDirectory(LinkOption.NOFOLLOW_LINKS)) { + Entry.Directory(it) + } else if (it.isRegularFile(LinkOption.NOFOLLOW_LINKS)) { + Entry.File(it) + } else if (it.isSymbolicLink()) { + Entry.Link(it) + } else { + null + } + } + } + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt new file mode 100644 index 0000000..d8ca900 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt @@ -0,0 +1,65 @@ +package org.the_jk.cleversync.io.impl + +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(private 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 open(): InputStream { + return path.inputStream(StandardOpenOption.READ) + } + + override fun equals(other: Any?) = other is PathFile && other.path == path + override fun hashCode() = path.hashCode() +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt new file mode 100644 index 0000000..5d11228 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt @@ -0,0 +1,49 @@ +package org.the_jk.cleversync.io.impl + +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, pathWatcher)) + } else if (target.isRegularFile()) { + ModifiableLink.ModifiableFileTarget(PathFile(target)) + } else { + ModifiableLink.ModifiableNoTarget + } + } + + override fun target(name: String) { + path.deleteIfExists() + path.createSymbolicLinkPointingTo(path.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 resolve(): Link.LinkTarget { + val target = path.readSymbolicLink() + return if (target.isDirectory()) { + Link.DirectoryTarget(PathDirectory(target, pathWatcher)) + } else if (target.isRegularFile()) { + Link.FileTarget(PathFile(target)) + } else { + Link.NoTarget + } + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt new file mode 100644 index 0000000..a8a74c5 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt @@ -0,0 +1,10 @@ +package org.the_jk.cleversync.io.impl + +import android.content.res.Resources +import org.the_jk.cleversync.R +import org.the_jk.cleversync.io.ModifiableTree +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/app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt new file mode 100644 index 0000000..945019a --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt @@ -0,0 +1,82 @@ +package org.the_jk.cleversync.io.impl + +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() + } + } +} -- cgit v1.2.3-70-g09d2