]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
f75d5864fc84040b496565549ec789f7bc09fe2e
[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.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;
17
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;
27
28 /**
29  * @author Jani
30  *
31  * TODO: get rid of {@link GraphClientImpl2#getInstance()} invocations somehow in a cleaner way
32  */
33 public class AcornBackupProvider implements IBackupProvider {
34
35     private static final Logger LOGGER = LoggerFactory.getLogger(AcornBackupProvider.class);
36
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;
41
42     public AcornBackupProvider() {
43         this.client = AcornSessionManagerImpl.getInstance().getClient();
44     }
45
46     public static Path getAcornMetadataFile(Path dbFolder) {
47         return dbFolder.getParent().resolve(IDENTIFIER);
48     }
49
50     @Override
51     public void lock() throws BackupException {
52         try {
53             if (trId != -1)
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);
58         }
59     }
60
61     @Override
62     public Future<BackupException> backup(Path targetPath, int revision) throws BackupException {
63         boolean releaseLock = true;
64         try {
65             lock.acquire();
66             Future<BackupException> r = client.getBackupRunnable(lock, targetPath, revision);
67             releaseLock = false;
68             return r;
69         } catch (InterruptedException e) {
70             releaseLock = false;
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);
76         } finally {
77             if (releaseLock)
78                 lock.release();
79         }
80     }
81
82     @Override
83     public void unlock() throws BackupException {
84         try {
85             if (trId == -1)
86                 throw new BackupException(this + " backup provider is not locked");
87             client.endTransaction(trId);
88             trId = -1;
89         } catch (ProCoreException e) {
90             throw new BackupException(e);
91         }
92     }
93
94     @Override
95     public void restore(Path fromPath, int revision) {
96         try {
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);
104             } else {
105                 Path dbRootParent = dbRoot.getParent();
106                 restorePath = dbRootParent == null ? Files.createTempDirectory("restore")
107                         : Files.createTempDirectory(dbRootParent, "restore");
108             }
109
110             // 2. Restore the backup.
111             Files.walkFileTree(fromPath, new RestoreCopyVisitor(restorePath, revision));
112
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);
117             }
118         } catch (IOException e) {
119             LOGGER.error("Failed to restore database revision {} from backup {}", revision, fromPath.toString(), e);
120         }
121     }
122
123     private class RestoreCopyVisitor extends SimpleFileVisitor<Path> {
124
125         private final Path toPath;
126         private final int revision;
127         private Path currentSubFolder;
128
129         public RestoreCopyVisitor(Path toPath, int revision) {
130             this.toPath = toPath;
131             this.revision = revision;
132         }
133
134         @Override
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);
144                 }
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;
150                 } else {
151                     return FileVisitResult.SKIP_SUBTREE;
152                 }
153             } else {
154                 return FileVisitResult.CONTINUE;
155             }
156         }
157
158         @Override
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;
166         }
167     }
168
169     public static class AcornBackupRunnable implements Runnable, Future<BackupException> {
170
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;
177
178         private boolean done = false;
179         private final Semaphore completion = new Semaphore(0);
180         private BackupException exception = null;
181
182         public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
183                 Path baseDir, int latestFolder, int newestFolder) {
184             this.lock = lock;
185             this.targetPath = targetPath;
186             this.revision = revision;
187             this.baseDir = baseDir;
188             this.latestFolder = latestFolder;
189             this.newestFolder = newestFolder;
190         }
191
192         @Override
193         public void run() {
194             try {
195                 doBackup();
196                 writeHeadstateFile();
197             } catch (IOException e) {
198                 exception = new BackupException("Acorn backup failed", e);
199                 rollback();
200             } finally {
201                 done = true;
202                 lock.release();
203                 completion.release();
204             }
205         }
206
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));
212         }
213
214         private void writeHeadstateFile() throws IOException {
215             Path AcornMetadataFile = getAcornMetadataFile(baseDir);
216             if (!Files.exists(AcornMetadataFile)) {
217                 Files.createFile(AcornMetadataFile);
218             }
219             Files.write(AcornMetadataFile,
220                     Arrays.asList(Integer.toString(newestFolder)),
221                     StandardOpenOption.WRITE,
222                     StandardOpenOption.TRUNCATE_EXISTING,
223                     StandardOpenOption.CREATE);
224         }
225
226         private void rollback() {
227             // TODO
228         }
229
230         private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
231
232             private Path fromPath;
233             private Path toPath;
234
235             public BackupCopyVisitor(Path fromPath, Path toPath) {
236                 this.fromPath = fromPath;
237                 this.toPath = toPath;
238             }
239
240             @Override
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);
248                     }
249                     return FileVisitResult.CONTINUE;
250                 } else {
251                     int dirNameInt = Integer.parseInt(dirName.toString());
252                     if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
253                         Path targetPath = toPath.resolve(fromPath
254                                 .relativize(dir));
255                         if (!Files.exists(targetPath)) {
256                             Files.createDirectory(targetPath);
257                         }
258                         return FileVisitResult.CONTINUE;
259                     }
260                     return FileVisitResult.SKIP_SUBTREE;
261                 }
262             }
263
264             @Override
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;
272             }
273         }
274
275         @Override
276         public boolean cancel(boolean mayInterruptIfRunning) {
277             return false;
278         }
279
280         @Override
281         public boolean isCancelled() {
282             return false;
283         }
284
285         @Override
286         public boolean isDone() {
287             return done;
288         }
289
290         @Override
291         public BackupException get() throws InterruptedException {
292             completion.acquire();
293             completion.release();
294             return exception;
295         }
296
297         @Override
298         public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
299             if (completion.tryAcquire(timeout, unit))
300                 completion.release();
301             else
302                 throw new TimeoutException("Acorn backup completion waiting timed out.");
303             return exception;
304         }
305
306     }
307
308 }