Skip to content
Open
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
9 changes: 7 additions & 2 deletions src/main/kotlin/org/dash/mobile/explore/sync/MainApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ const val UPLOAD_ARG = "-upload"
const val QUIET_ARG = "-quiet"
const val PROD_ARG = "-prod"
const val DEBUG_ARG = "-debug"
const val OFFLINE_ARG = "-offline"

@FlowPreview
fun main(args: Array<String>) {
val validParams = setOf(UPLOAD_ARG, QUIET_ARG, PROD_ARG, DEBUG_ARG)
val validParams = setOf(UPLOAD_ARG, QUIET_ARG, PROD_ARG, DEBUG_ARG, OFFLINE_ARG)

var upload = false
var quietMode = false
var prodMode = false
var debugMode = false
var offlineMode = false

if (args.isNotEmpty()) {
for (arg in args) {
Expand All @@ -31,20 +33,23 @@ fun main(args: Array<String>) {
println("$QUIET_ARG - quiet mode: no notifications are pushed to Slack")
println("$PROD_ARG - production mode: use production data sources/destinations")
println("$DEBUG_ARG - output to CSV files for unit tests, BODY logging")
println("$OFFLINE_ARG - offline mode: skip all publishing to Google Cloud Storage")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the -offline help text.

SyncProcessor skips all GCS interactions in offline mode, including locks and previous database comparison, not just publishing.

Suggested wording
- println("$OFFLINE_ARG - offline mode: skip all publishing to Google Cloud Storage")
+ println("$OFFLINE_ARG - offline mode: skip all Google Cloud Storage operations")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
println("$OFFLINE_ARG - offline mode: skip all publishing to Google Cloud Storage")
println("$OFFLINE_ARG - offline mode: skip all Google Cloud Storage operations")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/org/dash/mobile/explore/sync/MainApp.kt` at line 36, The help
text for OFFLINE_ARG currently says it only skips publishing; update the println
in MainApp.kt that prints "$OFFLINE_ARG - offline mode: skip all publishing to
Google Cloud Storage" to clearly state that offline mode disables all Google
Cloud Storage interactions (including acquiring locks and comparing against the
previous database) rather than only skipping publishing; edit the string emitted
where OFFLINE_ARG is used so it mentions locks and previous-database comparison.

exitProcess(1)
}
}
upload = args.contains(UPLOAD_ARG)
quietMode = args.contains(QUIET_ARG)
prodMode = args.contains(PROD_ARG)
debugMode = args.contains(DEBUG_ARG)
offlineMode = args.contains(OFFLINE_ARG)
}
configureConsoleLogging()

runBlocking(Dispatchers.IO) {
SyncProcessor(
if (prodMode) OperationMode.PRODUCTION else OperationMode.TESTNET,
debug = debugMode
debug = debugMode,
offlineMode = offlineMode
).syncData(File("."), upload, quietMode)
}

Expand Down
123 changes: 72 additions & 51 deletions src/main/kotlin/org/dash/mobile/explore/sync/SyncProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit
import java.util.zip.CheckedInputStream

@FlowPreview
class SyncProcessor(private val mode: OperationMode, private val debug: Boolean = false) {
class SyncProcessor(private val mode: OperationMode, private val debug: Boolean = false, private val offlineMode: Boolean = false) {
companion object {
const val CURRENT_VERSION = 4
const val BUILD = 6
Expand All @@ -58,93 +58,114 @@ class SyncProcessor(private val mode: OperationMode, private val debug: Boolean
slackMessenger.quietMode = quietMode
slackMessenger.postSlackMessage("### Sync started for v$CURRENT_VERSION ($BUILD) ### - $mode", logger)

if (offlineMode) {
logger.notice("Offline mode: skipping all Google Cloud Storage operations")
}

try {
val syncLock = gcManager.checkLock()
val syncLockCreateTime = syncLock.first
// lock expires after 10 minutes if it wasn't removed for any reason
val lockValid = System.currentTimeMillis() < (syncLockCreateTime + TimeUnit.MINUTES.toMillis(10))
if (lockValid) {
slackMessenger.postSlackMessage("Sync already in progress (${syncLock.second})", logger)
return
if (!offlineMode) {
val syncLock = gcManager.checkLock()
val syncLockCreateTime = syncLock.first
// lock expires after 10 minutes if it wasn't removed for any reason
val lockValid = System.currentTimeMillis() < (syncLockCreateTime + TimeUnit.MINUTES.toMillis(10))
if (lockValid) {
slackMessenger.postSlackMessage("Sync already in progress (${syncLock.second})", logger)
return
}
gcManager.createLockFile(mode.name)
}
gcManager.createLockFile(mode.name)

dbFile = createEmptyDB(workingDir)
val locationsDbFile = createLocationsDB(workingDir)
importData(dbFile, locationsDbFile)

val dbFileChecksum = calculateChecksum(dbFile)
logger.debug("DB file checksum $dbFileChecksum")

val dbZipFileName = when (mode) {
OperationMode.PRODUCTION -> "${dbFile.nameWithoutExtension}-v$CURRENT_VERSION.zip"
OperationMode.TESTNET -> "${dbFile.nameWithoutExtension}-v$CURRENT_VERSION-testnet.zip"
OperationMode.DEVNET -> "${dbFile.nameWithoutExtension}-v$CURRENT_VERSION-devnet.zip"
}
if (!offlineMode) {
val dbFileChecksum = calculateChecksum(dbFile)
logger.debug("DB file checksum $dbFileChecksum")

val dbZipFile = File(workingDir, dbZipFileName)

val remoteChecksum = gcManager.remoteChecksum(dbZipFile)
val changesDetected = dbFileChecksum != remoteChecksum
val dbZipFileName = when (mode) {
OperationMode.PRODUCTION -> "${dbFile.nameWithoutExtension}-v$CURRENT_VERSION.zip"
OperationMode.TESTNET -> "${dbFile.nameWithoutExtension}-v$CURRENT_VERSION-testnet.zip"
OperationMode.DEVNET -> "${dbFile.nameWithoutExtension}-v$CURRENT_VERSION-devnet.zip"
}

if (changesDetected || forceUpload) {
if (changesDetected) {
if (remoteChecksum != null) {
slackMessenger.postSlackMessage(
"Changes detected ($dbFileChecksum vs $remoteChecksum) - updating",
logger
)
val dbZipFile = File(workingDir, dbZipFileName)

val remoteChecksum = gcManager.remoteChecksum(dbZipFile)
val changesDetected = dbFileChecksum != remoteChecksum

if (changesDetected || forceUpload) {
if (changesDetected) {
if (remoteChecksum != null) {
slackMessenger.postSlackMessage(
"Changes detected ($dbFileChecksum vs $remoteChecksum) - updating",
logger
)
} else {
logger.notice("No remote data - uploading")
}
} else {
logger.notice("No remote data - uploading")
logger.notice("Force upload active - updating")
}

throwIfCanceled()
val timestamp = Calendar.getInstance().timeInMillis
val password = dbFileChecksum.toCharArray()
compress(dbFile, dbZipFile, password, timestamp, dbFileChecksum)

throwIfCanceled()
// upload the zipped database
gcManager.uploadObject(dbZipFile, timestamp, dbFileChecksum)
// copy and upload the uncompressed database
val locationsDbChecksum = calculateChecksum(locationsDbFile)
gcManager.uploadObject(locationsDbFile, timestamp, locationsDbChecksum)
} else {
logger.notice("Force upload active - updating")
logger.notice("No changes were detected, updating canceled")
slackMessenger.postSlackMessage("No changes detected, updating canceled")
}

throwIfCanceled()
val timestamp = Calendar.getInstance().timeInMillis
val password = dbFileChecksum.toCharArray()
compress(dbFile, dbZipFile, password, timestamp, dbFileChecksum)

throwIfCanceled()
// upload the zipped database
gcManager.uploadObject(dbZipFile, timestamp, dbFileChecksum)
// copy and upload the uncompressed database
val locationsDbChecksum = calculateChecksum(locationsDbFile)
gcManager.uploadObject(locationsDbFile, timestamp, locationsDbChecksum)
} else {
logger.notice("No changes were detected, updating canceled")
slackMessenger.postSlackMessage("No changes detected, updating canceled")
gcManager.deleteLockFile()
}

slackMessenger.postSlackMessage("### Sync finished ###", logger)

gcManager.deleteLockFile()
} catch (ex: InterruptedException) {
slackMessenger.postSlackMessage("!!! Sync canceled !!!", logger)
gcManager.deleteLockFile()
if (!offlineMode) gcManager.deleteLockFile()
} catch (ex: Exception) {
logger.error(ex.message, ex)
slackMessenger.postSlackMessage("### Sync failed ### ${ex.message}")
gcManager.deleteLockFile()
if (!offlineMode) gcManager.deleteLockFile()
}
}

@Throws(InterruptedException::class)
private fun throwIfCanceled() {
if (gcManager.cancelRequested()) {
if (!offlineMode && gcManager.cancelRequested()) {
throw InterruptedException("Sync canceled")
}
}

@Throws(SQLException::class)
private suspend fun importData(dbFile: File, locationsDbFile: File) {
val ctxDataSource = CTXSpendDataSource(slackMessenger, debug)
val ctxDataSource = CTXSpendDataSource(slackMessenger, mode, debug)
val ctxData = ctxDataSource.getDataList()
val ctxReport = ctxDataSource.getReport()
val piggyCardsDataSource = PiggyCardsDataSource(slackMessenger, mode, debug)
val piggyCardsData = piggyCardsDataSource.getDataList()
val piggyCardsReport = piggyCardsDataSource.getReport()

// Generate HTML reports
listOf(ctxDataSource, piggyCardsDataSource).forEach { dataSource ->
val htmlFileName = dataSource.generateHtmlFile()
if (htmlFileName != null && !offlineMode) {
val htmlFile = File(htmlFileName)
if (htmlFile.exists()) {
logger.info("Uploading HTML file to Google Cloud: $htmlFileName")
gcManager.uploadObject(htmlFile, Calendar.getInstance().timeInMillis, "")
}
}
}

val report = SyncReport(listOf(ctxReport, piggyCardsReport))
if (debug) {
saveMerchantDataToCsv(ctxData, "ctx.csv")
Expand Down Expand Up @@ -201,7 +222,7 @@ class SyncProcessor(private val mode: OperationMode, private val debug: Boolean
}

// Compare to the previously created database
val previousLocationsFile = gcManager.downloadMostRecentLocationsDb()
val previousLocationsFile = if (!offlineMode) gcManager.downloadMostRecentLocationsDb() else null

val ctxLocations = mutableListOf<MerchantData>()
val piggyCardsLocations = mutableListOf<MerchantData>()
Expand Down
Loading