mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Feature/automated backups (#595)
* Automatically create backups * Cleanup automated backups * Extract backup filename creation into function
This commit is contained in:
@@ -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.ProtoBackupExport
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -62,8 +60,7 @@ class BackupMutation {
|
|||||||
fun createBackup(
|
fun createBackup(
|
||||||
input: CreateBackupInput? = null
|
input: CreateBackupInput? = null
|
||||||
): CreateBackupPayload {
|
): CreateBackupPayload {
|
||||||
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
|
val filename = ProtoBackupExport.getBackupFilename()
|
||||||
val filename = "tachidesk_$currentDate.proto.gz"
|
|
||||||
|
|
||||||
val backup = ProtoBackupExport.createBackup(
|
val backup = ProtoBackupExport.createBackup(
|
||||||
BackupFlags(
|
BackupFlags(
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
|
|||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
import suwayomi.tachidesk.server.util.handler
|
import suwayomi.tachidesk.server.util.handler
|
||||||
import suwayomi.tachidesk.server.util.withOperation
|
import suwayomi.tachidesk.server.util.withOperation
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -105,9 +103,8 @@ object BackupController {
|
|||||||
},
|
},
|
||||||
behaviorOf = { ctx ->
|
behaviorOf = { ctx ->
|
||||||
ctx.contentType("application/octet-stream")
|
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(
|
ctx.future(
|
||||||
future {
|
future {
|
||||||
ProtoBackupExport.createBackup(
|
ProtoBackupExport.createBackup(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import mu.KotlinLogging
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
@@ -16,6 +17,9 @@ import org.jetbrains.exposed.sql.SortOrder
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
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.CategoryManga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
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.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
import java.io.InputStream
|
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.concurrent.TimeUnit
|
||||||
|
import java.util.prefs.Preferences
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
object ProtoBackupExport : ProtoBackupBase() {
|
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 {
|
fun createBackup(flags: BackupFlags): InputStream {
|
||||||
// Create root object
|
// Create root object
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ class ServerConfig(getConfig: () -> Config, moduleName: String = MODULE_NAME) :
|
|||||||
var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
|
var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
|
||||||
var systemTrayEnabled: Boolean by overridableConfig
|
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 {
|
companion object {
|
||||||
fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) })
|
fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.kodein.di.DI
|
|||||||
import org.kodein.di.bind
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.singleton
|
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.IUpdater
|
||||||
import suwayomi.tachidesk.manga.impl.update.Updater
|
import suwayomi.tachidesk.manga.impl.update.Updater
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
||||||
@@ -45,6 +46,7 @@ class ApplicationDirs(
|
|||||||
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
|
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
|
||||||
val localMangaRoot = "$dataRoot/local"
|
val localMangaRoot = "$dataRoot/local"
|
||||||
val webUIRoot = "$dataRoot/webUI"
|
val webUIRoot = "$dataRoot/webUI"
|
||||||
|
val automatedBackupRoot = serverConfig.backupPath.ifBlank { "$dataRoot/backups" }
|
||||||
|
|
||||||
val tempMangaCacheRoot = "$tempRoot/manga-cache"
|
val tempMangaCacheRoot = "$tempRoot/manga-cache"
|
||||||
}
|
}
|
||||||
@@ -165,4 +167,7 @@ fun applicationSetup() {
|
|||||||
|
|
||||||
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
|
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
|
||||||
Security.addProvider(BouncyCastleProvider())
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
|
||||||
|
// start automated backups
|
||||||
|
ProtoBackupExport.scheduleAutomatedBackupTask()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,3 +32,9 @@ server.basicAuthPassword = ""
|
|||||||
# misc
|
# misc
|
||||||
server.debugLogsEnabled = false
|
server.debugLogsEnabled = false
|
||||||
server.systemTrayEnabled = true
|
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
|
||||||
|
|||||||
@@ -25,3 +25,9 @@ server.webUIEnabled = true
|
|||||||
server.initialOpenInBrowserEnabled = true
|
server.initialOpenInBrowserEnabled = true
|
||||||
server.webUIInterface = "browser" # "browser" or "electron"
|
server.webUIInterface = "browser" # "browser" or "electron"
|
||||||
server.electronPath = ""
|
server.electronPath = ""
|
||||||
|
|
||||||
|
# backup
|
||||||
|
server.backupPath = ""
|
||||||
|
server.backupInterval = 1
|
||||||
|
server.automatedBackups = true
|
||||||
|
server.backupTTL = 14
|
||||||
|
|||||||
Reference in New Issue
Block a user