]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
a5aa4f2ef6bcfc7b8d19c70d2a0d7effeca9c4ab
[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     public 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             Future<BackupException> r = client.getBackupRunnable(lock, targetPath, revision);
64             releaseLock = false;
65             return r;
66         } catch (InterruptedException e) {
67             releaseLock = false;
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);
73         } finally {
74             if (releaseLock)
75                 lock.release();
76         }
77     }
78
79     @Override
80     public void unlock() throws BackupException {
81         try {
82             if (trId == -1)
83                 throw new BackupException(this + " backup provider is not locked");
84             client.endTransaction(trId);
85             trId = -1;
86         } catch (ProCoreException e) {
87             throw new BackupException(e);
88         }
89     }
90
91     @Override
92     public void restore(Path fromPath, int revision) {
93         try {
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);
101             } else {
102                 Path dbRootParent = dbRoot.getParent();
103                 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
104                         : Files.createTempDirectory(dbRootParent, "restore");
105             }
106
107             // 2. Restore the backup.
108             Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
109
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);
114             }
115         } catch (IOException e) {
116             e.printStackTrace();
117         }
118     }
119
120     private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
121
122         private final Path toPath;
123         private final int revision;
124         private Path currentSubFolder;
125
126         public RestoreCopyVisitor(Path toPath, int revision) {
127             this.toPath = toPath;
128             this.revision = revision;
129         }
130
131         @Override
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);
141                 }
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;
147                 } else {
148                     return FileVisitResult.SKIP_SUBTREE;
149                 }
150             } else {
151                 return FileVisitResult.CONTINUE;
152             }
153         }
154
155         @Override
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;
162         }
163     }
164
165     public static class AcornBackupRunnable implements Runnable, Future<BackupException> {
166
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;
173
174         private boolean done = false;
175         private final Semaphore completion = new Semaphore(0);
176         private BackupException exception = null;
177
178         public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
179                 Path baseDir, int latestFolder, int newestFolder) {
180             this.lock = lock;
181             this.targetPath = targetPath;
182             this.revision = revision;
183             this.baseDir = baseDir;
184             this.latestFolder = latestFolder;
185             this.newestFolder = newestFolder;
186         }
187
188         @Override
189         public void run() {
190             try {
191                 doBackup();
192                 writeHeadstateFile();
193             } catch (IOException e) {
194                 exception = new BackupException("Acorn backup failed", e);
195                 rollback();
196             } finally {
197                 done = true;
198                 lock.release();
199                 completion.release();
200             }
201         }
202
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));
209         }
210
211         private void writeHeadstateFile() throws IOException {
212             Path AcornMetadataFile = getAcornMetadataFile(baseDir);
213             if (!Files.exists(AcornMetadataFile)) {
214                 Files.createFile(AcornMetadataFile);
215             }
216             Files.write(AcornMetadataFile,
217                     Arrays.asList(Integer.toString(newestFolder)),
218                     StandardOpenOption.WRITE,
219                     StandardOpenOption.TRUNCATE_EXISTING,
220                     StandardOpenOption.CREATE);
221         }
222
223         private void rollback() {
224             // TODO
225         }
226
227         private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
228
229             private Path fromPath;
230             private Path toPath;
231
232             public BackupCopyVisitor(Path fromPath, Path toPath) {
233                 this.fromPath = fromPath;
234                 this.toPath = toPath;
235             }
236
237             @Override
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);
245                     }
246                     return FileVisitResult.CONTINUE;
247                 } else {
248                     int dirNameInt = Integer.parseInt(dirName.toString());
249                     if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
250                         Path targetPath = toPath.resolve(fromPath
251                                 .relativize(dir));
252                         if (!Files.exists(targetPath)) {
253                             Files.createDirectory(targetPath);
254                         }
255                         return FileVisitResult.CONTINUE;
256                     }
257                     return FileVisitResult.SKIP_SUBTREE;
258                 }
259             }
260
261             @Override
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;
269             }
270         }
271
272         @Override
273         public boolean cancel(boolean mayInterruptIfRunning) {
274             return false;
275         }
276
277         @Override
278         public boolean isCancelled() {
279             return false;
280         }
281
282         @Override
283         public boolean isDone() {
284             return done;
285         }
286
287         @Override
288         public BackupException get() throws InterruptedException {
289             completion.acquire();
290             completion.release();
291             return exception;
292         }
293
294         @Override
295         public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
296             if (completion.tryAcquire(timeout, unit))
297                 completion.release();
298             else
299                 throw new TimeoutException("Acorn backup completion waiting timed out.");
300             return exception;
301         }
302
303     }
304
305 }