]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
Sharing org.simantics.acorn for everyone to use
[simantics/platform.git] / bundles / org.simantics.acorn / src / org / simantics / acorn / backup / AcornBackupProvider.java
diff --git a/bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java b/bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
new file mode 100644 (file)
index 0000000..5ea0799
--- /dev/null
@@ -0,0 +1,316 @@
+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;
+        }
+
+    }
+
+}