]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.acorn/src/org/simantics/acorn/backup/AcornBackupProvider.java
Fixed invalid comparison from AcornBackupProvider.RestoreCopyVisitor
[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                 Files.createDirectories(targetPath);
143                 return FileVisitResult.CONTINUE;
144             } else if (dirName.toString().length() == 1 && Character.isDigit(dirName.toString().charAt(0))) {
145                 int dirNameInt = Integer.parseInt(dirName.toString());
146                 if (dirNameInt <= revision) {
147                     return FileVisitResult.CONTINUE;
148                 } else {
149                     return FileVisitResult.SKIP_SUBTREE;
150                 }
151             } else {
152                 return FileVisitResult.CONTINUE;
153             }
154         }
155
156         @Override
157         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
158             if (file.getFileName().toString().endsWith(".tar.gz"))
159                 return FileVisitResult.CONTINUE;
160             if (LOGGER.isTraceEnabled())
161                 LOGGER.trace("Restore " + file + " to " + toPath.resolve(currentSubFolder.relativize(file)));
162             Files.copy(file, toPath.resolve(currentSubFolder.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
163             return FileVisitResult.CONTINUE;
164         }
165     }
166
167     public static class AcornBackupRunnable implements Runnable, Future<BackupException> {
168
169         private final Semaphore lock;
170         private final Path targetPath;
171         private final int revision;
172         private final Path baseDir;
173         private final int latestFolder;
174         private final int newestFolder;
175
176         private boolean done = false;
177         private final Semaphore completion = new Semaphore(0);
178         private BackupException exception = null;
179
180         public AcornBackupRunnable(Semaphore lock, Path targetPath, int revision,
181                 Path baseDir, int latestFolder, int newestFolder) {
182             this.lock = lock;
183             this.targetPath = targetPath;
184             this.revision = revision;
185             this.baseDir = baseDir;
186             this.latestFolder = latestFolder;
187             this.newestFolder = newestFolder;
188         }
189
190         @Override
191         public void run() {
192             try {
193                 doBackup();
194                 writeHeadstateFile();
195             } catch (IOException e) {
196                 exception = new BackupException("Acorn backup failed", e);
197                 rollback();
198             } finally {
199                 done = true;
200                 lock.release();
201                 completion.release();
202             }
203         }
204
205         private void doBackup() throws IOException {
206             Path target = targetPath.resolve(String.valueOf(revision)).resolve(IDENTIFIER);
207             Files.createDirectories(target);
208             Files.walkFileTree(baseDir,
209                     new BackupCopyVisitor(baseDir, target));
210         }
211
212         private void writeHeadstateFile() throws IOException {
213             Path AcornMetadataFile = getAcornMetadataFile(baseDir);
214             if (!Files.exists(AcornMetadataFile)) {
215                 Files.createFile(AcornMetadataFile);
216             }
217             Files.write(AcornMetadataFile,
218                     Arrays.asList(Integer.toString(newestFolder)),
219                     StandardOpenOption.WRITE,
220                     StandardOpenOption.TRUNCATE_EXISTING,
221                     StandardOpenOption.CREATE);
222         }
223
224         private void rollback() {
225             // TODO
226         }
227
228         private class BackupCopyVisitor extends SimpleFileVisitor<Path> {
229
230             private Path fromPath;
231             private Path toPath;
232
233             public BackupCopyVisitor(Path fromPath, Path toPath) {
234                 this.fromPath = fromPath;
235                 this.toPath = toPath;
236             }
237
238             @Override
239             public FileVisitResult preVisitDirectory(Path dir,
240                     BasicFileAttributes attrs) throws IOException {
241                 if (dir.equals(fromPath)) {
242                     Path targetPath = toPath.resolve(fromPath.relativize(dir));
243                     Files.createDirectories(targetPath);
244                     return FileVisitResult.CONTINUE;
245                 } else {
246                     try {
247                         int dirNameInt = Integer.parseInt(dir.getFileName().toString());
248                         if (latestFolder < dirNameInt && dirNameInt <= newestFolder) {
249                             Path targetPath = toPath.resolve(fromPath.relativize(dir));
250                             Files.createDirectories(targetPath);
251                             return FileVisitResult.CONTINUE;
252                         }
253                     } catch (NumberFormatException e) {
254                     }
255                     return FileVisitResult.SKIP_SUBTREE;
256                 }
257             }
258
259             @Override
260             public FileVisitResult visitFile(Path file,
261                     BasicFileAttributes attrs) throws IOException {
262                 if (LOGGER.isTraceEnabled())
263                     LOGGER.trace("Backup " + file + " to " + toPath.resolve(fromPath.relativize(file)));
264                 Files.copy(file, toPath.resolve(fromPath.relativize(file)),
265                         StandardCopyOption.REPLACE_EXISTING);
266                 return FileVisitResult.CONTINUE;
267             }
268         }
269
270         @Override
271         public boolean cancel(boolean mayInterruptIfRunning) {
272             return false;
273         }
274
275         @Override
276         public boolean isCancelled() {
277             return false;
278         }
279
280         @Override
281         public boolean isDone() {
282             return done;
283         }
284
285         @Override
286         public BackupException get() throws InterruptedException {
287             completion.acquire();
288             completion.release();
289             return exception;
290         }
291
292         @Override
293         public BackupException get(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
294             if (completion.tryAcquire(timeout, unit))
295                 completion.release();
296             else
297                 throw new TimeoutException("Acorn backup completion waiting timed out.");
298             return exception;
299         }
300
301     }
302
303 }