summaryrefslogtreecommitdiff
path: root/libs/local
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2024-07-15 23:52:28 +0200
committerJoel Klinghed <the_jk@spawned.biz>2024-07-16 00:25:07 +0200
commit42564c71cfb70c28831c662a3b6bf4084e079353 (patch)
tree111456fd3e8dce884d0380a81d70950062c7d212 /libs/local
parent4a8f6807c9d3ee6bcfac25aee832163036b4e6fe (diff)
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.
Diffstat (limited to 'libs/local')
-rw-r--r--libs/local/build.gradle.kts12
-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
9 files changed, 766 insertions, 0 deletions
diff --git a/libs/local/build.gradle.kts b/libs/local/build.gradle.kts
new file mode 100644
index 0000000..46d8128
--- /dev/null
+++ b/libs/local/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ alias(libs.plugins.android.library)
+}
+
+android {
+ namespace = "org.the_jk.cleversync.local"
+}
+
+dependencies {
+ implementation(project(":libs:io"))
+ testImplementation(project(":libs:utils"))
+}
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()
+ }
+}