0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 03:52:15 +02:00

Relative File Paths

For use in conflict management in scoped storage.  Once we have a relative path,
we can move it to "/conflict/" + path and properly handle the creation of
directories.
This commit is contained in:
David Allison 2022-03-12 01:35:45 +00:00 committed by Mike Hardy
parent 38d00b0e11
commit 88e48e423e
3 changed files with 160 additions and 1 deletions

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022 David Allison <davidallisongithub@gmail.com>
* Copyright (c) 2022 Arthur Milchior <arthur@milchior.fr>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.model
import java.io.File
/**
* A relative path, with the final component representing the filename.
* During a recursive copy of a folder `source` to `destination`, this relative file path `relative`
* can be used both on top of source and destination folder to get the path of `source/relative`
* and `destination/relative`.
* It can also be used to move `source/relative` to `source/conflict/relative` in case of conflict.
*/
class RelativeFilePath private constructor(
/** Relative path, as a sequence of directory, excluding the file name. */
val path: List<String>,
val fileName: String
) {
/**
* Combination of [baseDir] and this relative Path.
*/
fun toFile(baseDir: Directory): File {
var directory = baseDir.directory
for (dirName in path) {
directory = File(directory, dirName)
}
return File(directory, fileName)
}
companion object {
/**
* Return the relative path from Folder [baseDir] to file [file]. If [file]
* is contained in [baseDir], return [null].
* Similar to [Path.relativize], but available in all APIs.
*/
fun fromPaths(baseDir: Directory, file: DiskFile): RelativeFilePath? =
fromPaths(baseDir.directory, file.file)
fun fromPaths(baseDir: File, file: File): RelativeFilePath? =
fromCanonicalFiles(baseDir.canonicalFile, file.canonicalFile)
/**
* Return the relative path from Folder [baseDir] to file [file]. If [file]
* is contained in [baseDir], return [null].
* Assumes that [file] is actually a file and [baseDir] a directory, hence distinct.
* Similar to [Path.relativize], but available in all APIs.
* @param baseDir A directory.
* @param file some file, assumed to be contained in baseDir.
*/
private fun fromCanonicalFiles(baseDir: File, file: File): RelativeFilePath? {
val name = file.name
val directoryPath = mutableListOf<String>()
var mutablePath = file.parentFile
while (mutablePath != baseDir) {
if (mutablePath == null) {
// File was not inside the directory
return null
}
directoryPath.add(mutablePath.name)
mutablePath = mutablePath.parentFile
}
// attempt to create a relative file path
return RelativeFilePath(directoryPath.reversed(), name)
}
}
}

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022 David Allison <davidallisongithub@gmail.com>
* Copyright (c) 2022 Arthur Milchior <arthur@milchior.fr>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
@ -19,11 +20,24 @@ package com.ichi2.anki.servicelayer
import android.content.Context
import android.content.SharedPreferences
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.model.Directory
import com.ichi2.anki.model.DiskFile
import com.ichi2.anki.model.RelativeFilePath
import com.ichi2.anki.servicelayer.scopedstorage.MigrateUserData
import java.io.File
/** A path to the AnkiDroid directory, named "AnkiDroid" by default */
typealias AnkiDroidDirectory = String
typealias AnkiDroidDirectory = Directory
/**
* Returns the relative file path from a given [AnkiDroidDirectory]
* @return null if the file was not inside the directory, or referred to the root directory
*/
fun AnkiDroidDirectory.getRelativeFilePath(file: DiskFile): RelativeFilePath? =
RelativeFilePath.fromPaths(
baseDir = this,
file = file
)
object ScopedStorageService {
/**

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2022 Arthur Milchior <arthur@milchior.fr>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.model
import com.ichi2.testutils.addTempFile
import com.ichi2.testutils.createTransientDirectory
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
import org.junit.Test
import java.io.File
class RelativeFilePathTest {
@Test
fun test_distinct_base() {
val file = DiskFile.createInstance(createTransientDirectory().addTempFile("fileName"))!!
val dir = Directory.createInstance(createTransientDirectory())!!
assertThat("If file is not in dir, fromPaths should return null.", RelativeFilePath.fromPaths(dir, file), nullValue())
}
@Test
fun test_recursive_file() {
val base = createTransientDirectory()
val subDir = base.createTransientDirectory("sub")
val file = subDir.addTempFile("fileName")
val relative = RelativeFilePath.fromPaths(base, file)!!
assertThat(relative.fileName, equalTo("fileName"))
assertThat(relative.path, hasSize(1))
assertThat(relative.path[0], equalTo("sub"))
checkBasePlusRelativeEqualsExpected(base, relative, file)
}
@Test
fun test_move_file() {
val source = createTransientDirectory()
val destination = createTransientDirectory()
val subDir = source.createTransientDirectory("sub")
val file = subDir.addTempFile("fileName")
val relative = RelativeFilePath.fromPaths(source, file)!!
assertThat(relative.fileName, equalTo("fileName"))
assertThat(relative.path, hasSize(1))
assertThat(relative.path[0], equalTo("sub"))
checkBasePlusRelativeEqualsExpected(destination, relative, File(File(destination, "sub"), "fileName"))
}
companion object {
fun checkBasePlusRelativeEqualsExpected(baseDir: File, relative: RelativeFilePath, expected: File) {
assertThat(relative.toFile(Directory.createInstance(baseDir)!!), equalTo(expected))
}
}
}