/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2009 Sun Microsystems, Inc.
* Portions Copyright 2013-2015 ForgeRock AS.
*/
/**
* A backup manager for any entity that is backupable (backend, storage).
*
* @see {@link Backupable}
*/
public class BackupManager
{
/**
* The common prefix for archive files.
*/
/**
* The name of the property that holds the name of the latest log file
* at the time the backup was created.
*/
/**
* The name of the property that holds the size of the latest log file
* at the time the backup was created.
*/
/**
* The name of the entry in an incremental backup archive file
* containing a list of log files that are unchanged since the
* previous backup.
*/
/**
* The name of a dummy entry in the backup archive file that will act
* as a placeholder in case a backup is done on an empty backend.
*/
/**
* The backend ID.
*/
/**
* Construct a backup manager for a backend.
*
* @param backendID
* The ID of the backend instance for which a backup manager is
* required.
*/
{
}
/** A cryptographic engine to use for backup creation or restore. */
private static abstract class CryptoEngine
{
final boolean shouldEncrypt;
/** Creates a crypto engine for archive creation. */
throws DirectoryException {
if (backupConfig.hashData())
{
if (backupConfig.signHash())
{
}
else
{
}
}
else
{
}
}
/** Creates a crypto engine for archive restore. */
throws DirectoryException {
if (hasHashData)
{
if (hasSignedHash)
{
return new MacCryptoEngine(backupInfo);
}
else
{
return new DigestCryptoEngine(backupInfo);
}
}
else
{
}
}
{
this.shouldEncrypt = shouldEncrypt;
}
/** Indicates if data is encrypted. */
final boolean shouldEncrypt() {
return shouldEncrypt;
}
/** Indicates if hashed data is signed. */
boolean hasSignedHash() {
return false;
}
/** Update the hash with the provided string. */
/** Update the hash with the provided buffer. */
/** Generates the hash bytes. */
abstract byte[] generateBytes();
/** Returns the error message to use in case of check failure. */
/** Check that generated hash is equal to the provided hash. */
{
byte[] bytes = generateBytes();
{
}
}
/** Wraps an output stream in a cipher output stream if encryption is required. */
{
if (!shouldEncrypt())
{
return output;
}
try
{
}
catch (CryptoManagerException e)
{
logger.traceException(e);
}
}
/** Wraps an input stream in a cipher input stream if encryption is required. */
{
if (!shouldEncrypt)
{
return inputStream;
}
try
{
}
catch (CryptoManagerException e)
{
logger.traceException(e);
}
}
}
/** Represents the cryptographic engine with no hash used for a backup. */
{
{
super(shouldEncrypt);
}
{
// nothing to do
}
{
// nothing to do
}
byte[] generateBytes()
{
return null;
}
{
// check never fails because bytes are always null
return null;
}
}
/**
* Represents the cryptographic engine with signed hash.
*/
{
/** Constructor for backup creation. */
private MacCryptoEngine(BackupConfig backupConfig, NewBackupParams backupParams) throws DirectoryException
{
super(backupConfig.encryptData());
try
{
}
catch (CryptoManagerException e)
{
}
}
/** Constructor for backup restore. */
{
super(backupInfo.isEncrypted());
}
{
try
{
}
catch (Exception e)
{
LocalizableMessage message = ERR_BACKUP_CANNOT_GET_MAC.get(macKeyID, stackTraceToSingleLineString(e));
}
}
/** {@inheritDoc} */
{
}
/** {@inheritDoc} */
{
}
byte[] generateBytes()
{
}
boolean hasSignedHash()
{
return true;
}
{
}
{
}
}
/** Represents the cryptographic engine with unsigned hash used for a backup. */
{
/** Constructor for backup creation. */
private DigestCryptoEngine(BackupConfig backupConfig, NewBackupParams backupParams) throws DirectoryException
{
super(backupConfig.encryptData());
}
/** Constructor for backup restore. */
{
super(backupInfo.isEncrypted());
}
{
try
{
}
catch (Exception e)
{
}
}
/** {@inheritDoc} */
{
}
/** {@inheritDoc} */
{
}
/** {@inheritDoc} */
public byte[] generateBytes()
{
}
/** {@inheritDoc} */
{
}
{
}
}
/**
* Contains all parameters for creation of a new backup.
*/
private static final class NewBackupParams
{
final boolean shouldCompress;
final boolean isIncremental;
{
backupProperties = new HashMap<>();
}
{
if (backupConfig.isIncremental())
{
{
// The default is to use the latest backup as base.
}
else
{
}
{
// No incremental backup ID: log a message informing that a backup
// could not be found and that a normal backup will be done.
}
}
return id;
}
}
{
}
}
/** Represents a new backup archive. */
private static final class NewBackupArchive {
private long latestFileSize;
{
this.newBackupParams = backupParams;
this.cryptoEngine = crypt;
dependencies = new HashSet<>();
{
}
}
{
return archiveFilename;
}
{
return backendID;
}
{
return newBackupParams.backupID;
}
}
void addBaseBackupAsDependency() {
}
{
try
{
}
catch (Exception e)
{
logger.traceException(e);
e);
}
}
/** Create a descriptor for the backup. */
{
return new BackupInfo(
}
{
}
}
/** Represents an existing backup archive. */
private static final class ExistingBackupArchive {
{
}
{
return archiveFile;
}
return backupInfo;
}
{
return backupID;
}
{
return cryptoEngine;
}
/**
* Obtains a list of the dependencies of this backup in order from
* the oldest (the full backup), to the most recent.
*
* @return A list of dependent backups.
* @throws DirectoryException If a Directory Server error occurs.
*/
{
{
if (currentBackupInfo != null)
{
}
}
return dependencies;
}
boolean hasDependencies()
{
}
/** Removes the archive from file system. */
{
try
{
}
catch (ConfigException e)
{
logger.traceException(e);
}
catch (Exception e)
{
logger.traceException(e);
}
return archiveFile.delete();
}
}
/** Represents a writer of a backup archive. */
{
}
{
}
/**
* Writes the provided file to a new entry in the archive.
*
* @param file
* The file to be written.
* @param cryptoMethod
* The cryptographic method for the written data.
* @param backupConfig
* The configuration, used to know if operation is cancelled.
*
* @return The number of bytes written from the file.
* @throws FileNotFoundException If the file to be archived does not exist.
* @throws IOException If an I/O error occurs while archiving the file.
*/
long writeFile(Path file, String relativePath, CryptoEngine cryptoMethod, BackupConfig backupConfig)
throws IOException, FileNotFoundException
{
long totalBytesRead = 0;
try {
byte[] buffer = new byte[8192];
{
}
}
finally {
}
return totalBytesRead;
}
/**
* Write a list of strings to an entry in the archive.
*
* @param stringList
* A list of strings to be written. The strings must not
* contain newlines.
* @param fileName
* The name of the zip entry to be written.
* @param cryptoMethod
* The cryptographic method for the written data.
* @throws IOException
* If an I/O error occurs while writing the archive entry.
*/
throws IOException
{
for (String s : stringList)
{
}
}
/** Writes a empty placeholder entry into the archive. */
{
try
{
}
catch (IOException e)
{
logger.traceException(e);
e);
}
}
/**
* Writes the files that are unchanged from the base backup (for an
* incremental backup only).
* <p>
* The unchanged files names are listed in the "unchanged.txt" file, which
* is put in the archive.
*
*/
throws DirectoryException
{
{
{
break;
}
}
if (!unchangedFilenames.isEmpty())
{
}
}
/** Writes the list of unchanged files names in a file as new entry in the archive. */
{
try
{
}
catch (IOException e)
{
logger.traceException(e);
throw new DirectoryException(
stackTraceToSingleLineString(e)), e);
}
}
/**
* Writes the new files in the archive.
*/
throws DirectoryException
{
{
try
{
}
catch (FileNotFoundException e)
{
// The file may have been deleted by a cleaner (i.e. for JE storage) since we started.
// The backupable entity is responsible for handling the changes through the files list iterator
logger.traceException(e);
}
catch (IOException e)
{
logger.traceException(e);
stackTraceToSingleLineString(e)), e);
}
}
}
{
return openZipStream(output);
}
private OutputStream openStream(String backupPath, String archiveFilename) throws DirectoryException {
try
{
int i = 1;
while (archiveFile.exists())
{
i++;
}
return output;
}
catch (Exception e)
{
logger.traceException(e);
}
}
/** Wraps the file output stream in a zip output stream. */
{
zipStream.setComment(ERR_BACKUP_ZIP_COMMENT.get(DynamicConstants.PRODUCT_NAME, archive.getBackupID())
.toString());
{
}
else
{
}
return zipStream;
}
{
}
}
/** Represents a reader of a backup archive. */
private static final class BackupArchiveReader {
{
this.identifier = identifier;
}
BackupArchiveReader(String identifier, BackupInfo backupInfo, String backupDirectoryPath) throws DirectoryException
{
this.identifier = identifier;
this.backupInfo = backupInfo;
}
/**
* Obtains the set of files in a backup that are unchanged from its
* dependent backup or backups.
* <p>
* The file set is stored as as the first entry in the archive file.
*
* @return The set of files that are listed in "unchanged.txt" file
* of the archive.
* @throws DirectoryException
* If an error occurs.
*/
{
try
{
zipStream = openZipStream();
// Iterate through the entries in the zip file.
{
// We are looking for the entry containing the list of unchanged files.
{
break;
}
}
return hashSet;
}
catch (IOException e)
{
logger.traceException(e);
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_BACKUP_CANNOT_RESTORE.get(
identifier, stackTraceToSingleLineString(e)), e);
}
finally {
}
}
/**
* Restore the provided list of files from the provided restore directory.
* @param restoreDir
* The target directory for restored files.
* @param filesToRestore
* The set of files to restore. If empty, all files in the archive
* are restored.
* @param restoreConfig
* The restore configuration, used to check for cancellation of
* this restore operation.
* @throws DirectoryException
* If an error occurs.
*/
void restoreArchive(Path restoreDir, Set<String> filesToRestore, RestoreConfig restoreConfig, Backupable backupable)
throws DirectoryException
{
try
{
}
catch (IOException e)
{
logger.traceException(e);
}
// check the hash
byte[] hash = backupInfo.getUnsignedHash() != null ? backupInfo.getUnsignedHash() : backupInfo.getSignedHash();
}
private void restoreArchive0(Path restoreDir, Set<String> filesToRestore, RestoreConfig restoreConfig,
try {
zipStream = openZipStream();
{
continue;
}
if (mustRestoreOnDisk)
{
}
else
{
}
}
}
finally {
}
}
/**
* Handle any special entry in the archive.
*
* @return the pair (true, zipEntry) if next entry was read, (false, null) otherwise
*/
throws IOException
{
{
// the backup contains no files
}
{
// This entry is treated specially. It is never restored,
// and its hash is computed on the strings, not the bytes.
{
}
}
}
/**
* Restores a zip entry virtually (no actual write on disk).
*/
private void restoreZipEntryVirtual(String zipEntryName, ZipInputStream zipStream, RestoreConfig restoreConfig)
throws FileNotFoundException, IOException
{
if (restoreConfig.verifyOnly())
{
}
}
/**
* Restores a zip entry with actual write on disk.
*/
{
long totalBytesRead = 0;
try
{
}
finally
{
}
}
{
{
try
{
}
catch (IOException e)
{
}
}
}
/**
* Restores the file provided by the zip input stream.
* <p>
* The restore can be virtual: if the outputStream is {@code null}, the file
* is not actually restored on disk.
*/
private long restoreFile(ZipInputStream zipInputStream, OutputStream outputStream, RestoreConfig restoreConfig)
throws IOException
{
long totalBytesRead = 0;
byte[] buffer = new byte[8192];
{
if (outputStream != null)
{
}
}
return totalBytesRead;
}
{
try
{
return new FileInputStream(archiveFile);
}
catch (FileNotFoundException e)
{
}
}
{
return new ZipInputStream(inputStream);
}
{
{
}
return results;
}
}
/**
* Creates a backup of the provided backupable entity.
* <p>
* The backup is stored in a single zip file in the backup directory.
* <p>
* If the backup is incremental, then the first entry in the zip is a text
* file containing a list of all the log files that are unchanged since the
* previous backup. The remaining zip entries are the log files themselves,
* which, for an incremental, only include those files that have changed.
*
* @param backupable
* The underlying entity (storage, backend) to be backed up.
* @param backupConfig
* The configuration to use when performing the backup.
* @throws DirectoryException
* If a Directory Server error occurs.
*/
public void createBackup(final Backupable backupable, final BackupConfig backupConfig) throws DirectoryException
{
try
{
{
if (backupParams.isIncremental) {
}
}
else {
}
}
finally
{
closeArchiveWriter(archiveWriter, newArchive.getArchiveFilename(), backupParams.backupDir.getPath());
}
if (backupConfig.isCancelled())
{
// Remove the backup since it may be incomplete
}
}
/**
* Restores a backupable entity from its backup, or verify the backup.
*
* @param backupable
* The underlying entity (storage, backend) to be backed up.
* @param restoreConfig
* The configuration to use when performing the restore.
* @throws DirectoryException
* If a Directory Server error occurs.
*/
public void restoreBackup(Backupable backupable, RestoreConfig restoreConfig) throws DirectoryException
{
if (!restoreConfig.verifyOnly())
{
}
final ExistingBackupArchive existingArchive =
if (existingArchive.hasDependencies())
{
{
restoreArchive(restoreDirectory, unchangedFilesToRestore, restoreConfig, backupable, dependencyBackupInfo);
}
}
// Restore the final archive file.
restoreArchive(restoreDirectory, filesToRestore, restoreConfig, backupable, existingArchive.getBackupInfo());
if (!restoreConfig.verifyOnly())
{
}
}
/**
* Removes the specified backup if it is possible to do so.
*
* @param backupDir The backup directory structure with which the
* specified backup is associated.
* @param backupID The backup ID for the backup to be removed.
*
* @throws DirectoryException If it is not possible to remove the specified
* backup for some reason (e.g., no such backup
* exists or there are other backups that are
* dependent upon it).
*/
{
}
{
if (!backupable.isDirectRestore())
{
}
return restoreDirectory.toPath();
}
private void closeArchiveWriter(BackupArchiveWriter archiveWriter, String backupFile, String backupPath)
throws DirectoryException
{
if (archiveWriter != null)
{
try
{
}
catch (Exception e)
{
logger.traceException(e);
ERR_BACKUP_CANNOT_CLOSE_ZIP_STREAM.get(backupFile, backupPath, stackTraceToSingleLineString(e)), e);
}
}
}
/**
* Restores the content of an archive file.
* <p>
* If set of files is not empty, only the specified files are restored.
* If set of files is empty, all files are restored.
*
* If the archive is being restored as a dependency, then only files in the
* specified set are restored, and the restored files are removed from the
* set. Otherwise all files from the archive are restored, and files that are
* to be found in dependencies are added to the set.
* @param restoreDir
* The directory in which files are to be restored.
* @param filesToRestore
* The set of files to restore. If empty, then all files are
* restored.
* @param restoreConfig
* The restore configuration.
* @param backupInfo
* The backup containing the files to be restored.
*
* @throws DirectoryException
* If a Directory Server error occurs.
* @throws IOException
* If an I/O exception occurs during the restore.
*/
{
BackupArchiveReader zipArchiveReader = new BackupArchiveReader(backupID, backupInfo, backupDirectoryPath);
}
/** Retrieves the full path of the archive file. */
{
}
/**
* Get the information for a given backup ID from the backup directory.
*
* @param backupDir The backup directory.
* @param backupID The backup ID.
* @return The backup information, never null.
* @throws DirectoryException If the backup information cannot be found.
*/
private static BackupInfo getBackupInfo(BackupDirectory backupDir, String backupID) throws DirectoryException
{
if (backupInfo == null)
{
}
return backupInfo;
}
/**
* Helper method to build a list of files to backup, in the simple case where all files are located
* under the provided directory.
*
* @param directory
* The directory containing files to backup.
* @param filter
* The filter to select files to backup.
* @param identifier
* Identifier of the backed-up entity
* @return the files to backup, which may be empty but never {@code null}
* @throws DirectoryException
* if an error occurs.
*/
throws DirectoryException
{
try
{
}
catch (Exception e)
{
}
{
}
{
}
return paths;
}
/**
* Helper method to save all current files of the provided backupable entity, using
* default behavior.
*
* @param backupable
* The entity to backup.
* @param identifier
* Identifier of the backup
* @return the directory where all files are saved.
* @throws DirectoryException
* If a problem occurs.
*/
public static Path saveCurrentFilesToDirectory(Backupable backupable, String identifier) throws DirectoryException
{
BackupManager.saveFilesToDirectory(rootDirectory.toPath(), filesToBackup, saveDirectory, identifier);
}
/**
* Helper method to move all provided files in a target directory created from
* provided target base path, keeping relative path information relative to
* root directory.
*
* @param rootDirectory
* A directory which is an ancestor of all provided files.
* @param files
* The files to move.
* @param targetBasePath
* Base path of the target directory. Actual directory is built by
* adding ".save" and a number, always ensuring that the directory is new.
* @param identifier
* Identifier of the backup
* @return the actual directory where all files are saved.
* @throws DirectoryException
* If a problem occurs.
*/
public static Path saveFilesToDirectory(Path rootDirectory, ListIterator<Path> files, String targetBasePath,
{
try
{
{
}
return targetDirectory;
}
catch (IOException e)
{
stackTraceToSingleLineString(e)), e);
}
}
/**
* Creates a new directory based on the provided directory path, by adding a
* suffix number that is guaranteed to be the highest.
*/
throws DirectoryException
{
try
{
return directory;
}
catch (IOException e)
{
stackTraceToSingleLineString(e)), e);
}
}
/**
* Returns a number that correspond to the highest suffix number existing for the provided base path.
* <p>
* Example: given the following directory structure
* <pre>
* +--- someDir
* | \--- directory
* | \--- directory1
* | \--- directory2
* | \--- directory10
* </pre>
* getHighestSuffixNumberForPath("directory") returns 10.
*
* @param basePath
* A base path to a file or directory, without any suffix number.
* @return the highest suffix number, or 0 if no suffix number exists
* @throws IOException
* if an error occurs.
*/
{
int highestNumber = 0;
{
{
}
}
return highestNumber;
}
}