1 package org.simantics.acorn.backup;
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.nio.file.FileVisitResult;
6 import java.nio.file.Files;
7 import java.nio.file.LinkOption;
8 import java.nio.file.Path;
9 import java.nio.file.SimpleFileVisitor;
10 import java.nio.file.StandardCopyOption;
11 import java.nio.file.StandardOpenOption;
12 import java.nio.file.attribute.BasicFileAttributes;
13 import java.util.Arrays;
14 import java.util.concurrent.Future;
15 import java.util.concurrent.Semaphore;
16 import java.util.concurrent.TimeUnit;
17 import java.util.concurrent.TimeoutException;
19 import org.simantics.acorn.AcornSessionManagerImpl;
20 import org.simantics.acorn.GraphClientImpl2;
21 import org.simantics.acorn.exception.IllegalAcornStateException;
22 import org.simantics.backup.BackupException;
23 import org.simantics.backup.IBackupProvider;
24 import org.simantics.db.server.ProCoreException;
25 import org.simantics.utils.FileUtils;
30 * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way
32 public class AcornBackupProvider implements IBackupProvider {
34 private static final String IDENTIFIER = "AcornBackupProvider";
35 private long trId = -1;
36 private final Semaphore lock = new Semaphore(1);
37 private final GraphClientImpl2 client;
39 public AcornBackupProvider() {
40 this.client = AcornSessionManagerImpl.getInstance().getClient();
43 private static Path getAcornMetadataFile(Path dbFolder) {
44 return dbFolder.getParent().resolve(IDENTIFIER);
48 public void lock() throws BackupException {
51 throw new IllegalStateException(this + " backup provider is already locked");
52 trId = client.askWriteTransaction(-1).getTransactionId();
53 } catch (ProCoreException e) {
59 public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
60 boolean releaseLock = true;
64 client.makeSnapshot(true);
66 Path dbDir = client.getDbFolder();
67 int newestFolder = client.clusters.mainState.headDir - 1;
68 int latestFolder = -2;
69 Path AcornMetadataFile = getAcornMetadataFile(dbDir);
70 if (Files.exists(AcornMetadataFile)) {
71 try (BufferedReader br = Files.newBufferedReader(AcornMetadataFile)) {
72 latestFolder = Integer.parseInt( br.readLine() );
76 AcornBackupRunnable r = new AcornBackupRunnable(
77 lock, targetPath, revision, dbDir, latestFolder, newestFolder);
78 new Thread(r, "Acorn backup thread").start();
82 } catch (InterruptedException e) {
84 throw new BackupException("Failed to lock Acorn for backup.", e);
85 } catch (NumberFormatException e) {
86 throw new BackupException("Failed to read Acorn head state file.", e);
87 } catch (IllegalAcornStateException | IOException e) {
88 throw new BackupException("I/O problem during Acorn backup.", e);
96 public void unlock() throws BackupException {
99 throw new BackupException(this + " backup provider is not locked");
100 client.endTransaction(trId);
102 } catch (ProCoreException e) {
103 throw new BackupException(e);
108 public void restore(Path fromPath, int revision) {
110 // 1. Resolve initial backup restore target.
111 // This can be DB directory directly or a temporary directory that
112 // will replace the DB directory.
113 Path dbRoot = client.getDbFolder();
114 Path restorePath = dbRoot;
115 if (!Files.exists(dbRoot, LinkOption.NOFOLLOW_LINKS)) {
116 Files.createDirectories(dbRoot);
118 Path dbRootParent = dbRoot.getParent();
119 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
120 : Files.createTempDirectory(dbRootParent, "restore");
123 // 2. Restore the backup.
124 Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
126 // 3. Override existing DB root with restored temporary copy if necessary.
127 if (dbRoot != restorePath) {
128 FileUtils.deleteAll(dbRoot.toFile());
129 Files.move(restorePath, dbRoot);
131 } catch (IOException e) {
136 private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
138 private final Path toPath;
139 private final int revision;
140 private Path currentSubFolder;
142 public RestoreCopyVisitor(Path toPath, int revision) {
143 this.toPath = toPath;
144 this.revision = revision;
148 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
149 Path dirName = dir.getFileName();
150 if (dirName.toString().equals(IDENTIFIER)) {
151 currentSubFolder = dir;
152 return FileVisitResult.CONTINUE;
153 } else if (dir.getParent().getFileName().toString().equals(IDENTIFIER)) {
154 Path targetPath = toPath.resolve(dirName);
155 if (!Files.exists(targetPath)) {
156 Files.createDirectory(targetPath);
158 return FileVisitResult.CONTINUE;
159 } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) {
160 int dirNameInt = Integer.parseInt(dirName.toString());
161 if (dirNameInt <= revision) {
162 return FileVisitResult.CONTINUE;
164 return FileVisitResult.SKIP_SUBTREE;
167 return FileVisitResult.CONTINUE;
172 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
173 if (file.getFileName().toString().endsWith(".tar.gz"))
174 return FileVisitResult.CONTINUE;
175 System.out.println("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file)));
176 Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
177 return FileVisitResult.CONTINUE;
181 private static class AcornBackupRunnable implements Runnable, Future<BackupException> {
183 private final Semaphore lock;
184 private final Path targetPath;
185 private final int revision;
186 private final Path baseDir;
187 private final int latestFolder;
188 private final int newestFolder;
190 private boolean done = false;
191 private final Semaphore completion = new Semaphore(0);
192 private BackupException exception = null;
194 public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
195 Path baseDir, int latestFolder, int newestFolder) {
197 this.targetPath = targetPath;
198 this.revision = revision;
199 this.baseDir = baseDir;
200 this.latestFolder = latestFolder;
201 this.newestFolder = newestFolder;
208 writeHeadstateFile();
209 } catch (IOException e) {
210 exception = new BackupException("Acorn backup failed", e);
215 completion.release();
219 private void doBackup() throws IOException {
220 Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER);
221 if (!Files.exists(target))
222 Files.createDirectories(target);
223 Files.walkFileTree(baseDir,
224 new BackupCopyVisitor(baseDir, target));
227 private void writeHeadstateFile() throws IOException {
228 Path AcornMetadataFile = getAcornMetadataFile(baseDir);
229 if (!Files.exists(AcornMetadataFile)) {
230 Files.createFile(AcornMetadataFile);
232 Files.write(AcornMetadataFile,
233 Arrays.asList(Integer.toString(newestFolder)),
234 StandardOpenOption.WRITE,
235 StandardOpenOption.TRUNCATE_EXISTING,
236 StandardOpenOption.CREATE);
239 private void rollback() {
243 private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
245 private Path fromPath;
248 public BackupCopyVisitor(Path fromPath, Path toPath) {
249 this.fromPath = fromPath;
250 this.toPath = toPath;
254 public FileVisitResult preVisitDirectory(Path dir,
255 BasicFileAttributes attrs) throws IOException {
256 Path dirName = dir.getFileName();
257 if (dirName.equals(fromPath)) {
258 Path targetPath = toPath.resolve(fromPath.relativize(dir));
259 if (!Files.exists(targetPath)) {
260 Files.createDirectory(targetPath);
262 return FileVisitResult.CONTINUE;
264 int dirNameInt = Integer.parseInt(dirName.toString());
265 if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
266 Path targetPath = toPath.resolve(fromPath
268 if (!Files.exists(targetPath)) {
269 Files.createDirectory(targetPath);
271 return FileVisitResult.CONTINUE;
273 return FileVisitResult.SKIP_SUBTREE;
278 public FileVisitResult visitFile(Path file,
279 BasicFileAttributes attrs) throws IOException {
280 System.out.println("Backup " + file + " to "
281 + toPath.resolve(fromPath.relativize(file)));
282 Files.copy(file, toPath.resolve(fromPath.relativize(file)),
283 StandardCopyOption.REPLACE_EXISTING);
284 return FileVisitResult.CONTINUE;
289 public boolean cancel(boolean mayInterruptIfRunning) {
294 public boolean isCancelled() {
299 public boolean isDone() {
304 public BackupException get() throws InterruptedException {
305 completion.acquire();
306 completion.release();
311 public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
312 if (completion.tryAcquire(timeout, unit))
313 completion.release();
315 throw new TimeoutException("Acorn backup completion waiting timed out.");