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
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ FileFlow scans your files periodically and organizes them according to your rule

- **Rules** - Use [regex](https://github.com/BURG3R5/FileFlow/wiki/Examples) to precisely target
files and template strings to rename them 🎯
- **Actions** - Choose what to do with the files ⚙
1. Copy or move 📁
- **History** - Recent executions are stored (locally) ⏳
- **Free, open-source & private**
- No ads, subscriptions, or in-app purchases 🆓
Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ android {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name_launcher", "FileFlow Debug")
manifestPlaceholders["allowBackup"] = false
}
create("nightly") {
isDebuggable = false
Expand All @@ -42,6 +43,7 @@ android {
)
applicationIdSuffix = ".nightly"
resValue("string", "app_name_launcher", "FileFlow Nightly")
manifestPlaceholders["allowBackup"] = true
}
release {
isDebuggable = false
Expand All @@ -51,6 +53,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard/release.pro",
)
manifestPlaceholders["allowBackup"] = true
}
}
buildFeatures {
Expand Down
85 changes: 85 additions & 0 deletions app/schemas/co.adityarajput.fileflow.data.FileFlowDatabase/2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "b5ed230e3566b0c5dc93a3990ae7aace",
"entities": [
{
"tableName": "rules",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`action` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `executions` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "executions",
"columnName": "executions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "executions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `actionVerb` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "fileName",
"columnName": "fileName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actionVerb",
"columnName": "actionVerb",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b5ed230e3566b0c5dc93a3990ae7aace')"
]
}
}
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<application
android:name=".FileFlowApplication"
android:allowBackup="true"
android:allowBackup="${allowBackup}"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/co/adityarajput/fileflow/data/Converters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package co.adityarajput.fileflow.data

import androidx.room.TypeConverter
import co.adityarajput.fileflow.data.models.Action
import kotlinx.serialization.json.Json

class Converters {
@TypeConverter
fun fromAction(action: Action) = action.toString()
fun fromAction(action: Action) = Json.encodeToString(action)

@TypeConverter
fun toAction(value: String) = Action.fromString(value)
fun toAction(value: String) = try {
Json.decodeFromString<Action>(value)
} catch (_: Exception) {
Action.MOVE("", "", "", "")
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package co.adityarajput.fileflow.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.*
import co.adityarajput.fileflow.data.models.Execution
import co.adityarajput.fileflow.data.models.Rule

@Database([Rule::class, Execution::class], version = 1)
@Database([Rule::class, Execution::class], version = 2, autoMigrations = [AutoMigration(1, 2)])
@TypeConverters(Converters::class)
abstract class FileFlowDatabase : RoomDatabase() {
abstract fun ruleDao(): RuleDao
Expand Down
24 changes: 3 additions & 21 deletions app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import co.adityarajput.fileflow.R
import co.adityarajput.fileflow.utils.Logger
import co.adityarajput.fileflow.utils.FileSuperlative
import co.adityarajput.fileflow.utils.getGetDirectoryFromUri
import kotlinx.serialization.Serializable

Expand All @@ -20,11 +20,8 @@ sealed class Action(val title: String) {
val destFileNameTemplate: String,
val keepOriginal: Boolean = true,
val overwriteExisting: Boolean = false,
) : Action(srcFileNamePattern) {
override fun toString() = when (this) {
is MOVE -> "MOVE\n$src\n$srcFileNamePattern\n$dest\n$destFileNameTemplate\n$keepOriginal\n$overwriteExisting"
}
}
val superlative: FileSuperlative = FileSuperlative.LATEST,
) : Action(srcFileNamePattern)

val verb
get() = when (this) {
Expand All @@ -44,19 +41,4 @@ sealed class Action(val title: String) {
append(destFileNameTemplate)
}
}

companion object {
fun fromString(value: String) = when {
value.startsWith("MOVE") -> {
val args = value.split("\n")

MOVE(args[1], args[2], args[3], args[4], args[5].toBoolean(), args[6].toBoolean())
}

else -> {
Logger.e("Action", value)
throw IllegalArgumentException("Can't convert value to Action, unknown value: $value")
}
}
}
}
99 changes: 62 additions & 37 deletions app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package co.adityarajput.fileflow.services

import android.content.Context
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import co.adityarajput.fileflow.data.AppContainer
import co.adityarajput.fileflow.data.models.Action
import co.adityarajput.fileflow.data.models.Execution
import co.adityarajput.fileflow.data.models.Rule
import co.adityarajput.fileflow.utils.Logger
import co.adityarajput.fileflow.utils.pathToFile
import kotlinx.coroutines.flow.first

class FlowExecutor(private val context: Context) {
Expand All @@ -22,52 +21,78 @@ class FlowExecutor(private val context: Context) {
if (!rule.enabled || rule.action !is Action.MOVE) continue

val regex = Regex(rule.action.srcFileNamePattern)
val destDir = context.pathToFile(rule.action.dest) ?: continue
val destDir = context.pathToFile(rule.action.dest)

for (srcFile in context.pathToFile(rule.action.src)?.listFiles() ?: arrayOf()) {
if (!srcFile.isFile || srcFile.name == null || !regex.matches(srcFile.name!!)) continue
if (destDir == null) {
Logger.e("FlowExecutor", "${rule.action.dest} is invalid")
continue
}

val destFileName = regex.replace(
srcFile.name!!,
rule.action.destFileNameTemplate,
)
val destFiles = destDir.listFiles().filter { it.isFile }
var destFile = destFiles.firstOrNull { it.name == destFileName }
val srcFile = context.pathToFile(rule.action.src)?.listFiles()
?.filter { it.isFile && it.name != null && regex.matches(it.name!!) }
?.maxByOrNull(rule.action.superlative.selector)
?: continue

if (destFile != null) {
if (!rule.action.overwriteExisting) {
Logger.e("FlowExecutor", "$destFileName already exists")
continue
}
val destFileName = regex.replace(srcFile.name!!, rule.action.destFileNameTemplate)
var destFile = destDir.listFiles().firstOrNull { it.isFile && it.name == destFileName }

Logger.i("FlowExecutor", "Deleting existing $destFileName")
destFile.delete()
if (destFile != null) {
if (!rule.action.overwriteExisting) {
Logger.e("FlowExecutor", "${destFile.name} already exists")
continue
}

destFile = destDir.createFile(
srcFile.type ?: "application/octet-stream",
destFileName,
) ?: continue

resolver.openInputStream(srcFile.uri).use { src ->
resolver.openOutputStream(destFile.uri).use { dest ->
if (src == null || dest == null) continue

src.copyTo(dest)
Logger.i("FlowExecutor", "Copied ${srcFile.name} to ${destFile.name}")
repository.registerExecution(
rule,
Execution(srcFile.name!!, rule.action.verb),
)
if (!rule.action.keepOriginal) {
Logger.i("FlowExecutor", "Deleting original ${srcFile.name}")
srcFile.delete()
resolver.openInputStream(destFile.uri).use { dest ->
if (src == null || dest == null) {
Logger.e("FlowExecutor", "Failed to open file(s)")
continue
}

if (src.readBytes().contentEquals(dest.readBytes())) {
Logger.i(
"FlowExecutor",
"Source and destination files are identical",
)
continue
}
}
}

Logger.i("FlowExecutor", "Deleting existing ${destFile.name}")
destFile.delete()
}

destFile = destDir.createFile(
srcFile.type ?: "application/octet-stream",
destFileName,
)

if (destFile == null) {
Logger.e("FlowExecutor", "Failed to create $destFileName")
continue
}

resolver.openInputStream(srcFile.uri).use { src ->
resolver.openOutputStream(destFile.uri).use { dest ->
if (src == null || dest == null) {
Logger.e("FlowExecutor", "Failed to open file(s)")
continue
}

Logger.i("FlowExecutor", "Copying ${srcFile.name} to ${destFile.name}")
src.copyTo(dest)
repository.registerExecution(
rule,
Execution(srcFile.name!!, rule.action.verb),
)

if (!rule.action.keepOriginal) {
Logger.i("FlowExecutor", "Deleting original ${srcFile.name}")
srcFile.delete()
}
}
}
}
}
}

fun Context.pathToFile(path: String): DocumentFile? = DocumentFile.fromTreeUri(this, path.toUri())
15 changes: 15 additions & 0 deletions app/src/main/java/co/adityarajput/fileflow/utils/Files.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package co.adityarajput.fileflow.utils

import android.content.Context
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import co.adityarajput.fileflow.R

fun Context.pathToFile(path: String) = DocumentFile.fromTreeUri(this, path.toUri())

enum class FileSuperlative(val displayName: Int, val selector: (DocumentFile) -> Long) {
EARLIEST(R.string.earliest, { -it.lastModified() }),
LATEST(R.string.latest, { it.lastModified() }),
SMALLEST(R.string.smallest, { -it.length() }),
LARGEST(R.string.largest, { it.length() }),
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ class RulesViewModel(private val repository: Repository) : ViewModel() {

var selectedRule by mutableStateOf<Rule?>(null)

fun executeRule(context: Context) {
fun executeRule(context: Context, showToast: (String) -> Unit) {
viewModelScope.launch {
val latestLogBeforeExecution = Logger.logs.lastOrNull()

FlowExecutor(context).run(listOf(selectedRule!!))

val recentErrorLog = Logger.logs
.dropWhile { it != latestLogBeforeExecution }.drop(1)
.firstOrNull { it.contains("[ERROR]") }
?: Logger.logs.lastOrNull { it.contains("[ERROR]") }
if (recentErrorLog != null) {
showToast("Error:" + recentErrorLog.substringAfter("[ERROR]"))
}
}
}

Expand Down
Loading