--- /dev/null
+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.GraphClientImpl2;
+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 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 = GraphClientImpl2.getInstance().askWriteTransaction(-1)
+ .getTransactionId();
+ } catch (ProCoreException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
+ boolean releaseLock = true;
+ try {
+ lock.acquire();
+
+ GraphClientImpl2 client = GraphClientImpl2.getInstance();
+ 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 (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");
+ GraphClientImpl2.getInstance().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 = GraphClientImpl2.getInstance().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<Path> {
+
+ 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<BackupException> {
+
+ 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<Path> {
+
+ 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;
+ }
+
+ }
+
+}