Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: test

on:
push:
branches:
- dev

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup JDK
uses: actions/setup-java@v6
with:
java-version: 17
distribution: "temurin"
cache: "gradle"

- name: Run tests
run: ./gradlew test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.gradle
/local.properties
.idea/*
!.idea/runConfigurations
!.idea/icon.png
.DS_Store
/build
Expand Down
68 changes: 68 additions & 0 deletions .idea/runConfigurations/app.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions .idea/runConfigurations/test.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ android {
applicationId = "co.adityarajput.fileflow"
minSdk = 29
targetSdk = 36
versionCode = 11
versionName = "1.9.0"
versionCode = 12
versionName = "1.10.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ sealed class Action {
val srcFileName = srcFile.name ?: continue
var destFileName = getDestFileName(srcFile)

val relativePath = srcFile.parent!!.pathRelativeTo(src)
val relativePath = srcFile.parent?.pathRelativeTo(src)
val destSubDir =
if (!preserveStructure || relativePath == null) destDir
else destDir.createDirectory(relativePath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ sealed class RemoteAction : Action() {
for ((srcFile, _) in srcFiles) {
val srcFileName = srcFile.name ?: continue
var destFileName = getDestFileName(srcFile)
val relativePath = srcFile.parent!!.pathRelativeTo(src)
val relativePath = srcFile.parent?.pathRelativeTo(src)

if (destServer == null) {
val destSubDir =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,35 @@ class RulesViewModel(private val repository: Repository) : ViewModel() {

var selectedRule by mutableStateOf<Rule?>(null)

var executingRuleIds by mutableStateOf(emptySet<Int>())
private set

fun executeRule(context: Context, showToast: (String) -> Unit) {
val rule = selectedRule ?: return
if (rule.id in executingRuleIds) return
executingRuleIds += rule.id

viewModelScope.launch {
val latestLogBeforeExecution = Logger.logs.lastOrNull()
try {
val latestLogBeforeExecution = Logger.logs.lastOrNull()

withContext(Dispatchers.IO) {
selectedRule!!.action.execute(context) {
repository.registerExecution(
selectedRule!!,
Execution(it, selectedRule!!.action.verb),
)
withContext(Dispatchers.IO) {
rule.action.execute(context) {
repository.registerExecution(
rule,
Execution(it, rule.action.verb),
)
}
}
}

val recentErrorLog = Logger.logs
.dropWhile { it != latestLogBeforeExecution }.drop(1)
.firstOrNull { it.contains("[ERROR]") }
if (recentErrorLog != null) {
showToast("Error:" + recentErrorLog.substringAfter("[ERROR]"))
val recentErrorLog = Logger.logs
.dropWhile { it != latestLogBeforeExecution }.drop(1)
.firstOrNull { it.contains("[ERROR]") }
if (recentErrorLog != null) {
showToast("Error:" + recentErrorLog.substringAfter("[ERROR]"))
}
} finally {
executingRuleIds -= rule.id
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package co.adityarajput.fileflow.views.screens

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
Expand Down Expand Up @@ -97,11 +94,16 @@ fun RulesScreen(
else viewModel.selectedRule = it
},
) {
IconButton({ viewModel.dialogState = DialogState.EXECUTE }) {
Icon(
painterResource(R.drawable.play_circle),
stringResource(R.string.execute_rule),
)
IconButton(
{ viewModel.dialogState = DialogState.EXECUTE },
enabled = it.id !in viewModel.executingRuleIds,
) {
if (it.id !in viewModel.executingRuleIds)
Icon(
painterResource(R.drawable.play_circle),
stringResource(R.string.execute_rule),
)
else CircularProgressIndicator(Modifier.size(24.dp))
}
IconButton({ viewModel.dialogState = DialogState.TOGGLE_RULE }) {
Icon(
Expand Down
139 changes: 139 additions & 0 deletions app/src/test/kotlin/co/adityarajput/fileflow/data/ConvertersTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package co.adityarajput.fileflow.data

import co.adityarajput.fileflow.data.models.Action
import co.adityarajput.fileflow.data.models.RemoteAction
import co.adityarajput.fileflow.data.models.Server
import co.adityarajput.fileflow.utils.FileSuperlative
import org.junit.Assert.assertEquals
import org.junit.Test

class ConvertersTest {
@Test
fun Converters_Action() {
val actions = Action.entries + RemoteAction.entries + listOf(
Action.MOVE(
"/storage/emulated/0/AntennaPod",
"AntennaPodBackup-\\d{4}-\\d{2}-\\d{2}\\.db",
"/storage/emulated/0/Backups",
"AntennaPod.db",
scanSubdirectories = true,
overwriteExisting = true,
preserveStructure = false,
),
Action.MOVE(
"/storage/emulated/0/Backups",
"PipePipeData-\\d{8}_\\d{6}\\.zip",
"/storage/emulated/0/Backups",
$$"PipePipe-${uuid}.zip",
keepOriginal = false,
superlative = FileSuperlative.LARGEST,
deleteEmptySrcSubdirectories = true,
),
Action.DELETE_STALE(
"/storage/emulated/0/Download",
"(NotiFilter|Alarmetrics|FileFlow|MinCal)_v[\\d\\.]+\\.apk",
90,
scanSubdirectories = true,
deleteEmptySrcSubdirectories = true,
),
Action.ZIP(
"/storage/emulated/0/Documents/Notes",
"(.*)\\.md",
"/storage/emulated/0/Backups",
"Notes.zip",
scanSubdirectories = true,
overwriteExisting = true,
preserveStructure = false,
),
Action.EMIT_CHANGES(
"/storage/emulated/0/Movies",
".*\\.mp4",
"org.amoradi.syncopoli.SYNC_PROFILE",
"org.amoradi.syncopoli",
"""{"profile_name": "Movies Backup"}""",
true,
3_600_000L * 3,
),
RemoteAction.MOVE(
Server(
"192.168.1.12",
3456,
"admin",
"hunter2",
null,
1,
),
"backups/navidrome",
"navidrome_backup_[\\d_\\.]{19}\\.db",
Server(
"192.168.1.78",
9012,
"sftp",
null,
"passkey",
2,
),
"backups/glacial",
"navidrome.db",
overwriteExisting = true,
),
)

val serializedActions = listOf(
"""{"type":"co.adityarajput.fileflow.data.models.Action.MOVE","src":"","srcFileNamePattern":"","dest":"","destFileNameTemplate":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.DELETE_STALE","src":"","srcFileNamePattern":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.ZIP","src":"","srcFileNamePattern":"","dest":"","destFileNameTemplate":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.EMIT_CHANGES","src":"","srcFileNamePattern":"","intent":"","packageName":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.RemoteAction.MOVE","srcServer":null,"src":"","srcFileNamePattern":"","destServer":null,"dest":"","destFileNameTemplate":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.RemoteAction.DELETE_STALE","srcServer":{"host":"","port":0,"username":"","encryptedPassword":null,"encryptedPrivateKey":null,"id":0},"src":"","srcFileNamePattern":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.RemoteAction.ZIP","srcServer":null,"src":"","srcFileNamePattern":"","destServer":null,"dest":"","destFileNameTemplate":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.EMIT_CHANGES","src":"","srcFileNamePattern":"","intent":"","packageName":""}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.MOVE","src":"/storage/emulated/0/AntennaPod","srcFileNamePattern":"AntennaPodBackup-\\d{4}-\\d{2}-\\d{2}\\.db","dest":"/storage/emulated/0/Backups","destFileNameTemplate":"AntennaPod.db","scanSubdirectories":true,"overwriteExisting":true,"preserveStructure":false}""",
$$"""{"type":"co.adityarajput.fileflow.data.models.Action.MOVE","src":"/storage/emulated/0/Backups","srcFileNamePattern":"PipePipeData-\\d{8}_\\d{6}\\.zip","dest":"/storage/emulated/0/Backups","destFileNameTemplate":"PipePipe-${uuid}.zip","keepOriginal":false,"superlative":"LARGEST","deleteEmptySrcSubdirectories":true}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.DELETE_STALE","src":"/storage/emulated/0/Download","srcFileNamePattern":"(NotiFilter|Alarmetrics|FileFlow|MinCal)_v[\\d\\.]+\\.apk","retentionDays":90,"scanSubdirectories":true,"deleteEmptySrcSubdirectories":true}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.ZIP","src":"/storage/emulated/0/Documents/Notes","srcFileNamePattern":"(.*)\\.md","dest":"/storage/emulated/0/Backups","destFileNameTemplate":"Notes.zip","scanSubdirectories":true,"overwriteExisting":true,"preserveStructure":false}""",
"""{"type":"co.adityarajput.fileflow.data.models.Action.EMIT_CHANGES","src":"/storage/emulated/0/Movies","srcFileNamePattern":".*\\.mp4","intent":"org.amoradi.syncopoli.SYNC_PROFILE","packageName":"org.amoradi.syncopoli","extras":"{\"profile_name\": \"Movies Backup\"}","scanSubdirectories":true,"modifiedWithin":10800000}""",
"""{"type":"co.adityarajput.fileflow.data.models.RemoteAction.MOVE","srcServer":{"host":"192.168.1.12","port":3456,"username":"admin","encryptedPassword":"hunter2","encryptedPrivateKey":null,"id":1},"src":"backups/navidrome","srcFileNamePattern":"navidrome_backup_[\\d_\\.]{19}\\.db","destServer":{"host":"192.168.1.78","port":9012,"username":"sftp","encryptedPassword":null,"encryptedPrivateKey":"passkey","id":2},"dest":"backups/glacial","destFileNameTemplate":"navidrome.db","overwriteExisting":true}""",
)

val incorrectSerializations =
listOf("", "{", "}", "{}", serializedActions[0].substring(0, 10))

Converters().run {
for (i in actions.indices) {
assertEquals(serializedActions[i], fromAction(actions[i]))
assertEquals(actions[i], toAction(serializedActions[i]))
assertEquals(
actions[i],
toAction(serializedActions[i].replaceFirst("{", """{"title": "deprecated",""")),
)
}
for (string in incorrectSerializations) {
assertEquals(Action.MOVE("", "", "", ""), toAction(string))
}
}
}

@Test
fun Converters_IntList() {
val intLists = listOf(
listOf(1, 2), listOf(2, 1), listOf(2, 4, 6), listOf(2, 3, 5),
listOf(0, 1, 2, 3, 4, 5, 6),
)

val incorrectSerializations =
listOf("", "null", "null,")

Converters().run {
for (list in intLists) {
assertEquals(list, toIntList(fromIntList(list)))
}

for (string in incorrectSerializations) {
assertEquals(emptyList<Int>(), toIntList(string))
}

assertEquals(listOf(1), toIntList("1,null"))
}
}
}
5 changes: 5 additions & 0 deletions metadata/en-US/changelogs/12.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
• fix: Fail gracefully for issues with encryption
• fix: Compare files in chunks to prevent OutOfMemoryError
• feat: Create subfolders if destination file name contains path separators
• feat: Let users choose to override OS-suggested extensions
• feat: Display spinner when executing rule
Loading