Skip to content

Commit bba47ae

Browse files
committed
Actually copy/move files instead of (create + copy [+ delete])
- Only when granted all-files access. - Falls back to previous implementation in case of exceptions.
1 parent 6ca0eac commit bba47ae

File tree

3 files changed

+141
-92
lines changed

3 files changed

+141
-92
lines changed

app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import co.adityarajput.fileflow.data.AppContainer
55
import co.adityarajput.fileflow.data.models.Action
66
import co.adityarajput.fileflow.data.models.Execution
77
import co.adityarajput.fileflow.data.models.Rule
8-
import co.adityarajput.fileflow.utils.*
8+
import co.adityarajput.fileflow.utils.File
9+
import co.adityarajput.fileflow.utils.FileSuperlative
10+
import co.adityarajput.fileflow.utils.Logger
11+
import co.adityarajput.fileflow.utils.isDebugBuild
912
import kotlinx.coroutines.flow.first
13+
import java.nio.file.FileAlreadyExistsException
1014

1115
class FlowExecutor(private val context: Context) {
1216
private val repository by lazy { AppContainer(context).repository }
@@ -22,7 +26,6 @@ class FlowExecutor(private val context: Context) {
2226
when (rule.action) {
2327
is Action.MOVE -> {
2428
val destDir = File.fromPath(context, rule.action.dest)
25-
2629
if (destDir == null) {
2730
Logger.e("FlowExecutor", "${rule.action.dest} is invalid")
2831
continue
@@ -32,18 +35,18 @@ class FlowExecutor(private val context: Context) {
3235
?.listChildren(rule.action.scanSubdirectories)
3336
?.filter { it.isFile && it.name != null && regex.matches(it.name!!) }
3437
?.let {
35-
if (rule.action.superlative != FileSuperlative.NONE)
38+
if (rule.action.superlative == FileSuperlative.NONE) it else
3639
listOf(it.maxByOrNull(rule.action.superlative.selector) ?: continue)
37-
else
38-
it
3940
} ?: continue
4041

4142
for (srcFile in srcFiles) {
43+
val srcFileName = srcFile.name ?: continue
44+
val destFileName = rule.action.getDestFileName(srcFile)
45+
4246
val relativePath = srcFile.parent!!.pathRelativeTo(rule.action.src)
4347
val destSubDir =
4448
if (!rule.action.preserveStructure || relativePath == null) destDir
4549
else destDir.createDirectory(relativePath)
46-
4750
if (destSubDir == null) {
4851
Logger.e(
4952
"FlowExecutor",
@@ -52,55 +55,41 @@ class FlowExecutor(private val context: Context) {
5255
continue
5356
}
5457

55-
val destFileName = rule.action.getDestFileName(srcFile)
56-
var destFile = destSubDir.listChildren(false)
57-
.firstOrNull { it.isFile && it.name == destFileName }
58-
59-
if (destFile != null) {
60-
if (!rule.action.overwriteExisting) {
61-
Logger.e("FlowExecutor", "${destFile.name} already exists")
62-
continue
63-
}
64-
65-
if (srcFile.isIdenticalTo(destFile, context)) {
66-
Logger.i(
67-
"FlowExecutor",
68-
"Source and destination files are identical",
69-
)
70-
continue
71-
}
72-
73-
74-
Logger.i("FlowExecutor", "Deleting existing ${destFile.name}")
75-
destFile.delete()
76-
}
77-
78-
destFile = destSubDir.createFile(srcFile.type, destFileName)
79-
80-
if (destFile == null) {
81-
Logger.e("FlowExecutor", "Failed to create $destFileName")
58+
if (
59+
destSubDir
60+
.listChildren(false)
61+
.firstOrNull { it.isFile && it.name == destFileName }
62+
?.isIdenticalTo(srcFile, context)
63+
== true
64+
) {
65+
Logger.i(
66+
"FlowExecutor",
67+
"Source and destination files are identical",
68+
)
8269
continue
8370
}
8471

85-
val result = context.copyFile(srcFile, destFile)
86-
if (!result) {
87-
Logger.e(
72+
try {
73+
Logger.i(
8874
"FlowExecutor",
89-
"Failed to copy ${srcFile.name} to ${destFile.name}",
75+
"Moving $srcFileName to ${destSubDir.path}/$destFileName",
9076
)
91-
destFile.delete()
77+
srcFile.moveTo(
78+
destSubDir,
79+
destFileName,
80+
rule.action.keepOriginal,
81+
rule.action.overwriteExisting,
82+
context,
83+
)
84+
} catch (e: FileAlreadyExistsException) {
85+
Logger.e("FlowExecutor", "$destFileName already exists", e)
86+
continue
87+
} catch (e: Exception) {
88+
Logger.e("FlowExecutor", "Failed to move $srcFileName", e)
9289
continue
9390
}
9491

95-
repository.registerExecution(
96-
rule,
97-
Execution(srcFile.name!!, rule.action.verb),
98-
)
99-
100-
if (!rule.action.keepOriginal) {
101-
Logger.i("FlowExecutor", "Deleting original ${srcFile.name}")
102-
srcFile.delete()
103-
}
92+
repository.registerExecution(rule, Execution(srcFileName, rule.action.verb))
10493
}
10594
}
10695

@@ -126,10 +115,7 @@ class FlowExecutor(private val context: Context) {
126115
continue
127116
}
128117

129-
repository.registerExecution(
130-
rule,
131-
Execution(srcFileName, rule.action.verb),
132-
)
118+
repository.registerExecution(rule, Execution(srcFileName, rule.action.verb))
133119
}
134120
}
135121
}

app/src/main/java/co/adityarajput/fileflow/utils/Files.kt

Lines changed: 97 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import android.os.storage.StorageManager
77
import androidx.core.net.toUri
88
import androidx.documentfile.provider.DocumentFile
99
import co.adityarajput.fileflow.data.models.Rule
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.withContext
1012
import java.net.URLDecoder
13+
import java.nio.file.FileAlreadyExistsException
14+
import java.nio.file.Files
15+
import java.nio.file.StandardCopyOption
1116
import java.io.File as IOFile
1217

1318
sealed class File {
@@ -31,9 +36,91 @@ sealed class File {
3136
}
3237
}
3338

34-
class SAFFile(val documentFile: DocumentFile) : File()
39+
class SAFFile(val documentFile: DocumentFile) : File() {
40+
override suspend fun moveTo(
41+
destDir: File,
42+
destFileName: String,
43+
keepOriginal: Boolean,
44+
overwriteExisting: Boolean,
45+
context: Context,
46+
) {
47+
if (destDir !is SAFFile)
48+
throw IllegalArgumentException("Destination directory must be a SAFFile")
49+
50+
destDir.documentFile.findFile(destFileName)?.run {
51+
if (overwriteExisting) delete()
52+
else throw Exception("$destFileName already exists")
53+
}
54+
55+
val destFile =
56+
destDir.documentFile.createFile(type ?: "application/octet-stream", destFileName)!!
57+
58+
val resolver = context.contentResolver
59+
resolver.openInputStream(this.documentFile.uri).use { srcStream ->
60+
resolver.openOutputStream(destFile.uri).use { destStream ->
61+
srcStream!!.copyTo(destStream!!)
62+
}
63+
}
64+
65+
if (!keepOriginal) delete()
66+
}
67+
}
68+
69+
class FSFile(val ioFile: IOFile) : File() {
70+
override suspend fun moveTo(
71+
destDir: File,
72+
destFileName: String,
73+
keepOriginal: Boolean,
74+
overwriteExisting: Boolean,
75+
context: Context,
76+
) {
77+
if (destDir !is FSFile)
78+
throw IllegalArgumentException("Destination directory must be a FSFile")
79+
80+
val options = mutableListOf<StandardCopyOption>()
81+
if (keepOriginal) options.add(StandardCopyOption.COPY_ATTRIBUTES)
82+
if (overwriteExisting) options.add(StandardCopyOption.REPLACE_EXISTING)
3583

36-
class FSFile(val ioFile: IOFile) : File()
84+
try {
85+
withContext(Dispatchers.IO) {
86+
if (keepOriginal) {
87+
Files.copy(
88+
this@FSFile.ioFile.toPath(),
89+
destDir.ioFile.toPath().resolve(destFileName),
90+
*options.toTypedArray(),
91+
)
92+
} else {
93+
Files.move(
94+
this@FSFile.ioFile.toPath(),
95+
destDir.ioFile.toPath().resolve(destFileName),
96+
*options.toTypedArray(),
97+
)
98+
}
99+
}
100+
} catch (e: Exception) {
101+
if (e is FileAlreadyExistsException) throw e
102+
103+
Logger.w(
104+
"Files",
105+
"Failed to move file, falling back to create + copy [+ delete].",
106+
e,
107+
)
108+
109+
val destFile = IOFile(destDir.ioFile, destFileName)
110+
withContext(Dispatchers.IO) {
111+
destFile.createNewFile()
112+
113+
this@FSFile.ioFile.inputStream().use { srcStream ->
114+
destFile.outputStream().use { destStream ->
115+
srcStream.copyTo(destStream)
116+
}
117+
}
118+
}
119+
120+
if (!keepOriginal) delete()
121+
}
122+
}
123+
}
37124

38125
val name
39126
get() = when (this) {
@@ -122,17 +209,6 @@ sealed class File {
122209
return false
123210
}
124211

125-
fun createFile(type: String?, name: String): File? {
126-
return when (this) {
127-
is SAFFile -> documentFile
128-
.createFile(type ?: "application/octet-stream", name)
129-
?.let { SAFFile(it) }
130-
131-
is FSFile -> IOFile(ioFile, name)
132-
.let { if (it.createNewFile()) FSFile(it) else null }
133-
}
134-
}
135-
136212
fun createDirectory(relativePath: String): File? {
137213
return when (this) {
138214
is SAFFile -> {
@@ -160,40 +236,20 @@ sealed class File {
160236
}
161237
}
162238

239+
abstract suspend fun moveTo(
240+
destDir: File,
241+
destFileName: String,
242+
keepOriginal: Boolean,
243+
overwriteExisting: Boolean,
244+
context: Context,
245+
)
246+
163247
fun delete() = when (this) {
164248
is SAFFile -> documentFile.delete()
165249
is FSFile -> ioFile.delete()
166250
}
167251
}
168252

169-
fun Context.copyFile(src: File, dest: File): Boolean {
170-
val resolver = contentResolver
171-
172-
if (src is File.SAFFile && dest is File.SAFFile) {
173-
resolver.openInputStream(src.documentFile.uri).use { srcStream ->
174-
resolver.openOutputStream(dest.documentFile.uri).use { destStream ->
175-
if (srcStream == null || destStream == null) {
176-
Logger.e("Files", "Failed to open file(s)")
177-
return false
178-
}
179-
Logger.i("Files", "Copying ${src.name} to ${dest.name}")
180-
srcStream.copyTo(destStream)
181-
return true
182-
}
183-
}
184-
} else if (src is File.FSFile && dest is File.FSFile) {
185-
src.ioFile.inputStream().use { srcStream ->
186-
dest.ioFile.outputStream().use { destStream ->
187-
Logger.i("Files", "Copying ${src.name} to ${dest.name}")
188-
srcStream.copyTo(destStream)
189-
return true
190-
}
191-
}
192-
}
193-
194-
return false
195-
}
196-
197253
fun String.getGetDirectoryFromUri() =
198254
if (isBlank()) {
199255
this

app/src/main/java/co/adityarajput/fileflow/utils/Logging.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ object Logger {
2020
logs.addLast("[${System.currentTimeMillis()}][$tag][INFO] $msg")
2121
}
2222

23+
fun w(tag: String, msg: String, tr: Throwable? = null) {
24+
Log.w(tag, msg, tr)
25+
26+
if (logs.size >= Constants.LOG_SIZE) logs.removeFirst()
27+
logs.addLast("[${System.currentTimeMillis()}][$tag][WARN] $msg")
28+
}
29+
2330
fun e(tag: String, msg: String, tr: Throwable? = null) {
2431
Log.e(tag, msg, tr)
2532

0 commit comments

Comments
 (0)