diff --git a/pom.xml b/pom.xml index 458b4b65..f9ad7892 100644 --- a/pom.xml +++ b/pom.xml @@ -201,6 +201,15 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 9 + 9 + + diff --git a/src/main/kotlin/com/ex_dock/ex_dock/database/JDBCStarter.kt b/src/main/kotlin/com/ex_dock/ex_dock/database/JDBCStarter.kt index aa7967bd..f09aa6fb 100644 --- a/src/main/kotlin/com/ex_dock/ex_dock/database/JDBCStarter.kt +++ b/src/main/kotlin/com/ex_dock/ex_dock/database/JDBCStarter.kt @@ -7,6 +7,7 @@ import com.ex_dock.ex_dock.database.home.HomeJdbcVerticle import com.ex_dock.ex_dock.database.product.* import com.ex_dock.ex_dock.database.scope.ScopeJdbcVerticle import com.ex_dock.ex_dock.database.server.ServerJDBCVerticle +import com.ex_dock.ex_dock.database.service.DatabaseBackupVerticle import com.ex_dock.ex_dock.database.text_pages.TextPagesJdbcVerticle import com.ex_dock.ex_dock.database.url.UrlJdbcVerticle import com.ex_dock.ex_dock.helper.deployWorkerVerticleHelper @@ -24,7 +25,7 @@ class JDBCStarter: AbstractVerticle() { Future.all(verticles) .onComplete { println("All JDBC verticles deployed") - TODO("Add all codecs to the eventbus") +// TODO("Add all codecs to the eventbus") } .onFailure { error -> println("Failed to deploy JDBC verticles: $error") @@ -47,6 +48,7 @@ class JDBCStarter: AbstractVerticle() { verticles.add(deployWorkerVerticleHelper(vertx, ProductStoreViewEavJdbcVerticle::class.qualifiedName.toString(), 5, 5)) verticles.add(deployWorkerVerticleHelper(vertx, ProductWebsiteEavJdbcVerticle::class.qualifiedName.toString(), 5, 5)) verticles.add(deployWorkerVerticleHelper(vertx, ProductCustomAttributesJdbcVerticle::class.qualifiedName.toString(), 5, 5)) + verticles.add(deployWorkerVerticleHelper(vertx, DatabaseBackupVerticle::class.qualifiedName.toString(), 1, 1)) } } diff --git a/src/main/kotlin/com/ex_dock/ex_dock/database/service/DatabaseBackupVerticle.kt b/src/main/kotlin/com/ex_dock/ex_dock/database/service/DatabaseBackupVerticle.kt new file mode 100644 index 00000000..53210ee1 --- /dev/null +++ b/src/main/kotlin/com/ex_dock/ex_dock/database/service/DatabaseBackupVerticle.kt @@ -0,0 +1,254 @@ +package com.ex_dock.ex_dock.database.service + +import com.ex_dock.ex_dock.database.connection.Connection +import io.vertx.core.AbstractVerticle +import io.vertx.core.eventbus.EventBus +import io.vertx.sqlclient.Pool +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoField +import java.time.temporal.ChronoUnit +import java.util.* + +/** + * Verticle for all events connected to database backups + */ +class DatabaseBackupVerticle: AbstractVerticle() { + private lateinit var client: Pool + private lateinit var eventBus: EventBus + private lateinit var frequency: String + private lateinit var time: String + private lateinit var dbUser: String + private lateinit var dbPassword: String + private var savedBackups = 1 + + /** + * Initialize variables from properties and start the eventbus and scheduler + */ + override fun start() { + client = Connection().getConnection(vertx) + eventBus = vertx.eventBus() + + //Read the variables from the properties file + try { + val props: Properties = javaClass.classLoader.getResourceAsStream("secret.properties").use { + Properties().apply { load(it) } + } + + frequency = props.getProperty("BACKUP_FREQUENCY") + time = props.getProperty("BACKUP_TIME") + savedBackups = props.getProperty("SAVED_BACKUPS").toInt() + dbUser = props.getProperty("DATABASE_USERNAME") + dbPassword = props.getProperty("DATABASE_PASSWORD") + + } catch (e: Exception) { + println(e.message) + try { + // Try to load the docker values when run in GitHub actions + val isDocker: Boolean = !System.getenv("GITHUB_RUN_NUMBER").isNullOrEmpty() + if (isDocker) { + frequency = "daily" + time = "00:00" + savedBackups = 1 + } else { + error("Could not load the Properties file!") + } + } catch (e: Exception) { + println(e.message) + error("Could not read the Properties file!") + } + } + + backupWithEventBusRequest() + scheduler() + } + + /** + * Makes a full backup of the current state of the database + */ + private fun backupDatabaseData() { + organizeBackups() + val zdt: ZonedDateTime = ZonedDateTime.now(ZoneId.systemDefault()) + val zdtTruncatedHour: ZonedDateTime = zdt.truncatedTo(ChronoUnit.HOURS) + val f: DateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH", Locale.getDefault()) + val timeString = zdtTruncatedHour.format(f) + .replace(" ", "_") + .replace("/", "_") + + ".sql" + + val backupDirectory = System.getProperty("user.dir") + "\\database\\backup\\backup_$timeString" + val processBuilder = ProcessBuilder( + "pg_dump", + "--host", "localhost", + "--port", "8890", + "--file", backupDirectory, + "--blobs", + "--verbose", + "--username", dbUser, + "--no-password", + "ex-dock" + ) + + try { + val env = processBuilder.environment() + env["PGPASSWORD"] = dbPassword + val p: Process = processBuilder.start() + val bufferedReader = BufferedReader(InputStreamReader(p.errorStream)) + var line = bufferedReader.readLine() + while (line!= null) { + println(line) + line = bufferedReader.readLine() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + /** + * Gets the request for a backup from the eventbus + */ + private fun backupWithEventBusRequest() { + eventBus.consumer("process.service.backup").handler { message -> + backupDatabaseData() + message.reply("Completed backup!") + } + } + + /** + * Creates a scheduler for making backups at specified intervals + */ + private fun scheduler() { + val frequencyInt: Long = getFrequencyTimeNumber(frequency) + val timeInt: Long = getTimeNumber(time) + val currentTime: Long = LocalTime.now(ZoneId.systemDefault())[ChronoField.MILLI_OF_DAY].toLong() + var waitingTime: Long = timeInt - currentTime + + if (waitingTime < 0) { + val tempTimeInt = getTimeNumber("23:59") + waitingTime = (tempTimeInt - currentTime) + timeInt + } + + vertx.setTimer(waitingTime) { + backupDatabaseData() + + // Sets the given interval + vertx.setPeriodic(frequencyInt*1000L) { + backupDatabaseData() + } + } + } + + /** + * Converts the given frequency to a usable number for the scheduler + */ + private fun getFrequencyTimeNumber(frequency: String): Long { + return when (frequency) { + "daily" -> 1*24*60*60 + "weekly" -> 7*24*60*60 + "monthly" -> 30*24*60*60 + "hourly" -> 60*60 + else -> 1 + } + } + + /** + * Gets the time in milliseconds for the wanted backup time + */ + private fun getTimeNumber(time: String): Long { + val parts = time.split(":") + return (parts[0].toInt() * 60 * 60 + parts[1].toInt() * 60) * 1000L + } + + /** + * Removes the oldest backup if the number of backups is greater than the maximum number of backups allowed + */ + private fun organizeBackups() { + val backupDirectory = System.getProperty("user.dir") + "\\database\\backup" + val folder = File(backupDirectory) + val listOfFiles = folder.listFiles() + val fileDateList: MutableList = emptyList().toMutableList() + var oldestDate = "" + + // Skip organizing when the number of backups is lower than the maximum number of backups allowed + if (listOfFiles!!.size >= savedBackups) { + for (f in listOfFiles) { + val fileDateString = f.name + .replace("backup_", "") + .replace(".sql", "") + fileDateList.add(fileDateString) + } + + for (dateString in fileDateList) { + oldestDate = if (oldestDate != "") { + val dateParts = dateString.split("_") + val oldestDateParts = oldestDate.split("_") + + checkIfOlderFile( + dateParts, + oldestDateParts, + dateString, + oldestDate) + } else { + dateString + } + } + + val oldestBackup = File("$backupDirectory\\backup_$oldestDate.sql") + + if (oldestBackup.delete()) { + println("Deleted oldest backup from: $oldestDate") + } else { + println("Could not delete oldest backup from: $oldestDate") + } + } + } + + /** + * Checks the backup name and returns the oldest backup date + */ + private fun checkIfOlderFile(dateParts: List, + oldestDateParts: List, + dateString: String, + oldestDate: String): String { + // Check if the Year is older + if (dateParts[2].toInt() < oldestDateParts[2].toInt()) { + return dateString + + // Check if the Year is the same + } else if (dateParts[2].toInt() == oldestDateParts[2].toInt()) { + + // Check if the Month is older + if (dateParts[1].toInt() < oldestDateParts[1].toInt()) { + return dateString + + // Check if the Month is the same + } else if (dateParts[1].toInt() == oldestDateParts[1].toInt()) { + + // Check if the Day is older + if (dateParts[0].toInt() < oldestDateParts[0].toInt()) { + return dateString + + // Check if the Day is the same + } else if (dateParts[0].toInt() == oldestDateParts[0].toInt()) { + + // Check if the Hour is older + if (dateParts[3].toInt() < oldestDateParts[3].toInt()) { + return dateString + + // Check if the Hour is the same + } else if (dateParts[3].toInt() == oldestDateParts[3].toInt()) { + return dateString + } + } + } + } + + return oldestDate + } +} diff --git a/src/main/resources/default.properties b/src/main/resources/default.properties index 2366c18a..659d9bd0 100644 --- a/src/main/resources/default.properties +++ b/src/main/resources/default.properties @@ -6,3 +6,6 @@ JDBC_URL=127.0.0.1 DATABASE_URL="test_url" DATABASE_PASSWORD="test_password" DATABASE_USERNAME=test_username +BACKUP_FREQUENCY="daily" +BACKUP_TIME="23:00" +SAVED_BACKUPS=1