]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
Merge "Upgrade pdfbox to 2.0.3 and fastutil to 7.0.13."
[simantics/platform.git] / bundles / org.simantics.acorn / src / org / simantics / acorn / backup / AcornBackupProvider.java
1 package org.simantics.acorn.backup;
2
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;
18
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;
26
27 /**
28  * @author Jani
29  *
30  * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way
31  */
32 public class AcornBackupProvider implements IBackupProvider {
33
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;
38
39     public AcornBackupProvider() {
40         this.client = AcornSessionManagerImpl.getInstance().getClient();
41     }
42     
43     private static Path getAcornMetadataFile(Path dbFolder) {
44         return dbFolder.getParent().resolve(IDENTIFIER);
45     }
46     
47     @Override
48     public void lock() throws BackupException {
49         try {
50             if (trId != -1)
51                 throw new IllegalStateException(this + " backup provider is already locked");
52             trId = client.askWriteTransaction(-1).getTransactionId();
53         } catch (ProCoreException e) {
54             e.printStackTrace();
55         }
56     }
57
58     @Override
59     public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
60         boolean releaseLock = true;
61         try {
62             lock.acquire();
63
64             client.makeSnapshot(true);
65
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() );
73                 }
74             }
75
76             AcornBackupRunnable r = new AcornBackupRunnable(
77                     lock, targetPath, revision, dbDir, latestFolder, newestFolder);
78             new Thread(r, "Acorn backup thread").start();
79
80              releaseLock = false;
81              return r;
82         } catch (InterruptedException e) {
83             releaseLock = false;
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);
89         } finally {
90             if (releaseLock)
91                 lock.release();
92         }
93     }
94
95     @Override
96     public void unlock() throws BackupException {
97         try {
98             if (trId == -1)
99                 throw new BackupException(this + " backup provider is not locked");
100             client.endTransaction(trId);
101             trId = -1;
102         } catch (ProCoreException e) {
103             throw new BackupException(e);
104         }
105     }
106
107     @Override
108     public void restore(Path fromPath, int revision) {
109         try {
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);
117             } else {
118                 Path dbRootParent = dbRoot.getParent();
119                 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
120                         : Files.createTempDirectory(dbRootParent, "restore");
121             }
122
123             // 2. Restore the backup.
124             Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
125
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);
130             } 
131         } catch (IOException e) {
132             e.printStackTrace();
133         }
134     }
135
136     private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
137
138         private final Path toPath;
139         private final int revision;
140         private Path currentSubFolder;
141
142         public RestoreCopyVisitor(Path toPath, int revision) {
143             this.toPath = toPath;
144             this.revision = revision;
145         }
146
147         @Override
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);
157                 }
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;
163                 } else {
164                     return FileVisitResult.SKIP_SUBTREE;
165                 }
166             } else {
167                 return FileVisitResult.CONTINUE;
168             }
169         }
170
171         @Override
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;
178         }
179     }
180
181     private static class AcornBackupRunnable implements Runnable, Future<BackupException> {
182
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;
189
190         private boolean done = false;
191         private final Semaphore completion = new Semaphore(0);
192         private BackupException exception = null;
193
194         public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
195                 Path baseDir, int latestFolder, int newestFolder) {
196             this.lock = lock;
197             this.targetPath = targetPath;
198             this.revision = revision;
199             this.baseDir = baseDir;
200             this.latestFolder = latestFolder;
201             this.newestFolder = newestFolder;
202         }
203
204         @Override
205         public void run() {
206             try {
207                 doBackup();
208                 writeHeadstateFile();
209             } catch (IOException e) {
210                 exception = new BackupException("Acorn backup failed", e);
211                 rollback();
212             } finally {
213                 done = true;
214                 lock.release();
215                 completion.release();
216             }
217         }
218
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));
225         }
226
227         private void writeHeadstateFile() throws IOException {
228             Path AcornMetadataFile = getAcornMetadataFile(baseDir);
229             if (!Files.exists(AcornMetadataFile)) {
230                 Files.createFile(AcornMetadataFile);
231             }
232             Files.write(AcornMetadataFile,
233                     Arrays.asList(Integer.toString(newestFolder)),
234                     StandardOpenOption.WRITE,
235                     StandardOpenOption.TRUNCATE_EXISTING,
236                     StandardOpenOption.CREATE);
237         }
238
239         private void rollback() {
240             // TODO
241         }
242
243         private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
244
245             private Path fromPath;
246             private Path toPath;
247
248             public BackupCopyVisitor(Path fromPath, Path toPath) {
249                 this.fromPath = fromPath;
250                 this.toPath = toPath;
251             }
252
253             @Override
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);
261                     }
262                     return FileVisitResult.CONTINUE;
263                 } else {
264                     int dirNameInt = Integer.parseInt(dirName.toString());
265                     if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
266                         Path targetPath = toPath.resolve(fromPath
267                                 .relativize(dir));
268                         if (!Files.exists(targetPath)) {
269                             Files.createDirectory(targetPath);
270                         }
271                         return FileVisitResult.CONTINUE;
272                     }
273                     return FileVisitResult.SKIP_SUBTREE;
274                 }
275             }
276
277             @Override
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;
285             }
286         }
287
288         @Override
289         public boolean cancel(boolean mayInterruptIfRunning) {
290             return false;
291         }
292
293         @Override
294         public boolean isCancelled() {
295             return false;
296         }
297
298         @Override
299         public boolean isDone() {
300             return done;
301         }
302
303         @Override
304         public BackupException get() throws InterruptedException {
305             completion.acquire();
306             completion.release();
307             return exception;
308         }
309
310         @Override
311         public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
312             if (completion.tryAcquire(timeout, unit))
313                 completion.release();
314             else
315                 throw new TimeoutException("Acorn backup completion waiting timed out.");
316             return exception;
317         }
318
319     }
320
321 }