diff options
author | Todor Boev | 2021-05-31 15:13:01 +0000 |
---|---|---|
committer | Todor Boev | 2021-06-30 19:16:47 +0000 |
commit | a57a0ce4a6b964c35a6172f4c043d7bf6057c6dd (patch) | |
tree | 1892133ebb581fb3a546eb0f435e0b9f44df602c | |
parent | 297545ddd241eca93f6e6423a9cfa800131b5483 (diff) | |
download | rt.equinox.p2-a57a0ce4a6b964c35a6172f4c043d7bf6057c6dd.tar.gz rt.equinox.p2-a57a0ce4a6b964c35a6172f4c043d7bf6057c6dd.tar.xz rt.equinox.p2-a57a0ce4a6b964c35a6172f4c043d7bf6057c6dd.zip |
Bug 537757 - Convert BackupStore to java.nio.fileI20210630-1800
- Cleanup to the path conversion methods
- Cleaned up the backup store root handling
- Using UUID for the unique part of the backup dir name
- Convert to Java 7 file API using only File.move()
for the most critical operation.
- Improved logging
- Changed BackupStore to SimpleBackupStore
- Fixed the backup tests to use Java 7
Signed-off-by: Todor Boev <rinsvind@gmail.com>
Change-Id: I16f3496bf9accacdb2932b0a2a14a96d3581d37b
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.p2/+/182585
Tested-by: Equinox Bot <equinox-bot@eclipse.org>
14 files changed, 1283 insertions, 1374 deletions
diff --git a/bundles/org.eclipse.equinox.p2.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.tests/META-INF/MANIFEST.MF index ff61d2971..fc73bbdd1 100644 --- a/bundles/org.eclipse.equinox.p2.tests/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.equinox.p2.tests/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.equinox.p2.tests;singleton:=true -Bundle-Version: 1.8.300.qualifier +Bundle-Version: 1.8.400.qualifier Bundle-ClassPath: . Bundle-Activator: org.eclipse.equinox.p2.tests.TestActivator Bundle-Vendor: %providerName diff --git a/bundles/org.eclipse.equinox.p2.tests/pom.xml b/bundles/org.eclipse.equinox.p2.tests/pom.xml index dbd85361d..85f1ee5b0 100644 --- a/bundles/org.eclipse.equinox.p2.tests/pom.xml +++ b/bundles/org.eclipse.equinox.p2.tests/pom.xml @@ -16,7 +16,7 @@ <groupId>org.eclipse.equinox</groupId> <artifactId>org.eclipse.equinox.p2.tests</artifactId> - <version>1.8.300-SNAPSHOT</version> + <version>1.8.400-SNAPSHOT</version> <packaging>eclipse-test-plugin</packaging> <properties> diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/core/BackupTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/core/BackupTest.java index daf3a5020..30faf7e96 100644 --- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/core/BackupTest.java +++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/core/BackupTest.java @@ -13,297 +13,189 @@ *******************************************************************************/ package org.eclipse.equinox.p2.tests.core; -import java.io.*; -import org.eclipse.equinox.internal.p2.touchpoint.natives.BackupStore; +import static java.nio.file.FileVisitResult.CONTINUE; +import static java.nio.file.StandardOpenOption.CREATE_NEW; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import org.eclipse.equinox.internal.p2.touchpoint.natives.SimpleBackupStore; import org.eclipse.equinox.p2.tests.AbstractProvisioningTest; public class BackupTest extends AbstractProvisioningTest { - private static final String BUPREFIX = "BackupTest"; - private File sourceDir; - private File aDir; - private File aaDir; - private File bDir; - private File aTxt; - private File bTxt; - private File abDir; - private File cTxt; - private File cTxtRelative; + private static final String BUPREFIX = "backup-test"; + + private Path sourceDir; + + private Path aDir; + private Path aaDir; + private Path aTxt; + + private Path bDir; + private Path bTxt; + + private Path abDir; + + private Path cTxt; + + private SimpleBackupStore store; /** - * Sets up directories and files under user.home - * <ul><li>P2BUTEST/</li> - * <ul><li>A/</li> - * <ul><li>AA/</li> - * <ul><li>a.txt</li> - * <li>b.txt</li> - * </ul> - * </ul> - * <li>B/</li> - * </ul> - * </ul> + * <pre> + * /p2-backup-test + * /a + * /aa + * /a.txt + * /b.txt + * /ab + * /c.txt + * /b + * </pre> */ @Override - public void setUp() { - // create some test files under user.home - // do not want them under /tmp as it may be on its own file system (and even - // be an in-memory file system). - // + public void setUp() throws IOException { + // Don't want to backup under /tmp since it may be it's own file system or an + // in-memory file system String userHome = System.getProperty("user.home"); - sourceDir = new File(new File(userHome), "P2BUTEST"); - aDir = new File(sourceDir, "A"); - aDir.mkdirs(); - aaDir = new File(aDir, "AA"); - aaDir.mkdir(); - abDir = new File(aDir, "AB"); - abDir.mkdir(); - - bDir = new File(sourceDir, "B"); - bDir.mkdirs(); - aTxt = new File(aaDir, "a.txt"); - bTxt = new File(aaDir, "b.txt"); - cTxt = new File(abDir, "c.txt"); - cTxtRelative = new File(aaDir, "../AB/c.txt"); - try { - writeToFile(aTxt, "A\nA file with an A"); - writeToFile(bTxt, "B\nA file with a B"); - writeToFile(cTxt, "C\nA file with a C"); - } catch (IOException e) { - fail(); - } - } + sourceDir = Path.of(userHome, "p2-backup-test"); + deleteAll(sourceDir); - private void writeToFile(File file, String content) throws IOException { - file.getParentFile().mkdirs(); - file.createNewFile(); + aDir = sourceDir.resolve("a"); + Files.createDirectories(aDir); - try (Writer writer = new BufferedWriter(new FileWriter(file))) { - writer.write(content); - } + aaDir = aDir.resolve("aa"); + Files.createDirectories(aaDir); + + aTxt = aaDir.resolve("a.txt"); + Files.write(aTxt, "A\nA file with an A".getBytes()); + + bTxt = aaDir.resolve("b.txt"); + Files.write(bTxt, "B\nA file with a B".getBytes()); + + abDir = aDir.resolve("ab"); + Files.createDirectories(abDir); + + cTxt = abDir.resolve("c.txt"); + Files.write(cTxt, "C\nA file with a C".getBytes()); + + bDir = sourceDir.resolve("b"); + Files.createDirectories(bDir); + + store = new SimpleBackupStore(null, BUPREFIX); } @Override - public void tearDown() { - fullyDelete(sourceDir); + public void tearDown() throws IOException { + deleteAll(sourceDir); } - /** - * Deletes a file, or a directory with all of it's children. - * @param file the file or directory to fully delete - * @return true if, and only if the file is deleted - */ - private boolean fullyDelete(File file) { - if (!file.exists()) - return true; - if (file.isDirectory()) { - File[] children = file.listFiles(); - for (File child : children) { - if (!fullyDelete(new File(file, child.getName()))) { - return false; + private static void deleteAll(Path path) throws IOException { + if (!Files.exists(path)) { + return; + } + + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (exc != null) { + throw exc; } + Files.delete(dir); + return CONTINUE; } - } - return file.delete(); + }); } /** * Test that a path containing ".." can be backed up and restored. */ - public void testBackupRelative() { - BackupStore store = new BackupStore(null, BUPREFIX); - // backup and overwrite a.txt - try { - store.backup(cTxtRelative); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up cTxtRelative"); - } - if (cTxt.exists()) - fail("File not moved to backup - still exists"); - try { - writeToFile(cTxt, "XXXX\n- This file should be restored with C"); - } catch (IOException e) { - e.printStackTrace(); - fail("Could not write a file for testing purposes."); - } + public void testBackupRelative() throws IOException { + Path cTxtRelative = aaDir.resolve(aaDir.relativize(cTxt)); - // restore - try { - store.restore(); - } catch (IOException e) { - e.printStackTrace(); - fail("Restore operation failed with IOException"); - } - // assert restore - assertFileContent("Restore of C failed - not original content", cTxt, "C"); + store.backup(cTxtRelative.toFile()); + assertFalse(Files.exists(cTxt)); + + Files.write(cTxt, "XXXX\n- This file should be restored with C".getBytes()); + + store.restore(); + assertFileContent("Restore of C failed - not original content", cTxt.toFile(), "C"); assertNoGarbage(store); } - public void testBackupRestore() { - BackupStore store = new BackupStore(null, BUPREFIX); - // backup and overwrite a.txt - try { - store.backup(aTxt); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up aTxt"); - } - if (aTxt.exists()) - fail("File not moved to backup - still exists"); - try { - writeToFile(aTxt, "XXXX\n- This file should be restored with A"); - } catch (IOException e) { - e.printStackTrace(); - fail("Could not write a file for testing purposes."); - } + public void testBackupRestore() throws IOException { + store.backup(aTxt.toFile()); + assertFalse("File not moved to backup - still exists", Files.exists(aTxt)); - // backup the empty B directory - try { - store.backup(bDir); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up bDir"); - } - if (bDir.exists()) - fail("Backed up directory was not moved"); - - // backup b as a copy - try { - store.backupCopy(bTxt); - assertFileContent("File should have been copied", bTxt, "B"); - } catch (IOException e) { - fail("Could not backupCopy bTxt"); - } + Files.write(aTxt, "XXXX\n- This file should be restored with A".getBytes()); - // restore - try { - store.restore(); - } catch (IOException e) { - e.printStackTrace(); - fail("Restore operation failed with IOException"); - } + store.backup(bDir.toFile()); + assertFalse("Backed up directory was not moved", Files.isDirectory(bDir)); - // assert restore - assertFileContent("Restore of A failed - not original content", aTxt, "A"); - if (!bDir.isDirectory() && bDir.listFiles().length != 0) - fail("Empty directory not restored ok"); + store.backupCopy(bTxt.toFile()); + assertFileContent("File should have been copied", bTxt.toFile(), "B"); + store.restore(); + assertFileContent("Restore of A failed - not original content", aTxt.toFile(), "A"); + assertTrue("Empty directory not restored ok", Files.isDirectory(bDir) && Files.list(bDir).count() == 0); assertNoGarbage(store); } - public void testBackupDiscard() { - BackupStore store = new BackupStore(null, BUPREFIX); - // backup and overwrite a.txt - try { - store.backup(aTxt); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up aTxt"); - } - if (aTxt.exists()) - fail("File not moved to backup - still exists"); - try { - writeToFile(aTxt, "XXXX\n- This file should be restored with A"); - } catch (IOException e) { - e.printStackTrace(); - fail("Could not write a file for testing purposes."); - } - - // backup the empty B directory - try { - store.backup(bDir); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up bDir"); - } - if (bDir.exists()) - fail("Backed up directory was not moved"); + public void testBackupDiscard() throws IOException { + store.backup(aTxt.toFile()); + assertFalse("File not moved to backup - still exists", Files.exists(aTxt)); - // restore - store.discard(); + Files.write(aTxt, "XXXX\n- This file should be restored with A".getBytes()); - // assert discard - assertFileContent("Discard of A failed - not new content", aTxt, "XXXX"); - if (bDir.isDirectory()) - fail("Remove of empty directory not discarded ok"); + store.backup(bDir.toFile()); + assertFalse("Backed up directory was not moved", Files.exists(bDir)); + store.discard(); + assertFileContent("Discard of a.txt failed - not new content", aTxt.toFile(), "XXXX"); + assertFalse("Empty directory not discarded - still exists", Files.isDirectory(bDir)); assertNoGarbage(store); } - public void testBackupAll() { - BackupStore store = new BackupStore(null, BUPREFIX); - // backup and overwrite a.txt - try { - store.backupAll(aDir); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up aDir"); - } - if (aTxt.exists()) - fail("File not moved to backup - still exists"); - if (bTxt.exists()) - fail("File bTxt not moved to backup - still exists"); - - try { - writeToFile(aTxt, "XXXX\n- This file should be restored with A"); - } catch (IOException e) { - e.printStackTrace(); - fail("Could not write a file for testing purposes."); - } - try { - store.restore(); - } catch (IOException e) { - fail("Restore failed"); - } - assertFileContent("A not restored", aTxt, "A"); - assertFileContent("B not restored", bTxt, "B"); + public void testBackupAll() throws IOException { + store.backupAll(aDir.toFile()); + assertFalse("File not moved to backup - still exists", Files.exists(aTxt)); + assertFalse("File bTxt not moved to backup - still exists", Files.exists(bTxt)); + + Files.createDirectories(aTxt.getParent()); + Files.write(aTxt, "XXXX\n- This file should be restored with A".getBytes(), CREATE_NEW); + + store.restore(); + assertFileContent("A not restored", aTxt.toFile(), "A"); + assertFileContent("B not restored", bTxt.toFile(), "B"); assertNoGarbage(store); } - public void testBackupCopyAll() { - BackupStore store = new BackupStore(null, BUPREFIX); - // backup and overwrite a.txt - try { - store.backupCopyAll(aDir); - } catch (IOException e) { - e.printStackTrace(); - fail("IO Exception when backing up aDir"); - } - if (!aTxt.exists()) - fail("File not copied to backup - does not exist"); - if (!bTxt.exists()) - fail("File bTxt not copied to backup - does not exists"); - - try { - writeToFile(aTxt, "XXXX\n- This file should be restored with A"); - writeToFile(bTxt, "XXXX\n- This file should be restored with B"); - } catch (IOException e) { - e.printStackTrace(); - fail("Could not write a file for testing purposes."); - } - try { - store.restore(); - } catch (IOException e) { - fail("Restore failed"); - } - assertFileContent("A not restored", aTxt, "A"); - assertFileContent("B not restored", bTxt, "B"); + public void testBackupCopyAll() throws IOException { + store.backupCopyAll(aDir.toFile()); + assertTrue("File not copied to backup - does not exist", Files.exists(aTxt)); + assertTrue("File bTxt not copied to backup - does not exists", Files.exists(bTxt)); + + Files.write(aTxt, "XXXX\n- This file should be restored with A".getBytes()); + Files.write(bTxt, "XXXX\n- This file should be restored with B".getBytes()); + + store.restore(); + assertFileContent("A not restored", aTxt.toFile(), "A"); + assertFileContent("B not restored", bTxt.toFile(), "B"); assertNoGarbage(store); } - private void assertNoGarbage(BackupStore store) { - File buDir = new File(store.getBackupRoot(), BUPREFIX); - if (buDir.exists()) - fail("Backup directory not cleaned up"); - - // Set roots = store.getBackupRoots(); - // if (roots.size() == 0) - // assertTrue("Root set is empty", true); - // for (Iterator itor = roots.iterator(); itor.hasNext();) { - // File root = (File) itor.next(); - // File buDir = new File(root, BUPREFIX); - // if (buDir.exists()) - // fail("Backup directory not cleaned up"); - // } + private static void assertNoGarbage(SimpleBackupStore store) { + File buDir = store.getBackupRoot(); + assertFalse("Backup directory not cleaned up", buDir.exists()); } } diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/AllTests.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/AllTests.java index 08ffbaf7c..27f0c1618 100644 --- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/AllTests.java +++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/AllTests.java @@ -23,7 +23,7 @@ import org.junit.runners.Suite; @Suite.SuiteClasses({ ChmodActionTest.class, CleanupzipActionTest.class, CollectActionTest.class, LinkActionTest.class, MkdirActionTest.class, NativeTouchpointTest.class, RmdirActionTest.class, UnzipActionTest.class, - CopyActionTest.class, RemoveActionTest.class, BackupStoreTest.class, + CopyActionTest.class, RemoveActionTest.class, SimpleBackupStoreTest.class, CheckAndPromptNativePackageWindowsRegistryTest.class }) public class AllTests { diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/BackupStoreTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/BackupStoreTest.java deleted file mode 100644 index 011c4d3cd..000000000 --- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/BackupStoreTest.java +++ /dev/null @@ -1,189 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2014, 2017 EclipseSource and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * EclipseSource - initial API and implementation - *******************************************************************************/ -package org.eclipse.equinox.p2.tests.touchpoint.natives; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import org.eclipse.equinox.internal.p2.touchpoint.natives.BackupStore; -import org.eclipse.equinox.p2.tests.AbstractProvisioningTest; - -public class BackupStoreTest extends AbstractProvisioningTest { - - private static final String BUPREFIX = "BackupTest"; - private File sourceDir; - private File aDir; - private File aaDir; - private File aTxt; - private File bDir; - private File bTxt; - - /** - * Sets up directories and files under user.home - * <ul><li>P2BUTEST/</li> - * <ul><li>A/</li> - * <ul><li>AA/</li> - * <ul><li>a.txt</li> - * </ul> - * </ul> - * </ul> - * </ul> - */ - @Override - public void setUp() { - // create some test files under user.home - // do not want them under /tmp as it may be on its own file system (and even - // be an in-memory file system). - // - String userHome = System.getProperty("user.home"); - sourceDir = new File(new File(userHome), "P2BUTEST"); - fullyDelete(sourceDir); - aDir = new File(sourceDir, "A"); - aDir.mkdirs(); - aaDir = new File(aDir, "AA"); - aaDir.mkdir(); - aTxt = new File(aaDir, "eclipse.exe"); - bDir = new File(sourceDir, "B"); - bTxt = new File(bDir, "b.txt"); - try { - writeToFile(aTxt, "A\nA file with an A"); - } catch (IOException e) { - fail(); - } - } - - private void writeToFile(File file, String content) throws IOException { - file.getParentFile().mkdirs(); - file.createNewFile(); - - try (Writer writer = new BufferedWriter(new FileWriter(file))) { - writer.write(content); - } - } - - @Override - public void tearDown() { - fullyDelete(sourceDir); - } - - /** - * Deletes a file, or a directory with all of it's children. - * @param file the file or directory to fully delete - * @return true if, and only if the file is deleted - */ - private boolean fullyDelete(File file) { - if (!file.exists()) - return true; - if (file.isDirectory()) { - File[] children = file.listFiles(); - for (File child : children) { - if (!fullyDelete(new File(file, child.getName()))) { - return false; - } - } - } - return file.delete(); - } - - public void testBackupByRenamingFile() { - String filePath = aTxt.getAbsolutePath(); - class TestBackupByRenamingFileBackupStore extends BackupStore { - public TestBackupByRenamingFileBackupStore() { - super(null, BUPREFIX); - } - - @Override - public void renameInPlace(File file) { - super.renameInPlace(file); - } - - @Override - protected String getTimeStamp() { - return "-123"; - } - } - TestBackupByRenamingFileBackupStore backupStore = new TestBackupByRenamingFileBackupStore(); - backupStore.renameInPlace(aTxt); - - assertFalse(aTxt.exists()); - assertTrue(new File(filePath + "-123.p2bu").exists()); - - backupStore.discard(); - assertFalse(new File(filePath + "-123.p2bu").exists()); - } - - public void testRenameIfMoveToBackupFails() throws IOException { - String filePath = aTxt.getAbsolutePath(); - class TestRenameIfMoveToBackupFailsBackupStore extends BackupStore { - public TestRenameIfMoveToBackupFailsBackupStore() { - super(null, BUPREFIX); - } - - @Override - public void renameInPlace(File file) { - super.renameInPlace(file); - } - - @Override - public boolean moveToBackupStore(File a, File b) { - return false; - } - - @Override - public void moveToBackup(File a, File b) throws IOException { - super.moveToBackup(a, b); - } - - @Override - protected String getTimeStamp() { - return "-123"; - } - } - TestRenameIfMoveToBackupFailsBackupStore backupStore = new TestRenameIfMoveToBackupFailsBackupStore(); - backupStore.moveToBackup(aTxt, bTxt); - - assertFalse(aTxt.exists()); - assertTrue(new File(filePath + "-123.p2bu").exists()); - assertFalse(bTxt.exists()); - - backupStore.discard(); - assertFalse(new File(filePath + "-123.p2bu").exists()); - } - - public void testDoNotRenameIfMoveToBackupWorks() throws IOException { - String filePath = aTxt.getAbsolutePath(); - new BackupStore(null, BUPREFIX) { - @Override - public void renameInPlace(File file) { - super.renameInPlace(file); - } - - @Override - public boolean moveToBackupStore(File a, File b) { - return super.moveToBackupStore(a, b); - } - - @Override - public void moveToBackup(File a, File b) throws IOException { - super.moveToBackup(a, b); - } - }.moveToBackup(aTxt, bTxt); - - assertFalse(aTxt.exists()); - assertFalse(new File(filePath + ".p2bu").exists()); - assertTrue(bTxt.exists()); - } -} diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/SimpleBackupStoreTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/SimpleBackupStoreTest.java new file mode 100644 index 000000000..0fa1cda76 --- /dev/null +++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/natives/SimpleBackupStoreTest.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2014, 2021 EclipseSource and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * EclipseSource - initial API and implementation + * Todor Boev - refactor to the java 7 file api + *******************************************************************************/ +package org.eclipse.equinox.p2.tests.touchpoint.natives; + +import static java.nio.file.FileVisitResult.CONTINUE; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import org.eclipse.equinox.internal.p2.touchpoint.natives.SimpleBackupStore; +import org.eclipse.equinox.p2.tests.AbstractProvisioningTest; + +public class SimpleBackupStoreTest extends AbstractProvisioningTest { + private static final String BACKUP_PREFIX = "backup-test"; + + private Path sourceDir; + + private Path aDir; + private Path aaDir; + private Path aFile; + + /** + * <pre> + * /p2-backup-test + * /a + * /aa + * /eclipse.exe + * </pre> + */ + @Override + public void setUp() throws IOException { + String userHome = System.getProperty("user.home"); + + sourceDir = Path.of(userHome, "p2-backup-test"); + deleteAll(sourceDir); + + aDir = sourceDir.resolve("a"); + Files.createDirectories(aDir); + + aaDir = aDir.resolve("aa"); + Files.createDirectories(aaDir); + + // The eclipse.exe is the only one eligible for backup-in-place + aFile = aaDir.resolve("eclipse.exe"); + Files.createFile(aFile); + } + + @Override + public void tearDown() throws IOException { + deleteAll(sourceDir); + } + + private static void deleteAll(Path path) throws IOException { + if (!Files.exists(path)) { + return; + } + + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + Files.delete(dir); + return CONTINUE; + } + }); + } + + public void testBackupInPlace() throws IOException { + class TestMoveInPlaceStore extends SimpleBackupStore { + public TestMoveInPlaceStore() { + super(null, BACKUP_PREFIX); + } + + @Override + public Path toInPlaceBackupPath(Path path) { + return super.toInPlaceBackupPath(path); + } + + @Override + protected void move(Path a, Path b) throws IOException { + // In place backup - allow + if (b.equals(toInPlaceBackupPath(a))) { + super.move(a, b); + } + // In place restore - allow + else if (a.equals(toInPlaceBackupPath(b))) { + super.move(a, b); + } + // Everything else - fail + else { + throw new IOException("Test fail move: " + a + " -> " + b); + } + } + } + + TestMoveInPlaceStore buStore = new TestMoveInPlaceStore(); + + final Path path = aFile; + final Path inPlaceBuPath = buStore.toInPlaceBackupPath(aFile); + + buStore.backup(path.toFile()); + + assertFalse(Files.exists(path)); + assertTrue(Files.exists(inPlaceBuPath)); + + buStore.restore(); + + assertTrue(Files.exists(path)); + assertFalse(Files.exists(inPlaceBuPath)); + } + + public void testNoBackupInPlace() throws IOException { + class TestNoBackupInPlaceStore extends SimpleBackupStore { + public TestNoBackupInPlaceStore() { + super(null, BACKUP_PREFIX); + } + + @Override + public Path toInPlaceBackupPath(Path path) { + return super.toInPlaceBackupPath(path); + } + } + + TestNoBackupInPlaceStore buStore = new TestNoBackupInPlaceStore(); + + final Path path = aFile; + final Path inPlaceBuPath = buStore.toInPlaceBackupPath(aFile); + + buStore.backup(path.toFile()); + + assertFalse(Files.exists(path)); + assertFalse(Files.exists(inPlaceBuPath)); + + buStore.restore(); + + assertTrue(Files.exists(path)); + } +} diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.touchpoint.natives/META-INF/MANIFEST.MF index 21663c044..d5c588998 100644 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/META-INF/MANIFEST.MF @@ -6,13 +6,16 @@ Bundle-Version: 1.4.100.qualifier Bundle-Activator: org.eclipse.equinox.internal.p2.touchpoint.natives.Activator Bundle-Vendor: %providerName Bundle-Localization: plugin -Export-Package: org.eclipse.equinox.internal.p2.touchpoint.natives;x-internal:=true, +Export-Package: + org.eclipse.equinox.internal.p2.touchpoint.natives;x-internal:=true, org.eclipse.equinox.internal.p2.touchpoint.natives.actions;x-internal:=true -Require-Bundle: org.eclipse.equinox.common, +Require-Bundle: + org.eclipse.equinox.common, org.eclipse.equinox.app;bundle-version="1.3.0";resolution:=optional Bundle-RequiredExecutionEnvironment: JavaSE-11 Bundle-ActivationPolicy: lazy -Import-Package: org.eclipse.equinox.internal.p2.core.helpers, +Import-Package: + org.eclipse.equinox.internal.p2.core.helpers, org.eclipse.equinox.internal.p2.engine, org.eclipse.equinox.p2.core;version="[2.0.0,3.0.0)", org.eclipse.equinox.p2.engine;version="[2.0.0,3.0.0)", diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/BackupStore.java b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/BackupStore.java deleted file mode 100644 index 9206eab61..000000000 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/BackupStore.java +++ /dev/null @@ -1,896 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2009, 2018 Cloudsmith Inc. and others. - * - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Cloudsmith Inc. - initial API and implementation - * SAP AG - Ongoing development - *******************************************************************************/ - -package org.eclipse.equinox.internal.p2.touchpoint.natives; - -import java.io.*; -import java.net.*; -import java.util.*; -import java.util.Map.Entry; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.equinox.internal.p2.core.helpers.LogHelper; -import org.eclipse.osgi.util.NLS; - -/** - * Stores files by copying them to a uniquely named temporary directory. - * The BackupStore remembers filenames and can recreate them in their original location. - * - * <h3>Usage</h3> - * The user of this class should instantiate the BackupStore with some prefix that is - * meaningful to a human. Uniqueness is obtained without the prefix - the prefix is used to - * be able to differentiate between different backup directories by a human (in case of crashes etc). - * - * If instantiated with a directory this directory will be used to store the backup root directory. If - * this directory is null, the users home directory is used by default. - * - * Once instantiated, use the {@link #backup(File)} and {@link #backupDirectory(File)} methods - * to move files to backup instead of deleting them. A file that - * is backed up should not be deleted - it is simply moved out of the way. - * Use {@link #backupCopy(File)} to - * move the file out of harms way, but keep a copy of it in the original location. - * The methods {@link #backupAll(File)} and {@link #backupCopyAll(File)} backs up an entire structure. - * - * When backup is finished - the user should either call {@link #restore()} to put all - * of the files back, or call {@link #discard()} to remove all of the backed up "copies". - * - * If {@link #restore()} or {@link #discard()} is not called the backup files will never be deleted. - * - * The backup store does not synchronize directories - actions that write new files are - * responsible for removing them. Overwriting existing files should be done by first backing - * up the file, and then creating a new file. Modifying a file, should be done by - * using {@link #backupCopy(File)} or - * first making a copy, then backing up the original, and then renaming the copy. - * - * <h3>Read Only and Permissions</h3> - * Directories that are read only (to current user) can not be backed up. - * Backup is performed using {@link File#renameTo(File)} and handling of permissions - * is operating system dependent. It is expected that a Un*x type system retains the - * permissions as a file is moved to the backup store and later gets restored. - * Backup directories are created as they are needed and will (at least on Un*x) inherit the - * permissions from its parent directory. - * - * If a rename can not be performed, the backup store will make a copy and delete the original - * file. This makes it possible to backup and restore across volume boundaries. - * - * When restoring directories they - * will be created with permissions in a platform specific way (on UN*IX they will inherit the permissions - * of the parent directory). - * - * <h3>Checkpointing</h3> - * Checkpointing (i.e. to be able to rollback to a particular point) can be implemented by using - * multiple instances of BackupStore. The client code will need to remember the individual order - * among the backup stores. - * - * <h3>Restartability</h3> - * Not implemented - it is possible to obtain the name of the backup directories, - * so manual restore is possible after a crash. An idea is to add persistence to a file, and - * be able to read it back in again. - * - * <h3>A note about exceptions</h3> - * In general {@link IllegalArgumentException} is thrown when attempting an operation - * that is considered "wrong use", and an {@link IllegalStateException} or subclass thereof is thrown on an overall - * wrong use of BackupStore (i.e. attempt to backup when store has been restored). Some cases of - * "wrong use" can not be differentiated from I/O errors (like a "file not found" as this could - * be caused by an entire disk disappearing - in these case an {@link IOException} is thrown. - * - * <h3>Implementation Note</h3> - * The backup root directory will contain folders that reflects file system roots. These are encoded using - * "_" for the UNI*X root directory, "__" for a Windows network mounted directory, and single "drive letter" folders - * corresponding to Windows drive letters. Typically, on UN*X there will only be a "_" directory in the backup root, - * and on windows there will typically be a single directory called "C". - * - * - */ -public class BackupStore implements IBackupStore { - - private static final String BACKUP_FILE_EXTENSION = ".p2bu"; //$NON-NLS-1$ - - /** - * The name to use for a directory that represents leading separator (i.e. "/" or "\"). - */ - private static final String ROOTCHAR = "_"; //$NON-NLS-1$ - - /** - * Map of directory File to backup root (File) - the backup root has - * a directory named {@link #backupName} where the backup is found. - */ - // private Map backups = new HashMap(); - private final File backupRoot; - - /** - * The name of the backup directory (no path - relative to the backup root). - */ - private final String backupName; - - /** - * The name of a dummy file used to backup empty directories - */ - private final String dummyName; - - /** - * A server socket that is used to obtain a port (a shared resource on this machine) - * and thus create a unique number. Used as part of the unique id of backup directories - * and probe files. - */ - private ServerSocket socket = null; - - /** - * Counter of how many files where backed up. Used as a simple check mechanism if - * everything was restored (a guard against manual/external tampering with the backup directories). - */ - private long backupCounter; - - /** - * Counter of how many files where restored. See {@link #backupCounter}. - */ - private long restoreCounter; - - /** - * Flag indicating if this BackupStore has been restored or canceled. - */ - private boolean closed; - - private final Map<String, String> renamedInPlace = new HashMap<>(); - - /** - * Generates a BackupStore with a default prefix of ".p2bu" for backup directory and - * probe file. - * The full id of the store is on the format "prefix_hextime_hexIPport" - * - see {@link #genUnique()} for more info. - */ - public BackupStore() { - this(null, BACKUP_FILE_EXTENSION); - } - - /** - * Generates a BackupStore with a specified prefix for backup directories and - * probe file. - * The full id of the store is on the format "prefix_hextime_hexipport" - * - see {@link #genUnique()} for more info. - * - * @param buParentDirectory - name of directory where the backup directory should be created - if null, - * java.io.tmpdir is used - * @param prefix - prefix used for human identification of backup directories - */ - public BackupStore(File buParentDirectory, String prefix) { - if (buParentDirectory == null) { - buParentDirectory = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$ - } - backupRoot = buParentDirectory; - - // generate a name for the backup store and the dummy file used for empty directories - String unique = genUnique(); - dummyName = prefix + "d_" + unique; //$NON-NLS-1$ - backupName = prefix + "_" + unique; //$NON-NLS-1$ - backupCounter = 0; - restoreCounter = 0; - closed = false; - } - - /** - * Since a socket port is used to create a unique number, the socket - * must be closed if this instance is garbage collected and the user - * of the instance has not either restored or discarded. - */ - @Override - protected void finalize() throws Throwable { - try { - if (socket != null && !socket.isClosed()) { - socket.close(); - } - } finally { - super.finalize(); - } - } - - /** - * Returns the unique backup name (this is the name of generated backup directories). - * - * @return the backup name. - */ - @Override - public String getBackupName() { - return backupName; - } - - public File getBackupRoot() { - return backupRoot; - } - - /** - * Backup the file by moving it to the backup store (for later (optional) restore). - * Calling this method with a file that represents a directory is equivalent to calling - * {@link #backupDirectory(File)}. - * - * A file (path) can only be backed up once per BackupStore instance. - * When the file is backed up, it is moved to a directory under this BackupStore instance's directory - * with a relative path corresponding to the original relative path from the backup root e.g. - * the file /A/B/C/foo.txt could be moved to /A/.p2bu_ffffff_ffffff/B/C/foo.txt when /A is the - * backup root. - * - * If a directory is first backed up, and later replaced by a regular file, and this file - * is backed up (or vice versa) - an {@link IllegalArgumentException} is thrown - * - * A backup can not be performed on a closed BackupStore. - * - * @param file - the file (or directory) to backup - * @return true if the file was backed up, false if this file (path) has already been backed up (the file is not - * moved to the store). - * @throws IOException - if the backup operation fails, or the file does not exist - * @throws ClosedBackupStoreException - if the BackupStore has been closed - * @throws IllegalArgumentException - on type mismatch (file vs. directory) of earlier backup, or if file does not - * exist - */ - @Override - public boolean backup(File file) throws IOException { - if (closed) { - throw new ClosedBackupStoreException("Can not perform backup()"); //$NON-NLS-1$ - } - if (!file.exists()) { - throw new IOException(NLS.bind(Messages.BackupStore_file_not_found, file.getAbsolutePath())); - } - if (file.isDirectory()) { - return backupDirectory(file); - } - file = makeParentCanonical(file); - File buFile = getBackupFile(file); - // already backed up, but was a directory = wrong usage - if (buFile.isDirectory()) { - throw new IllegalArgumentException( - NLS.bind(Messages.BackupStore_directory_file_mismatch, buFile.getAbsolutePath())); - } - // has already been backed up - can only be done once with one BackupStore - if (buFile.exists()) { - // although backed up, the file can be still on the file system when, for example, - // two IUs are unzipping their contents to the same location and share a few common file, - // which have to be removed twice - if (file.exists() && !file.delete()) { - throw new IOException(NLS.bind(Messages.BackupStore_can_not_remove, file.getAbsolutePath())); - } - return false; - } - - moveToBackup(file, buFile); - - return true; - } - - /** - * Move/rename file to a backup file. Callers of the method must have ensured that the source file exists and the - * backup file has not been created yet. - * - * @param file source file to move; should already exist and must not be directory - * @param buFile destination backup file to move to; should not exist and must be a directory - * @throws IOException if the backup operation fails - */ - protected void moveToBackup(File file, File buFile) throws IOException { - // make sure all of the directories exist / gets created - buFile.getParentFile().mkdirs(); - if (buFile.getParentFile().exists() && !buFile.getParentFile().isDirectory()) { - throw new IllegalArgumentException( - NLS.bind(Messages.BackupStore_file_directory_mismatch, buFile.getParentFile().getAbsolutePath())); - } - if (moveToBackupStore(file, buFile)) { - backupCounter++; - return; - } - // could not move - this can happen because source and target are on different volumes, or - // that source is locked "in use" on a windows machine. The copy will work across volumes, - // but the locked file will fail on the subsequent delete. - // - // Rename in place - if (isEclipseExe(file)) { - renameInPlace(file); - } else { - Util.copyStream(new FileInputStream(file), true, new FileOutputStream(buFile), true); - backupCounter++; - } - - // File.exists() is not reliable so always attempt to delete first and check why it may have failed second. - if (!file.delete() && file.exists()) { - throw new IOException(NLS.bind(Messages.BackupStore_can_not_delete_after_copy_0, file)); - } - } - - private boolean isEclipseExe(File file) { - String launcher = System.getProperty("eclipse.launcher"); //$NON-NLS-1$ - if (launcher != null) { - String base = new File(launcher).getName(); - if (file.getName().equalsIgnoreCase(base)) { - return true; - } - } - return file.getName().equalsIgnoreCase("eclipse.exe") || file.getName().equalsIgnoreCase("eclipsec.exe"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - protected boolean moveToBackupStore(File file, File buFile) { - if (file.renameTo(buFile)) { - // if the original file still exists, we have a problem. - if (!file.exists()) { - return true; - } - // If the renamed work, but the file still exists, remove the backup - // and return false - if (buFile.exists()) { - buFile.delete(); - } - } - return false; - } - - protected void renameInPlace(File file) { - String newName = file.getAbsolutePath() + getTimeStamp() + BACKUP_FILE_EXTENSION; - renamedInPlace.put(file.getAbsolutePath(), newName); - file.renameTo(new File(newName)); - } - - protected String getTimeStamp() { - return "-" + new Date().getTime(); //$NON-NLS-1$ - } - - private File getBackupFile(File file) { - File buRoot = backupRoot; - File buDir = new File(buRoot, backupName); - // create the relative path from root and use that in buDir - File buFile = new File(buDir, makeRelativeFromRoot(file).getPath()); - return buFile; - } - - /** - * Backs up a file, or everything under a directory. - * - * @param file - file to backup or directory - * @throws IOException if backup operation failed - */ - @Override - public void backupAll(File file) throws IOException { - if (!file.exists()) { - return; - } - file = makeParentCanonical(file); - if (file.isDirectory()) { - File[] files = file.listFiles(); - if (files != null) { - for (File f : files) { - backupAll(f); - } - } - } - backup(file); - } - - /** - * Backs up a file, or everything under a directory. - * A copy of the backup is left in the original place. - * - * @param file - * @throws IOException - */ - @Override - public void backupCopyAll(File file) throws IOException { - if (!file.exists()) { - return; - } - file = makeParentCanonical(file); - if (file.isDirectory()) { - File[] files = file.listFiles(); - if (files != null) { - for (File f : files) { - backupCopyAll(f); - } - } - // if directory was empty, it needs to be backed up and then recreated - // - if (files == null || files.length == 0) { - backupDirectory(file); - file.mkdir(); - } - } else { - backupCopy(file); - } - } - - /** - * Backup the file by moving it to the backup store (for later (optional) restore) but leaving - * a copy of the contents in the original location. - * Calling this method with a file that represents a directory throws an {@link IllegalArgumentException}. - * - * A file (path) can only be backed up once per BackupStore instance. - * When the file is backed up, it is moved to a directory under this BackupStore instance's directory - * with a relative path corresponding to the original relative path from the backup root e.g. - * the file /A/B/C/foo.txt could be moved to /A/.p2bu_ffffff_ffffff/B/C/foo.txt when /A is the - * backup root. - * - * If a directory is first backed up, and later replaced by a regular file, and this file - * is backed up (or vice versa) - an {@link IllegalArgumentException} is thrown - * - * A backup can not be performed on a closed BackupStore. - * - * @param file - the file (or directory) to backup - * @return true if the file was backed up, false if this file (path) has already been backed up (the file is not - * moved to the store). - * @throws IOException - if the backup operation fails, or the file does not exist - * @throws ClosedBackupStoreException - if the BackupStore has been closed - * @throws IllegalArgumentException - on type mismatch (file vs. directory) of earlier backup, or if file is a - * Directory - */ - @Override - public boolean backupCopy(File file) throws IOException { - if (closed) { - throw new ClosedBackupStoreException(Messages.BackupStore_backupCopy_closed_store); - } - if (!file.exists()) { - throw new IOException(NLS.bind(Messages.BackupStore_file_not_found, file.getAbsolutePath())); - } - if (file.isDirectory()) { - throw new IllegalArgumentException( - NLS.bind(Messages.BackupStore_can_not_copy_directory, file.getAbsolutePath())); - } - file = makeParentCanonical(file); - // File buRoot = backupRoot; - // File buRoot = findBackupRoot(file); - File buDir = new File(backupRoot, backupName); - // move the file - // create the relative path from root and use that in buDir - File buFile = new File(buDir, makeRelativeFromRoot(file).getPath()); - // already backed up, but was a directory = wrong usage - if (buFile.isDirectory()) { - throw new IllegalArgumentException( - NLS.bind(Messages.BackupStore_directory_file_mismatch, buFile.getAbsolutePath())); - } - // has already been backed up - can only be done once with one BackupStore - if (buFile.exists()) { - return false; - } - - // make sure all of the directories exist / gets created - buFile.getParentFile().getCanonicalFile().mkdirs(); - if (buFile.getParentFile().exists() && !buFile.getParentFile().isDirectory()) { - throw new IllegalArgumentException( - NLS.bind(Messages.BackupStore_file_directory_mismatch, buFile.getParentFile().getAbsolutePath())); - } - - // just make a copy - one has to be made in one direction anyway - // A renameTo followed by a copy is preferred as it preserves file permissions on the moved file - // but it is easier to just copy and keep original. - Util.copyStream(new FileInputStream(file), true, new FileOutputStream(buFile), true); - backupCounter++; - return true; - } - - /** - * Performs backup of an empty directory. The directory must be empty before it can be backed up (i.e. - * similar to a delete of a directory). Backup the files of the directory first. - * A call to backup a directory is really only needed for empty directories as a restore - * of a file will also restore all of its parent directories. - * - * @param file - the (empty) directory to back up - * @return true if the directory was moved to backup. false if the directory was already backed up - * @throws IllegalArgumentException if file is not a directory, or is not empty. - * @throws IOException if directory can not be moved to the backup store, or if the directory is not - * writeable - */ - @Override - public boolean backupDirectory(File file) throws IOException { - if (!file.isDirectory()) { - throw new IllegalArgumentException(NLS.bind(Messages.BackupStore_not_a_directory, file.getAbsolutePath())); - } - file = makeParentCanonical(file); - if (file.list().length != 0) { - throw new IllegalArgumentException( - NLS.bind(Messages.BackupStore_directory_not_empty, file.getAbsolutePath())); - } - // the easiest way is to create a dummy file and back that up (the dummy is simply ignored when restoring). - File dummy = new File(file, dummyName); - dummy = makeParentCanonical(dummy); - File buFile = getBackupFile(dummy); - boolean backedUp = buFile.exists(); - // backup only if the folder has not been already backed up; - // this can happen if, for example, two IUs unzip to the same folder and then want to delete it - if (!backedUp) { - if (closed) { - throw new ClosedBackupStoreException("Can not perform backup()"); //$NON-NLS-1$ - } - if (!dummy.createNewFile()) { - throw new IOException(NLS.bind(Messages.BackupStore_can_not_create_dummy, dummy.getAbsolutePath())); - } - moveToBackup(dummy, buFile); - } - // previous checks have verified that the directory exists - if (!file.delete()) { - throw new IOException(NLS.bind(Messages.BackupStore_can_not_remove, dummy.getAbsolutePath())); - } - // will return true if the directory was already backed up at the beginning of the operation and false otherwise - return !backedUp; - } - - /** - * Restores all backup files from backup store. - * Note that restore of a (non directory) file deletes an existing file or directory found - * in the restore location. - * When the backup has been restored this BackupStore instance is closed and can not be - * used for further backup or restore. - * - * If there are unrestorable items (non writable directories, or general IO exceptions) these items - * are written to the log, and the backup copies remain in the file system and can be manually restored - * (using a simple zip of the backup directory, and an unzip to the buRoot once the problem has been corrected). - * - * @throws IOException if the backup was not fully restored - unrestored items have been logged. - * @throws ClosedBackupStoreException if the backup is already closed. - */ - @Override - public void restore() throws IOException { - if (closed) { - throw new ClosedBackupStoreException(Messages.BackupStore_restore_closed_store); - } - // put back all files - // collect things that could not be restored (so final status can be reported) - Set<File> unrestorable = new HashSet<>(); - boolean restored = true; - if (!backupRoot.exists()) { - logError(NLS.bind(Messages.BackupStore_missing_backup_directory, backupRoot.getAbsolutePath())); - restored = false; - } else { - restoreRoots(new File(backupRoot, backupName), unrestorable); - } - - logUnrestorables(unrestorable); - if (unrestorable.size() > 0) { - restored = false; - } - close(restored); - closed = true; - } - - private void logUnrestorables(Set<File> unrestorable) { - // if there are unrestorable units log them - // - if (unrestorable != null && unrestorable.size() > 0) { - for (File file : unrestorable) { - logError(NLS.bind(Messages.BackupStore_manual_restore_needed, file.getAbsolutePath())); - } - } - } - - /** - * Discards and closes this BackupStore. Does nothing if this store is already - * restored or discarded. - */ - @Override - public void discard() { - if (closed) { - return; - } - closeSocket(); - removeBackups(); - closed = true; - } - - private void close(boolean fullyRestored) throws IOException { - closeSocket(); - // check external tampering with backup store - if (backupCounter != restoreCounter) { - if (!fullyRestored) { - logError(NLS.bind(Messages.BackupStore_0_of_1_items_restored, Long.valueOf(restoreCounter), - Long.valueOf(backupCounter))); - } else { - logError(NLS.bind(Messages.BackupStore_externally_modified_0_of_1_restored, - Long.valueOf(restoreCounter), Long.valueOf(backupCounter))); - fullyRestored = false; - } - } - if (!fullyRestored) { - throw new IOException(Messages.BackupStore_errors_while_restoring_see_log); - } - // everything has been restored - the backup can now be removed - removeBackups(); - } - - private void closeSocket() { - if (socket != null && !socket.isClosed()) { - try { - socket.close(); - } catch (IOException e) { /* ignored */ - logWarning( - NLS.bind(Messages.BackupStore_can_not_close_tcp_port, Integer.valueOf(socket.getLocalPort()))); - } - } - } - - private void removeBackups() { - File buRoot = new File(backupRoot, backupName); - if (!fullyDelete(buRoot)) { - logWarning(NLS.bind(Messages.BackupStore_can_not_remove_bu_directory, buRoot.getAbsolutePath())); - } - for (String newName : renamedInPlace.values()) { - File buFile = new File(newName); - if (!fullyDelete(buFile)) { - logWarning(NLS.bind(Messages.BackupStore_can_not_remove_bu_file, buRoot.getAbsolutePath())); - } - } - } - - private static void logWarning(String message) { - LogHelper.log(createWarning(message)); - } - - private static IStatus createWarning(String message) { - return new Status(IStatus.WARNING, Activator.ID, message); - } - - private static void logError(String message) { - LogHelper.log(createError(message)); - } - - private static IStatus createError(String message) { - return new Status(IStatus.ERROR, Activator.ID, message); - } - - /** - * Deletes a file, or a directory with all of it's children. - * - * @param file the file or directory to fully delete - * @return true if, and only if the file is deleted without errors - */ - private boolean fullyDelete(File file) { - if (file.isDirectory()) { - File[] children = file.listFiles(); - if (children != null) { - for (File child : children) { - // we will not stop even if some deletion failed - fullyDelete(new File(file, child.getName())); - } - } - } - // will attempt to delete before exists check to get rid of dead links - if (file.delete()) { - return true; - } - // will return true if files does not actually exist even delete fails - return !file.exists(); - } - - private void restore(File root, File buRoot, Set<File> unrestorable) { - File[] children = buRoot.listFiles(); - if (children == null) { // error - can't read the backup directory - unrestorable.add(buRoot); - return; - } - for (File child : children) { - File bu = new File(buRoot, child.getName()); - File target = new File(root, bu.getName()); - if (bu.isDirectory()) { - if (!target.exists() && !target.mkdir()) { - unrestorable.add(bu); - continue; // give up on this branch - } - if (target.exists() && !target.isDirectory()) { - // ouch, there is a file where we need a directory - // that must be deleted. - target.delete(); - if (!target.mkdir()) { - unrestorable.add(bu); - continue; // give up on branch - } - } - restore(target, bu, unrestorable); - } else { - // do not restore the dummies (as they are used to trigger creation of - // empty directories and are not wanted in the restored location. - if (bu.getName().equals(dummyName)) { - restoreCounter++; // count of the restored directory in this case. - continue; - } - // if the original was overwritten by something and this file was not - // removed, it needs to be deleted now. If it can't be deleted, the - // renameTo will fail, and the bu is reported as not restorable. - // fullyDelete will remove a directory completely - we are restoring a file so it can - // not be kept. - if (target.exists()) { - fullyDelete(target); - } - - // rename if possible, but must copy if not possible to just rename - if (!bu.renameTo(target)) { - // did not work to rename, probably because of volume boundaries. Try to copy instead, - try { - Util.copyStream(new FileInputStream(bu), true, new FileOutputStream(target), true); - restoreCounter++; // consider it restored - } catch (FileNotFoundException e) { - unrestorable.add(bu); - continue; - } catch (IOException e) { - unrestorable.add(bu); - continue; - } - if (!bu.delete()) { // cleanup - // could not remove the backup after copy - log, safe to remove manually - logWarning(NLS.bind(Messages.BackupStore_can_not_delete_tmp_file, bu.getAbsolutePath())); - } - } else { - restoreCounter++; - } - } - } - } - - /** - * Restores everything backed up in the buRoot. Responsible for decoding the specially named root - * target directories (i.e. _/, __/, C/, etc.) into the real system names. - * - * @param buRoot - * @param unrestorable - */ - private void restoreRoots(File buRoot, Set<File> unrestorable) { - File[] children = buRoot.listFiles(); - if (children == null) { // error - can't read the backup directory - unrestorable.add(buRoot); - return; - } - for (File child : children) { - // Names are root-chars, or drive letters in the root bu directory - String name = child.getName(); - String rName = name; - String prefix = ""; //$NON-NLS-1$ - while (rName.startsWith(ROOTCHAR)) { - prefix += File.separator; - rName = rName.substring(1); - } - if (prefix.length() < 1) { - // The name is a drive name - rName = rName + ":" + File.separator; //$NON-NLS-1$ - } else { - rName = prefix + rName; - } - // File root = new File(rName); - File bu = new File(buRoot, name); - File target = new File(rName); - if (!bu.isDirectory()) { - // the roots should all be directories - so this can only happen if someone manually - // stored files in the backup root - mark them as unrestorable and continue. - unrestorable.add(bu); - continue; - } - // the backup roots are system roots, and can not be created - but check root is directory and exists. - // (Network drives could have gone away etc). - // - if (!(target.exists() && target.isDirectory())) { - unrestorable.add(bu); - continue; // give up on this branch - } - // then perform a recursive restore - restore(target, bu, unrestorable); - } - restoreRenamedFiles(unrestorable); - } - - private void restoreRenamedFiles(Set<File> unrestorable) { - for (Entry<String, String> entry : renamedInPlace.entrySet()) { - File bu = new File(entry.getValue()); - if (!bu.renameTo(new File(entry.getKey()))) { - unrestorable.add(bu); - } - } - } - - private static long msCounter = 0; - - /** - * Generates a unique hex string by taking currentTimeMillis + sequence - * number at the end allowing for 32 numbers to be generated per ms. - * This is sufficient uniqueness in the same VM. (And is still just a fallback solution - * if there is no access to a TCP port) - * - * To make number unique over multiple VMs - the PID of the process would be enough, but - * it is complicated to get hold of - a separate program must be launched and its PPID - * investigated. There is no standard API in Java to get the PID. Instead, a socket port is bound - * to ensure local uniqueness. - * - * To make number unique across multiple hosts (we may be provisioning over NFS), the - * 48 LS bits of the IP address is used (this is more than enough for an IPv4 address). - * (If there is no IP address, the machine is not on a - * network) - unfortunately the MAC address can not be used as this requires Java 6 (where - * there also is a UUID that should be used instead of this method). - * - * This method needs to be modified when IPv6 addressing is the norm - at that time, the - * restriction on Java 1.4 has hopefully been lifted, and it is possible to use the MAC address, - * or the UUID provided since java 1.6 - * - * @return a unique string - */ - private String genUnique() { - // use 5 LSB bits for counter within ms - i.e. 32 instances can be created - // per millisecond. - long timePart = (System.currentTimeMillis() << 5) | (msCounter++ & 31); - // can't use the MAC address - but take IP address if provisioning across NFS - long ipPart = 0; - try { - // the returned address can be 32 bits IPv4, or 128 bits IPv6 (?) - // In any case use the LSB bits (as many as will fit - byte[] address = InetAddress.getLocalHost().getAddress(); - for (byte element : address) { - ipPart = ((ipPart << 8) | (element & 0xff)); - } - } catch (UnknownHostException e) { - // there is no IP address, and there and hence no concurrency from other machines. - // use the default ip part 0 - } - int port = 0; - try { - // TODO: this should be replaced by InetAddress.getLoopbackAddress() when 1.7 compatibility is OK - // on a system where solely IPv6 is available this address resolution will fail: - socket = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); //$NON-NLS-1$ - port = socket.getLocalPort(); - } catch (IOException e) { - try { - if (socket != null) { - socket.close(); - } - } catch (IOException e1) { // ignore failure to close - - } - // use a random number as port in this case - port = new Random().nextInt() & 0xffff; - } - // port is never > 0xffff - long aPart = (ipPart << 16) | (port & 0xffff); - return Long.toHexString(timePart) + "_" + Long.toHexString(aPart); //$NON-NLS-1$ - - } - - /** - * Turns a file into a "relativized" absolute file. - * A leading "root" is transformed to the ROOTCHAR character. On Windows, network mapped drives starts - * with two separators - and are encoded as two ROOTCHAR. - * e.g. - * \\Host\C$\File becomes __\Host\C$\File - * /users/test/file becomes _/users/test/file - * C:/somewhere/file becomes C/somewhere/file - * - * @param file - * @return a relativized absolute abstract file - */ - private File makeRelativeFromRoot(File file) { - File absolute = file.getAbsoluteFile(); - String path = absolute.getPath(); - String prefix = ""; //$NON-NLS-1$ - while (path.startsWith(File.separator)) { - prefix += ROOTCHAR; - path = path.substring(1); - } - if (prefix.length() > 0) { - path = prefix + File.separator + path; - return new File(path); - } - // it is a windows drive letter first. - // Transform C:/foo to C/foo - // - int idx = path.indexOf(":"); //$NON-NLS-1$ - if (idx < 1) { - throw new InternalError("File is neither absolute nor has a drive name: " + path); //$NON-NLS-1$ - } - path = path.substring(0, idx) + path.substring(idx + 1); - - return new File(path); - } - - /** - * The parent path may include ".." as a directory name - this must be made canonical. But if the file itself is - * a symbolic link, it should not be resolved. - */ - private File makeParentCanonical(File file) throws IOException { - return new File(file.getParentFile().getCanonicalFile(), file.getName()); - } -} diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/LazyBackupStore.java b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/LazyBackupStore.java index 9ea5d5f56..f0aae9319 100644 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/LazyBackupStore.java +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/LazyBackupStore.java @@ -21,7 +21,7 @@ import java.io.IOException; * when needed. */ public class LazyBackupStore implements IBackupStore { - private BackupStore delegate; + private SimpleBackupStore delegate; private final String prefix; /** @@ -61,7 +61,7 @@ public class LazyBackupStore implements IBackupStore { private void loadDelegate() { if (delegate != null) return; - delegate = new BackupStore(null, prefix); + delegate = new SimpleBackupStore(null, prefix); } @Override diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Messages.java b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Messages.java index 87e0a837e..9a3bd9c03 100644 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Messages.java +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Messages.java @@ -26,10 +26,10 @@ public class Messages extends NLS { } public static String BackupStore_0_of_1_items_restored; - public static String BackupStore_backupCopy_closed_store; - public static String BackupStore_can_not_close_tcp_port; + public static String BackupStore_externally_modified_0_of_1_restored; + public static String BackupStore_errors_while_restoring_see_log; public static String BackupStore_can_not_copy_directory; - public static String BackupStore_can_not_create_dummy; + public static String BackupStore_can_not_create_placeholder; public static String BackupStore_can_not_delete_after_copy_0; public static String BackupStore_can_not_delete_tmp_file; public static String BackupStore_can_not_remove; @@ -37,14 +37,12 @@ public class Messages extends NLS { public static String BackupStore_can_not_remove_bu_file; public static String BackupStore_directory_file_mismatch; public static String BackupStore_directory_not_empty; - public static String BackupStore_errors_while_restoring_see_log; - public static String BackupStore_externally_modified_0_of_1_restored; public static String BackupStore_file_directory_mismatch; public static String BackupStore_file_not_found; public static String BackupStore_manual_restore_needed; public static String BackupStore_missing_backup_directory; public static String BackupStore_not_a_directory; - public static String BackupStore_restore_closed_store; + public static String BackupStore_closed_store; public static String BlockMacUpdate_0; public static String BlockMacUpdate_1; diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/SimpleBackupStore.java b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/SimpleBackupStore.java new file mode 100644 index 000000000..07bf4f9c5 --- /dev/null +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/SimpleBackupStore.java @@ -0,0 +1,894 @@ +/******************************************************************************* + * Copyright (c) 2009, 2018 Cloudsmith Inc. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Cloudsmith Inc. - initial API and implementation + * SAP AG - Ongoing development + *******************************************************************************/ + +package org.eclipse.equinox.internal.p2.touchpoint.natives; + +import static java.nio.file.FileVisitResult.CONTINUE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.stream.Collectors.joining; +import static org.eclipse.equinox.internal.p2.touchpoint.natives.Util.logError; +import static org.eclipse.equinox.internal.p2.touchpoint.natives.Util.logWarning; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import org.eclipse.osgi.util.NLS; + +/** + * Stores files by moving them to a uniquely named temporary directory. + * + * TheBackupStore remembers filenames and can recreate them in their original + * location. + * + * <h3>Usage</h3> The user of this class should instantiate the BackupStore with + * some prefix that is meaningful to a human. Uniqueness is obtained without the + * prefix - the prefix is used to be able to differentiate between different + * backup directories by a human (in case of crashes etc). + * + * If instantiated with a directory this directory will be used to store the + * backup root directory. If this directory is null, the users home directory is + * used by default. + * + * Once instantiated, use the {@link #backup(File)} and + * {@link #backupDirectory(File)} methods to move files to backup instead of + * deleting them. A file that is backed up should not be deleted - it is simply + * moved out of the way. Use {@link #backupCopy(File)} to move the file out of + * harms way, but keep a copy of it in the original location. The methods + * {@link #backupAll(File)} and {@link #backupCopyAll(File)} backs up an entire + * structure. + * + * When backup is finished - the user should either call {@link #restore()} to + * put all of the files back, or call {@link #discard()} to remove all of the + * backed up "copies". + * + * If {@link #restore()} or {@link #discard()} is not called the backup files + * will never be deleted. + * + * The backup store does not synchronize directories - actions that write new + * files are responsible for removing them. Overwriting existing files should be + * done by first backing up the file, and then creating a new file. Modifying a + * file, should be done by using {@link #backupCopy(File)} or first making a + * copy, then backing up the original, and then renaming the copy. + * + * <h3>Read Only and Permissions</h3> Directories that are read only (to current + * user) can not be backed up. Backup is performed using + * {@link File#renameTo(File)} and handling of permissions is operating system + * dependent. It is expected that a Un*x type system retains the permissions as + * a file is moved to the backup store and later gets restored. Backup + * directories are created as they are needed and will (at least on Un*x) + * inherit the permissions from its parent directory. + * + * If a rename can not be performed, the backup store will make a copy and + * delete the original file. This makes it possible to backup and restore across + * volume boundaries. + * + * When restoring directories they will be created with permissions in a + * platform specific way (on UN*IX they will inherit the permissions of the + * parent directory). + * + * <h3>Checkpointing</h3> Checkpointing (i.e. to be able to rollback to a + * particular point) can be implemented by using multiple instances of + * BackupStore. The client code will need to remember the individual order among + * the backup stores. + * + * <h3>Restartability</h3> Not implemented - it is possible to obtain the name + * of the backup directories, so manual restore is possible after a crash. An + * idea is to add persistence to a file, and be able to read it back in again. + * + * <h3>A note about exceptions</h3> In general {@link IllegalArgumentException} + * is thrown when attempting an operation that is considered "wrong use", and an + * {@link IllegalStateException} or subclass thereof is thrown on an overall + * wrong use of BackupStore (i.e. attempt to backup when store has been + * restored). Some cases of "wrong use" can not be differentiated from I/O + * errors (like a "file not found" as this could be caused by an entire disk + * disappearing - in these case an {@link IOException} is thrown. + * + * <h3>Implementation Note</h3> The backup root directory will contain folders + * that reflects file system roots. These are encoded using "_" for the UNI*X + * root directory, "__" for a Windows network mounted directory, and single + * "drive letter" folders corresponding to Windows drive letters. Typically, on + * UN*X there will only be a "_" directory in the backup root, and on windows + * there will typically be a single directory called "C". + */ +public class SimpleBackupStore implements IBackupStore { + public static final String BACKUP_FILE_EXTENSION = "p2bu"; //$NON-NLS-1$ + + public static final String DIR_PLACEHOLDER = "emptydir"; //$NON-NLS-1$ + + /** + * The name to use for a directory that represents leading separator (i.e. "/" + * or "\"). + */ + private static final String ROOTCHAR = "_"; //$NON-NLS-1$ + + /** + * Map of directory File to backup root (File) - the backup root has a directory + * named {@link #buStoreName} where the backup is found. + */ + private final Path buStoreRoot; + + private String buInPlaceSuffix; + + /** + * Backup files that sit next to the original rather than in the backup store. + */ + private List<Path> buInPlace; + + /** + * Counter of how many files where backed up. Used as a simple check mechanism + * if everything was restored (a guard against manual/external tampering with + * the backup directories). + */ + private long backupCounter; + + /** + * Counter of how many files where restored. See {@link #backupCounter}. + */ + private long restoreCounter; + + /** + * Flag indicating if this BackupStore has been restored or canceled. + */ + private boolean closed; + + /** + * Generates a BackupStore with a default prefix of ".p2bu" for backup directory + * and probe file. + */ + public SimpleBackupStore() { + this(null, "." + BACKUP_FILE_EXTENSION); //$NON-NLS-1$ + } + + /** + * Generates a BackupStore with a specified prefix for backup directories and + * probe file. + * + * @param buStoreParent Parent under which the backup store will be created. If + * null, java.io.tmpdir is used + * @param prefix Prefix used for human identification of backup stores. + */ + public SimpleBackupStore(File buStoreParent, String prefix) { + String unique = UUID.randomUUID().toString(); + + String buStoreName = prefix + "_" + unique; //$NON-NLS-1$ + this.buStoreRoot = (buStoreParent != null) ? buStoreParent.toPath().resolve(buStoreName) + : Paths.get(System.getProperty("java.io.tmpdir")).resolve(buStoreName); //$NON-NLS-1$ + + this.buInPlaceSuffix = String.format("-%s.%s", unique, BACKUP_FILE_EXTENSION); //$NON-NLS-1$ + this.buInPlace = new ArrayList<>(); + } + + /** + * Returns the unique backup name (this is the name of generated backup + * directories). + * + * @return the backup name. + */ + @Override + public String getBackupName() { + return buStoreRoot.getFileName().toString(); + } + + /** + * @return the parent dire under which backups are created + */ + public File getBackupRoot() { + return buStoreRoot.toFile(); + } + + /** + * Backup the file by moving it to the backup store (for later (optional) + * restore). Calling this method with a file that represents a directory is + * equivalent to calling {@link #backupDirectory(File)}. + * + * A file (path) can only be backed up once per BackupStore instance. When the + * file is backed up, it is moved to a directory under this BackupStore + * instance's directory with a relative path corresponding to the original + * relative path from the backup root e.g. the file /A/B/C/foo.txt could be + * moved to /A/.p2bu_ffffff_ffffff/B/C/foo.txt when /A is the backup root. + * + * If a directory is first backed up, and later replaced by a regular file, and + * this file is backed up (or vice versa) - an {@link IllegalArgumentException} + * is thrown + * + * A backup can not be performed on a closed BackupStore. + * + * @param file - the file (or directory) to backup + * + * @return true if the file was backed up, false if this file (path) has already + * been backed up (the file is not moved to the store). + * + * @throws IOException - if the backup operation fails, or the + * file does not exist + * @throws ClosedBackupStoreException - if the BackupStore has been closed + * @throws IllegalArgumentException - on type mismatch (file vs. directory) of + * earlier backup, or if file does not exist + */ + @Override + public boolean backup(File file) throws IOException { + assertOpen(); + + Path path = file.toPath(); + + if (Files.isDirectory(path)) { + return backupDirectory(path.toFile()); + } + + if (!Files.exists(path)) { + throw new IOException(NLS.bind(Messages.BackupStore_file_not_found, path.toAbsolutePath())); + } + + Path buPath = toBackupPath(path); + + // Already backed up, but was a directory - wrong usage + if (Files.isDirectory(buPath)) { + throw new IllegalArgumentException( + NLS.bind(Messages.BackupStore_directory_file_mismatch, buPath.toAbsolutePath())); + } + + return moveToBackup(path, buPath); + } + + /** + * Performs backup of an empty directory. + * + * The directory must be empty before it can be backed up (i.e. similar to a + * delete of a directory). The called must backup the files of the directory + * first. A call to backup a directory is really only needed for empty + * directories as a restore of a file will also restore all of its parent + * directories. + * + * @param file - the (empty) directory to back up + * + * @return true if the directory was moved to backup. false if the directory was + * already backed up. + * + * @throws IllegalArgumentException if file is not a directory, or is not empty. + * @throws IOException if directory can not be moved to the backup + * store, or if the directory is not writeable + */ + @Override + public boolean backupDirectory(File file) throws IOException { + assertOpen(); + + Path path = file.toPath(); + + if (!Files.isDirectory(path)) { + throw new IllegalArgumentException(NLS.bind(Messages.BackupStore_not_a_directory, file.getAbsolutePath())); + } + + if (Files.list(path).count() > 0) { + throw new IllegalArgumentException( + NLS.bind(Messages.BackupStore_directory_not_empty, file.getAbsolutePath())); + } + + return moveDirToBackup(path); + } + + /** + * Backs up a file, or everything under a directory. + * + * @param file - file to backup or directory + * + * @throws IOException if backup operation failed + */ + @Override + public void backupAll(File file) throws IOException { + assertOpen(); + + Path path = file.toPath().normalize(); + + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path f, BasicFileAttributes attrs) throws IOException { + backup(f.toFile()); + return CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + moveDirToBackup(dir); + return CONTINUE; + } + }); + } + + /** + * Backup the file by leaving a copy of the contents in the original location. + * + * Calling this method with a file that represents a directory throws an + * {@link IllegalArgumentException}. + * + * A file (path) can only be backed up once per BackupStore instance. When the + * file is backed up, it is moved to a directory under this BackupStore + * instance's directory with a relative path corresponding to the original + * relative path from the backup root e.g. the file /A/B/C/foo.txt could be + * moved to /A/.p2bu_ffffff_ffffff/B/C/foo.txt when /A is the backup root. + * + * If a directory is first backed up, and later replaced by a regular file, and + * this file is backed up (or vice versa) - an {@link IllegalArgumentException} + * is thrown + * + * A backup can not be performed on a closed BackupStore. + * + * @param file - the file (or directory) to backup + * + * @return true if the file was backed up, false if this file (path) has already + * been backed up (the file is not moved to the store). + * + * @throws IOException if the backup operation fails, or the file + * does not exist + * @throws ClosedBackupStoreException if the BackupStore has been closed + * @throws IllegalArgumentException on type mismatch (file vs. directory) of + * earlier backup, or if file is a Directory + */ + @Override + public boolean backupCopy(File file) throws IOException { + assertOpen(); + + Path path = file.toPath(); + + if (!Files.exists(path)) { + throw new IOException(NLS.bind(Messages.BackupStore_file_not_found, file.getAbsolutePath())); + } + + if (Files.isDirectory(path)) { + throw new IllegalArgumentException( + NLS.bind(Messages.BackupStore_can_not_copy_directory, file.getAbsolutePath())); + } + + Path buPath = toBackupPath(path); + + // Already backed up, but was a directory = wrong usage + if (Files.isDirectory(buPath)) { + throw new IllegalArgumentException( + NLS.bind(Messages.BackupStore_directory_file_mismatch, buPath.toAbsolutePath())); + } + + // Already backed up, can only be done once with one BackupStore + if (Files.exists(buPath)) { + return false; + } + + Files.createDirectories(buPath.getParent()); + Files.copy(path, buPath, REPLACE_EXISTING); + + backupCounter++; + return true; + } + + /** + * Backs up a file, or everything under a directory. + * + * A copy of the backup is left in the original place. + * + * @param file + * + * @throws IOException + */ + @Override + public void backupCopyAll(File file) throws IOException { + assertOpen(); + + Path path = file.toPath(); + if (!Files.exists(path)) { + return; + } + + path = path.normalize(); + + if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + backupCopy(file); + } else if (Files.isDirectory(path)) { + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path f, BasicFileAttributes attrs) throws IOException { + backupCopy(f.toFile()); + return CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + copyDirToBackup(dir); + return CONTINUE; + } + }); + } + } + + /** + * Restores all backup files from backup store. Note that restore of a (non + * directory) file deletes an existing file or directory found in the restore + * location. When the backup has been restored this BackupStore instance is + * closed and can not be used for further backup or restore. + * + * If there are unrestorable items (non writable directories, or general IO + * exceptions) these items are written to the log, and the backup copies remain + * in the file system and can be manually restored (using a simple zip of the + * backup directory, and an unzip to the buRoot once the problem has been + * corrected). + * + * @throws IOException if the backup was not fully restored - + * unrestored items have been logged. + * @throws ClosedBackupStoreException if the backup is already closed. + */ + @Override + public void restore() throws IOException { + assertOpen(); + closed = true; + + // Put back all files. + // Collect things that could not be restored + Map<Path, Throwable> unrestorable = new HashMap<>(); + + restoreBackups(unrestorable); + restoreInPlaceBackups(unrestorable); + + boolean restored = true; + + // Checked failed attempts to restore + if (!unrestorable.isEmpty()) { + restored = false; + + unrestorable.forEach((p, err) -> { + logError(NLS.bind(Messages.BackupStore_manual_restore_needed, err, p.toAbsolutePath())); + }); + } + + // Check external tampering with backup store + if (backupCounter != restoreCounter) { + restored = false; + + if (!unrestorable.isEmpty()) { + logError(NLS.bind(Messages.BackupStore_0_of_1_items_restored, restoreCounter, backupCounter)); + } else { + logError(NLS.bind(Messages.BackupStore_externally_modified_0_of_1_restored, restoreCounter, + backupCounter)); + } + } + + if (!restored) { + throw new IOException(Messages.BackupStore_errors_while_restoring_see_log); + } + } + + /** + * Discards and closes this BackupStore. Does nothing if this store is already + * restored or discarded. + */ + @Override + public void discard() { + if (closed) { + return; + } + closed = true; + + try { + deleteAll(buStoreRoot); + } catch (IOException e) { + logWarning(NLS.bind(Messages.BackupStore_can_not_remove_bu_directory, buStoreRoot.toAbsolutePath())); + } + + for (Path buFile : buInPlace) { + try { + deleteAll(buFile); + } catch (IOException e) { + logWarning(NLS.bind(Messages.BackupStore_can_not_remove_bu_file, buFile.toAbsolutePath())); + } + } + } + + private void assertOpen() { + if (closed) { + throw new ClosedBackupStoreException(Messages.BackupStore_closed_store); + } + } + + /** + * Makes sure a directory exists in the backup store without touching the original directory content + * + * @param path + * + * @return false if the directory is already created in the backup store, false if a placeholder had + * to be created and backed up. + * + * @throws IOException + */ + private boolean copyDirToBackup(Path path) throws IOException { + Path buPath = toBackupPath(path); + + if (Files.exists(buPath)) { + return false; + } + + Path placeholderPath = path.resolve(DIR_PLACEHOLDER); + try { + Files.createFile(placeholderPath); + } catch (IOException e) { + throw new IOException( + NLS.bind(Messages.BackupStore_can_not_create_placeholder, placeholderPath.toAbsolutePath()), e); + } + + Path buPlaceholderPath = buPath.resolve(DIR_PLACEHOLDER); + moveToBackup(placeholderPath, buPlaceholderPath); + return true; + } + + private boolean moveDirToBackup(Path dir) throws IOException { + boolean copied = copyDirToBackup(dir); + + try { + Files.delete(dir); + } catch (IOException e) { + throw new IOException(NLS.bind(Messages.BackupStore_can_not_remove, dir.toAbsolutePath())); + } + + return copied; + } + + /** + * Move/rename file to a backup file. + * + * Exposed for testing purposes. + * + * Callers of the method must have ensured that the source file exists and the + * backup file does not exist. + * + * @param file source file to move; should already exist and must not be + * directory + * @param buFile destination backup file to move to; should not exist and must + * be a directory + * + * @throws IOException if the backup operation fails + */ + private boolean moveToBackup(Path path, Path buPath) throws IOException { + // Already backed up. Can only be done once with one BackupStore. + if (Files.exists(buPath)) { + /* + * Although backed up, the file can be still on the file system. For example, + * two IUs may be unzipping their contents to the same location and share a few + * common files, which have to be removed twice. + */ + try { + Files.delete(path); + } catch (IOException e) { + throw new IOException(NLS.bind(Messages.BackupStore_can_not_remove, path.toAbsolutePath()), e); + } + + return false; + } + + // make sure all of the directories exist / gets created + Path buPathDir = buPath.getParent(); + try { + Files.createDirectories(buPathDir); + } catch (IOException e) { + throw new IllegalArgumentException( + NLS.bind(Messages.BackupStore_file_directory_mismatch, buPathDir.toAbsolutePath()), e); + } + + try { + move(path, buPath); + } catch (IOException e) { + // TODO Log exception? + if (!isEclipseExe(path)) { + throw e; + } + + Path inPlaceBuPath = toInPlaceBackupPath(path); + move(path, inPlaceBuPath); + buInPlace.add(inPlaceBuPath); + } + + backupCounter++; + return true; + } + + /** + * Restores everything stored in the backup root + * + * Responsible for converting the root prefix of the path from backup format + * back to the original real OS names. I.e. "_/" to "//", "__/" to "///", "C/" + * to "C:", etc. + * + * @param unrestorable accumulate unrestorable paths (including the entire + * backup store). + * + * @throws IOException + */ + private void restoreBackups(Map<Path, Throwable> unrestorable) throws IOException { + if (!Files.exists(buStoreRoot)) { + unrestorable.put(buStoreRoot, new IOException( + NLS.bind(Messages.BackupStore_missing_backup_directory, buStoreRoot.toAbsolutePath()))); + return; + } + + Files.walkFileTree(buStoreRoot, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult preVisitDirectory(Path buDir, BasicFileAttributes attrs) { + try { + if (Files.isSameFile(buStoreRoot, buDir)) { + return CONTINUE; + } + + Path dir = toSourcePath(buDir); + + // There is a file where we the original directory used to be - delete it + if (Files.isRegularFile(dir)) { + Files.delete(dir); + } + + // Make the original directory if needed + Files.createDirectories(dir); + } catch (IOException e) { + unrestorable.put(buDir, e); + } + return CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path buFile, BasicFileAttributes attrs) { + Path file = toSourcePath(buFile); + try { + // The first level children of buStoreRoot are always directories since they + // model file system roots + if (Files.isSameFile(buFile.getParent(), buStoreRoot)) { + unrestorable.put(buFile, new IOException("Not a directory")); //$NON-NLS-1$ + } else { + /* + * Do not restore the place-holders as they are used to trigger creation of + * empty directories and are not wanted in the restored location. + * + * They are counted as restored non the less. + */ + if (!DIR_PLACEHOLDER.equals(buFile.getFileName().toString())) { + // Clean up the site where the original used to be. + // It may be that a file or a directory now occupies it. + deleteAll(file); + + // Move the backup to the original location + move(buFile, file); + } else { + Files.delete(buFile); + } + + restoreCounter++; + } + } catch (IOException e) { + unrestorable.put(buFile, e); + } + return CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + unrestorable.put(file, exc); + throw exc; + } + + @Override + public FileVisitResult postVisitDirectory(Path buDir, IOException exc) throws IOException { + if (exc != null) { + unrestorable.put(buDir, exc); + throw exc; + } + try { + Files.delete(buDir); + } catch (DirectoryNotEmptyException e) { + String children = Files.list(buDir) + .map(p -> p.relativize(buDir)) + .map(Path::toString) + .collect(joining(",")); //$NON-NLS-1$ + unrestorable.put(buDir, new IOException(String.format( + "Directory %s not empty: %s", buDir, children, e))); //$NON-NLS-1$ + } catch (IOException e) { + unrestorable.put(buDir, e); + } + return CONTINUE; + } + }); + } + + private void restoreInPlaceBackups(Map<Path, Throwable> unrestorable) { + for (Path buPath : buInPlace) { + Path path = toInPlaceSourcePath(buPath); + + try { + move(buPath, path); + restoreCounter++; + } catch (IOException e) { + unrestorable.put(buPath, e); + } + } + } + + /** + * Converts a source path to a backup path. + * + * Exposed for testing purposes. + * + * A leading "root" is transformed to the ROOTCHAR character. On Windows, + * network mapped drives starts with two separators - and are encoded as two + * ROOTCHAR. + * + * E.g. \\Host\C$\file becomes __\Host\C$\file /users/test/file becomes + * _/users/test/file C:/file becomes C/file + * + * @param file a source file that needs to be backed up + * + * @return a file to which the original content can be backed up + * + * @throws IOException + */ + protected Path toBackupPath(Path path) throws IOException { + Path pathNormal = path.normalize(); + + String buPath = pathNormal.toAbsolutePath().toString(); + + String buPrefix = ""; //$NON-NLS-1$ + while (buPath.startsWith(File.separator)) { + buPrefix += ROOTCHAR; + buPath = buPath.substring(1); + } + + // Linux or Windows net mount + if (!buPrefix.isEmpty()) { + buPath = Paths.get(buPrefix, buPath).toString(); + } + // Windows + else { + // It is a windows drive letter first. + // Transform C:/foo to C/foo + int idx = buPath.indexOf(":"); //$NON-NLS-1$ + if (idx < 1) { + throw new IllegalArgumentException("File is neither absolute nor has a drive name: " + buPath); //$NON-NLS-1$ + } + buPath = buPath.substring(0, idx) + buPath.substring(idx + 1); + } + + Path buFile = buStoreRoot.resolve(buPath); + return buFile; + } + + /** + * Converts a backup file to the original source file. + * + * ///x/y/z -> ___x/y/z \\x\y\z c:\x\y\z -> c\x\y\z + * + * @param buPath an absolute file under {@link #buStoreRoot} to which some + * content is backed up. + * + * @return the original source file to which the content can be restored. + */ + protected Path toSourcePath(Path buPath) { + Path buPathRel = buStoreRoot.relativize(buPath); + + String pathName = buPathRel.toString(); + + String prefix = ""; //$NON-NLS-1$ + while (pathName.startsWith(ROOTCHAR)) { + prefix += File.separator; + pathName = pathName.substring(1); + } + + if (prefix.isEmpty()) { + // The first char is a windows drive name + pathName = pathName.charAt(0) + ":" + pathName.substring(1); //$NON-NLS-1$ + } else { + pathName = prefix + pathName; + } + + return Paths.get(pathName); + } + + /** + * Converts a path to an in-place backup path. + * + * Exposed for testing purposes. + * + * @param path + * + * @return a path next to the original where the original will be moved, rather + * than will be moved + */ + protected Path toInPlaceBackupPath(Path path) { + String buPathName = path.getFileName() + buInPlaceSuffix; + Path buPath = path.toAbsolutePath().resolveSibling(buPathName); + return buPath; + } + + /** + * Converts a in-place backup path to the original source path. + * + * Exposed for testing purposes. + * + * @param path + * + * @return a source path + */ + protected Path toInPlaceSourcePath(Path buPath) { + String buPathName = buPath.getFileName().toString(); + + int suffixIdx = buPathName.indexOf(buInPlaceSuffix); + if (suffixIdx <= 0) { + throw new IllegalArgumentException(); + } + + String pathName = buPathName.substring(0, suffixIdx); + Path path = buPath.resolveSibling(pathName); + return path; + } + + /** + * A generic file operation that attempts to move a file. + * + * Exposed in a separate method for testing purposes. + */ + protected void move(Path source, Path target) throws IOException { + Files.move(source, target, REPLACE_EXISTING); + } + + private static boolean isEclipseExe(Path file) { + String name = file.getFileName().toString(); + + String launcher = System.getProperty("eclipse.launcher"); //$NON-NLS-1$ + if (launcher != null) { + String launcherName = Paths.get(launcher).getFileName().toString(); + if (name.equalsIgnoreCase(launcherName)) { + return true; + } + } + + return name.equalsIgnoreCase("eclipse.exe") || name.equalsIgnoreCase("eclipsec.exe"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + /** + * Deletes a file, or a directory with all of it's children. + * + * @param path + * + * @throws IOException + */ + private static void deleteAll(Path path) throws IOException { + if (!Files.exists(path)) { + return; + } + + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + Files.delete(dir); + return CONTINUE; + } + }); + } +} diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Util.java b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Util.java index 3edabc078..afde945b3 100644 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Util.java +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/Util.java @@ -31,10 +31,9 @@ import org.eclipse.equinox.p2.repository.artifact.*; import org.eclipse.osgi.util.NLS; public class Util { - - public static void log(String message) { - LogHelper.log(createError(message)); - } + /* + * Logging + */ public static IStatus createError(String message) { return new Status(IStatus.ERROR, Activator.ID, message); @@ -44,6 +43,34 @@ public class Util { return new Status(IStatus.ERROR, Activator.ID, message, exception); } + public static void logError(String message, Throwable exception) { + LogHelper.log(createError(message, exception)); + } + + public static void logError(String message) { + LogHelper.log(createError(message)); + } + + public static IStatus createWarning(String message) { + return new Status(IStatus.WARNING, Activator.ID, message); + } + + public static IStatus createWarning(String message, Throwable exception) { + return new Status(IStatus.WARNING, Activator.ID, message, exception); + } + + public static void logWarning(String message, Throwable exception) { + LogHelper.log(createWarning(message, exception)); + } + + public static void logWarning(String message) { + LogHelper.log(createWarning(message)); + } + + /* + * Locations and caches + */ + public static String getInstallFolder(IProfile profile) { return profile.getProperty(IProfile.PROP_INSTALL_FOLDER); } @@ -58,11 +85,13 @@ public class Util { public static IFileArtifactRepository getDownloadCacheRepo(IProvisioningAgent agent) throws ProvisionException { URI location = getDownloadCacheLocation(agent); - if (location == null) + if (location == null) { throw new IllegalStateException(Messages.could_not_obtain_download_cache); + } IArtifactRepositoryManager manager = getArtifactRepositoryManager(agent); - if (manager == null) + if (manager == null) { throw new IllegalStateException(Messages.artifact_repo_not_found); + } IArtifactRepository repository; try { repository = manager.loadRepository(location, null); @@ -76,18 +105,24 @@ public class Util { } IFileArtifactRepository downloadCache = repository.getAdapter(IFileArtifactRepository.class); - if (downloadCache == null) + if (downloadCache == null) { throw new ProvisionException(NLS.bind(Messages.download_cache_not_writeable, location)); + } return downloadCache; } - static private URI getDownloadCacheLocation(IProvisioningAgent agent) { + private static URI getDownloadCacheLocation(IProvisioningAgent agent) { IAgentLocation location = getAgentLocation(agent); - if (location == null) + if (location == null) { return null; + } return URIUtil.append(location.getDataArea("org.eclipse.equinox.p2.core"), "cache/"); //$NON-NLS-1$ //$NON-NLS-2$ } + /* + * File operations + */ + /** * Unzip from a File to an output directory, with progress indication and * backup. monitor and backup store may be null. @@ -113,9 +148,8 @@ public class Util { monitor); } catch (IOException e) { // add the file name to the message - IOException ioException = new IOException(NLS.bind(Messages.Util_Error_Unzipping, zipFile, e.getMessage())); - ioException.initCause(e); - throw ioException; + IOException ioExc = new IOException(NLS.bind(Messages.Util_Error_Unzipping, zipFile, e.getMessage()), e); + throw ioExc; } } @@ -150,8 +184,9 @@ public class Util { throw new IOException(Messages.Util_Invalid_Zip_File_Format); } - if (path != null && path.trim().length() == 0) + if (path != null && path.trim().length() == 0) { path = null; + } Pattern pathRegex = path == null ? null : createAntStylePattern("(" + path + ")(*)"); //$NON-NLS-1$ //$NON-NLS-2$ Collection<Pattern> includeRegexp = new ArrayList<>(); @@ -177,8 +212,9 @@ public class Util { boolean unzip = includeRegexp.isEmpty(); for (Pattern pattern : includeRegexp) { unzip = pattern.matcher(name).matches(); - if (unzip) + if (unzip) { break; + } } if (unzip && !excludeRegexp.isEmpty()) { for (Pattern pattern : excludeRegexp) { @@ -193,8 +229,9 @@ public class Util { Matcher matcher = pathRegex.matcher(name); if (matcher.matches()) { name = matcher.group(2); - if (name.startsWith("/")) //$NON-NLS-1$ + if (name.startsWith("/")) { //$NON-NLS-1$ name = name.substring(1); + } } } File outFile = createSubPathFile(outputDir, name); @@ -203,10 +240,11 @@ public class Util { outFile.mkdirs(); } else { if (outFile.exists()) { - if (store != null) + if (store != null) { store.backup(outFile); - else + } else { outFile.delete(); + } } else { outFile.getParentFile().mkdirs(); } diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/actions/UnzipAction.java b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/actions/UnzipAction.java index 77961095b..6dc4cf962 100644 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/actions/UnzipAction.java +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/actions/UnzipAction.java @@ -44,19 +44,22 @@ public class UnzipAction extends ProvisioningAction { /** * Unzip as directed by parameters. * Record what was zipped in the profile. + * * @param parameters * @param restoreable - if the unzip should be backed up * @return status */ public IStatus unzip(Map<String, Object> parameters, boolean restoreable) { String source = (String) parameters.get(ActionConstants.PARM_SOURCE); - if (source == null) + if (source == null) { return Util.createError(NLS.bind(Messages.param_not_set, ActionConstants.PARM_SOURCE, ACTION_UNZIP)); + } String originalSource = source; String target = (String) parameters.get(ActionConstants.PARM_TARGET); - if (target == null) + if (target == null) { return Util.createError(NLS.bind(Messages.param_not_set, ActionConstants.PARM_TARGET, ACTION_UNZIP)); + } IInstallableUnit iu = (IInstallableUnit) parameters.get(ActionConstants.PARM_IU); Profile profile = (Profile) parameters.get(ActionConstants.PARM_PROFILE); @@ -87,7 +90,9 @@ public class UnzipAction extends ProvisioningAction { unzippedFileNameBuffer.append(unzippedFile.getAbsolutePath()).append(ActionConstants.PIPE); } - profile.setInstallableUnitProperty(iu, "unzipped" + ActionConstants.PIPE + originalSource + ActionConstants.PIPE + target, unzippedFileNameBuffer.toString()); //$NON-NLS-1$ + profile.setInstallableUnitProperty(iu, + "unzipped" + ActionConstants.PIPE + originalSource + ActionConstants.PIPE + target, //$NON-NLS-1$ + unzippedFileNameBuffer.toString()); return Status.OK_STATUS; } @@ -96,18 +101,23 @@ public class UnzipAction extends ProvisioningAction { * Unzips a source zip into the given destination. Any existing contents in the destination * are backed up in the provided backup store. */ - private static File[] unzip(String source, String destination, String path, String includePattern, String excludePattern, IBackupStore store) { + private static File[] unzip(String source, String destination, String path, String includePattern, + String excludePattern, IBackupStore store) { File zipFile = new File(source); if (zipFile == null || !zipFile.exists()) { - Util.log(UnzipAction.class.getName() + " the files to be unzipped is not here"); //$NON-NLS-1$ + Util.logError(UnzipAction.class.getName() + " the files to be unzipped is not here", null); //$NON-NLS-1$ } try { String taskName = NLS.bind(Messages.unzipping, source); String[] includes = includePattern == null ? null : includePattern.split("\\s+"); //$NON-NLS-1$ String[] excludes = excludePattern == null ? null : excludePattern.split("\\s+"); //$NON-NLS-1$ - return Util.unzipFile(zipFile, new File(destination), path, includes, excludes, store, taskName, new NullProgressMonitor()); + return Util.unzipFile(zipFile, new File(destination), path, includes, excludes, store, taskName, + new NullProgressMonitor()); } catch (IOException e) { - Util.log(UnzipAction.class.getName() + " error unzipping zipfile: " + zipFile.getAbsolutePath() + "destination: " + destination); //$NON-NLS-1$ //$NON-NLS-2$ + Util.logError( + UnzipAction.class.getName() + " error unzipping zipfile: " + zipFile.getAbsolutePath() //$NON-NLS-1$ + + "destination: " + destination, //$NON-NLS-1$ + null); } return new File[0]; } diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/messages.properties b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/messages.properties index b5885bc36..a7204930b 100644 --- a/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/messages.properties +++ b/bundles/org.eclipse.equinox.p2.touchpoint.natives/src/org/eclipse/equinox/internal/p2/touchpoint/natives/messages.properties @@ -14,11 +14,10 @@ # SAP SE - bug 465602 ############################################################################### -BackupStore_backupCopy_closed_store=Can not perform backup on closed backup store. +BackupStore_closed_store=Can not perform operation on closed backup store. BackupStore_0_of_1_items_restored={0} items out of {1} items restored. -BackupStore_can_not_close_tcp_port=Could not close tcp socket for port: {0} BackupStore_can_not_copy_directory=Can not copy a directory: {0} -BackupStore_can_not_create_dummy=Can not create dummy file: {0} +BackupStore_can_not_create_placeholder=Can not create placeholder file: {0} BackupStore_can_not_delete_after_copy_0=File that was copied to backup could not be deleted: {0} BackupStore_can_not_delete_tmp_file=Can not delete temporary file - it is safe to delete it manually: {0} BackupStore_can_not_remove=Can not remove : {0} @@ -30,10 +29,9 @@ BackupStore_errors_while_restoring_see_log=Errors while restoring - see earlier BackupStore_externally_modified_0_of_1_restored=Backup store modified externally\! {0} items out of {1} items restored. Remaining items can not be found. BackupStore_file_directory_mismatch=File already backed up as a file - and is now required as a directory: {0} BackupStore_file_not_found=File does not exist: {0}. -BackupStore_manual_restore_needed=Manual restore of backup needed for: {0} +BackupStore_manual_restore_needed=Restore failed: {0}. Manual restore of backup needed for: {1} BackupStore_missing_backup_directory=Missing backup directory - can not restore: {0} BackupStore_not_a_directory=File is not a directory: {0} -BackupStore_restore_closed_store=Can not perform restore on closed backup store BlockMacUpdate_0=Installation impossible BlockMacUpdate_1=The installation/update you are trying to perform can not be completed because of structural changes to Eclipse. The installation/update will stop and will leave your existing Eclipse installation intact. You need to retrieve a new version of Eclipse from http://download.eclipse.org/. |