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 public 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;
63 Future<BackupException> r = client.getBackupRunnable(lock, targetPath, revision);
66 } catch (InterruptedException e) {
68 throw new BackupException("Failed to lock Acorn for backup.", e);
69 } catch (NumberFormatException e) {
70 throw new BackupException("Failed to read Acorn head state file.", e);
71 } catch (IllegalAcornStateException | IOException e) {
72 throw new BackupException("I/O problem during Acorn backup.", e);
80 public void unlock() throws BackupException {
83 throw new BackupException(this + " backup provider is not locked");
84 client.endTransaction(trId);
86 } catch (ProCoreException e) {
87 throw new BackupException(e);
92 public void restore(Path fromPath, int revision) {
94 // 1. Resolve initial backup restore target.
95 // This can be DB directory directly or a temporary directory that
96 // will replace the DB directory.
97 Path dbRoot = client.getDbFolder();
98 Path restorePath = dbRoot;
99 if (!Files.exists(dbRoot, LinkOption.NOFOLLOW_LINKS)) {
100 Files.createDirectories(dbRoot);
102 Path dbRootParent = dbRoot.getParent();
103 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
104 : Files.createTempDirectory(dbRootParent, "restore");
107 // 2. Restore the backup.
108 Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
110 // 3. Override existing DB root with restored temporary copy if necessary.
111 if (dbRoot != restorePath) {
112 FileUtils.deleteAll(dbRoot.toFile());
113 Files.move(restorePath, dbRoot);
115 } catch (IOException e) {
120 private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
122 private final Path toPath;
123 private final int revision;
124 private Path currentSubFolder;
126 public RestoreCopyVisitor(Path toPath, int revision) {
127 this.toPath = toPath;
128 this.revision = revision;
132 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
133 Path dirName = dir.getFileName();
134 if (dirName.toString().equals(IDENTIFIER)) {
135 currentSubFolder = dir;
136 return FileVisitResult.CONTINUE;
137 } else if (dir.getParent().getFileName().toString().equals(IDENTIFIER)) {
138 Path targetPath = toPath.resolve(dirName);
139 if (!Files.exists(targetPath)) {
140 Files.createDirectory(targetPath);
142 return FileVisitResult.CONTINUE;
143 } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) {
144 int dirNameInt = Integer.parseInt(dirName.toString());
145 if (dirNameInt <= revision) {
146 return FileVisitResult.CONTINUE;
148 return FileVisitResult.SKIP_SUBTREE;
151 return FileVisitResult.CONTINUE;
156 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
157 if (file.getFileName().toString().endsWith(".tar.gz"))
158 return FileVisitResult.CONTINUE;
159 System.out.println("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file)));
160 Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
161 return FileVisitResult.CONTINUE;
165 public static class AcornBackupRunnable implements Runnable, Future<BackupException> {
167 private final Semaphore lock;
168 private final Path targetPath;
169 private final int revision;
170 private final Path baseDir;
171 private final int latestFolder;
172 private final int newestFolder;
174 private boolean done = false;
175 private final Semaphore completion = new Semaphore(0);
176 private BackupException exception = null;
178 public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
179 Path baseDir, int latestFolder, int newestFolder) {
181 this.targetPath = targetPath;
182 this.revision = revision;
183 this.baseDir = baseDir;
184 this.latestFolder = latestFolder;
185 this.newestFolder = newestFolder;
192 writeHeadstateFile();
193 } catch (IOException e) {
194 exception = new BackupException("Acorn backup failed", e);
199 completion.release();
203 private void doBackup() throws IOException {
204 Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER);
205 if (!Files.exists(target))
206 Files.createDirectories(target);
207 Files.walkFileTree(baseDir,
208 new BackupCopyVisitor(baseDir, target));
211 private void writeHeadstateFile() throws IOException {
212 Path AcornMetadataFile = getAcornMetadataFile(baseDir);
213 if (!Files.exists(AcornMetadataFile)) {
214 Files.createFile(AcornMetadataFile);
216 Files.write(AcornMetadataFile,
217 Arrays.asList(Integer.toString(newestFolder)),
218 StandardOpenOption.WRITE,
219 StandardOpenOption.TRUNCATE_EXISTING,
220 StandardOpenOption.CREATE);
223 private void rollback() {
227 private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
229 private Path fromPath;
232 public BackupCopyVisitor(Path fromPath, Path toPath) {
233 this.fromPath = fromPath;
234 this.toPath = toPath;
238 public FileVisitResult preVisitDirectory(Path dir,
239 BasicFileAttributes attrs) throws IOException {
240 Path dirName = dir.getFileName();
241 if (dirName.equals(fromPath)) {
242 Path targetPath = toPath.resolve(fromPath.relativize(dir));
243 if (!Files.exists(targetPath)) {
244 Files.createDirectory(targetPath);
246 return FileVisitResult.CONTINUE;
248 int dirNameInt = Integer.parseInt(dirName.toString());
249 if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
250 Path targetPath = toPath.resolve(fromPath
252 if (!Files.exists(targetPath)) {
253 Files.createDirectory(targetPath);
255 return FileVisitResult.CONTINUE;
257 return FileVisitResult.SKIP_SUBTREE;
262 public FileVisitResult visitFile(Path file,
263 BasicFileAttributes attrs) throws IOException {
264 System.out.println("Backup " + file + " to "
265 + toPath.resolve(fromPath.relativize(file)));
266 Files.copy(file, toPath.resolve(fromPath.relativize(file)),
267 StandardCopyOption.REPLACE_EXISTING);
268 return FileVisitResult.CONTINUE;
273 public boolean cancel(boolean mayInterruptIfRunning) {
278 public boolean isCancelled() {
283 public boolean isDone() {
288 public BackupException get() throws InterruptedException {
289 completion.acquire();
290 completion.release();
295 public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
296 if (completion.tryAcquire(timeout, unit))
297 completion.release();
299 throw new TimeoutException("Acorn backup completion waiting timed out.");