/*
|
* 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
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* 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.
|
*/
|
package org.opends.server.util;
|
|
import static java.util.Collections.*;
|
|
import static org.opends.messages.BackendMessages.*;
|
import static org.opends.messages.UtilityMessages.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
import java.io.BufferedReader;
|
import java.io.Closeable;
|
import java.io.File;
|
import java.io.FileFilter;
|
import java.io.FileInputStream;
|
import java.io.FileNotFoundException;
|
import java.io.FileOutputStream;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.InputStreamReader;
|
import java.io.OutputStream;
|
import java.io.OutputStreamWriter;
|
import java.io.Writer;
|
import java.nio.file.Files;
|
import java.nio.file.Path;
|
import java.nio.file.Paths;
|
import java.security.MessageDigest;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.ListIterator;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.regex.Pattern;
|
import java.util.zip.Deflater;
|
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipOutputStream;
|
|
import javax.crypto.Mac;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.forgerock.opendj.config.server.ConfigException;
|
import org.forgerock.opendj.ldap.ResultCode;
|
import org.forgerock.util.Pair;
|
import org.opends.server.api.Backupable;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.types.BackupConfig;
|
import org.opends.server.types.BackupDirectory;
|
import org.opends.server.types.BackupInfo;
|
import org.opends.server.types.CryptoManager;
|
import org.opends.server.types.CryptoManagerException;
|
import org.opends.server.types.DirectoryException;
|
import org.opends.server.types.RestoreConfig;
|
|
/**
|
* A backup manager for any entity that is backupable (backend, storage).
|
*
|
* @see {@link Backupable}
|
*/
|
public class BackupManager
|
{
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
/**
|
* The common prefix for archive files.
|
*/
|
private static final String BACKUP_BASE_FILENAME = "backup-";
|
|
/**
|
* The name of the property that holds the name of the latest log file
|
* at the time the backup was created.
|
*/
|
private static final String PROPERTY_LAST_LOGFILE_NAME = "last_logfile_name";
|
|
/**
|
* The name of the property that holds the size of the latest log file
|
* at the time the backup was created.
|
*/
|
private static final String PROPERTY_LAST_LOGFILE_SIZE = "last_logfile_size";
|
|
|
/**
|
* The name of the entry in an incremental backup archive file
|
* containing a list of log files that are unchanged since the
|
* previous backup.
|
*/
|
private static final String ZIPENTRY_UNCHANGED_LOGFILES = "unchanged.txt";
|
|
/**
|
* 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.
|
*/
|
private static final String ZIPENTRY_EMPTY_PLACEHOLDER = "empty.placeholder";
|
|
|
/**
|
* The backend ID.
|
*/
|
private final String backendID;
|
|
/**
|
* Construct a backup manager for a backend.
|
*
|
* @param backendID
|
* The ID of the backend instance for which a backup manager is
|
* required.
|
*/
|
public BackupManager(String backendID)
|
{
|
this.backendID = backendID;
|
}
|
|
/** A cryptographic engine to use for backup creation or restore. */
|
private static abstract class CryptoEngine
|
{
|
final CryptoManager cryptoManager;
|
final boolean shouldEncrypt;
|
|
/** Creates a crypto engine for archive creation. */
|
static CryptoEngine forCreation(BackupConfig backupConfig, NewBackupParams backupParams)
|
throws DirectoryException {
|
if (backupConfig.hashData())
|
{
|
if (backupConfig.signHash())
|
{
|
return new MacCryptoEngine(backupConfig, backupParams);
|
}
|
else
|
{
|
return new DigestCryptoEngine(backupConfig, backupParams);
|
}
|
}
|
else
|
{
|
return new NoHashCryptoEngine(backupConfig.encryptData());
|
}
|
}
|
|
/** Creates a crypto engine for archive restore. */
|
static CryptoEngine forRestore(BackupInfo backupInfo)
|
throws DirectoryException {
|
boolean hasSignedHash = backupInfo.getSignedHash() != null;
|
boolean hasHashData = hasSignedHash || backupInfo.getUnsignedHash() != null;
|
if (hasHashData)
|
{
|
if (hasSignedHash)
|
{
|
return new MacCryptoEngine(backupInfo);
|
}
|
else
|
{
|
return new DigestCryptoEngine(backupInfo);
|
}
|
}
|
else
|
{
|
return new NoHashCryptoEngine(backupInfo.isEncrypted());
|
}
|
}
|
|
CryptoEngine(boolean shouldEncrypt)
|
{
|
cryptoManager = DirectoryServer.getCryptoManager();
|
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. */
|
abstract void updateHashWith(String s);
|
|
/** Update the hash with the provided buffer. */
|
abstract void updateHashWith(byte[] buffer, int offset, int len);
|
|
/** Generates the hash bytes. */
|
abstract byte[] generateBytes();
|
|
/** Returns the error message to use in case of check failure. */
|
abstract LocalizableMessage getErrorMessageForCheck(String backupID);
|
|
/** Check that generated hash is equal to the provided hash. */
|
final void check(byte[] hash, String backupID) throws DirectoryException
|
{
|
byte[] bytes = generateBytes();
|
if (bytes != null && !Arrays.equals(bytes, hash))
|
{
|
LocalizableMessage message = getErrorMessageForCheck(backupID);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message);
|
}
|
}
|
|
/** Wraps an output stream in a cipher output stream if encryption is required. */
|
final OutputStream encryptOutput(OutputStream output) throws DirectoryException
|
{
|
if (!shouldEncrypt())
|
{
|
return output;
|
}
|
try
|
{
|
return cryptoManager.getCipherOutputStream(output);
|
}
|
catch (CryptoManagerException e)
|
{
|
logger.traceException(e);
|
StaticUtils.close(output);
|
LocalizableMessage message = ERR_BACKUP_CANNOT_GET_CIPHER.get(stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
}
|
|
/** Wraps an input stream in a cipher input stream if encryption is required. */
|
final InputStream encryptInput(InputStream inputStream) throws DirectoryException
|
{
|
if (!shouldEncrypt)
|
{
|
return inputStream;
|
}
|
|
try
|
{
|
return cryptoManager.getCipherInputStream(inputStream);
|
}
|
catch (CryptoManagerException e)
|
{
|
logger.traceException(e);
|
StaticUtils.close(inputStream);
|
LocalizableMessage message = ERR_BACKUP_CANNOT_GET_CIPHER.get(stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
}
|
|
}
|
|
/** Represents the cryptographic engine with no hash used for a backup. */
|
private static final class NoHashCryptoEngine extends CryptoEngine
|
{
|
|
NoHashCryptoEngine(boolean shouldEncrypt)
|
{
|
super(shouldEncrypt);
|
}
|
|
@Override
|
void updateHashWith(String s)
|
{
|
// nothing to do
|
}
|
|
@Override
|
void updateHashWith(byte[] buffer, int offset, int len)
|
{
|
// nothing to do
|
}
|
|
@Override
|
byte[] generateBytes()
|
{
|
return null;
|
}
|
|
@Override
|
LocalizableMessage getErrorMessageForCheck(String backupID)
|
{
|
// check never fails because bytes are always null
|
return null;
|
}
|
|
}
|
|
/**
|
* Represents the cryptographic engine with signed hash.
|
*/
|
private static final class MacCryptoEngine extends CryptoEngine
|
{
|
private Mac mac;
|
|
/** Constructor for backup creation. */
|
private MacCryptoEngine(BackupConfig backupConfig, NewBackupParams backupParams) throws DirectoryException
|
{
|
super(backupConfig.encryptData());
|
|
String macKeyID = null;
|
try
|
{
|
macKeyID = cryptoManager.getMacEngineKeyEntryID();
|
backupParams.putProperty(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID);
|
}
|
catch (CryptoManagerException e)
|
{
|
LocalizableMessage message = ERR_BACKUP_CANNOT_GET_MAC_KEY_ID.get(backupParams.backupID,
|
stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
retrieveMacEngine(macKeyID);
|
}
|
|
/** Constructor for backup restore. */
|
private MacCryptoEngine(BackupInfo backupInfo) throws DirectoryException
|
{
|
super(backupInfo.isEncrypted());
|
HashMap<String,String> backupProperties = backupInfo.getBackupProperties();
|
String macKeyID = backupProperties.get(BACKUP_PROPERTY_MAC_KEY_ID);
|
retrieveMacEngine(macKeyID);
|
}
|
|
private void retrieveMacEngine(String macKeyID) throws DirectoryException
|
{
|
try
|
{
|
mac = cryptoManager.getMacEngine(macKeyID);
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_BACKUP_CANNOT_GET_MAC.get(macKeyID, stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
void updateHashWith(String s)
|
{
|
mac.update(getBytes(s));
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
void updateHashWith(byte[] buffer, int offset, int len)
|
{
|
mac.update(buffer, offset, len);
|
}
|
|
@Override
|
byte[] generateBytes()
|
{
|
return mac.doFinal();
|
}
|
|
@Override
|
boolean hasSignedHash()
|
{
|
return true;
|
}
|
|
@Override
|
LocalizableMessage getErrorMessageForCheck(String backupID)
|
{
|
return ERR_BACKUP_SIGNED_HASH_ERROR.get(backupID);
|
}
|
|
@Override
|
public String toString()
|
{
|
return "MacCryptoEngine [mac=" + mac + "]";
|
}
|
|
}
|
|
/** Represents the cryptographic engine with unsigned hash used for a backup. */
|
private static final class DigestCryptoEngine extends CryptoEngine
|
{
|
private final MessageDigest digest;
|
|
/** Constructor for backup creation. */
|
private DigestCryptoEngine(BackupConfig backupConfig, NewBackupParams backupParams) throws DirectoryException
|
{
|
super(backupConfig.encryptData());
|
String digestAlgorithm = cryptoManager.getPreferredMessageDigestAlgorithm();
|
backupParams.putProperty(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm);
|
digest = retrieveMessageDigest(digestAlgorithm);
|
}
|
|
/** Constructor for backup restore. */
|
private DigestCryptoEngine(BackupInfo backupInfo) throws DirectoryException
|
{
|
super(backupInfo.isEncrypted());
|
HashMap<String, String> backupProperties = backupInfo.getBackupProperties();
|
String digestAlgorithm = backupProperties.get(BACKUP_PROPERTY_DIGEST_ALGORITHM);
|
digest = retrieveMessageDigest(digestAlgorithm);
|
}
|
|
private MessageDigest retrieveMessageDigest(String digestAlgorithm) throws DirectoryException
|
{
|
try
|
{
|
return cryptoManager.getMessageDigest(digestAlgorithm);
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message =
|
ERR_BACKUP_CANNOT_GET_DIGEST.get(digestAlgorithm, stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void updateHashWith(String s)
|
{
|
digest.update(getBytes(s));
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void updateHashWith(byte[] buffer, int offset, int len)
|
{
|
digest.update(buffer, offset, len);
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public byte[] generateBytes()
|
{
|
return digest.digest();
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
LocalizableMessage getErrorMessageForCheck(String backupID)
|
{
|
return ERR_BACKUP_UNSIGNED_HASH_ERROR.get(backupID);
|
}
|
|
@Override
|
public String toString()
|
{
|
return "DigestCryptoEngine [digest=" + digest + "]";
|
}
|
|
}
|
|
/**
|
* Contains all parameters for creation of a new backup.
|
*/
|
private static final class NewBackupParams
|
{
|
final String backupID;
|
final BackupDirectory backupDir;
|
final HashMap<String,String> backupProperties;
|
|
final boolean shouldCompress;
|
|
final boolean isIncremental;
|
final String incrementalBaseID;
|
final BackupInfo baseBackupInfo;
|
|
NewBackupParams(BackupConfig backupConfig) throws DirectoryException
|
{
|
backupID = backupConfig.getBackupID();
|
backupDir = backupConfig.getBackupDirectory();
|
backupProperties = new HashMap<>();
|
shouldCompress = backupConfig.compressData();
|
|
incrementalBaseID = retrieveIncrementalBaseID(backupConfig);
|
isIncremental = incrementalBaseID != null;
|
baseBackupInfo = isIncremental ? getBackupInfo(backupDir, incrementalBaseID) : null;
|
}
|
|
private String retrieveIncrementalBaseID(BackupConfig backupConfig)
|
{
|
String id = null;
|
if (backupConfig.isIncremental())
|
{
|
if (backupConfig.getIncrementalBaseID() == null && backupDir.getLatestBackup() != null)
|
{
|
// The default is to use the latest backup as base.
|
id = backupDir.getLatestBackup().getBackupID();
|
}
|
else
|
{
|
id = backupConfig.getIncrementalBaseID();
|
}
|
|
if (id == null)
|
{
|
// No incremental backup ID: log a message informing that a backup
|
// could not be found and that a normal backup will be done.
|
logger.warn(WARN_BACKUPDB_INCREMENTAL_NOT_FOUND_DOING_NORMAL, backupDir.getPath());
|
}
|
}
|
return id;
|
}
|
|
void putProperty(String name, String value) {
|
backupProperties.put(name, value);
|
}
|
|
@Override
|
public String toString()
|
{
|
return "BackupCreationParams [backupID=" + backupID + ", backupDir=" + backupDir.getPath() + "]";
|
}
|
|
}
|
|
/** Represents a new backup archive. */
|
private static final class NewBackupArchive {
|
|
private final String archiveFilename;
|
|
private String latestFileName;
|
private long latestFileSize;
|
|
private final HashSet<String> dependencies;
|
|
private final String backendID;
|
private final NewBackupParams newBackupParams;
|
private final CryptoEngine cryptoEngine;
|
|
NewBackupArchive(String backendID, NewBackupParams backupParams, CryptoEngine crypt)
|
{
|
this.backendID = backendID;
|
this.newBackupParams = backupParams;
|
this.cryptoEngine = crypt;
|
dependencies = new HashSet<>();
|
if (backupParams.isIncremental)
|
{
|
HashMap<String,String> properties = backupParams.baseBackupInfo.getBackupProperties();
|
latestFileName = properties.get(PROPERTY_LAST_LOGFILE_NAME);
|
latestFileSize = Long.parseLong(properties.get(PROPERTY_LAST_LOGFILE_SIZE));
|
}
|
archiveFilename = BACKUP_BASE_FILENAME + backendID + "-" + backupParams.backupID;
|
}
|
|
String getArchiveFilename()
|
{
|
return archiveFilename;
|
}
|
|
String getBackendID()
|
{
|
return backendID;
|
}
|
|
String getBackupID()
|
{
|
return newBackupParams.backupID;
|
}
|
|
String getBackupPath() {
|
return newBackupParams.backupDir.getPath();
|
}
|
|
void addBaseBackupAsDependency() {
|
dependencies.add(newBackupParams.baseBackupInfo.getBackupID());
|
}
|
|
void updateBackupDirectory() throws DirectoryException
|
{
|
BackupInfo backupInfo = createDescriptorForBackup();
|
try
|
{
|
newBackupParams.backupDir.addBackup(backupInfo);
|
newBackupParams.backupDir.writeBackupDirectoryDescriptor();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get(
|
newBackupParams.backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)),
|
e);
|
}
|
}
|
|
/** Create a descriptor for the backup. */
|
private BackupInfo createDescriptorForBackup()
|
{
|
byte[] bytes = cryptoEngine.generateBytes();
|
byte[] digestBytes = cryptoEngine.hasSignedHash() ? null : bytes;
|
byte[] macBytes = cryptoEngine.hasSignedHash() ? bytes : null;
|
newBackupParams.putProperty(PROPERTY_LAST_LOGFILE_NAME, latestFileName);
|
newBackupParams.putProperty(PROPERTY_LAST_LOGFILE_SIZE, String.valueOf(latestFileSize));
|
return new BackupInfo(
|
newBackupParams.backupDir, newBackupParams.backupID, new Date(), newBackupParams.isIncremental,
|
newBackupParams.shouldCompress, cryptoEngine.shouldEncrypt(), digestBytes, macBytes,
|
dependencies, newBackupParams.backupProperties);
|
}
|
|
@Override
|
public String toString()
|
{
|
return "NewArchive [archive file=" + archiveFilename + ", latestFileName=" + latestFileName
|
+ ", backendID=" + backendID + "]";
|
}
|
|
}
|
|
/** Represents an existing backup archive. */
|
private static final class ExistingBackupArchive {
|
|
private final String backupID;
|
private final BackupDirectory backupDir;
|
private final BackupInfo backupInfo;
|
private final CryptoEngine cryptoEngine;
|
private final File archiveFile;
|
|
ExistingBackupArchive(String backupID, BackupDirectory backupDir) throws DirectoryException
|
{
|
this.backupID = backupID;
|
this.backupDir = backupDir;
|
this.backupInfo = BackupManager.getBackupInfo(backupDir, backupID);
|
this.cryptoEngine = CryptoEngine.forRestore(backupInfo);
|
this.archiveFile = BackupManager.retrieveArchiveFile(backupInfo, backupDir.getPath());
|
}
|
|
File getArchiveFile()
|
{
|
return archiveFile;
|
}
|
|
BackupInfo getBackupInfo() {
|
return backupInfo;
|
}
|
|
String getBackupID()
|
{
|
return backupID;
|
}
|
|
CryptoEngine getCryptoEngine()
|
{
|
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.
|
*/
|
List<BackupInfo> getBackupDependencies() throws DirectoryException
|
{
|
List<BackupInfo> dependencies = new ArrayList<>();
|
BackupInfo currentBackupInfo = backupInfo;
|
while (currentBackupInfo != null && !currentBackupInfo.getDependencies().isEmpty())
|
{
|
String backupID = currentBackupInfo.getDependencies().iterator().next();
|
currentBackupInfo = backupDir.getBackupInfo(backupID);
|
if (currentBackupInfo != null)
|
{
|
dependencies.add(currentBackupInfo);
|
}
|
}
|
Collections.reverse(dependencies);
|
return dependencies;
|
}
|
|
boolean hasDependencies()
|
{
|
return !backupInfo.getDependencies().isEmpty();
|
}
|
|
/** Removes the archive from file system. */
|
boolean removeArchive() throws DirectoryException
|
{
|
try
|
{
|
backupDir.removeBackup(backupID);
|
backupDir.writeBackupDirectoryDescriptor();
|
}
|
catch (ConfigException e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject());
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
LocalizableMessage message = ERR_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get(
|
backupDir.getDescriptorPath(), stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
|
return archiveFile.delete();
|
}
|
|
}
|
|
/** Represents a writer of a backup archive. */
|
private static final class BackupArchiveWriter implements Closeable {
|
|
private final ZipOutputStream zipOutputStream;
|
private final NewBackupArchive archive;
|
private final CryptoEngine cryptoEngine;
|
|
BackupArchiveWriter(NewBackupArchive archive) throws DirectoryException
|
{
|
this.archive = archive;
|
this.cryptoEngine = archive.cryptoEngine;
|
this.zipOutputStream = open(archive.getBackupPath(), archive.getArchiveFilename());
|
}
|
|
@Override
|
public void close() throws IOException
|
{
|
StaticUtils.close(zipOutputStream);
|
}
|
|
/**
|
* 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
|
{
|
ZipEntry zipEntry = new ZipEntry(relativePath);
|
zipOutputStream.putNextEntry(zipEntry);
|
|
cryptoMethod.updateHashWith(relativePath);
|
|
InputStream inputStream = null;
|
long totalBytesRead = 0;
|
try {
|
inputStream = new FileInputStream(file.toFile());
|
byte[] buffer = new byte[8192];
|
int bytesRead = inputStream.read(buffer);
|
while (bytesRead > 0 && !backupConfig.isCancelled())
|
{
|
cryptoMethod.updateHashWith(buffer, 0, bytesRead);
|
zipOutputStream.write(buffer, 0, bytesRead);
|
totalBytesRead += bytesRead;
|
bytesRead = inputStream.read(buffer);
|
}
|
}
|
finally {
|
StaticUtils.close(inputStream);
|
}
|
|
zipOutputStream.closeEntry();
|
logger.info(NOTE_BACKUP_ARCHIVED_FILE, zipEntry.getName());
|
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.
|
*/
|
void writeStrings(List<String> stringList, String fileName, CryptoEngine cryptoMethod)
|
throws IOException
|
{
|
ZipEntry zipEntry = new ZipEntry(fileName);
|
zipOutputStream.putNextEntry(zipEntry);
|
|
cryptoMethod.updateHashWith(fileName);
|
|
Writer writer = new OutputStreamWriter(zipOutputStream);
|
for (String s : stringList)
|
{
|
cryptoMethod.updateHashWith(s);
|
writer.write(s);
|
writer.write(EOL);
|
}
|
writer.flush();
|
zipOutputStream.closeEntry();
|
}
|
|
/** Writes a empty placeholder entry into the archive. */
|
void writeEmptyPlaceHolder() throws DirectoryException
|
{
|
try
|
{
|
ZipEntry emptyPlaceholder = new ZipEntry(ZIPENTRY_EMPTY_PLACEHOLDER);
|
zipOutputStream.putNextEntry(emptyPlaceholder);
|
}
|
catch (IOException e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(ZIPENTRY_EMPTY_PLACEHOLDER, archive.getBackupID(),
|
stackTraceToSingleLineString(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.
|
*
|
*/
|
void writeUnchangedFiles(Path rootDirectory, ListIterator<Path> files, BackupConfig backupConfig)
|
throws DirectoryException
|
{
|
List<String> unchangedFilenames = new ArrayList<>();
|
while (files.hasNext() && !backupConfig.isCancelled())
|
{
|
Path file = files.next();
|
String relativePath = rootDirectory.relativize(file).toString();
|
int cmp = relativePath.compareTo(archive.latestFileName);
|
if (cmp > 0 || (cmp == 0 && file.toFile().length() != archive.latestFileSize))
|
{
|
files.previous();
|
break;
|
}
|
logger.info(NOTE_BACKUP_FILE_UNCHANGED, relativePath);
|
unchangedFilenames.add(relativePath);
|
}
|
|
if (!unchangedFilenames.isEmpty())
|
{
|
writeUnchangedFilenames(unchangedFilenames);
|
}
|
}
|
|
/** Writes the list of unchanged files names in a file as new entry in the archive. */
|
private void writeUnchangedFilenames(List<String> unchangedList) throws DirectoryException
|
{
|
String zipEntryName = ZIPENTRY_UNCHANGED_LOGFILES;
|
try
|
{
|
writeStrings(unchangedList, zipEntryName, archive.cryptoEngine);
|
}
|
catch (IOException e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(
|
DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(zipEntryName, archive.getBackupID(),
|
stackTraceToSingleLineString(e)), e);
|
}
|
archive.addBaseBackupAsDependency();
|
}
|
|
/**
|
* Writes the new files in the archive.
|
*/
|
void writeChangedFiles(Path rootDirectory, ListIterator<Path> files, BackupConfig backupConfig)
|
throws DirectoryException
|
{
|
while (files.hasNext() && !backupConfig.isCancelled())
|
{
|
Path file = files.next();
|
String relativePath = rootDirectory.relativize(file).toString();
|
try
|
{
|
archive.latestFileSize = writeFile(file, relativePath, archive.cryptoEngine, backupConfig);
|
archive.latestFileName = relativePath;
|
}
|
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);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(relativePath, archive.getBackupID(),
|
stackTraceToSingleLineString(e)), e);
|
}
|
}
|
}
|
|
private ZipOutputStream open(String backupPath, String archiveFilename) throws DirectoryException
|
{
|
OutputStream output = openStream(backupPath, archiveFilename);
|
output = cryptoEngine.encryptOutput(output);
|
return openZipStream(output);
|
}
|
|
private OutputStream openStream(String backupPath, String archiveFilename) throws DirectoryException {
|
OutputStream output = null;
|
try
|
{
|
File archiveFile = new File(backupPath, archiveFilename);
|
int i = 1;
|
while (archiveFile.exists())
|
{
|
archiveFile = new File(backupPath, archiveFilename + "." + i);
|
i++;
|
}
|
output = new FileOutputStream(archiveFile, false);
|
archive.newBackupParams.putProperty(BACKUP_PROPERTY_ARCHIVE_FILENAME, archiveFilename);
|
return output;
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
StaticUtils.close(output);
|
LocalizableMessage message = ERR_BACKUP_CANNOT_CREATE_ARCHIVE_FILE.
|
get(archiveFilename, backupPath, archive.getBackupID(), stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
|
}
|
}
|
|
/** Wraps the file output stream in a zip output stream. */
|
private ZipOutputStream openZipStream(OutputStream outputStream)
|
{
|
ZipOutputStream zipStream = new ZipOutputStream(outputStream);
|
|
zipStream.setComment(ERR_BACKUP_ZIP_COMMENT.get(DynamicConstants.PRODUCT_NAME, archive.getBackupID())
|
.toString());
|
|
if (archive.newBackupParams.shouldCompress)
|
{
|
zipStream.setLevel(Deflater.DEFAULT_COMPRESSION);
|
}
|
else
|
{
|
zipStream.setLevel(Deflater.NO_COMPRESSION);
|
}
|
return zipStream;
|
}
|
|
@Override
|
public String toString()
|
{
|
return "BackupArchiveWriter [archive file=" + archive.getArchiveFilename() + ", backendId="
|
+ archive.getBackendID() + "]";
|
}
|
|
}
|
|
/** Represents a reader of a backup archive. */
|
private static final class BackupArchiveReader {
|
|
private final CryptoEngine cryptoEngine;
|
private final File archiveFile;
|
private final String identifier;
|
private final BackupInfo backupInfo;
|
|
BackupArchiveReader(String identifier, ExistingBackupArchive archive)
|
{
|
this.identifier = identifier;
|
this.backupInfo = archive.getBackupInfo();
|
this.archiveFile = archive.getArchiveFile();
|
this.cryptoEngine = archive.getCryptoEngine();
|
}
|
|
BackupArchiveReader(String identifier, BackupInfo backupInfo, String backupDirectoryPath) throws DirectoryException
|
{
|
this.identifier = identifier;
|
this.backupInfo = backupInfo;
|
this.archiveFile = BackupManager.retrieveArchiveFile(backupInfo, backupDirectoryPath);
|
this.cryptoEngine = CryptoEngine.forRestore(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.
|
*/
|
Set<String> readUnchangedDependentFiles() throws DirectoryException
|
{
|
Set<String> hashSet = new HashSet<>();
|
ZipInputStream zipStream = null;
|
try
|
{
|
zipStream = openZipStream();
|
|
// Iterate through the entries in the zip file.
|
ZipEntry zipEntry = zipStream.getNextEntry();
|
while (zipEntry != null)
|
{
|
// We are looking for the entry containing the list of unchanged files.
|
if (ZIPENTRY_UNCHANGED_LOGFILES.equals(zipEntry.getName()))
|
{
|
hashSet.addAll(readAllLines(zipStream));
|
break;
|
}
|
zipEntry = zipStream.getNextEntry();
|
}
|
return hashSet;
|
}
|
catch (IOException e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_BACKUP_CANNOT_RESTORE.get(
|
identifier, stackTraceToSingleLineString(e)), e);
|
}
|
finally {
|
StaticUtils.close(zipStream);
|
}
|
}
|
|
/**
|
* 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
|
{
|
restoreArchive0(restoreDir, filesToRestore, restoreConfig, backupable);
|
}
|
catch (IOException e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_RESTORE.get(identifier, stackTraceToSingleLineString(e)), e);
|
}
|
|
// check the hash
|
byte[] hash = backupInfo.getUnsignedHash() != null ? backupInfo.getUnsignedHash() : backupInfo.getSignedHash();
|
cryptoEngine.check(hash, backupInfo.getBackupID());
|
}
|
|
private void restoreArchive0(Path restoreDir, Set<String> filesToRestore, RestoreConfig restoreConfig,
|
Backupable backupable) throws DirectoryException, IOException {
|
|
ZipInputStream zipStream = null;
|
try {
|
zipStream = openZipStream();
|
|
ZipEntry zipEntry = zipStream.getNextEntry();
|
while (zipEntry != null && !restoreConfig.isCancelled())
|
{
|
String zipEntryName = zipEntry.getName();
|
|
Pair<Boolean, ZipEntry> result = handleSpecialEntries(zipStream, zipEntryName);
|
if (result.getFirst()) {
|
zipEntry = result.getSecond();
|
continue;
|
}
|
|
boolean mustRestoreOnDisk = !restoreConfig.verifyOnly()
|
&& (filesToRestore.isEmpty() || filesToRestore.contains(zipEntryName));
|
|
if (mustRestoreOnDisk)
|
{
|
restoreZipEntry(zipEntryName, zipStream, restoreDir, restoreConfig);
|
}
|
else
|
{
|
restoreZipEntryVirtual(zipEntryName, zipStream, restoreConfig);
|
}
|
|
zipEntry = zipStream.getNextEntry();
|
}
|
}
|
finally {
|
StaticUtils.close(zipStream);
|
}
|
}
|
|
/**
|
* Handle any special entry in the archive.
|
*
|
* @return the pair (true, zipEntry) if next entry was read, (false, null) otherwise
|
*/
|
private Pair<Boolean, ZipEntry> handleSpecialEntries(ZipInputStream zipStream, String zipEntryName)
|
throws IOException
|
{
|
if (ZIPENTRY_EMPTY_PLACEHOLDER.equals(zipEntryName))
|
{
|
// the backup contains no files
|
return Pair.of(true, zipStream.getNextEntry());
|
}
|
|
if (ZIPENTRY_UNCHANGED_LOGFILES.equals(zipEntryName))
|
{
|
// This entry is treated specially. It is never restored,
|
// and its hash is computed on the strings, not the bytes.
|
cryptoEngine.updateHashWith(zipEntryName);
|
List<String> lines = readAllLines(zipStream);
|
for (String line : lines)
|
{
|
cryptoEngine.updateHashWith(line);
|
}
|
return Pair.of(true, zipStream.getNextEntry());
|
}
|
return Pair.of(false, null);
|
}
|
|
/**
|
* 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())
|
{
|
logger.info(NOTE_BACKUP_VERIFY_FILE, zipEntryName);
|
}
|
cryptoEngine.updateHashWith(zipEntryName);
|
restoreFile(zipStream, null, restoreConfig);
|
}
|
|
/**
|
* Restores a zip entry with actual write on disk.
|
*/
|
private void restoreZipEntry(String zipEntryName, ZipInputStream zipStream, Path restoreDir,
|
RestoreConfig restoreConfig) throws IOException, DirectoryException
|
{
|
OutputStream outputStream = null;
|
long totalBytesRead = 0;
|
try
|
{
|
Path fileToRestore = restoreDir.resolve(zipEntryName);
|
ensureFileCanBeRestored(fileToRestore);
|
outputStream = new FileOutputStream(fileToRestore.toFile());
|
cryptoEngine.updateHashWith(zipEntryName);
|
totalBytesRead = restoreFile(zipStream, outputStream, restoreConfig);
|
logger.info(NOTE_BACKUP_RESTORED_FILE, zipEntryName, totalBytesRead);
|
}
|
finally
|
{
|
StaticUtils.close(outputStream);
|
}
|
}
|
|
private void ensureFileCanBeRestored(Path fileToRestore) throws DirectoryException
|
{
|
Path parent = fileToRestore.getParent();
|
if (!Files.exists(parent))
|
{
|
try
|
{
|
Files.createDirectories(parent);
|
}
|
catch (IOException e)
|
{
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_CREATE_DIRECTORY_TO_RESTORE_FILE.get(fileToRestore, identifier));
|
}
|
}
|
}
|
|
/**
|
* 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];
|
int bytesRead = zipInputStream.read(buffer);
|
while (bytesRead > 0 && !restoreConfig.isCancelled())
|
{
|
totalBytesRead += bytesRead;
|
|
cryptoEngine.updateHashWith(buffer, 0, bytesRead);
|
|
if (outputStream != null)
|
{
|
outputStream.write(buffer, 0, bytesRead);
|
}
|
|
bytesRead = zipInputStream.read(buffer);
|
}
|
return totalBytesRead;
|
}
|
|
private InputStream openStream() throws DirectoryException
|
{
|
try
|
{
|
return new FileInputStream(archiveFile);
|
}
|
catch (FileNotFoundException e)
|
{
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_RESTORE.get(identifier, stackTraceToSingleLineString(e)), e);
|
}
|
}
|
|
private ZipInputStream openZipStream() throws DirectoryException
|
{
|
InputStream inputStream = openStream();
|
inputStream = cryptoEngine.encryptInput(inputStream);
|
return new ZipInputStream(inputStream);
|
}
|
|
private List<String> readAllLines(ZipInputStream zipStream) throws IOException
|
{
|
final ArrayList<String> results = new ArrayList<>();
|
String line;
|
BufferedReader reader = new BufferedReader(new InputStreamReader(zipStream));
|
while ((line = reader.readLine()) != null)
|
{
|
results.add(line);
|
}
|
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
|
{
|
final NewBackupParams backupParams = new NewBackupParams(backupConfig);
|
final CryptoEngine cryptoEngine = CryptoEngine.forCreation(backupConfig, backupParams);
|
final NewBackupArchive newArchive = new NewBackupArchive(backendID, backupParams, cryptoEngine);
|
|
BackupArchiveWriter archiveWriter = null;
|
try
|
{
|
final ListIterator<Path> files = backupable.getFilesToBackup();
|
final Path rootDirectory = backupable.getDirectory().toPath();
|
archiveWriter = new BackupArchiveWriter(newArchive);
|
|
if (files.hasNext())
|
{
|
if (backupParams.isIncremental) {
|
archiveWriter.writeUnchangedFiles(rootDirectory, files, backupConfig);
|
}
|
archiveWriter.writeChangedFiles(rootDirectory, files, backupConfig);
|
}
|
else {
|
archiveWriter.writeEmptyPlaceHolder();
|
}
|
}
|
finally
|
{
|
closeArchiveWriter(archiveWriter, newArchive.getArchiveFilename(), backupParams.backupDir.getPath());
|
}
|
|
newArchive.updateBackupDirectory();
|
|
if (backupConfig.isCancelled())
|
{
|
// Remove the backup since it may be incomplete
|
removeBackup(backupParams.backupDir, backupParams.backupID);
|
}
|
}
|
|
/**
|
* 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
|
{
|
Path saveDirectory = null;
|
if (!restoreConfig.verifyOnly())
|
{
|
saveDirectory = backupable.beforeRestore();
|
}
|
|
final String backupID = restoreConfig.getBackupID();
|
final ExistingBackupArchive existingArchive =
|
new ExistingBackupArchive(backupID, restoreConfig.getBackupDirectory());
|
final Path restoreDirectory = getRestoreDirectory(backupable, backupID);
|
|
if (existingArchive.hasDependencies())
|
{
|
final BackupArchiveReader zipArchiveReader = new BackupArchiveReader(backupID, existingArchive);
|
final Set<String> unchangedFilesToRestore = zipArchiveReader.readUnchangedDependentFiles();
|
final List<BackupInfo> dependencies = existingArchive.getBackupDependencies();
|
for (BackupInfo dependencyBackupInfo : dependencies)
|
{
|
restoreArchive(restoreDirectory, unchangedFilesToRestore, restoreConfig, backupable, dependencyBackupInfo);
|
}
|
}
|
|
// Restore the final archive file.
|
Set<String> filesToRestore = emptySet();
|
restoreArchive(restoreDirectory, filesToRestore, restoreConfig, backupable, existingArchive.getBackupInfo());
|
|
if (!restoreConfig.verifyOnly())
|
{
|
backupable.afterRestore(restoreDirectory, saveDirectory);
|
}
|
}
|
|
/**
|
* 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).
|
*/
|
public void removeBackup(BackupDirectory backupDir, String backupID) throws DirectoryException
|
{
|
ExistingBackupArchive archive = new ExistingBackupArchive(backupID, backupDir);
|
archive.removeArchive();
|
}
|
|
private Path getRestoreDirectory(Backupable backupable, String backupID)
|
{
|
File restoreDirectory = backupable.getDirectory();
|
if (!backupable.isDirectRestore())
|
{
|
restoreDirectory = new File(restoreDirectory.getAbsoluteFile() + "-restore-" + backupID);
|
}
|
return restoreDirectory.toPath();
|
}
|
|
private void closeArchiveWriter(BackupArchiveWriter archiveWriter, String backupFile, String backupPath)
|
throws DirectoryException
|
{
|
if (archiveWriter != null)
|
{
|
try
|
{
|
archiveWriter.close();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
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.
|
*/
|
private void restoreArchive(Path restoreDir,
|
Set<String> filesToRestore,
|
RestoreConfig restoreConfig,
|
Backupable backupable,
|
BackupInfo backupInfo) throws DirectoryException
|
{
|
String backupID = backupInfo.getBackupID();
|
String backupDirectoryPath = restoreConfig.getBackupDirectory().getPath();
|
|
BackupArchiveReader zipArchiveReader = new BackupArchiveReader(backupID, backupInfo, backupDirectoryPath);
|
zipArchiveReader.restoreArchive(restoreDir, filesToRestore, restoreConfig, backupable);
|
}
|
|
/** Retrieves the full path of the archive file. */
|
private static File retrieveArchiveFile(BackupInfo backupInfo, String backupDirectoryPath)
|
{
|
Map<String,String> backupProperties = backupInfo.getBackupProperties();
|
String archiveFilename = backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME);
|
return new File(backupDirectoryPath, archiveFilename);
|
}
|
|
/**
|
* 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
|
{
|
BackupInfo backupInfo = backupDir.getBackupInfo(backupID);
|
if (backupInfo == null)
|
{
|
LocalizableMessage message = ERR_BACKUP_MISSING_BACKUPID.get(backupID, backupDir.getPath());
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message);
|
}
|
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.
|
*/
|
public static List<Path> getFiles(File directory, FileFilter filter, String identifier)
|
throws DirectoryException
|
{
|
File[] files = null;
|
try
|
{
|
files = directory.listFiles(filter);
|
}
|
catch (Exception e)
|
{
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_LIST_LOG_FILES.get(directory.getAbsolutePath(), identifier), e);
|
}
|
if (files == null)
|
{
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
|
ERR_BACKUP_CANNOT_LIST_LOG_FILES.get(directory.getAbsolutePath(), identifier));
|
}
|
|
List<Path> paths = new ArrayList<>();
|
for (File file : files)
|
{
|
paths.add(file.toPath());
|
}
|
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
|
{
|
ListIterator<Path> filesToBackup = backupable.getFilesToBackup();
|
File rootDirectory = backupable.getDirectory();
|
String saveDirectory = rootDirectory.getAbsolutePath() + ".save";
|
BackupManager.saveFilesToDirectory(rootDirectory.toPath(), filesToBackup, saveDirectory, identifier);
|
return Paths.get(saveDirectory);
|
}
|
|
/**
|
* 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,
|
String identifier) throws DirectoryException
|
{
|
Path targetDirectory = null;
|
try
|
{
|
targetDirectory = createDirectoryWithNumericSuffix(targetBasePath, identifier);
|
while (files.hasNext())
|
{
|
Path file = files.next();
|
Path relativeFilePath = rootDirectory.relativize(file);
|
Path targetFile = targetDirectory.resolve(relativeFilePath);
|
Files.createDirectories(targetFile.getParent());
|
Files.move(file, targetFile);
|
}
|
return targetDirectory;
|
}
|
catch (IOException e)
|
{
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_SAVE_FILES_BEFORE_RESTORE.get(rootDirectory, targetDirectory, identifier,
|
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.
|
*/
|
static Path createDirectoryWithNumericSuffix(final String baseDirectoryPath, String identifier)
|
throws DirectoryException
|
{
|
try
|
{
|
int number = getHighestSuffixNumberForPath(baseDirectoryPath);
|
String path = baseDirectoryPath + (number + 1);
|
Path directory = Paths.get(path);
|
Files.createDirectories(directory);
|
return directory;
|
}
|
catch (IOException e)
|
{
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
ERR_BACKUP_CANNOT_CREATE_SAVE_DIRECTORY.get(baseDirectoryPath, identifier,
|
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.
|
*/
|
private static int getHighestSuffixNumberForPath(final String basePath) throws IOException
|
{
|
final File baseFile = new File(basePath).getCanonicalFile();
|
final File[] existingFiles = baseFile.getParentFile().listFiles();
|
final Pattern pattern = Pattern.compile(baseFile + "\\d*");
|
int highestNumber = 0;
|
for (File file : existingFiles)
|
{
|
final String name = file.getCanonicalPath();
|
if (pattern.matcher(name).matches())
|
{
|
String numberAsString = name.substring(baseFile.getPath().length());
|
int number = numberAsString.isEmpty() ? 0 : Integer.valueOf(numberAsString);
|
highestNumber = number > highestNumber ? number : highestNumber;
|
}
|
}
|
return highestNumber;
|
}
|
|
}
|