mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-02 13:10:55 +00:00
Add S3 support to backups, fixes #7.
This commit is contained in:
parent
da820b8a0d
commit
fca1db8802
@ -24,13 +24,19 @@ abstract class FoundationPlugin : JavaPlugin() {
|
|||||||
features = createFeatures()
|
features = createFeatures()
|
||||||
module = createModule()
|
module = createModule()
|
||||||
|
|
||||||
// TODO: If we have another plugin using this class, we may need to use context isolation.
|
// TODO: If we have another plugin using Koin, we may need to use context isolation and ensure
|
||||||
|
// it uses the same context so they can fetch stuff from us.
|
||||||
// https://insert-koin.io/docs/reference/koin-core/context-isolation
|
// https://insert-koin.io/docs/reference/koin-core/context-isolation
|
||||||
pluginApplication = startKoin {
|
pluginApplication = startKoin {
|
||||||
modules(pluginModule)
|
modules(pluginModule)
|
||||||
modules(module)
|
modules(module)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is probably a bit of a hack.
|
||||||
|
pluginApplication.modules(module {
|
||||||
|
single { pluginApplication }
|
||||||
|
})
|
||||||
|
|
||||||
features.forEach {
|
features.forEach {
|
||||||
pluginApplication.modules(it.module())
|
pluginApplication.modules(it.module())
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import org.bukkit.Server
|
|||||||
import org.bukkit.command.Command
|
import org.bukkit.command.Command
|
||||||
import org.bukkit.command.CommandExecutor
|
import org.bukkit.command.CommandExecutor
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@ -19,9 +21,12 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
// TODO: Clean up dependency injection.
|
||||||
class BackupCommand(
|
class BackupCommand(
|
||||||
private val plugin: FoundationCorePlugin,
|
private val plugin: FoundationCorePlugin,
|
||||||
private val backupPath: Path
|
private val backupsPath: Path,
|
||||||
|
private val config: BackupConfig,
|
||||||
|
private val s3Client: S3Client,
|
||||||
) : CommandExecutor {
|
) : CommandExecutor {
|
||||||
override fun onCommand(
|
override fun onCommand(
|
||||||
sender: CommandSender, command: Command, label: String, args: Array<String>
|
sender: CommandSender, command: Command, label: String, args: Array<String>
|
||||||
@ -47,26 +52,38 @@ class BackupCommand(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runBackup(server: Server) {
|
// TODO: Pull backup creation code into a separate service.
|
||||||
|
private fun runBackup(server: Server) = try {
|
||||||
RUNNING.set(true)
|
RUNNING.set(true)
|
||||||
|
|
||||||
server.sendMessage(Util.formatSystemMessage("Backup started."))
|
server.sendMessage(Util.formatSystemMessage("Backup started."))
|
||||||
|
|
||||||
val backupFile =
|
val backupFileName = String.format("backup-%s.zip", Instant.now().toString())
|
||||||
backupPath.resolve(String.format("backup-%s.zip", Instant.now().toString())).toFile()
|
val backupPath = backupsPath.resolve(backupFileName)
|
||||||
|
val backupFile = backupPath.toFile()
|
||||||
|
|
||||||
try {
|
|
||||||
FileOutputStream(backupFile).use { zipFileStream ->
|
FileOutputStream(backupFile).use { zipFileStream ->
|
||||||
ZipOutputStream(BufferedOutputStream(zipFileStream)).use { zipStream ->
|
ZipOutputStream(BufferedOutputStream(zipFileStream)).use { zipStream ->
|
||||||
backupPlugins(server, zipStream)
|
backupPlugins(server, zipStream)
|
||||||
backupWorlds(server, zipStream)
|
backupWorlds(server, zipStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Pull upload code out into a separate service.
|
||||||
|
if (config.s3.accessKeyId.isNotEmpty()) {
|
||||||
|
s3Client.putObject(
|
||||||
|
PutObjectRequest.builder().apply {
|
||||||
|
bucket(config.s3.bucket)
|
||||||
|
key("${config.s3.baseDirectory}/$backupFileName")
|
||||||
|
}.build(),
|
||||||
|
backupPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Unit
|
||||||
} finally {
|
} finally {
|
||||||
RUNNING.set(false)
|
RUNNING.set(false)
|
||||||
server.sendMessage(Util.formatSystemMessage("Backup finished."))
|
server.sendMessage(Util.formatSystemMessage("Backup finished."))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupPlugins(server: Server, zipStream: ZipOutputStream) {
|
private fun backupPlugins(server: Server, zipStream: ZipOutputStream) {
|
||||||
try {
|
try {
|
||||||
@ -106,7 +123,7 @@ class BackupCommand(
|
|||||||
.filter { path: Path? -> Files.isRegularFile(path) }
|
.filter { path: Path? -> Files.isRegularFile(path) }
|
||||||
.toList()
|
.toList()
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(1024)
|
||||||
val backupsPath = backupPath.toRealPath()
|
val backupsPath = backupsPath.toRealPath()
|
||||||
|
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
val realPath = path.toRealPath()
|
val realPath = path.toRealPath()
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package cloud.kubelet.foundation.core.features.backup
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupConfig(
|
||||||
|
val s3: S3Config = S3Config(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class S3Config(
|
||||||
|
val accessKeyId: String = "",
|
||||||
|
val secretAccessKey: String = "",
|
||||||
|
val region: String = "",
|
||||||
|
val endpointOverride: String = "",
|
||||||
|
val bucket: String = "",
|
||||||
|
val baseDirectory: String = "",
|
||||||
|
)
|
@ -1,18 +1,64 @@
|
|||||||
package cloud.kubelet.foundation.core.features.backup
|
package cloud.kubelet.foundation.core.features.backup
|
||||||
|
|
||||||
import cloud.kubelet.foundation.core.FoundationCorePlugin
|
import cloud.kubelet.foundation.core.FoundationCorePlugin
|
||||||
|
import cloud.kubelet.foundation.core.Util
|
||||||
import cloud.kubelet.foundation.core.abstraction.Feature
|
import cloud.kubelet.foundation.core.abstraction.Feature
|
||||||
|
import com.charleskorn.kaml.Yaml
|
||||||
|
import org.koin.core.KoinApplication
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
|
||||||
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
|
||||||
|
import software.amazon.awssdk.regions.Region
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client
|
||||||
|
import java.net.URI
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
|
||||||
class BackupFeature : Feature() {
|
class BackupFeature : Feature() {
|
||||||
private val plugin by inject<FoundationCorePlugin>()
|
private val plugin by inject<FoundationCorePlugin>()
|
||||||
|
private val s3Client by inject<S3Client>()
|
||||||
|
private val config by inject<BackupConfig>()
|
||||||
|
|
||||||
override fun enable() {
|
override fun enable() {
|
||||||
// Create backup directory.
|
// Create backup directory.
|
||||||
val backupPath = plugin.pluginDataPath.resolve(BACKUPS_DIRECTORY)
|
val backupPath = plugin.pluginDataPath.resolve(BACKUPS_DIRECTORY)
|
||||||
backupPath.toFile().mkdir()
|
backupPath.toFile().mkdir()
|
||||||
|
|
||||||
registerCommandExecutor("fbackup", BackupCommand(plugin, backupPath))
|
registerCommandExecutor("fbackup", BackupCommand(plugin, backupPath, config, s3Client))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun module() = module {
|
||||||
|
single {
|
||||||
|
val configPath = Util.copyDefaultConfig<FoundationCorePlugin>(
|
||||||
|
plugin.slF4JLogger,
|
||||||
|
plugin.pluginDataPath,
|
||||||
|
"backup.yaml",
|
||||||
|
)
|
||||||
|
return@single Yaml.default.decodeFromStream(
|
||||||
|
BackupConfig.serializer(),
|
||||||
|
configPath.inputStream()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
val config = get<BackupConfig>()
|
||||||
|
|
||||||
|
val creds = StaticCredentialsProvider.create(
|
||||||
|
AwsSessionCredentials.create(config.s3.accessKeyId, config.s3.secretAccessKey, "")
|
||||||
|
)
|
||||||
|
val builder = S3Client.builder().credentialsProvider(creds)
|
||||||
|
|
||||||
|
if (config.s3.endpointOverride.isNotEmpty()) {
|
||||||
|
builder.endpointOverride(URI.create(config.s3.endpointOverride))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.s3.region.isNotEmpty()) {
|
||||||
|
builder.region(Region.of(config.s3.region))
|
||||||
|
} else {
|
||||||
|
builder.region(Region.US_WEST_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
23
foundation-core/src/main/resources/backup.yaml
Normal file
23
foundation-core/src/main/resources/backup.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Configuration of S3 service to upload back-ups to.
|
||||||
|
s3:
|
||||||
|
# The access key ID from your S3-compliant storage provider.
|
||||||
|
# If empty, backups will not be uploaded to S3.
|
||||||
|
accessKeyId: ""
|
||||||
|
|
||||||
|
# The secret access key from your S3-compliant storage provider.
|
||||||
|
secretAccessKey: ""
|
||||||
|
|
||||||
|
# The region the bucket is located in. If using something other than AWS, this field can be set to
|
||||||
|
# any valid region (us-west-1, etc.), or blank which defaults to us-west-1.
|
||||||
|
region: ""
|
||||||
|
|
||||||
|
# An endpoint override, this is typically used for S3-compatible services like Backblaze B2.
|
||||||
|
# If not specified, it will use the AWS region specified.
|
||||||
|
endpointOverride: ""
|
||||||
|
|
||||||
|
# Name of the bucket to upload to.
|
||||||
|
bucket: ""
|
||||||
|
|
||||||
|
# Base directory to store backups in. Value being set to "my-server" will store backups with a
|
||||||
|
# path like bucket-name/my-server/backup-2021-12-21T00:06:41.760568Z.zip
|
||||||
|
baseDirectory: ""
|
Loading…
Reference in New Issue
Block a user