package org.simantics.acorn.backup; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.simantics.acorn.AcornSessionManagerImpl; import org.simantics.acorn.GraphClientImpl2; import org.simantics.acorn.exception.IllegalAcornStateException; import org.simantics.backup.BackupException; import org.simantics.backup.IBackupProvider; import org.simantics.db.server.ProCoreException; import org.simantics.utils.FileUtils; /** * @author Jani * * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way */ public class AcornBackupProvider implements IBackupProvider { private static final String IDENTIFIER = "AcornBackupProvider"; private long trId = -1; private final Semaphore lock = new Semaphore(1); private final GraphClientImpl2 client; public AcornBackupProvider() { this.client = AcornSessionManagerImpl.getInstance().getClient(); } private static Path getAcornMetadataFile(Path dbFolder) { return dbFolder.getParent().resolve(IDENTIFIER); } @Override public void lock() throws BackupException { try { if (trId != -1) throw new IllegalStateException(this + " backup provider is already locked"); trId = client.askWriteTransaction(-1).getTransactionId(); } catch (ProCoreException e) { e.printStackTrace(); } } @Override public Future backup(Path targetPath, int revision) throws BackupException { boolean releaseLock = true; try { lock.acquire(); client.makeSnapshot(true); Path dbDir = client.getDbFolder(); int newestFolder = client.clusters.mainState.headDir - 1; int latestFolder = -2; Path AcornMetadataFile = getAcornMetadataFile(dbDir); if (Files.exists(AcornMetadataFile)) { try (BufferedReader br = Files.newBufferedReader(AcornMetadataFile)) { latestFolder = Integer.parseInt( br.readLine() ); } } AcornBackupRunnable r = new AcornBackupRunnable( lock, targetPath, revision, dbDir, latestFolder, newestFolder); new Thread(r, "Acorn backup thread").start(); releaseLock = false; return r; } catch (InterruptedException e) { releaseLock = false; throw new BackupException("Failed to lock Acorn for backup.", e); } catch (NumberFormatException e) { throw new BackupException("Failed to read Acorn head state file.", e); } catch (IllegalAcornStateException | IOException e) { throw new BackupException("I/O problem during Acorn backup.", e); } finally { if (releaseLock) lock.release(); } } @Override public void unlock() throws BackupException { try { if (trId == -1) throw new BackupException(this + " backup provider is not locked"); client.endTransaction(trId); trId = -1; } catch (ProCoreException e) { throw new BackupException(e); } } @Override public void restore(Path fromPath, int revision) { try { // 1. Resolve initial backup restore target. // This can be DB directory directly or a temporary directory that // will replace the DB directory. Path dbRoot = client.getDbFolder(); Path restorePath = dbRoot; if (!Files.exists(dbRoot, LinkOption.NOFOLLOW_LINKS)) { Files.createDirectories(dbRoot); } else { Path dbRootParent = dbRoot.getParent(); restorePath = dbRootParent == null ? Files.createTempDirectory("restore") : Files.createTempDirectory(dbRootParent, "restore"); } // 2. Restore the backup. Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision)); // 3. Override existing DB root with restored temporary copy if necessary. if (dbRoot != restorePath) { FileUtils.deleteAll(dbRoot.toFile()); Files.move(restorePath, dbRoot); } } catch (IOException e) { e.printStackTrace(); } } private class RestoreCopyVisitor extends SimpleFileVisitor { private final Path toPath; private final int revision; private Path currentSubFolder; public RestoreCopyVisitor(Path toPath, int revision) { this.toPath = toPath; this.revision = revision; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path dirName = dir.getFileName(); if (dirName.toString().equals(IDENTIFIER)) { currentSubFolder = dir; return FileVisitResult.CONTINUE; } else if (dir.getParent().getFileName().toString().equals(IDENTIFIER)) { Path targetPath = toPath.resolve(dirName); if (!Files.exists(targetPath)) { Files.createDirectory(targetPath); } return FileVisitResult.CONTINUE; } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) { int dirNameInt = Integer.parseInt(dirName.toString()); if (dirNameInt <= revision) { return FileVisitResult.CONTINUE; } else { return FileVisitResult.SKIP_SUBTREE; } } else { return FileVisitResult.CONTINUE; } } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.getFileName().toString().endsWith(".tar.gz")) return FileVisitResult.CONTINUE; System.out.println("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file))); Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } } private static class AcornBackupRunnable implements Runnable, Future { private final Semaphore lock; private final Path targetPath; private final int revision; private final Path baseDir; private final int latestFolder; private final int newestFolder; private boolean done = false; private final Semaphore completion = new Semaphore(0); private BackupException exception = null; public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision, Path baseDir, int latestFolder, int newestFolder) { this.lock = lock; this.targetPath = targetPath; this.revision = revision; this.baseDir = baseDir; this.latestFolder = latestFolder; this.newestFolder = newestFolder; } @Override public void run() { try { doBackup(); writeHeadstateFile(); } catch (IOException e) { exception = new BackupException("Acorn backup failed", e); rollback(); } finally { done = true; lock.release(); completion.release(); } } private void doBackup() throws IOException { Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER); if (!Files.exists(target)) Files.createDirectories(target); Files.walkFileTree(baseDir, new BackupCopyVisitor(baseDir, target)); } private void writeHeadstateFile() throws IOException { Path AcornMetadataFile = getAcornMetadataFile(baseDir); if (!Files.exists(AcornMetadataFile)) { Files.createFile(AcornMetadataFile); } Files.write(AcornMetadataFile, Arrays.asList(Integer.toString(newestFolder)), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } private void rollback() { // TODO } private class BackupCopyVisitor extends SimpleFileVisitor { private Path fromPath; private Path toPath; public BackupCopyVisitor(Path fromPath, Path toPath) { this.fromPath = fromPath; this.toPath = toPath; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path dirName = dir.getFileName(); if (dirName.equals(fromPath)) { Path targetPath = toPath.resolve(fromPath.relativize(dir)); if (!Files.exists(targetPath)) { Files.createDirectory(targetPath); } return FileVisitResult.CONTINUE; } else { int dirNameInt = Integer.parseInt(dirName.toString()); if (latestFolder < dirNameInt && dirNameInt <= newestFolder) { Path targetPath = toPath.resolve(fromPath .relativize(dir)); if (!Files.exists(targetPath)) { Files.createDirectory(targetPath); } return FileVisitResult.CONTINUE; } return FileVisitResult.SKIP_SUBTREE; } } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("Backup " + file + " to " + toPath.resolve(fromPath.relativize(file))); Files.copy(file, toPath.resolve(fromPath.relativize(file)), StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } } @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return done; } @Override public BackupException get() throws InterruptedException { completion.acquire(); completion.release(); return exception; } @Override public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { if (completion.tryAcquire(timeout, unit)) completion.release(); else throw new TimeoutException("Acorn backup completion waiting timed out."); return exception; } } }