| /******************************************************************************* |
| * 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()) { |
| // If the renamed work, but the file still exists, remove the backup |
| // and return false |
| if (buFile.exists()) |
| buFile.delete(); |
| } else { |
| return true; |
| } |
| } |
| 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 |
| } else 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 (int i = 0; i < address.length; i++) |
| ipPart = ((ipPart << 8) | (address[i] & 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()); |
| } |
| } |