mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-12 07:14:35 -05:00
Feature/improve automated backup (#597)
* Add option to disable cleanup of backups * Ensure the minimum TTL of backups to 1 day * Schedule the automated backup on a specific time of the day * Introduce scheduler that takes system hibernation time into account In case the system was hibernating/suspended scheduled task that should have been executed during that time would not get triggered and thus, miss an execution. To prevent this, this new scheduler periodically checks if the system was suspended and in case it was, triggers any task that missed its last execution * Use new scheduler
This commit is contained in:
@@ -36,13 +36,12 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.util.HAScheduler
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.prefs.Preferences
|
||||
import kotlin.time.Duration.Companion.days
|
||||
@@ -50,40 +49,37 @@ import kotlin.time.Duration.Companion.days
|
||||
object ProtoBackupExport : ProtoBackupBase() {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
private var backupSchedulerJobId: String = ""
|
||||
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
|
||||
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
|
||||
|
||||
private val backupTimer = Timer()
|
||||
private var currentAutomatedBackupTask: TimerTask? = null
|
||||
|
||||
fun scheduleAutomatedBackupTask() {
|
||||
HAScheduler.deschedule(backupSchedulerJobId)
|
||||
|
||||
if (!serverConfig.automatedBackups) {
|
||||
currentAutomatedBackupTask?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val minInterval = 1.days
|
||||
val interval = serverConfig.backupInterval.days
|
||||
val backupInterval = interval.coerceAtLeast(minInterval).inWholeMilliseconds
|
||||
|
||||
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, 0)
|
||||
val initialDelay =
|
||||
backupInterval - (System.currentTimeMillis() - lastAutomatedBackup) % backupInterval
|
||||
|
||||
currentAutomatedBackupTask?.cancel()
|
||||
currentAutomatedBackupTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
cleanupAutomatedBackups()
|
||||
createAutomatedBackup()
|
||||
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
}
|
||||
val task = {
|
||||
cleanupAutomatedBackups()
|
||||
createAutomatedBackup()
|
||||
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
backupTimer.scheduleAtFixedRate(
|
||||
currentAutomatedBackupTask,
|
||||
initialDelay,
|
||||
backupInterval
|
||||
)
|
||||
val (hour, minute) = serverConfig.backupTime.split(":").map { it.toInt() }
|
||||
val backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
|
||||
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59)
|
||||
val backupInterval = serverConfig.backupInterval.days.coerceAtLeast(1.days)
|
||||
|
||||
// trigger last backup in case the server wasn't running on the scheduled time
|
||||
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
val wasPreviousBackupTriggered =
|
||||
(System.currentTimeMillis() - lastAutomatedBackup) < backupInterval.inWholeMilliseconds
|
||||
if (!wasPreviousBackupTriggered) {
|
||||
task()
|
||||
}
|
||||
|
||||
HAScheduler.schedule(task, "$backupMinute $backupHour */${backupInterval.inWholeDays} * *", "backup")
|
||||
}
|
||||
|
||||
private fun createAutomatedBackup() {
|
||||
@@ -105,11 +101,15 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
|
||||
backupFile.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun cleanupAutomatedBackups() {
|
||||
logger.debug { "Cleanup automated backups" }
|
||||
logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL})" }
|
||||
|
||||
val isCleanupDisabled = serverConfig.backupTTL == 0
|
||||
if (isCleanupDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
|
||||
if (!automatedBackupDir.isDirectory) {
|
||||
@@ -132,7 +132,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
|
||||
val lastAccessTime = file.lastModified()
|
||||
val isTTLReached =
|
||||
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.inWholeMilliseconds
|
||||
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.coerceAtLeast(1.days).inWholeMilliseconds
|
||||
if (isTTLReached) {
|
||||
file.delete()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package suwayomi.tachidesk.manga.impl.update
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import it.sauronsoftware.cron4j.Task
|
||||
import it.sauronsoftware.cron4j.TaskExecutionContext
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -24,11 +26,13 @@ import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.util.HAScheduler
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
@@ -51,41 +55,46 @@ class Updater : IUpdater {
|
||||
private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey"
|
||||
private val preferences = Preferences.userNodeForPackage(Updater::class.java)
|
||||
|
||||
private val updateTimer = Timer()
|
||||
private var currentUpdateTask: TimerTask? = null
|
||||
private var currentUpdateTaskId = ""
|
||||
|
||||
init {
|
||||
scheduleUpdateTask()
|
||||
}
|
||||
|
||||
|
||||
private fun autoUpdateTask() {
|
||||
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
|
||||
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis())
|
||||
|
||||
if (status.value.running) {
|
||||
logger.debug { "Global update is already in progress" }
|
||||
return
|
||||
}
|
||||
|
||||
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
|
||||
addCategoriesToUpdateQueue(Category.getCategoryList(), true)
|
||||
}
|
||||
|
||||
private fun scheduleUpdateTask() {
|
||||
HAScheduler.deschedule(currentUpdateTaskId)
|
||||
|
||||
if (!serverConfig.automaticallyTriggerGlobalUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
val minInterval = 6.hours
|
||||
val interval = serverConfig.globalUpdateInterval.hours
|
||||
val updateInterval = interval.coerceAtLeast(minInterval).inWholeMilliseconds
|
||||
val updateInterval = interval.coerceAtLeast(minInterval)
|
||||
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, System.currentTimeMillis())
|
||||
|
||||
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
|
||||
val initialDelay = updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate) % updateInterval
|
||||
|
||||
currentUpdateTask?.cancel()
|
||||
currentUpdateTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis())
|
||||
|
||||
if (status.value.running) {
|
||||
logger.debug { "Global update is already in progress, do not trigger global update" }
|
||||
return
|
||||
}
|
||||
|
||||
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
|
||||
addCategoriesToUpdateQueue(Category.getCategoryList(), true)
|
||||
}
|
||||
// trigger update in case the server wasn't running on the scheduled time
|
||||
val wasPreviousUpdateTriggered =
|
||||
(System.currentTimeMillis() - lastAutomatedUpdate) < updateInterval.inWholeMilliseconds
|
||||
if (!wasPreviousUpdateTriggered) {
|
||||
autoUpdateTask()
|
||||
}
|
||||
|
||||
updateTimer.scheduleAtFixedRate(currentUpdateTask, initialDelay, updateInterval)
|
||||
HAScheduler.schedule(::autoUpdateTask, "* */${updateInterval.inWholeHours} * * *", "global-update")
|
||||
}
|
||||
|
||||
private fun getOrCreateUpdateChannelFor(source: String): Channel<UpdateJob> {
|
||||
|
||||
Reference in New Issue
Block a user