1 package org.simantics.acorn.backup;
3 import java.io.IOException;
4 import java.nio.file.FileVisitResult;
5 import java.nio.file.Files;
6 import java.nio.file.LinkOption;
7 import java.nio.file.Path;
8 import java.nio.file.SimpleFileVisitor;
9 import java.nio.file.StandardCopyOption;
10 import java.nio.file.StandardOpenOption;
11 import java.nio.file.attribute.BasicFileAttributes;
12 import java.util.Arrays;
13 import java.util.concurrent.Future;
14 import java.util.concurrent.Semaphore;
15 import java.util.concurrent.TimeUnit;
16 import java.util.concurrent.TimeoutException;
18 import org.simantics.acorn.AcornSessionManagerImpl;
19 import org.simantics.acorn.GraphClientImpl2;
20 import org.simantics.acorn.exception.IllegalAcornStateException;
21 import org.simantics.backup.BackupException;
22 import org.simantics.backup.IBackupProvider;
23 import org.simantics.db.server.ProCoreException;
24 import org.simantics.utils.FileUtils;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
31 * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way
33 public class AcornBackupProvider implements IBackupProvider {
35 private static final Logger LOGGER = LoggerFactory.getLogger(AcornBackupProvider.class);
37 private static final String IDENTIFIER = "AcornBackupProvider";
38 private long trId = -1;
39 private final Semaphore lock = new Semaphore(1);
40 private final GraphClientImpl2 client;
42 public AcornBackupProvider() {
43 this.client = AcornSessionManagerImpl.getInstance().getClient();
46 public static Path getAcornMetadataFile(Path dbFolder) {
47 return dbFolder.getParent().resolve(IDENTIFIER);
51 public void lock() throws BackupException {
54 throw new IllegalStateException(this + " backup provider is already locked");
55 trId = client.askWriteTransaction(-1).getTransactionId();
56 } catch (ProCoreException e) {
57 LOGGER.error("Failed to lock backup provider", e);
62 public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
63 boolean releaseLock = true;
66 Future<BackupException> r = client.getBackupRunnable(lock, targetPath, revision);
69 } catch (InterruptedException e) {
71 throw new BackupException("Failed to lock Acorn for backup.", e);
72 } catch (NumberFormatException e) {
73 throw new BackupException("Failed to read Acorn head state file.", e);
74 } catch (IllegalAcornStateException | IOException e) {
75 throw new BackupException("I/O problem during Acorn backup.", e);
83 public void unlock() throws BackupException {
86 throw new BackupException(this + " backup provider is not locked");
87 client.endTransaction(trId);
89 } catch (ProCoreException e) {
90 throw new BackupException(e);
95 public void restore(Path fromPath, int revision) {
97 // 1. Resolve initial backup restore target.
98 // This can be DB directory directly or a temporary directory that
99 // will replace the DB directory.
100 Path dbRoot = client.getDbFolder();
101 Path restorePath = dbRoot;
102 if (!Files.exists(dbRoot, LinkOption.NOFOLLOW_LINKS)) {
103 Files.createDirectories(dbRoot);
105 Path dbRootParent = dbRoot.getParent();
106 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
107 : Files.createTempDirectory(dbRootParent, "restore");
110 // 2. Restore the backup.
111 Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
113 // 3. Override existing DB root with restored temporary copy if necessary.
114 if (dbRoot != restorePath) {
115 FileUtils.deleteAll(dbRoot.toFile());
116 Files.move(restorePath, dbRoot);
118 } catch (IOException e) {
119 LOGGER.error("Failed to restore database revision {} from backup {}", revision, fromPath.toString(), e);
123 private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
125 private final Path toPath;
126 private final int revision;
127 private Path currentSubFolder;
129 public RestoreCopyVisitor(Path toPath, int revision) {
130 this.toPath = toPath;
131 this.revision = revision;
135 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
136 Path dirName = dir.getFileName();
137 if (dirName.toString().equals(IDENTIFIER)) {
138 currentSubFolder = dir;
139 return FileVisitResult.CONTINUE;
140 } else if (dir.getParent().getFileName().toString().equals(IDENTIFIER)) {
141 Path targetPath = toPath.resolve(dirName);
142 Files.createDirectories(targetPath);
143 return FileVisitResult.CONTINUE;
144 } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) {
145 int dirNameInt = Integer.parseInt(dirName.toString());
146 if (dirNameInt <= revision) {
147 return FileVisitResult.CONTINUE;
149 return FileVisitResult.SKIP_SUBTREE;
152 return FileVisitResult.CONTINUE;
157 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
158 if (file.getFileName().toString().endsWith(".tar.gz"))
159 return FileVisitResult.CONTINUE;
160 if (LOGGER.isTraceEnabled())
161 LOGGER.trace("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file)));
162 Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
163 return FileVisitResult.CONTINUE;
167 public static class AcornBackupRunnable implements Runnable, Future<BackupException> {
169 private final Semaphore lock;
170 private final Path targetPath;
171 private final int revision;
172 private final Path baseDir;
173 private final int latestFolder;
174 private final int newestFolder;
176 private boolean done = false;
177 private final Semaphore completion = new Semaphore(0);
178 private BackupException exception = null;
180 public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
181 Path baseDir, int latestFolder, int newestFolder) {
183 this.targetPath = targetPath;
184 this.revision = revision;
185 this.baseDir = baseDir;
186 this.latestFolder = latestFolder;
187 this.newestFolder = newestFolder;
194 writeHeadstateFile();
195 } catch (IOException e) {
196 exception = new BackupException("Acorn backup failed", e);
201 completion.release();
205 private void doBackup() throws IOException {
206 Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER);
207 Files.createDirectories(target);
208 Files.walkFileTree(baseDir,
209 new BackupCopyVisitor(baseDir, target));
212 private void writeHeadstateFile() throws IOException {
213 Path AcornMetadataFile = getAcornMetadataFile(baseDir);
214 if (!Files.exists(AcornMetadataFile)) {
215 Files.createFile(AcornMetadataFile);
217 Files.write(AcornMetadataFile,
218 Arrays.asList(Integer.toString(newestFolder)),
219 StandardOpenOption.WRITE,
220 StandardOpenOption.TRUNCATE_EXISTING,
221 StandardOpenOption.CREATE);
224 private void rollback() {
228 private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
230 private Path fromPath;
233 public BackupCopyVisitor(Path fromPath, Path toPath) {
234 this.fromPath = fromPath;
235 this.toPath = toPath;
239 public FileVisitResult preVisitDirectory(Path dir,
240 BasicFileAttributes attrs) throws IOException {
241 if (dir.equals(fromPath)) {
242 Path targetPath = toPath.resolve(fromPath.relativize(dir));
243 Files.createDirectories(targetPath);
244 return FileVisitResult.CONTINUE;
247 int dirNameInt = Integer.parseInt(dir.getFileName().toString());
248 if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
249 Path targetPath = toPath.resolve(fromPath.relativize(dir));
250 Files.createDirectories(targetPath);
251 return FileVisitResult.CONTINUE;
253 } catch (NumberFormatException e) {
255 return FileVisitResult.SKIP_SUBTREE;
260 public FileVisitResult visitFile(Path file,
261 BasicFileAttributes attrs) throws IOException {
262 if (LOGGER.isTraceEnabled())
263 LOGGER.trace("Backup " + file + " to " + toPath.resolve(fromPath.relativize(file)));
264 Files.copy(file, toPath.resolve(fromPath.relativize(file)),
265 StandardCopyOption.REPLACE_EXISTING);
266 return FileVisitResult.CONTINUE;
271 public boolean cancel(boolean mayInterruptIfRunning) {
276 public boolean isCancelled() {
281 public boolean isDone() {
286 public BackupException get() throws InterruptedException {
287 completion.acquire();
288 completion.release();
293 public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
294 if (completion.tryAcquire(timeout, unit))
295 completion.release();
297 throw new TimeoutException("Acorn backup completion waiting timed out.");