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 if (!Files.exists(targetPath)) {
143 Files.createDirectory(targetPath);
145 return FileVisitResult.CONTINUE;
146 } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) {
147 int dirNameInt = Integer.parseInt(dirName.toString());
148 if (dirNameInt <= revision) {
149 return FileVisitResult.CONTINUE;
151 return FileVisitResult.SKIP_SUBTREE;
154 return FileVisitResult.CONTINUE;
159 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
160 if (file.getFileName().toString().endsWith(".tar.gz"))
161 return FileVisitResult.CONTINUE;
162 if (LOGGER.isTraceEnabled())
163 LOGGER.trace("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file)));
164 Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
165 return FileVisitResult.CONTINUE;
169 public static class AcornBackupRunnable implements Runnable, Future<BackupException> {
171 private final Semaphore lock;
172 private final Path targetPath;
173 private final int revision;
174 private final Path baseDir;
175 private final int latestFolder;
176 private final int newestFolder;
178 private boolean done = false;
179 private final Semaphore completion = new Semaphore(0);
180 private BackupException exception = null;
182 public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
183 Path baseDir, int latestFolder, int newestFolder) {
185 this.targetPath = targetPath;
186 this.revision = revision;
187 this.baseDir = baseDir;
188 this.latestFolder = latestFolder;
189 this.newestFolder = newestFolder;
196 writeHeadstateFile();
197 } catch (IOException e) {
198 exception = new BackupException("Acorn backup failed", e);
203 completion.release();
207 private void doBackup() throws IOException {
208 Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER);
209 Files.createDirectories(target);
210 Files.walkFileTree(baseDir,
211 new BackupCopyVisitor(baseDir, target));
214 private void writeHeadstateFile() throws IOException {
215 Path AcornMetadataFile = getAcornMetadataFile(baseDir);
216 if (!Files.exists(AcornMetadataFile)) {
217 Files.createFile(AcornMetadataFile);
219 Files.write(AcornMetadataFile,
220 Arrays.asList(Integer.toString(newestFolder)),
221 StandardOpenOption.WRITE,
222 StandardOpenOption.TRUNCATE_EXISTING,
223 StandardOpenOption.CREATE);
226 private void rollback() {
230 private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
232 private Path fromPath;
235 public BackupCopyVisitor(Path fromPath, Path toPath) {
236 this.fromPath = fromPath;
237 this.toPath = toPath;
241 public FileVisitResult preVisitDirectory(Path dir,
242 BasicFileAttributes attrs) throws IOException {
243 Path dirName = dir.getFileName();
244 if (dirName.equals(fromPath)) {
245 Path targetPath = toPath.resolve(fromPath.relativize(dir));
246 if (!Files.exists(targetPath)) {
247 Files.createDirectory(targetPath);
249 return FileVisitResult.CONTINUE;
251 int dirNameInt = Integer.parseInt(dirName.toString());
252 if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
253 Path targetPath = toPath.resolve(fromPath
255 if (!Files.exists(targetPath)) {
256 Files.createDirectory(targetPath);
258 return FileVisitResult.CONTINUE;
260 return FileVisitResult.SKIP_SUBTREE;
265 public FileVisitResult visitFile(Path file,
266 BasicFileAttributes attrs) throws IOException {
267 if (LOGGER.isTraceEnabled())
268 LOGGER.trace("Backup " + file + " to " + toPath.resolve(fromPath.relativize(file)));
269 Files.copy(file, toPath.resolve(fromPath.relativize(file)),
270 StandardCopyOption.REPLACE_EXISTING);
271 return FileVisitResult.CONTINUE;
276 public boolean cancel(boolean mayInterruptIfRunning) {
281 public boolean isCancelled() {
286 public boolean isDone() {
291 public BackupException get() throws InterruptedException {
292 completion.acquire();
293 completion.release();
298 public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
299 if (completion.tryAcquire(timeout, unit))
300 completion.release();
302 throw new TimeoutException("Acorn backup completion waiting timed out.");