]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
Merge commit 'fd452722e97db9cf876f4f03a9e44fe750625a92'
[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.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;
24
25 /**
26  * @author Jani
27  *
28  * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way
29  */
30 public class AcornBackupProvider implements IBackupProvider {
31
32     private static final String IDENTIFIER = "AcornBackupProvider";
33     private long trId = -1;
34     private final Semaphore lock = new Semaphore(1);
35
36     private static Path getAcornMetadataFile(Path dbFolder) {
37         return dbFolder.getParent().resolve(IDENTIFIER);
38     }
39     
40     @Override
41     public void lock() throws BackupException {
42         try {
43             if (trId != -1)
44                 throw new IllegalStateException(this + " backup provider is already locked");
45             trId = GraphClientImpl2.getInstance().askWriteTransaction(-1)
46                     .getTransactionId();
47         } catch (ProCoreException e) {
48             e.printStackTrace();
49         }
50     }
51
52     @Override
53     public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
54         boolean releaseLock = true;
55         try {
56             lock.acquire();
57
58             GraphClientImpl2 client = GraphClientImpl2.getInstance();
59             client.makeSnapshot(true);
60
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() );
68                 }
69             }
70
71             AcornBackupRunnable r = new AcornBackupRunnable(
72                     lock, targetPath, revision, dbDir, latestFolder, newestFolder);
73             new Thread(r, "Acorn backup thread").start();
74
75              releaseLock = false;
76              return r;
77         } catch (InterruptedException e) {
78             releaseLock = false;
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);
84         } finally {
85             if (releaseLock)
86                 lock.release();
87         }
88     }
89
90     @Override
91     public void unlock() throws BackupException {
92         try {
93             if (trId == -1)
94                 throw new BackupException(this + " backup provider is not locked");
95             GraphClientImpl2.getInstance().endTransaction(trId);
96             trId = -1;
97         } catch (ProCoreException e) {
98             throw new BackupException(e);
99         }
100     }
101
102     @Override
103     public void restore(Path fromPath, int revision) {
104         try {
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);
112             } else {
113                 Path dbRootParent = dbRoot.getParent();
114                 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
115                         : Files.createTempDirectory(dbRootParent, "restore");
116             }
117
118             // 2. Restore the backup.
119             Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
120
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);
125             } 
126         } catch (IOException e) {
127             e.printStackTrace();
128         }
129     }
130
131     private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
132
133         private final Path toPath;
134         private final int revision;
135         private Path currentSubFolder;
136
137         public RestoreCopyVisitor(Path toPath, int revision) {
138             this.toPath = toPath;
139             this.revision = revision;
140         }
141
142         @Override
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);
152                 }
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;
158                 } else {
159                     return FileVisitResult.SKIP_SUBTREE;
160                 }
161             } else {
162                 return FileVisitResult.CONTINUE;
163             }
164         }
165
166         @Override
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;
173         }
174     }
175
176     private static class AcornBackupRunnable implements Runnable, Future<BackupException> {
177
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;
184
185         private boolean done = false;
186         private final Semaphore completion = new Semaphore(0);
187         private BackupException exception = null;
188
189         public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
190                 Path baseDir, int latestFolder, int newestFolder) {
191             this.lock = lock;
192             this.targetPath = targetPath;
193             this.revision = revision;
194             this.baseDir = baseDir;
195             this.latestFolder = latestFolder;
196             this.newestFolder = newestFolder;
197         }
198
199         @Override
200         public void run() {
201             try {
202                 doBackup();
203                 writeHeadstateFile();
204             } catch (IOException e) {
205                 exception = new BackupException("Acorn backup failed", e);
206                 rollback();
207             } finally {
208                 done = true;
209                 lock.release();
210                 completion.release();
211             }
212         }
213
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));
220         }
221
222         private void writeHeadstateFile() throws IOException {
223             Path AcornMetadataFile = getAcornMetadataFile(baseDir);
224             if (!Files.exists(AcornMetadataFile)) {
225                 Files.createFile(AcornMetadataFile);
226             }
227             Files.write(AcornMetadataFile,
228                     Arrays.asList(Integer.toString(newestFolder)),
229                     StandardOpenOption.WRITE,
230                     StandardOpenOption.TRUNCATE_EXISTING,
231                     StandardOpenOption.CREATE);
232         }
233
234         private void rollback() {
235             // TODO
236         }
237
238         private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
239
240             private Path fromPath;
241             private Path toPath;
242
243             public BackupCopyVisitor(Path fromPath, Path toPath) {
244                 this.fromPath = fromPath;
245                 this.toPath = toPath;
246             }
247
248             @Override
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);
256                     }
257                     return FileVisitResult.CONTINUE;
258                 } else {
259                     int dirNameInt = Integer.parseInt(dirName.toString());
260                     if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
261                         Path targetPath = toPath.resolve(fromPath
262                                 .relativize(dir));
263                         if (!Files.exists(targetPath)) {
264                             Files.createDirectory(targetPath);
265                         }
266                         return FileVisitResult.CONTINUE;
267                     }
268                     return FileVisitResult.SKIP_SUBTREE;
269                 }
270             }
271
272             @Override
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;
280             }
281         }
282
283         @Override
284         public boolean cancel(boolean mayInterruptIfRunning) {
285             return false;
286         }
287
288         @Override
289         public boolean isCancelled() {
290             return false;
291         }
292
293         @Override
294         public boolean isDone() {
295             return done;
296         }
297
298         @Override
299         public BackupException get() throws InterruptedException {
300             completion.acquire();
301             completion.release();
302             return exception;
303         }
304
305         @Override
306         public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
307             if (completion.tryAcquire(timeout, unit))
308                 completion.release();
309             else
310                 throw new TimeoutException("Acorn backup completion waiting timed out.");
311             return exception;
312         }
313
314     }
315
316 }