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.GraphClientImpl2;
20 import org.simantics.backup.BackupException;
21 import org.simantics.backup.IBackupProvider;
22 import org.simantics.db.server.ProCoreException;
23 import org.simantics.utils.FileUtils;
28 * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way
30 public class AcornBackupProvider implements IBackupProvider {
32 private static final String IDENTIFIER = "AcornBackupProvider";
33 private long trId = -1;
34 private final Semaphore lock = new Semaphore(1);
36 private static Path getAcornMetadataFile(Path dbFolder) {
37 return dbFolder.getParent().resolve(IDENTIFIER);
41 public void lock() throws BackupException {
44 throw new IllegalStateException(this + " backup provider is already locked");
45 trId = GraphClientImpl2.getInstance().askWriteTransaction(-1)
47 } catch (ProCoreException e) {
53 public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
54 boolean releaseLock = true;
58 GraphClientImpl2 client = GraphClientImpl2.getInstance();
59 client.makeSnapshot(true);
61 Path dbDir = client.getDbFolder();
62 int newestFolder = client.clusters.mainState.headDir - 1;
63 int latestFolder = -2;
64 Path AcornMetadataFile = getAcornMetadataFile(dbDir);
65 if (Files.exists(AcornMetadataFile)) {
66 try (BufferedReader br = Files.newBufferedReader(AcornMetadataFile)) {
67 latestFolder = Integer.parseInt( br.readLine() );
71 AcornBackupRunnable r = new AcornBackupRunnable(
72 lock, targetPath, revision, dbDir, latestFolder, newestFolder);
73 new Thread(r, "Acorn backup thread").start();
77 } catch (InterruptedException e) {
79 throw new BackupException("Failed to lock Acorn for backup.", e);
80 } catch (NumberFormatException e) {
81 throw new BackupException("Failed to read Acorn head state file.", e);
82 } catch (IOException e) {
83 throw new BackupException("I/O problem during Acorn backup.", e);
91 public void unlock() throws BackupException {
94 throw new BackupException(this + " backup provider is not locked");
95 GraphClientImpl2.getInstance().endTransaction(trId);
97 } catch (ProCoreException e) {
98 throw new BackupException(e);
103 public void restore(Path fromPath, int revision) {
105 // 1. Resolve initial backup restore target.
106 // This can be DB directory directly or a temporary directory that
107 // will replace the DB directory.
108 Path dbRoot = GraphClientImpl2.getInstance().getDbFolder();
109 Path restorePath = dbRoot;
110 if (!Files.exists(dbRoot, LinkOption.NOFOLLOW_LINKS)) {
111 Files.createDirectories(dbRoot);
113 Path dbRootParent = dbRoot.getParent();
114 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
115 : Files.createTempDirectory(dbRootParent, "restore");
118 // 2. Restore the backup.
119 Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
121 // 3. Override existing DB root with restored temporary copy if necessary.
122 if (dbRoot != restorePath) {
123 FileUtils.deleteAll(dbRoot.toFile());
124 Files.move(restorePath, dbRoot);
126 } catch (IOException e) {
131 private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
133 private final Path toPath;
134 private final int revision;
135 private Path currentSubFolder;
137 public RestoreCopyVisitor(Path toPath, int revision) {
138 this.toPath = toPath;
139 this.revision = revision;
143 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
144 Path dirName = dir.getFileName();
145 if (dirName.toString().equals(IDENTIFIER)) {
146 currentSubFolder = dir;
147 return FileVisitResult.CONTINUE;
148 } else if (dir.getParent().getFileName().toString().equals(IDENTIFIER)) {
149 Path targetPath = toPath.resolve(dirName);
150 if (!Files.exists(targetPath)) {
151 Files.createDirectory(targetPath);
153 return FileVisitResult.CONTINUE;
154 } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) {
155 int dirNameInt = Integer.parseInt(dirName.toString());
156 if (dirNameInt <= revision) {
157 return FileVisitResult.CONTINUE;
159 return FileVisitResult.SKIP_SUBTREE;
162 return FileVisitResult.CONTINUE;
167 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
168 if (file.getFileName().toString().endsWith(".tar.gz"))
169 return FileVisitResult.CONTINUE;
170 System.out.println("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file)));
171 Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
172 return FileVisitResult.CONTINUE;
176 private static class AcornBackupRunnable implements Runnable, Future<BackupException> {
178 private final Semaphore lock;
179 private final Path targetPath;
180 private final int revision;
181 private final Path baseDir;
182 private final int latestFolder;
183 private final int newestFolder;
185 private boolean done = false;
186 private final Semaphore completion = new Semaphore(0);
187 private BackupException exception = null;
189 public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
190 Path baseDir, int latestFolder, int newestFolder) {
192 this.targetPath = targetPath;
193 this.revision = revision;
194 this.baseDir = baseDir;
195 this.latestFolder = latestFolder;
196 this.newestFolder = newestFolder;
203 writeHeadstateFile();
204 } catch (IOException e) {
205 exception = new BackupException("Acorn backup failed", e);
210 completion.release();
214 private void doBackup() throws IOException {
215 Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER);
216 if (!Files.exists(target))
217 Files.createDirectories(target);
218 Files.walkFileTree(baseDir,
219 new BackupCopyVisitor(baseDir, target));
222 private void writeHeadstateFile() throws IOException {
223 Path AcornMetadataFile = getAcornMetadataFile(baseDir);
224 if (!Files.exists(AcornMetadataFile)) {
225 Files.createFile(AcornMetadataFile);
227 Files.write(AcornMetadataFile,
228 Arrays.asList(Integer.toString(newestFolder)),
229 StandardOpenOption.WRITE,
230 StandardOpenOption.TRUNCATE_EXISTING,
231 StandardOpenOption.CREATE);
234 private void rollback() {
238 private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
240 private Path fromPath;
243 public BackupCopyVisitor(Path fromPath, Path toPath) {
244 this.fromPath = fromPath;
245 this.toPath = toPath;
249 public FileVisitResult preVisitDirectory(Path dir,
250 BasicFileAttributes attrs) throws IOException {
251 Path dirName = dir.getFileName();
252 if (dirName.equals(fromPath)) {
253 Path targetPath = toPath.resolve(fromPath.relativize(dir));
254 if (!Files.exists(targetPath)) {
255 Files.createDirectory(targetPath);
257 return FileVisitResult.CONTINUE;
259 int dirNameInt = Integer.parseInt(dirName.toString());
260 if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
261 Path targetPath = toPath.resolve(fromPath
263 if (!Files.exists(targetPath)) {
264 Files.createDirectory(targetPath);
266 return FileVisitResult.CONTINUE;
268 return FileVisitResult.SKIP_SUBTREE;
273 public FileVisitResult visitFile(Path file,
274 BasicFileAttributes attrs) throws IOException {
275 System.out.println("Backup " + file + " to "
276 + toPath.resolve(fromPath.relativize(file)));
277 Files.copy(file, toPath.resolve(fromPath.relativize(file)),
278 StandardCopyOption.REPLACE_EXISTING);
279 return FileVisitResult.CONTINUE;
284 public boolean cancel(boolean mayInterruptIfRunning) {
289 public boolean isCancelled() {
294 public boolean isDone() {
299 public BackupException get() throws InterruptedException {
300 completion.acquire();
301 completion.release();
306 public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
307 if (completion.tryAcquire(timeout, unit))
308 completion.release();
310 throw new TimeoutException("Acorn backup completion waiting timed out.");