Files
Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt
schroda 0d79ac68f8 Feature/backup import add backup flags (#1697)
* Add backup flags to backup restore

* Cleanup default backup flags handling

* Optionally exclude manga from backup
2025-10-05 18:52:45 -04:00

182 lines
6.9 KiB
Kotlin

package suwayomi.tachidesk.manga.controller
import io.javalin.http.HttpStatus
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.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object BackupController {
/** expects a Tachiyomi protobuf backup in the body */
val protobufImport =
handler(
documentWith = {
withOperation {
summary("Restore a backup")
description("Expects a Tachiyomi protobuf backup in the body")
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
ProtoBackupImport.restoreLegacy(ctx.bodyInputStream())
}.thenApply {
ctx.json(it)
}
}
},
withResults = {
httpCode(HttpStatus.OK)
},
)
/** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
val protobufImportFile =
handler(
documentWith = {
withOperation {
summary("Restore a backup file")
description("Expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future {
future {
ProtoBackupImport.restoreLegacy(ctx.uploadedFile("backup.proto.gz")!!.content())
}.thenApply {
ctx.json(it)
}
}
},
withResults = {
httpCode(HttpStatus.OK)
httpCode(HttpStatus.NOT_FOUND)
},
)
/** returns a Tachiyomi protobuf backup created from the current database as a body */
val protobufExport =
handler(
documentWith = {
withOperation {
summary("Create a backup")
description("Returns a Tachiyomi protobuf backup created from the current database as a body")
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.contentType("application/octet-stream")
ctx.future {
future {
ProtoBackupExport.createBackup(BackupFlags.DEFAULT)
}.thenApply { ctx.result(it) }
}
},
withResults = {
stream(HttpStatus.OK)
},
)
/** returns a Tachiyomi protobuf backup created from the current database as a file */
val protobufExportFile =
handler(
documentWith = {
withOperation {
summary("Create a backup file")
description("Returns a Tachiyomi protobuf backup created from the current database as a file")
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.contentType("application/octet-stream")
ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""")
ctx.future {
future {
ProtoBackupExport.createBackup(BackupFlags.DEFAULT)
}.thenApply { ctx.result(it) }
}
},
withResults = {
stream(HttpStatus.OK)
},
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */
val protobufValidate =
handler(
documentWith = {
withOperation {
summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
ProtoBackupValidator.validate(ctx.bodyInputStream())
}.thenApply {
ctx.json(it)
}
}
},
withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpStatus.OK)
},
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
val protobufValidateFile =
handler(
documentWith = {
withOperation {
summary("Validate a backup file")
description(
"Reports missing sources and trackers, " +
"expects a Tachiyomi protobuf backup as a file upload, " +
"the file must be named \"backup.proto.gz\"",
)
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content())
}.thenApply {
ctx.json(it)
}
}
},
withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpStatus.OK)
},
)
}