All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tech.harmonysoft.oss.common.backup.file.impl.FileSystemBackupServiceImpl.kt Maven / Gradle / Ivy

package tech.harmonysoft.oss.common.backup.file.impl

import org.slf4j.LoggerFactory
import tech.harmonysoft.oss.common.backup.file.FileSystemBackupService
import tech.harmonysoft.oss.common.time.clock.ClockProvider
import tech.harmonysoft.oss.common.time.util.DateTimeHelper
import java.io.File
import java.io.FileFilter
import java.time.LocalDate
import java.time.LocalTime
import java.util.Stack
import javax.inject.Named

@Named
class FileSystemBackupServiceImpl(
    private val clockProvider: ClockProvider,
    private val dateTimeHelper: DateTimeHelper
) : FileSystemBackupService {

    private val logger = LoggerFactory.getLogger(FileSystemBackupServiceImpl::class.java)

    override fun prepareNewBackupDir(rootBackupDir: File): File {
        if (rootBackupDir.isFile) {
            throw IllegalArgumentException(
                "failed to prepare new backup dir - expected ${rootBackupDir.canonicalPath} to be a backup directory "
                + "but it points to a file"
            )
        }

        val clock = clockProvider.data

        val dateDir = File(rootBackupDir, dateTimeHelper.formatDate(LocalDate.now(clock)))
        if (dateDir.isFile) {
            throw IllegalStateException(
                "failed to prepare new backup dir - expected ${dateDir.canonicalPath} to be a backup directory "
                + "but it points to a file"
            )
        }

        val timeDir = File(dateDir, dateTimeHelper.formatTime(LocalTime.now(clock)))
        if (timeDir.isFile) {
            throw IllegalStateException(
                "failed to prepare new backup dir - expected ${timeDir.canonicalPath} to be a backup directory "
                + "but it points to a file"
            )
        }

        return if (timeDir.isDirectory) {
            timeDir
        } else {
            val created = timeDir.mkdirs()
            if (created) {
                timeDir
            } else {
                throw IllegalStateException(
                    "failed to prepare a backup dir at path ${timeDir.canonicalPath} - it doesn't exist and we "
                    + "failed to create it"
                )
            }
        }
    }

    override fun getLastAvailableBackupDir(rootBackupDir: File, ttlInDays: Int): File? {
        if (!rootBackupDir.isDirectory) {
            return null
        }

        return findMostRecentDirectory(rootBackupDir) {
            dateTimeHelper.parseDate(it, DateTimeHelper.DATE_PATTERN)
        }?.let { dateDirectory ->
            findMostRecentDirectory(dateDirectory) { timeDirectory ->
                dateTimeHelper.parseTime(timeDirectory, DateTimeHelper.TIME_PATTERN)
            }
        }
    }

    private fun > findMostRecentDirectory(rootDir: File, childDirNameParser: (String) -> T): File? {
        return rootDir.listFiles { file -> file?.isDirectory ?: false }?.mapNotNull {
            try {
                childDirNameParser(it.name) to it
            } catch (e: Exception) {
                logger.warn("Unexpected directory is found under backup dir {}", rootDir.canonicalPath, e)
                null
            }
        }?.maxByOrNull { it.first }?.second
    }

    override fun backup(backupDir: File, toBackup: File, toBackupFilter: (File) -> Boolean) {
        if (!backupDir.isDirectory) {
            val created = backupDir.mkdirs()
            if (!created) {
                logger.warn("Failed to backup files from '{}' to '{}' - can't create the output dir",
                            toBackup.canonicalPath, backupDir.canonicalPath)
                return
            }
        }

        val toProcess = Stack().apply { push(toBackup) }
        while (toProcess.isNotEmpty()) {
            val file = toProcess.pop()
            if (file.isFile) {
                if (toBackupFilter(file)) {
                    val destination = File(backupDir, file.name)
                    if (!destination.parentFile.exists()) {
                        val created = destination.parentFile.mkdirs()
                        if (!created) {
                            logger.warn("Failed to backup files from '{}' can't create the output dir {}",
                                        toBackup.canonicalPath, backupDir.canonicalPath)
                            return
                        }
                    }
                    file.copyTo(destination, overwrite = true)
                    logger.info("Backed up file {} as {}", file.canonicalPath, destination.canonicalPath)
                }
            } else if (file.isDirectory && file != backupDir) {
                file.listFiles()?.forEach {
                    toProcess.push(it)
                }
            }
        }
    }

    override fun cleanOutdated(rootDir: File, ttlDays: Int) {
        if (!rootDir.isDirectory) {
            logger.info("Skip cleaning backup as target dir doesn't exist ({})", rootDir.canonicalPath)
            return
        }

        val firstLiveData = LocalDate.now(clockProvider.data).minusDays(ttlDays.toLong())
        rootDir.listFiles(FileFilter {
            it.isDirectory
        })?.forEach { dir ->
            try {
                dateTimeHelper.parseDate(dir.name)
            } catch (e: Exception) {
                logger.warn(
                    "Unexpected directory named '{}' is found in {} backup dir. It doesn't conform to pattern '{}'. "
                    + "Keeping it as-is, take care of it manually if necessary",
                    dir.name, rootDir.canonicalPath, DateTimeHelper.DATE_PATTERN
                )
                null
            }?.takeIf {
                it.isBefore(firstLiveData)
            }?.let {
                val success = dir.deleteRecursively()
                if (success) {
                    logger.info("Cleaned outdated backup directory '{}'", dir.canonicalPath)
                } else {
                    logger.warn("Failed to clean up an outdated directory '{}'. Please take care of it manually",
                                dir.canonicalPath)
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy