summaryrefslogtreecommitdiff
path: root/libs/local/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/local/src')
-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
-rw-r--r--libs/local/src/main/res/values/strings.xml3
-rw-r--r--libs/local/src/test/java/org/the_jk/cleversync/local/LocalTreeTest.kt327
8 files changed, 754 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)
+ }
+}
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 @@
+<resources>
+ <string name="local_directory">Local directory</string>
+</resources>
diff --git a/libs/local/src/test/java/org/the_jk/cleversync/local/LocalTreeTest.kt b/libs/local/src/test/java/org/the_jk/cleversync/local/LocalTreeTest.kt
new file mode 100644
index 0000000..21002e3
--- /dev/null
+++ b/libs/local/src/test/java/org/the_jk/cleversync/local/LocalTreeTest.kt
@@ -0,0 +1,327 @@
+package org.the_jk.cleversync.local
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLooper
+import org.the_jk.cleversync.io.Directory
+import org.the_jk.cleversync.io.Link
+import org.the_jk.cleversync.io.ModifiableLink
+import org.the_jk.cleversync.io.ModifiableTree
+import org.the_jk.cleversync.safeValue
+
+@Config(manifest=Config.NONE)
+@RunWith(RobolectricTestRunner::class)
+class LocalTreeTest {
+ @get:Rule
+ val folder = TemporaryFolder()
+
+ private lateinit var tree: ModifiableTree
+
+ @Before
+ fun setUp() {
+ tree = LocalTreeFactory.modifiableTree(folder.root.toPath())
+ }
+
+ @Test
+ fun empty() {
+ val content = tree.list()
+ assertThat(content.directories).isEmpty()
+ assertThat(content.files).isEmpty()
+ assertThat(content.links).isEmpty()
+ }
+
+ @Test
+ fun emptyLive() {
+ val content = tree.liveList().safeValue()
+ assertThat(content?.directories).isEmpty()
+ assertThat(content?.files).isEmpty()
+ assertThat(content?.links).isEmpty()
+ }
+
+ @Test
+ fun createDirectory() {
+ val foo = tree.createDirectory("foo")
+ assertThat(foo.name).isEqualTo("foo")
+ val fooContent = foo.list()
+ assertThat(fooContent.directories).isEmpty()
+ assertThat(fooContent.files).isEmpty()
+ assertThat(fooContent.links).isEmpty()
+ val content = tree.list()
+ assertThat(content.directories).contains(foo)
+ assertThat(content.files).isEmpty()
+ assertThat(content.links).isEmpty()
+ }
+
+ @Test
+ fun observeCreateDirectory() {
+ val content = tree.liveList()
+ var dir: Directory? = null
+ content.observeForever {
+ if (it.directories.size == 1) dir = it.directories[0]
+ }
+ tree.createDirectory("foo")
+ while (dir == null) {
+ ShadowLooper.idleMainLooper()
+ }
+ assertThat(dir?.name).isEqualTo("foo")
+ }
+
+ @Test
+ fun createFile() {
+ val foo = tree.createFile("foo")
+ // Files are not created until you write to them.
+ assertThat(tree.list().files).isEmpty()
+ foo.write().use { os ->
+ os.write(byteArrayOf(1, 2, 3, 4))
+ }
+ assertThat(tree.list().files).contains(foo)
+ assertThat(foo.size).isEqualTo(4.toULong())
+ foo.read().use {
+ assertThat(it.readBytes()).isEqualTo(byteArrayOf(1, 2, 3, 4))
+ }
+ }
+
+ @Test
+ fun overwriteFile() {
+ val foo = tree.createFile("foo")
+ foo.write().use { os ->
+ os.write(byteArrayOf(1, 2, 3, 4))
+ }
+ foo.write().use { os ->
+ os.write(127)
+ os.write(byteArrayOf(1))
+ os.write(byteArrayOf(2), 0, 0)
+ assertThat(foo.size).isEqualTo(4.toULong())
+ }
+ assertThat(foo.size).isEqualTo(2.toULong())
+ assertThat(tree.list().files).hasSize(1)
+ foo.read().use {
+ assertThat(it.readBytes()).isEqualTo(byteArrayOf(127, 1))
+ }
+ }
+
+ @Test
+ fun removeDir() {
+ tree.createDirectory("foo")
+ tree.removeDirectory("foo")
+ assertThat(tree.list().directories).isEmpty()
+ }
+
+ @Test
+ fun removeDirLive() {
+ tree.createDirectory("foo")
+ val content = tree.liveList()
+ var done = false
+ content.observeForever {
+ if (it.directories.isEmpty()) done = true
+ }
+ tree.removeDirectory("foo")
+ while (!done) {
+ ShadowLooper.idleMainLooper()
+ }
+ }
+
+ @Test
+ fun createLink() {
+ val dir = tree.createDirectory("dir")
+ val file = tree.createFile("file")
+ val link = tree.createLink("link", dir.name)
+ var target = link.resolve()
+ when (target) {
+ is Link.DirectoryTarget -> assertThat(target.directory).isEqualTo(
+ dir
+ )
+ is Link.FileTarget -> Assert.fail()
+ is Link.NoTarget -> Assert.fail()
+ }
+ assertThat(tree.openDir("link")).isEqualTo(dir)
+
+ link.target(file)
+ target = link.resolve()
+ when (target) {
+ is Link.DirectoryTarget -> Assert.fail()
+ is Link.FileTarget -> Assert.fail()
+ is Link.NoTarget -> Unit
+ }
+ file.write().use { it.write(1) }
+ target = link.resolve()
+ when (target) {
+ is Link.DirectoryTarget -> Assert.fail()
+ is Link.FileTarget -> assertThat(target.file).isEqualTo(file)
+ is Link.NoTarget -> Assert.fail()
+ }
+
+ assertThat(tree.openFile("link")).isEqualTo(file)
+ }
+
+ @Test
+ fun createLinkSubdir() {
+ val foo = tree.createDirectory("foo")
+ val bar = foo.createDirectory("bar")
+ val link1 = tree.createLink("link1", "foo/bar")
+ val link2 = tree.createLink("link2", bar)
+ assertThat(link1.resolve()).isEqualTo(link2.resolve())
+ assertThat((link1.resolve() as Link.DirectoryTarget).directory).isEqualTo(bar)
+ val link3 = foo.createLink("link3", "../link1")
+ assertThat((link3.resolve() as Link.DirectoryTarget).directory).isEqualTo(bar)
+ }
+
+ @Test
+ fun createLiveLink() {
+ val content = tree.liveList()
+ var link: Link? = null
+ content.observeForever {
+ if (it.links.size == 1) link = it.links[0]
+ }
+ val dir = tree.createDirectory("dir")
+ tree.createLink("link", "dir")
+ while (link == null) {
+ ShadowLooper.idleMainLooper()
+ }
+ assertThat((link?.resolve() as Link.DirectoryTarget).directory).isEqualTo(dir)
+ }
+
+ @Test
+ fun sameDir() {
+ val dir1 = tree.createDirectory("dir")
+ val dir2 = tree.openDir("dir")
+ assertThat(dir1).isEqualTo(dir2)
+ assertThat(dir1.hashCode()).isEqualTo(dir2.hashCode())
+ assertThat(dir1.toString()).isEqualTo(dir2.toString())
+ }
+
+ @Test
+ fun sameFile() {
+ val file1 = tree.createFile("file")
+ file1.write().use { it.write(127) }
+ val file2 = tree.openFile("file")
+ assertThat(file1).isEqualTo(file2)
+ assertThat(file1.hashCode()).isEqualTo(file2.hashCode())
+ assertThat(file1.toString()).isEqualTo(file2.toString())
+ }
+
+ @Test
+ fun sameLink() {
+ val link1 = tree.createLink("link", "foo")
+ val link2 = tree.openLink("link")
+ assertThat(link1).isEqualTo(link2)
+ assertThat(link1.hashCode()).isEqualTo(link2.hashCode())
+ assertThat(link1.toString()).isEqualTo(link2.toString())
+ }
+
+ @Test
+ fun removeDirWithContent() {
+ val foo = tree.createDirectory("foo")
+ foo.createDirectory("dir")
+ foo.createFile("file").write().use { it.write(byteArrayOf(1, 2, 3, 4)) }
+ foo.createLink("link", "file")
+ assertThat(tree.list().directories).hasSize(1)
+ assertThat(tree.removeDirectory("foo")).isTrue()
+ assertThat(tree.list().directories).isEmpty()
+ }
+
+ @Test
+ fun removeWrongType() {
+ tree.createDirectory("dir")
+ assertThat(tree.removeFile("dir")).isFalse()
+ assertThat(tree.removeLink("dir")).isFalse()
+ tree.createFile("file").write().use { it.write(byteArrayOf(1, 2, 3, 4)) }
+ assertThat(tree.removeDirectory("file")).isFalse()
+ assertThat(tree.removeLink("file")).isFalse()
+ tree.createLink("link", "doesn't exist")
+ assertThat(tree.removeDirectory("link")).isFalse()
+ assertThat(tree.removeFile("link")).isFalse()
+ val content = tree.list()
+ assertThat(content.directories).hasSize(1)
+ assertThat(content.files).hasSize(1)
+ assertThat(content.links).hasSize(1)
+ }
+
+ @Test
+ fun removeFile() {
+ tree.createFile("file").write().use { it.write(byteArrayOf(1, 2, 3, 4)) }
+ assertThat(tree.list().files).hasSize(1)
+ tree.removeFile("file")
+ assertThat(tree.list().files).isEmpty()
+ }
+
+ @Test
+ fun removeLink() {
+ val dir = tree.createDirectory("dir")
+ val file = tree.createFile("file")
+ file.write().use { it.write(127) }
+ tree.createLink("link1", dir)
+ tree.createLink("link2", file)
+ assertThat(tree.list().links).hasSize(2)
+ tree.removeLink("link1")
+ tree.removeLink("link2")
+ assertThat(tree.list().links).isEmpty()
+ }
+
+ @Test
+ fun changeLink() {
+ val dir = tree.createDirectory("dir")
+ val file = tree.createFile("file")
+ file.write().use { it.write(127) }
+ val link = tree.createLink("link", "doesn't exist")
+ assertThat(link.resolve() is Link.NoTarget).isTrue()
+ link.target(file)
+ assertThat((link.resolve() as Link.FileTarget).file).isEqualTo(file)
+ link.target(dir)
+ assertThat((link.resolve() as Link.DirectoryTarget).directory).isEqualTo(dir)
+ link.target("bad")
+ assertThat(link.resolve() is Link.NoTarget).isTrue()
+ }
+
+ @Test
+ fun changeModifiableLink() {
+ val dir = tree.createDirectory("dir")
+ val file = tree.createFile("file")
+ file.write().use { it.write(127) }
+ val link = tree.createLink("link", "doesn't exist")
+ assertThat(link.modifiableResolve() is ModifiableLink.NoTarget).isTrue()
+ link.target(file)
+ assertThat((link.modifiableResolve() as ModifiableLink.ModifiableFileTarget).file).isEqualTo(file)
+ link.target(dir)
+ assertThat((link.modifiableResolve() as ModifiableLink.ModifiableDirectoryTarget).directory).isEqualTo(dir)
+ link.target("bad")
+ assertThat(link.modifiableResolve() is ModifiableLink.NoTarget).isTrue()
+ }
+
+ @Test
+ fun recursiveLink() {
+ val link = tree.createLink("link", "link")
+ assertThat(link.resolve() is Link.NoTarget).isTrue()
+ }
+
+ @Test
+ fun names() {
+ assertThat(tree.createDirectory("dir").name).isEqualTo("dir")
+ assertThat(tree.createFile("file").name).isEqualTo("file")
+ assertThat(tree.createLink("link", "file").name).isEqualTo("link")
+ }
+
+ @Test
+ fun openNonExistent() {
+ assertThat(tree.openDir("dir")).isNull()
+ assertThat(tree.openFile("file")).isNull()
+ assertThat(tree.openLink("link")).isNull()
+ }
+
+ @Test
+ fun lastModified() {
+ val file = tree.createFile("foo")
+ file.write().use { it.write(1) }
+ val old = file.lastModified
+ file.write().use { it.write(2); it.flush() }
+ val new = file.lastModified
+ assertThat(old.isBefore(new) || old == new).isTrue()
+ }
+}