summaryrefslogtreecommitdiff
path: root/libs/io/src/main/java/org/the_jk
diff options
context:
space:
mode:
Diffstat (limited to 'libs/io/src/main/java/org/the_jk')
-rw-r--r--libs/io/src/main/java/org/the_jk/cleversync/io/Action.kt26
-rw-r--r--libs/io/src/main/java/org/the_jk/cleversync/io/Modifier.kt152
-rw-r--r--libs/io/src/main/java/org/the_jk/cleversync/io/SingleMerge.kt198
3 files changed, 376 insertions, 0 deletions
diff --git a/libs/io/src/main/java/org/the_jk/cleversync/io/Action.kt b/libs/io/src/main/java/org/the_jk/cleversync/io/Action.kt
new file mode 100644
index 0000000..fdce54d
--- /dev/null
+++ b/libs/io/src/main/java/org/the_jk/cleversync/io/Action.kt
@@ -0,0 +1,26 @@
+package org.the_jk.cleversync.io
+
+sealed interface Action {
+ // Remove file, noop if name no longer exists
+ data class RemoveFile(val name: String): Action
+
+ // Remove link, noop if name no longer exists
+ data class RemoveLink(val name: String): Action
+
+ // Remove directory and its content recursively. Fails if
+ // not a directory. Noop if name no longer exists.
+ data class RemoveDir(val name: String): Action
+
+ // Create a symlink called name pointing to target. noop if link pointing to target already exists.
+ // If overwrite is true, existing link will be overwritten. Fails if a file or directory
+ // exists with the name.
+ data class Link(val name: String, val target: String, val overwrite: Boolean = false): Action
+
+ // Create file called name in target and copy content from source. If overwrite is true
+ // any file already existing will be overwritten with new content.
+ data class Copy(val name: String, val overwrite: Boolean = false): Action
+
+ // Change directory to name and execute actions there. If create is true and target directory
+ // does not exist it is created.
+ data class ChangeDir(val name: String, val actions: List<Action>, val create: Boolean = false): Action
+}
diff --git a/libs/io/src/main/java/org/the_jk/cleversync/io/Modifier.kt b/libs/io/src/main/java/org/the_jk/cleversync/io/Modifier.kt
new file mode 100644
index 0000000..d0138b1
--- /dev/null
+++ b/libs/io/src/main/java/org/the_jk/cleversync/io/Modifier.kt
@@ -0,0 +1,152 @@
+package org.the_jk.cleversync.io
+
+import org.the_jk.cleversync.PathUtils
+import java.io.IOException
+
+object Modifier {
+ fun apply(target: ModifiableDirectory, source: Directory, actions: List<Action>): List<String> {
+ val errors = mutableListOf<String>()
+ apply(target, source, actions, "", errors)
+ return errors
+ }
+
+ @Suppress("CyclomaticComplexMethod", "LongMethod", "NestedBlockDepth")
+ private fun apply(
+ target: ModifiableDirectory,
+ source: Directory,
+ actions: List<Action>,
+ path: String,
+ errors: MutableList<String>,
+ ) {
+ actions.forEach { action ->
+ when (action) {
+ is Action.ChangeDir -> {
+ val newPath = PathUtils.join(path, action.name)
+ val sourceDir = source.openDir(action.name)
+ if (sourceDir == null) {
+ errors.add("$path/${action.name}: Source directory does not exist")
+ } else {
+ val targetDir = if (action.create) {
+ try {
+ target.createDirectory(action.name)
+ } catch (_: IOException) {
+ target.modifiableOpenDir(action.name)
+ }
+ } else {
+ target.modifiableOpenDir(action.name)
+ }
+ if (targetDir == null) {
+ errors.add(
+ if (action.create) {
+ "$newPath: Unable to create directory"
+ } else {
+ "$newPath: Target directory does not exist"
+ }
+ )
+ } else {
+ apply(
+ targetDir,
+ sourceDir,
+ action.actions,
+ newPath,
+ errors
+ )
+ }
+ }
+ }
+ is Action.Copy -> {
+ val sourceFile = source.openFile(action.name)
+ if (sourceFile == null) {
+ errors.add("$path/${action.name}: Unable to open file")
+ } else {
+ try {
+ target.createFile(action.name)
+ } catch (e: IOException) {
+ if (action.overwrite) {
+ target.modifiableOpenFile(action.name)
+ } else {
+ errors.add("$path/${action.name}: Unable to create file: ${e.message}")
+ null
+ }
+ }?.let { targetFile ->
+ try {
+ val buffer = ByteArray(BUFFER_SIZE)
+ targetFile.write().use { output ->
+ sourceFile.read().use { input ->
+ val got = input.read(buffer)
+ output.write(buffer, 0, got)
+ }
+ output.flush()
+ }
+ } catch (e: IOException) {
+ errors.add("$path/${action.name}: Error writing file: ${e.message}")
+ }
+ }
+ }
+ }
+ is Action.Link -> {
+ try {
+ target.createLink(action.name, action.target)
+ } catch (e: IOException) {
+ val targetLink = target.modifiableOpenLink(action.name)
+ if (targetLink == null) {
+ errors.add("$path/${action.name}: Unable to create link: ${e.message}")
+ } else {
+ if (targetLink.resolve().path == action.target) {
+ // No need to do anything
+ } else {
+ try {
+ targetLink.target(action.target)
+ } catch (e2: IOException) {
+ errors.add("$path/${action.name}: Unable modify link: ${e2.message}")
+ }
+ }
+ }
+ }
+ }
+ is Action.RemoveDir -> {
+ try {
+ if (!target.removeDirectory(action.name)) {
+ if (exists(target, action.name)) {
+ errors.add("$path/${action.name}: Unable to remove dir")
+ }
+ }
+ } catch (e: IOException) {
+ errors.add("$path/${action.name}: Unable to remove dir: ${e.message}")
+ }
+ }
+ is Action.RemoveFile -> {
+ try {
+ if (!target.removeFile(action.name)) {
+ if (exists(target, action.name)) {
+ errors.add("$path/${action.name}: Unable to remove file")
+ }
+ }
+ } catch (e: IOException) {
+ errors.add("$path/${action.name}: Unable to remove file: ${e.message}")
+ }
+ }
+ is Action.RemoveLink -> {
+ try {
+ if (!target.removeLink(action.name)) {
+ if (exists(target, action.name)) {
+ errors.add("$path/${action.name}: Unable to remove link")
+ }
+ }
+ } catch (e: IOException) {
+ errors.add("$path/${action.name}: Unable to remove link: ${e.message}")
+ }
+ }
+ }
+ }
+ }
+
+ private fun exists(target: Directory, name: String): Boolean {
+ val content = target.list()
+ return content.directories.any { it.name == name } ||
+ content.files.any { it.name == name } ||
+ content.links.any { it.name == name }
+ }
+
+ private const val BUFFER_SIZE = 65535
+}
diff --git a/libs/io/src/main/java/org/the_jk/cleversync/io/SingleMerge.kt b/libs/io/src/main/java/org/the_jk/cleversync/io/SingleMerge.kt
new file mode 100644
index 0000000..33c87fc
--- /dev/null
+++ b/libs/io/src/main/java/org/the_jk/cleversync/io/SingleMerge.kt
@@ -0,0 +1,198 @@
+package org.the_jk.cleversync.io
+
+import org.the_jk.cleversync.PathUtils
+import java.time.Instant
+
+object SingleMerge {
+ fun calculate(target: Directory, source: Directory, options: Options = Options()): List<Action> {
+ return visit(target, source.list(), source, "/", options)
+ }
+
+ private fun visit(
+ target: Directory?,
+ source: Directory.Content,
+ root: Directory,
+ path: String,
+ options: Options,
+ ) : List<Action> {
+ val targetContent = target?.list()
+ val targetNames = if (targetContent != null && options.deleteFilesOnlyInTarget) {
+ val ret = mutableMapOf<String, Type>()
+ targetContent.directories.forEach { ret[it.name] = Type.DIRECTORY }
+ targetContent.files.forEach { ret[it.name] = Type.FILE }
+ targetContent.links.forEach { ret[it.name] = Type.LINK }
+ ret
+ } else null
+ return buildList {
+ source.directories.forEach { sourceDir ->
+ targetNames?.remove(sourceDir.name)
+ visitDir(targetContent, sourceDir.name, sourceDir.list(), this@buildList, root, path, options)
+ }
+ source.files.forEach { sourceFile ->
+ targetNames?.remove(sourceFile.name)
+ visitFile(targetContent, sourceFile.name, sourceFile.size, sourceFile.lastModified, this@buildList)
+ }
+ source.links.forEach { sourceLink ->
+ targetNames?.remove(sourceLink.name)
+ visitLink(targetContent, sourceLink, this@buildList, root, path, options)
+ }
+ targetNames?.forEach { (name, type) ->
+ when (type) {
+ Type.DIRECTORY -> add(Action.RemoveDir(name))
+ Type.FILE -> add(Action.RemoveFile(name))
+ Type.LINK -> add(Action.RemoveLink(name))
+ }
+ }
+ }
+ }
+
+ @Suppress("LongParameterList")
+ private fun visitDir(
+ targetContent: Directory.Content?,
+ sourceName: String,
+ sourceContent: Directory.Content,
+ actions: MutableList<Action>,
+ root: Directory,
+ path: String,
+ options: Options,
+ ) {
+ val sourcePath = PathUtils.join(path, sourceName)
+ if (targetContent == null) {
+ actions.add(
+ Action.ChangeDir(
+ sourceName,
+ visit(null, sourceContent, root, sourcePath, options),
+ create = true,
+ ),
+ )
+ } else {
+ val targetDir = targetContent.directories.find { it.name == sourceName }
+ if (targetDir != null) {
+ actions.add(
+ Action.ChangeDir(sourceName, visit(targetDir, sourceContent, root, sourcePath, options)),
+ )
+ } else {
+ if (targetContent.files.any { it.name == sourceName }) {
+ actions.add(Action.RemoveFile(sourceName))
+ } else if (targetContent.links.any { it.name == sourceName }) {
+ actions.add(Action.RemoveLink(sourceName))
+ }
+ actions.add(
+ Action.ChangeDir(
+ sourceName,
+ visit(null, sourceContent, root, sourcePath, options),
+ create = true,
+ ),
+ )
+ }
+ }
+ }
+
+ private fun visitFile(
+ targetContent: Directory.Content?,
+ sourceName: String,
+ sourceSize: ULong,
+ sourceLastModified: Instant,
+ actions: MutableList<Action>,
+ ) {
+ if (targetContent == null) {
+ actions.add(Action.Copy(sourceName))
+ } else {
+ val targetFile = targetContent.files.find { it.name == sourceName }
+ if (targetFile != null) {
+ if (targetFile.size != sourceSize || sourceLastModified > targetFile.lastModified) {
+ actions.add(Action.Copy(sourceName, overwrite = true))
+ }
+ } else {
+ if (targetContent.directories.any { it.name == sourceName }) {
+ actions.add(Action.RemoveDir(sourceName))
+ } else if (targetContent.links.any { it.name == sourceName }) {
+ actions.add(Action.RemoveLink(sourceName))
+ }
+ actions.add(Action.Copy(sourceName))
+ }
+ }
+ }
+
+ @Suppress("LongParameterList", "NestedBlockDepth")
+ private fun visitLink(
+ targetContent: Directory.Content?,
+ sourceLink: Link,
+ actions: MutableList<Action>,
+ root: Directory,
+ path: String,
+ options: Options,
+ ) {
+ val linkTarget = sourceLink.resolve()
+ if (!options.resolveLinks && insideRoot(root, path, linkTarget.path)) {
+ if (targetContent == null) {
+ actions.add(Action.Link(sourceLink.name, linkTarget.path))
+ } else {
+ val targetLink =
+ targetContent.links.find { it.name == sourceLink.name }
+ if (targetLink != null) {
+ if (targetLink.resolve().path != linkTarget.path) {
+ actions.add(
+ Action.Link(
+ sourceLink.name,
+ linkTarget.path,
+ overwrite = true
+ )
+ )
+ }
+ } else {
+ if (targetContent.directories.any { it.name == sourceLink.name }) {
+ actions.add(Action.RemoveDir(sourceLink.name))
+ } else if (targetContent.files.any { it.name == sourceLink.name }) {
+ actions.add(Action.RemoveFile(sourceLink.name))
+ }
+ actions.add(Action.Link(sourceLink.name, linkTarget.path))
+ }
+ }
+ } else {
+ when (linkTarget) {
+ is Link.DirectoryTarget -> {
+ visitDir(
+ targetContent,
+ sourceLink.name,
+ linkTarget.directory.list(),
+ actions,
+ root,
+ path,
+ options,
+ )
+ }
+
+ is Link.FileTarget -> {
+ visitFile(
+ targetContent,
+ sourceLink.name,
+ linkTarget.file.size,
+ linkTarget.file.lastModified,
+ actions,
+ )
+ }
+
+ is Link.NoTarget -> Unit
+ }
+ }
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ private fun insideRoot(root: Directory, path: String, linkTarget: String): Boolean {
+ val fakeRoot = "/--root--"
+ val resolved = PathUtils.resolve(PathUtils.join(PathUtils.join(fakeRoot, path.substring(1)), linkTarget))
+ return resolved.startsWith("$fakeRoot/")
+ }
+
+ data class Options(
+ val deleteFilesOnlyInTarget: Boolean = false,
+ val resolveLinks: Boolean = false,
+ )
+
+ private enum class Type {
+ DIRECTORY,
+ FILE,
+ LINK,
+ }
+}