/* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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. */ package org.opends.server.backends.jeb; import org.opends.messages.Message; import org.opends.server.config.ConfigException; import org.opends.server.core.DirectoryServer; import org.opends.server.util.DynamicConstants; import org.opends.server.types.CryptoManagerException; import javax.crypto.Mac; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; 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.Set; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.opends.server.types.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.*; import org.opends.server.loggers.debug.DebugTracer; import static org.opends.messages.JebMessages.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * A backup manager for JE backends. */ public class BackupManager { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The common prefix for archive files. */ public 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. */ public 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. */ public 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. */ public 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. */ public static final String ZIPENTRY_EMPTY_PLACEHOLDER = "empty.placeholder"; /** * The backend ID. */ private String backendID; /** * Construct a backup manager for a JE backend. * @param backendID The ID of the backend instance for which a backup * manager is required. */ public BackupManager(String backendID) { this.backendID = backendID; } /** * Create a backup of the JE backend. The backup is stored in a single zip * file in the backup directory. If the backup is incremental, then the * first entry in the zip is a text file containing a list of all the JE * log files that are unchanged since the previous backup. The remaining * zip entries are the JE log files themselves, which, for an incremental, * only include those files that have changed. * @param backendDir The directory of the backend instance for * which the backup is required. * @param backupConfig The configuration to use when performing the backup. * @throws DirectoryException If a Directory Server error occurs. */ public void createBackup(File backendDir, BackupConfig backupConfig) throws DirectoryException { // Get the properties to use for the backup. String backupID = backupConfig.getBackupID(); BackupDirectory backupDir = backupConfig.getBackupDirectory(); boolean incremental = backupConfig.isIncremental(); String incrBaseID = backupConfig.getIncrementalBaseID(); boolean compress = backupConfig.compressData(); boolean encrypt = backupConfig.encryptData(); boolean hash = backupConfig.hashData(); boolean signHash = backupConfig.signHash(); // Create a hash map that will hold the extra backup property information // for this backup. HashMap<String,String> backupProperties = new HashMap<String,String>(); // Get the crypto manager and use it to obtain references to the message // digest and/or MAC to use for hashing and/or signing. CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); Mac mac = null; MessageDigest digest = null; String digestAlgorithm = null; String macKeyID = null; if (hash) { if (signHash) { try { macKeyID = cryptoManager.getMacEngineKeyEntryID(); backupProperties.put(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID); mac = cryptoManager.getMacEngine(macKeyID); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get( macKeyID, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } else { digestAlgorithm = cryptoManager.getPreferredMessageDigestAlgorithm(); backupProperties.put(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm); try { digest = cryptoManager.getPreferredMessageDigest(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get( digestAlgorithm, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } } // Date the backup. Date backupDate = new Date(); // If this is an incremental, determine the base backup for this backup. HashSet<String> dependencies = new HashSet<String>(); BackupInfo baseBackup = null; /* FilenameFilter backupTagFilter = new FilenameFilter() { public boolean accept(File dir, String name) { return name.startsWith(BackupInfo.PROPERTY_BACKUP_ID); } }; */ if (incremental) { if (incrBaseID == null) { // The default is to use the latest backup as base. if (backupDir.getLatestBackup() != null) { incrBaseID = backupDir.getLatestBackup().getBackupID(); } } // Get the set of possible base backups from the current database. /* String[] files = backendDir.list(backupTagFilter); if (files == null || files.length == 0) { // Incremental not allowed until after a full. Message msg = ERR_JEB_INCR_BACKUP_REQUIRES_FULL.get(); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); } HashSet<String> backups = new HashSet<String>(); int prefixLen = BackupInfo.PROPERTY_BACKUP_ID.length()+1; for (String s : files) { String actualBaseID = s.substring(prefixLen); backups.add(actualBaseID); } // Check that it makes sense to do this incremental. if (incrBaseID == null || !backups.contains(incrBaseID)) { Message msg = ERR_JEB_INCR_BACKUP_FROM_WRONG_BASE.get(backups.toString()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); } */ if (incrBaseID == null) { // No incremental backup ID: log a message informing that a backup // could not be found and that a normal backup will be done. incremental = false; Message message = WARN_BACKUPDB_INCREMENTAL_NOT_FOUND_DOING_NORMAL.get( backupDir.getPath()); logError(message); } else { baseBackup = getBackupInfo(backupDir, incrBaseID); } } // Get information about the latest log file from the base backup. String latestFileName = null; long latestFileSize = 0; if (baseBackup != null) { HashMap<String,String> properties = baseBackup.getBackupProperties(); latestFileName = properties.get(PROPERTY_LAST_LOGFILE_NAME); latestFileSize = Long.parseLong( properties.get(PROPERTY_LAST_LOGFILE_SIZE)); } // Create an output stream that will be used to write the archive file. At // its core, it will be a file output stream to put a file on the disk. If // we are to encrypt the data, then that file output stream will be wrapped // in a cipher output stream. The resulting output stream will then be // wrapped by a zip output stream (which may or may not actually use // compression). String archiveFilename = null; OutputStream outputStream; File archiveFile; try { archiveFilename = BACKUP_BASE_FILENAME + backendID + "-" + backupID; archiveFile = new File(backupDir.getPath(), archiveFilename); if (archiveFile.exists()) { int i=1; while (true) { archiveFile = new File(backupDir.getPath(), archiveFilename + "." + i); if (archiveFile.exists()) { i++; } else { archiveFilename = archiveFilename + "." + i; break; } } } outputStream = new FileOutputStream(archiveFile, false); backupProperties.put(BACKUP_PROPERTY_ARCHIVE_FILENAME, archiveFilename); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_CREATE_ARCHIVE_FILE. get(String.valueOf(archiveFilename), backupDir.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // If we should encrypt the data, then wrap the output stream in a cipher // output stream. if (encrypt) { try { outputStream = cryptoManager.getCipherOutputStream(outputStream); } catch (CryptoManagerException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Wrap the file output stream in a zip output stream. ZipOutputStream zipStream = new ZipOutputStream(outputStream); Message message = ERR_JEB_BACKUP_ZIP_COMMENT.get( DynamicConstants.PRODUCT_NAME, backupID, backendID); zipStream.setComment(message.toString()); if (compress) { zipStream.setLevel(Deflater.DEFAULT_COMPRESSION); } else { zipStream.setLevel(Deflater.NO_COMPRESSION); } // Record this backup in the database itself. /* String backupTag = BackupInfo.PROPERTY_BACKUP_ID + "_" + backupID; File tagFile = new File(backendDir, backupTag); try { tagFile.createNewFile(); } catch (IOException e) { assert debugException(CLASS_NAME, "createBackup", e); Message msg = ERR_JEB_CANNOT_CREATE_BACKUP_TAG_FILE.get( backupTag, backendDir.getPath()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); } */ // Get a list of all the log files comprising the database. FilenameFilter filenameFilter = new FilenameFilter() { public boolean accept(File d, String name) { return name.endsWith(".jdb"); } }; File[] logFiles; try { logFiles = backendDir.listFiles(filenameFilter); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get( backendDir.getAbsolutePath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Check to see if backend is empty. If so, insert placeholder entry into // archive if(logFiles.length <= 0) { try { ZipEntry emptyPlaceholder = new ZipEntry(ZIPENTRY_EMPTY_PLACEHOLDER); zipStream.putNextEntry(emptyPlaceholder); } catch (IOException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( ZIPENTRY_EMPTY_PLACEHOLDER, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } // Sort the log files from oldest to youngest since this is the order // in which they must be copied. // This is easy since the files are created in alphabetical order by JE. Arrays.sort(logFiles); try { // Archive the backup tag files. /* File[] tagFiles = backendDir.listFiles(backupTagFilter); if (tagFiles != null) { for (File f : tagFiles) { try { archiveFile(zipStream, mac, digest, f); } catch (IOException e) { assert debugException(CLASS_NAME, "createBackup", e); Message message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( backupTag, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } } } */ // Process log files that are unchanged from the base backup. int indexCurrent = 0; if (latestFileName != null) { ArrayList<String> unchangedList = new ArrayList<String>(); while (indexCurrent < logFiles.length && !backupConfig.isCancelled()) { File logFile = logFiles[indexCurrent]; String logFileName = logFile.getName(); // Stop when we get to the first log file that has been // written since the base backup. int compareResult = logFileName.compareTo(latestFileName); if (compareResult > 0 || (compareResult == 0 && logFile.length() != latestFileSize)) { break; } message = NOTE_JEB_BACKUP_FILE_UNCHANGED.get(logFileName); logError(message); unchangedList.add(logFileName); indexCurrent++; } // Write a file containing the list of unchanged log files. if (!unchangedList.isEmpty()) { String zipEntryName = ZIPENTRY_UNCHANGED_LOGFILES; try { archiveList(zipStream, mac, digest, zipEntryName, unchangedList); } catch (IOException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( zipEntryName, stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } // Set the dependency. dependencies.add(baseBackup.getBackupID()); } } // Write the new log files to the zip file. do { boolean deletedFiles = false; while (indexCurrent < logFiles.length && !backupConfig.isCancelled()) { File logFile = logFiles[indexCurrent]; try { latestFileSize = archiveFile(zipStream, mac, digest, logFile, backupConfig); latestFileName = logFile.getName(); } catch (FileNotFoundException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } // A log file has been deleted by the cleaner since we started. deletedFiles = true; } catch (IOException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( logFile.getName(), stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } indexCurrent++; } if (deletedFiles) { // The cleaner is active and has deleted one or more of the log files // since we started. The in-use data from those log files will have // been written to new log files, so we must include those new files. final String latest = logFiles[logFiles.length-1].getName(); final long latestSize = latestFileSize; FilenameFilter filter = new FilenameFilter() { public boolean accept(File d, String name) { if (!name.endsWith(".jdb")) return false; int compareTo = name.compareTo(latest); if (compareTo > 0) return true; if (compareTo == 0 && d.length() > latestSize) return true; return false; } }; try { logFiles = backendDir.listFiles(filter); indexCurrent = 0; } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get( backendDir.getAbsolutePath(), stackTraceToSingleLineString(e)); throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), message, e); } if (logFiles == null) { break; } Arrays.sort(logFiles); message = NOTE_JEB_BACKUP_CLEANER_ACTIVITY.get( String.valueOf(logFiles.length)); logError(message); } else { // We are done. break; } } while (true); } catch (DirectoryException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } try { zipStream.close(); } catch (Exception e2) {} } // We're done writing the file, so close the zip stream (which should also // close the underlying stream). try { zipStream.close(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_CLOSE_ZIP_STREAM. get(archiveFilename, backupDir.getPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Get the digest or MAC bytes if appropriate. byte[] digestBytes = null; byte[] macBytes = null; if (hash) { if (signHash) { macBytes = mac.doFinal(); } else { digestBytes = digest.digest(); } } // Create a descriptor for this backup. backupProperties.put(PROPERTY_LAST_LOGFILE_NAME, latestFileName); backupProperties.put(PROPERTY_LAST_LOGFILE_SIZE, String.valueOf(latestFileSize)); BackupInfo backupInfo = new BackupInfo(backupDir, backupID, backupDate, incremental, compress, encrypt, digestBytes, macBytes, dependencies, backupProperties); try { backupDir.addBackup(backupInfo); backupDir.writeBackupDirectoryDescriptor(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Remove the backup if this operation was cancelled since the // backup may be incomplete if (backupConfig.isCancelled()) { removeBackup(backupDir, backupID); } } /** * Restore a JE backend from backup, or verify the backup. * @param backendDir The configuration of the backend instance to be * restored. * @param restoreConfig The configuration to use when performing the restore. * @throws DirectoryException If a Directory Server error occurs. */ public void restoreBackup(File backendDir, RestoreConfig restoreConfig) throws DirectoryException { // Get the properties to use for the restore. String backupID = restoreConfig.getBackupID(); BackupDirectory backupDir = restoreConfig.getBackupDirectory(); boolean verifyOnly = restoreConfig.verifyOnly(); BackupInfo backupInfo = getBackupInfo(backupDir, backupID); // Create a restore directory with a different name to the backend // directory. File restoreDir = new File(backendDir.getPath() + "-restore-" + backupID); if (!verifyOnly) { File[] files = restoreDir.listFiles(); if (files != null) { for (File f : files) { f.delete(); } } restoreDir.mkdir(); } // Get the set of restore files that are in dependencies. Set<String> includeFiles; try { includeFiles = getUnchanged(backupDir, backupInfo); } catch (IOException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( backupInfo.getBackupID(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Restore any dependencies. List<BackupInfo> dependents = getDependents(backupDir, backupInfo); for (BackupInfo dependent : dependents) { try { restoreArchive(restoreDir, restoreConfig, dependent, includeFiles); } catch (IOException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( dependent.getBackupID(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Restore the final archive file. try { restoreArchive(restoreDir, restoreConfig, backupInfo, null); } catch (IOException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( backupInfo.getBackupID(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Delete the current backend directory and rename the restore directory. if (!verifyOnly) { File[] files = backendDir.listFiles(); if (files != null) { for (File f : files) { f.delete(); } } backendDir.delete(); if (!restoreDir.renameTo(backendDir)) { Message msg = ERR_JEB_CANNOT_RENAME_RESTORE_DIRECTORY.get( restoreDir.getPath(), backendDir.getPath()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); } } } /** * 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 { BackupInfo backupInfo = getBackupInfo(backupDir, backupID); HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); String archiveFilename = backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); File archiveFile = new File(backupDir.getPath(), archiveFilename); try { backupDir.removeBackup(backupID); } catch (ConfigException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } try { backupDir.writeBackupDirectoryDescriptor(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } // Remove the archive file. archiveFile.delete(); } /** * Restore the contents of an archive file. 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 restoreConfig The restore configuration. * @param backupInfo The backup containing the files to be restored. * @param includeFiles The set of files to be restored. If null, then * all files are restored. * @throws DirectoryException If a Directory Server error occurs. * @throws IOException If an I/O exception occurs during the restore. */ private void restoreArchive(File restoreDir, RestoreConfig restoreConfig, BackupInfo backupInfo, Set<String> includeFiles) throws DirectoryException,IOException { BackupDirectory backupDir = restoreConfig.getBackupDirectory(); boolean verifyOnly = restoreConfig.verifyOnly(); String backupID = backupInfo.getBackupID(); boolean encrypt = backupInfo.isEncrypted(); byte[] hash = backupInfo.getUnsignedHash(); byte[] signHash = backupInfo.getSignedHash(); HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); String archiveFilename = backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); File archiveFile = new File(backupDir.getPath(), archiveFilename); InputStream inputStream = new FileInputStream(archiveFile); // Get the crypto manager and use it to obtain references to the message // digest and/or MAC to use for hashing and/or signing. CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); Mac mac = null; MessageDigest digest = null; String digestAlgorithm = null; String macKeyID = null; if (signHash != null) { macKeyID = backupProperties.get(BACKUP_PROPERTY_MAC_KEY_ID); try { mac = cryptoManager.getMacEngine(macKeyID); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get( macKeyID, stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } if (hash != null) { digestAlgorithm = backupProperties.get(BACKUP_PROPERTY_DIGEST_ALGORITHM); try { digest = cryptoManager.getMessageDigest(digestAlgorithm); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get( digestAlgorithm, stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // If the data is encrypted, then wrap the input stream in a cipher // input stream. if (encrypt) { try { inputStream = cryptoManager.getCipherInputStream(inputStream); } catch (CryptoManagerException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Wrap the file input stream in a zip input stream. ZipInputStream zipStream = new ZipInputStream(inputStream); // Iterate through the entries in the zip file. ZipEntry zipEntry = zipStream.getNextEntry(); while (zipEntry != null && !restoreConfig.isCancelled()) { String name = zipEntry.getName(); if (name.equals(ZIPENTRY_EMPTY_PLACEHOLDER)) { // This entry is treated specially to indicate a backup of an empty // backend was attempted. zipEntry = zipStream.getNextEntry(); continue; } if (name.equals(ZIPENTRY_UNCHANGED_LOGFILES)) { // This entry is treated specially. It is never restored, // and its hash is computed on the strings, not the bytes. if (mac != null || digest != null) { // The file name is part of the hash. if (mac != null) { mac.update(getBytes(name)); } if (digest != null) { digest.update(getBytes(name)); } InputStreamReader reader = new InputStreamReader(zipStream); BufferedReader bufferedReader = new BufferedReader(reader); String line = bufferedReader.readLine(); while (line != null) { if (mac != null) { mac.update(getBytes(line)); } if (digest != null) { digest.update(getBytes(line)); } line = bufferedReader.readLine(); } } zipEntry = zipStream.getNextEntry(); continue; } // See if we need to restore the file. File file = new File(restoreDir, name); OutputStream outputStream = null; if (includeFiles == null || includeFiles.contains(zipEntry.getName())) { if (!verifyOnly) { outputStream = new FileOutputStream(file); } } if (outputStream != null || mac != null || digest != null) { if (verifyOnly) { Message message = NOTE_JEB_BACKUP_VERIFY_FILE.get(zipEntry.getName()); logError(message); } // The file name is part of the hash. if (mac != null) { mac.update(getBytes(name)); } if (digest != null) { digest.update(getBytes(name)); } // Process the file. long totalBytesRead = 0; byte[] buffer = new byte[8192]; int bytesRead = zipStream.read(buffer); while (bytesRead > 0 && !restoreConfig.isCancelled()) { totalBytesRead += bytesRead; if (mac != null) { mac.update(buffer, 0, bytesRead); } if (digest != null) { digest.update(buffer, 0, bytesRead); } if (outputStream != null) { outputStream.write(buffer, 0, bytesRead); } bytesRead = zipStream.read(buffer); } if (outputStream != null) { outputStream.close(); Message message = NOTE_JEB_BACKUP_RESTORED_FILE.get( zipEntry.getName(), totalBytesRead); logError(message); } } zipEntry = zipStream.getNextEntry(); } zipStream.close(); // Check the hash. if (digest != null) { if (!Arrays.equals(digest.digest(), hash)) { Message message = ERR_JEB_BACKUP_UNSIGNED_HASH_ERROR.get(backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } } if (mac != null) { byte[] computedSignHash = mac.doFinal(); if (!Arrays.equals(computedSignHash, signHash)) { Message message = ERR_JEB_BACKUP_SIGNED_HASH_ERROR.get(backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } } } /** * Writes a file to an entry in the archive file. * @param zipStream The zip output stream to which the file is to be * written. * @param mac A message authentication code to be updated, if not null. * @param digest A message digest to be updated, if not null. * @param file The file to be written. * @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. */ private long archiveFile(ZipOutputStream zipStream, Mac mac, MessageDigest digest, File file, BackupConfig backupConfig) throws IOException, FileNotFoundException { ZipEntry zipEntry = new ZipEntry(file.getName()); // Open the file for reading. InputStream inputStream = new FileInputStream(file); // Start the zip entry. zipStream.putNextEntry(zipEntry); // Put the name in the hash. if (mac != null) { mac.update(getBytes(file.getName())); } if (digest != null) { digest.update(getBytes(file.getName())); } // Write the file. long totalBytesRead = 0; byte[] buffer = new byte[8192]; int bytesRead = inputStream.read(buffer); while (bytesRead > 0 && !backupConfig.isCancelled()) { if (mac != null) { mac.update(buffer, 0, bytesRead); } if (digest != null) { digest.update(buffer, 0, bytesRead); } zipStream.write(buffer, 0, bytesRead); totalBytesRead += bytesRead; bytesRead = inputStream.read(buffer); } inputStream.close(); // Finish the zip entry. zipStream.closeEntry(); Message message = NOTE_JEB_BACKUP_ARCHIVED_FILE.get(zipEntry.getName()); logError(message); return totalBytesRead; } /** * Write a list of strings to an entry in the archive file. * @param zipStream The zip output stream to which the entry is to be * written. * @param mac An optional MAC to be updated. * @param digest An optional message digest to be updated. * @param fileName The name of the zip entry to be written. * @param list A list of strings to be written. The strings must not * contain newlines. * @throws IOException If an I/O error occurs while writing the archive entry. */ private void archiveList(ZipOutputStream zipStream, Mac mac, MessageDigest digest, String fileName, List<String> list) throws IOException { ZipEntry zipEntry = new ZipEntry(fileName); // Start the zip entry. zipStream.putNextEntry(zipEntry); // Put the name in the hash. if (mac != null) { mac.update(getBytes(fileName)); } if (digest != null) { digest.update(getBytes(fileName)); } Writer writer = new OutputStreamWriter(zipStream); for (String s : list) { if (mac != null) { mac.update(getBytes(s)); } if (digest != null) { digest.update(getBytes(s)); } writer.write(s); writer.write(EOL); } writer.flush(); // Finish the zip entry. zipStream.closeEntry(); } /** * Obtains the set of files in a backup that are unchanged from its * dependent backup or backups. This list is stored as the first entry * in the archive file. * @param backupDir The backup directory. * @param backupInfo The backup info. * @return The set of files that were unchanged. * @throws DirectoryException If an error occurs while trying to get the * appropriate cipher algorithm for an encrypted backup. * @throws IOException If an I/O error occurs while reading the backup * archive file. */ private Set<String> getUnchanged(BackupDirectory backupDir, BackupInfo backupInfo) throws DirectoryException, IOException { HashSet<String> hashSet = new HashSet<String>(); boolean encrypt = backupInfo.isEncrypted(); HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); String archiveFilename = backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); File archiveFile = new File(backupDir.getPath(), archiveFilename); InputStream inputStream = new FileInputStream(archiveFile); // Get the crypto manager and use it to obtain references to the message // digest and/or MAC to use for hashing and/or signing. CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); // If the data is encrypted, then wrap the input stream in a cipher // input stream. if (encrypt) { try { inputStream = cryptoManager.getCipherInputStream(inputStream); } catch (CryptoManagerException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); } } // Wrap the file input stream in a zip input stream. ZipInputStream zipStream = new ZipInputStream(inputStream); // 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.getName().equals(ZIPENTRY_UNCHANGED_LOGFILES)) { InputStreamReader reader = new InputStreamReader(zipStream); BufferedReader bufferedReader = new BufferedReader(reader); String line = bufferedReader.readLine(); while (line != null) { hashSet.add(line); line = bufferedReader.readLine(); } break; } zipEntry = zipStream.getNextEntry(); } zipStream.close(); return hashSet; } /** * Obtains a list of the dependencies of a given backup in order from * the oldest (the full backup), to the most recent. * @param backupDir The backup directory. * @param backupInfo The backup for which dependencies are required. * @return A list of dependent backups. * @throws DirectoryException If a Directory Server error occurs. */ private ArrayList<BackupInfo> getDependents(BackupDirectory backupDir, BackupInfo backupInfo) throws DirectoryException { ArrayList<BackupInfo> dependents = new ArrayList<BackupInfo>(); while (backupInfo != null && !backupInfo.getDependencies().isEmpty()) { String backupID = backupInfo.getDependencies().iterator().next(); backupInfo = getBackupInfo(backupDir, backupID); if (backupInfo != null) { dependents.add(backupInfo); } } Collections.reverse(dependents); return dependents; } /** * 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 BackupInfo getBackupInfo(BackupDirectory backupDir, String backupID) throws DirectoryException { BackupInfo backupInfo = backupDir.getBackupInfo(backupID); if (backupInfo == null) { Message message = ERR_JEB_BACKUP_MISSING_BACKUPID.get(backupDir.getPath(), backupID); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } return backupInfo; } }