Feature/automated backups (#595)

* Automatically create backups

* Cleanup automated backups

* Extract backup filename creation into function
This commit is contained in:
schroda
2023-07-10 11:43:53 +02:00
committed by GitHub
parent 9a80992aec
commit 49f2d8588a
7 changed files with 133 additions and 8 deletions

View File

@@ -14,8 +14,6 @@ import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.server.JavalinSetup.future
import java.text.SimpleDateFormat
import java.util.Date
import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds
@@ -62,8 +60,7 @@ class BackupMutation {
fun createBackup(
input: CreateBackupInput? = null
): CreateBackupPayload {
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
val filename = "tachidesk_$currentDate.proto.gz"
val filename = ProtoBackupExport.getBackupFilename()
val backup = ProtoBackupExport.createBackup(
BackupFlags(

View File

@@ -8,8 +8,6 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
import java.text.SimpleDateFormat
import java.util.Date
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -105,9 +103,8 @@ object BackupController {
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""")
ctx.header("Content-Disposition", """attachment; filename="${ProtoBackupExport.getBackupFilename()}"""")
ctx.future(
future {
ProtoBackupExport.createBackup(

View File

@@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import mu.KotlinLogging
import okio.buffer
import okio.gzip
import okio.sink
@@ -16,6 +17,9 @@ import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
@@ -30,11 +34,115 @@ import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
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 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
object ProtoBackupExport : ProtoBackupBase() {
private val logger = KotlinLogging.logger { }
private val applicationDirs by DI.global.instance<ApplicationDirs>()
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
private val backupTimer = Timer()
private var currentAutomatedBackupTask: TimerTask? = null
fun scheduleAutomatedBackupTask() {
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())
}
}
backupTimer.scheduleAtFixedRate(
currentAutomatedBackupTask,
initialDelay,
backupInterval
)
}
private fun createAutomatedBackup() {
logger.info { "Creating automated backup..." }
createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true
)
).use { input ->
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
automatedBackupDir.mkdirs()
val backupFile = File(applicationDirs.automatedBackupRoot, getBackupFilename())
backupFile.outputStream().use { output -> input.copyTo(output) }
}
}
private fun cleanupAutomatedBackups() {
logger.debug { "Cleanup automated backups" }
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
if (!automatedBackupDir.isDirectory) {
return
}
automatedBackupDir.walkTopDown().forEach { file ->
try {
cleanupAutomatedBackupFile(file)
} catch (e: Exception) {
// ignore, will be retried on next cleanup
}
}
}
private fun cleanupAutomatedBackupFile(file: File) {
if (!file.isFile) {
return
}
val lastAccessTime = file.lastModified()
val isTTLReached =
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.inWholeMilliseconds
if (isTTLReached) {
file.delete()
}
}
fun getBackupFilename(): String {
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
return "tachidesk_$currentDate.proto.gz"
}
fun createBackup(flags: BackupFlags): InputStream {
// Create root object

View File

@@ -48,6 +48,12 @@ class ServerConfig(getConfig: () -> Config, moduleName: String = MODULE_NAME) :
var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
var systemTrayEnabled: Boolean by overridableConfig
// backup
var backupPath: String by overridableConfig
var backupInterval: Int by overridableConfig
var automatedBackups: Boolean by overridableConfig
var backupTTL: Int by overridableConfig
companion object {
fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) })
}

View File

@@ -18,6 +18,7 @@ import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.conf.global
import org.kodein.di.singleton
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.update.Updater
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
@@ -45,6 +46,7 @@ class ApplicationDirs(
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
val localMangaRoot = "$dataRoot/local"
val webUIRoot = "$dataRoot/webUI"
val automatedBackupRoot = serverConfig.backupPath.ifBlank { "$dataRoot/backups" }
val tempMangaCacheRoot = "$tempRoot/manga-cache"
}
@@ -165,4 +167,7 @@ fun applicationSetup() {
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
Security.addProvider(BouncyCastleProvider())
// start automated backups
ProtoBackupExport.scheduleAutomatedBackupTask()
}

View File

@@ -32,3 +32,9 @@ server.basicAuthPassword = ""
# misc
server.debugLogsEnabled = false
server.systemTrayEnabled = true
# backup
server.backupPath = ""
server.backupInterval = 1 # time in days - range: 1 <= n < ∞ - default: 1 day - interval in which the server will automatically create a backup
server.automatedBackups = true
server.backupTTL = 14 # time in days - range: 1 <= n < ∞ - default: 14 days - how long backup files will be kept before they will get deleted

View File

@@ -25,3 +25,9 @@ server.webUIEnabled = true
server.initialOpenInBrowserEnabled = true
server.webUIInterface = "browser" # "browser" or "electron"
server.electronPath = ""
# backup
server.backupPath = ""
server.backupInterval = 1
server.automatedBackups = true
server.backupTTL = 14