2023-10-14 10:07:40 +00:00
|
|
|
package nu.marginalia.svc;
|
2023-08-25 12:57:43 +00:00
|
|
|
|
|
|
|
import com.github.luben.zstd.ZstdInputStream;
|
|
|
|
import com.github.luben.zstd.ZstdOutputStream;
|
2023-10-14 10:07:40 +00:00
|
|
|
import nu.marginalia.IndexLocations;
|
2024-02-23 12:26:11 +00:00
|
|
|
import nu.marginalia.linkdb.LinkdbFileNames;
|
2024-01-01 14:20:57 +00:00
|
|
|
import nu.marginalia.service.control.ServiceHeartbeat;
|
2023-10-14 10:07:40 +00:00
|
|
|
import nu.marginalia.storage.FileStorageService;
|
|
|
|
import nu.marginalia.storage.model.FileStorageId;
|
|
|
|
import nu.marginalia.storage.model.FileStorageType;
|
2024-02-15 09:51:49 +00:00
|
|
|
import nu.marginalia.index.journal.IndexJournalFileNames;
|
2023-08-25 12:57:43 +00:00
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
|
|
2023-08-28 10:58:18 +00:00
|
|
|
import com.google.inject.Inject;
|
2023-08-25 12:57:43 +00:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.file.Files;
|
2023-10-14 10:07:40 +00:00
|
|
|
import java.nio.file.Path;
|
2023-08-25 12:57:43 +00:00
|
|
|
import java.sql.SQLException;
|
|
|
|
import java.time.LocalDateTime;
|
2023-09-22 16:34:34 +00:00
|
|
|
import java.util.List;
|
2023-08-25 12:57:43 +00:00
|
|
|
|
|
|
|
public class BackupService {
|
|
|
|
|
|
|
|
private final FileStorageService storageService;
|
2024-01-01 14:20:57 +00:00
|
|
|
private final ServiceHeartbeat serviceHeartbeat;
|
|
|
|
|
|
|
|
public enum BackupHeartbeatSteps {
|
|
|
|
LINKS,
|
2024-01-08 14:53:13 +00:00
|
|
|
DOCS,
|
2024-01-01 14:20:57 +00:00
|
|
|
JOURNAL,
|
|
|
|
DONE
|
|
|
|
}
|
2023-08-25 12:57:43 +00:00
|
|
|
|
|
|
|
@Inject
|
2024-01-01 14:20:57 +00:00
|
|
|
public BackupService(FileStorageService storageService,
|
|
|
|
ServiceHeartbeat serviceHeartbeat) {
|
2023-08-25 12:57:43 +00:00
|
|
|
this.storageService = storageService;
|
2024-01-01 14:20:57 +00:00
|
|
|
this.serviceHeartbeat = serviceHeartbeat;
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Create a new backup of the contents in the _STAGING storage areas.
|
|
|
|
* This backup can later be dehydrated and quickly loaded into _LIVE.
|
|
|
|
* */
|
2023-09-22 16:34:34 +00:00
|
|
|
public void createBackupFromStaging(List<FileStorageId> associatedIds) throws SQLException, IOException {
|
2023-08-25 12:57:43 +00:00
|
|
|
String desc = "Pre-load backup snapshot " + LocalDateTime.now();
|
|
|
|
|
2024-01-25 12:36:30 +00:00
|
|
|
var backupStorage = storageService.allocateStorage(
|
2023-10-14 10:07:40 +00:00
|
|
|
FileStorageType.BACKUP, "snapshot", desc);
|
2023-08-25 12:57:43 +00:00
|
|
|
|
2023-09-22 16:34:34 +00:00
|
|
|
for (var associatedId : associatedIds) {
|
|
|
|
storageService.relateFileStorages(associatedId, backupStorage.id());
|
|
|
|
}
|
2023-08-25 12:57:43 +00:00
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
var indexStagingStorage = IndexLocations.getIndexConstructionArea(storageService);
|
|
|
|
var linkdbStagingStorage = IndexLocations.getLinkdbWritePath(storageService);
|
|
|
|
|
2024-01-01 14:20:57 +00:00
|
|
|
|
|
|
|
try (var heartbeat = serviceHeartbeat.createServiceTaskHeartbeat(BackupHeartbeatSteps.class, "Backup")) {
|
2024-01-08 14:53:13 +00:00
|
|
|
heartbeat.progress(BackupHeartbeatSteps.DOCS);
|
2024-02-23 12:26:11 +00:00
|
|
|
backupFileCompressed(LinkdbFileNames.DOCDB_FILE_NAME, linkdbStagingStorage, backupStorage.asPath());
|
2024-01-08 14:53:13 +00:00
|
|
|
|
2024-01-01 14:20:57 +00:00
|
|
|
heartbeat.progress(BackupHeartbeatSteps.LINKS);
|
2024-02-23 12:26:11 +00:00
|
|
|
backupFileCompressed(LinkdbFileNames.DOMAIN_LINKS_FILE_NAME, linkdbStagingStorage, backupStorage.asPath());
|
2024-01-01 14:20:57 +00:00
|
|
|
|
|
|
|
heartbeat.progress(BackupHeartbeatSteps.JOURNAL);
|
|
|
|
// This file format is already compressed
|
|
|
|
backupJournal(indexStagingStorage, backupStorage.asPath());
|
|
|
|
|
|
|
|
heartbeat.progress(BackupHeartbeatSteps.DONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Read back a backup into _STAGING */
|
|
|
|
public void restoreBackup(FileStorageId backupId) throws SQLException, IOException {
|
2023-10-14 10:07:40 +00:00
|
|
|
var backupStorage = storageService.getStorage(backupId).asPath();
|
2023-08-25 12:57:43 +00:00
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
var indexStagingStorage = IndexLocations.getIndexConstructionArea(storageService);
|
|
|
|
var linkdbStagingStorage = IndexLocations.getLinkdbWritePath(storageService);
|
2023-08-25 12:57:43 +00:00
|
|
|
|
2024-01-01 14:20:57 +00:00
|
|
|
try (var heartbeat = serviceHeartbeat.createServiceTaskHeartbeat(BackupHeartbeatSteps.class, "Restore Backup")) {
|
2024-01-08 14:53:13 +00:00
|
|
|
heartbeat.progress(BackupHeartbeatSteps.DOCS);
|
2024-02-23 12:26:11 +00:00
|
|
|
restoreBackupCompressed(LinkdbFileNames.DOCDB_FILE_NAME, linkdbStagingStorage, backupStorage);
|
2024-01-08 14:53:13 +00:00
|
|
|
|
2024-01-01 14:20:57 +00:00
|
|
|
heartbeat.progress(BackupHeartbeatSteps.LINKS);
|
2024-02-23 12:26:11 +00:00
|
|
|
restoreBackupCompressed(LinkdbFileNames.DOMAIN_LINKS_FILE_NAME, linkdbStagingStorage, backupStorage);
|
2024-01-01 14:20:57 +00:00
|
|
|
|
|
|
|
heartbeat.progress(BackupHeartbeatSteps.JOURNAL);
|
|
|
|
restoreJournal(indexStagingStorage, backupStorage);
|
|
|
|
|
|
|
|
heartbeat.progress(BackupHeartbeatSteps.DONE);
|
|
|
|
}
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
private void backupJournal(Path inputStorage, Path backupStorage) throws IOException
|
2023-08-25 12:57:43 +00:00
|
|
|
{
|
2023-10-14 10:07:40 +00:00
|
|
|
for (var source : IndexJournalFileNames.findJournalFiles(inputStorage)) {
|
|
|
|
var dest = backupStorage.resolve(source.toFile().getName());
|
2023-08-28 10:58:18 +00:00
|
|
|
|
|
|
|
try (var is = Files.newInputStream(source);
|
|
|
|
var os = Files.newOutputStream(dest)
|
|
|
|
) {
|
|
|
|
IOUtils.copyLarge(is, os);
|
|
|
|
}
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
2023-08-28 10:58:18 +00:00
|
|
|
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
private void restoreJournal(Path destStorage, Path backupStorage) throws IOException {
|
2023-08-29 09:58:01 +00:00
|
|
|
|
|
|
|
// Remove any old journal files first to avoid them getting loaded
|
2023-10-14 10:07:40 +00:00
|
|
|
for (var garbage : IndexJournalFileNames.findJournalFiles(destStorage)) {
|
2023-08-29 09:58:01 +00:00
|
|
|
Files.delete(garbage);
|
|
|
|
}
|
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
for (var source : IndexJournalFileNames.findJournalFiles(backupStorage)) {
|
|
|
|
var dest = destStorage.resolve(source.toFile().getName());
|
2023-08-28 10:58:18 +00:00
|
|
|
|
|
|
|
try (var is = Files.newInputStream(source);
|
|
|
|
var os = Files.newOutputStream(dest)
|
|
|
|
) {
|
|
|
|
IOUtils.copyLarge(is, os);
|
|
|
|
}
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
2023-08-28 10:58:18 +00:00
|
|
|
|
2023-08-25 12:57:43 +00:00
|
|
|
}
|
|
|
|
|
2023-10-14 10:07:40 +00:00
|
|
|
private void backupFileCompressed(String fileName, Path inputStorage, Path backupStorage) throws IOException
|
2023-08-25 12:57:43 +00:00
|
|
|
{
|
2023-10-14 10:07:40 +00:00
|
|
|
try (var is = Files.newInputStream(inputStorage.resolve(fileName));
|
|
|
|
var os = new ZstdOutputStream(Files.newOutputStream(backupStorage.resolve(fileName)))
|
2023-08-25 12:57:43 +00:00
|
|
|
) {
|
|
|
|
IOUtils.copyLarge(is, os);
|
|
|
|
}
|
|
|
|
}
|
2023-10-14 10:07:40 +00:00
|
|
|
private void restoreBackupCompressed(String fileName, Path destStorage, Path backupStorage) throws IOException
|
2023-08-25 12:57:43 +00:00
|
|
|
{
|
2023-10-14 10:07:40 +00:00
|
|
|
try (var is = new ZstdInputStream(Files.newInputStream(backupStorage.resolve(fileName)));
|
|
|
|
var os = Files.newOutputStream(destStorage.resolve(fileName))
|
2023-08-25 12:57:43 +00:00
|
|
|
) {
|
|
|
|
IOUtils.copyLarge(is, os);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|