diff options
7 files changed, 250 insertions, 21 deletions
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 index 3ecf5e6..c05f29e 100644 --- a/app/src/main/java/org/the_jk/cleversync/io/Link.kt +++ b/app/src/main/java/org/the_jk/cleversync/io/Link.kt @@ -7,7 +7,7 @@ interface Link { sealed class LinkTarget - class DirectoryTarget(val directory: Directory): LinkTarget() - class FileTarget(val file: File): LinkTarget() + data class DirectoryTarget(val directory: Directory): LinkTarget() + data 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 index 32d288a..8bddc2c 100644 --- a/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt @@ -12,6 +12,8 @@ interface ModifiableDirectory : Directory { fun createDirectory(name: String): ModifiableDirectory fun createFile(name: String): ModifiableFile + fun createLink(name: String, target: Directory): ModifiableLink + fun createLink(name: String, target: File): ModifiableLink fun createLink(name: String, target: String): ModifiableLink fun removeDirectory(name: String): Boolean 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 index a20bb6a..7dd565b 100644 --- a/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt @@ -3,13 +3,13 @@ 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(directory: Directory) + fun target(file: File) fun target(name: String) sealed class ModifiableLinkTarget - class ModifiableDirectoryTarget(val directory: ModifiableDirectory): ModifiableLinkTarget() - class ModifiableFileTarget(val file: ModifiableFile): ModifiableLinkTarget() - data object ModifiableNoTarget: ModifiableLinkTarget() + data class ModifiableDirectoryTarget(val directory: ModifiableDirectory): ModifiableLinkTarget() + data class ModifiableFileTarget(val file: ModifiableFile): ModifiableLinkTarget() + data object NoTarget: ModifiableLinkTarget() } 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 index 730e048..55bed6a 100644 --- 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 @@ -5,6 +5,7 @@ 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 @@ -25,7 +26,7 @@ import kotlin.io.path.readSymbolicLink @OptIn(ExperimentalPathApi::class) internal open class PathDirectory( - private val path: Path, + internal val path: Path, private val pathWatcher: PathWatcher, ) : ModifiableDirectory { private val watcher: DirectoryWatcher by lazy { @@ -51,7 +52,7 @@ internal open class PathDirectory( if (path.isDirectory(LinkOption.NOFOLLOW_LINKS)) return PathDirectory(path, pathWatcher) if (path.isSymbolicLink()) { val target = path.readSymbolicLink() - if (target.isDirectory()) return PathDirectory(target, pathWatcher) + if (target.isDirectory()) return PathDirectory(target.toRealPath(), pathWatcher) } return null } @@ -61,7 +62,7 @@ internal open class PathDirectory( if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS)) return PathFile(path) if (path.isSymbolicLink()) { val target = path.readSymbolicLink() - if (target.isRegularFile()) return PathFile(target) + if (target.isRegularFile()) return PathFile(target.toRealPath()) } return null } @@ -84,9 +85,20 @@ internal open class PathDirectory( 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(path.resolve(target)), pathWatcher) + return PathLink(path.createSymbolicLinkPointingTo(targetPath), pathWatcher) } override fun removeDirectory(name: String): Boolean { @@ -123,6 +135,7 @@ internal open class PathDirectory( 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> 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 index 2ccc37c..9a8d160 100644 --- 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 @@ -16,7 +16,7 @@ import kotlin.io.path.inputStream import kotlin.io.path.name import kotlin.io.path.outputStream -internal class PathFile(private val path: Path) : ModifiableFile { +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)) @@ -62,4 +62,5 @@ internal class PathFile(private val path: Path) : ModifiableFile { override fun equals(other: Any?) = other is PathFile && other.path == path override fun hashCode() = path.hashCode() + override fun toString() = path.toString() } 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 index 5d11228..9ae8b51 100644 --- 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 @@ -1,5 +1,7 @@ package org.the_jk.cleversync.io.impl +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 @@ -17,17 +19,27 @@ internal class PathLink( override fun modifiableResolve(): ModifiableLink.ModifiableLinkTarget { val target = path.readSymbolicLink() return if (target.isDirectory()) { - ModifiableLink.ModifiableDirectoryTarget(PathDirectory(target, pathWatcher)) + ModifiableLink.ModifiableDirectoryTarget(PathDirectory(target.toRealPath(), pathWatcher)) } else if (target.isRegularFile()) { - ModifiableLink.ModifiableFileTarget(PathFile(target)) + ModifiableLink.ModifiableFileTarget(PathFile(target.toRealPath())) } else { - ModifiableLink.ModifiableNoTarget + 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.resolve(name)) + path.createSymbolicLinkPointingTo(path.parent.resolve(name)) } override val name: String @@ -35,13 +47,14 @@ internal class PathLink( 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, pathWatcher)) + Link.DirectoryTarget(PathDirectory(target.toRealPath(), pathWatcher)) } else if (target.isRegularFile()) { - Link.FileTarget(PathFile(target)) + Link.FileTarget(PathFile(target.toRealPath())) } else { Link.NoTarget } diff --git a/app/src/test/java/org/the_jk/cleversync/io/LocalTreeTest.kt b/app/src/test/java/org/the_jk/cleversync/io/LocalTreeTest.kt index ea2fd91..7b83c98 100644 --- a/app/src/test/java/org/the_jk/cleversync/io/LocalTreeTest.kt +++ b/app/src/test/java/org/the_jk/cleversync/io/LocalTreeTest.kt @@ -1,6 +1,7 @@ package org.the_jk.cleversync.io import com.google.common.truth.Truth.assertThat +import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test @@ -90,13 +91,15 @@ class LocalTreeTest { os.write(byteArrayOf(1, 2, 3, 4)) } foo.write().use { os -> - os.write(byteArrayOf(127)) + os.write(127) + os.write(byteArrayOf(1)) + os.write(byteArrayOf(2), 0, 0) assertThat(foo.size).isEqualTo(4.toULong()) } - assertThat(foo.size).isEqualTo(1.toULong()) + assertThat(foo.size).isEqualTo(2.toULong()) assertThat(tree.list().files).hasSize(1) foo.read().use { - assertThat(it.readBytes()).isEqualTo(byteArrayOf(127)) + assertThat(it.readBytes()).isEqualTo(byteArrayOf(127, 1)) } } @@ -120,4 +123,201 @@ class LocalTreeTest { 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() + } } |
