001 /* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.backends.jeb; 028 import org.opends.messages.Message; 029 030 import org.opends.server.config.ConfigException; 031 import org.opends.server.core.DirectoryServer; 032 import org.opends.server.util.DynamicConstants; 033 import org.opends.server.types.CryptoManagerException; 034 035 import javax.crypto.Mac; 036 import java.io.BufferedReader; 037 import java.io.File; 038 import java.io.FileInputStream; 039 import java.io.FileNotFoundException; 040 import java.io.FileOutputStream; 041 import java.io.FilenameFilter; 042 import java.io.InputStream; 043 import java.io.InputStreamReader; 044 import java.io.IOException; 045 import java.io.OutputStream; 046 import java.io.OutputStreamWriter; 047 import java.io.Writer; 048 import java.security.MessageDigest; 049 import java.util.ArrayList; 050 import java.util.Arrays; 051 import java.util.Collections; 052 import java.util.Date; 053 import java.util.HashMap; 054 import java.util.HashSet; 055 import java.util.List; 056 import java.util.Set; 057 import java.util.zip.Deflater; 058 import java.util.zip.ZipEntry; 059 import java.util.zip.ZipInputStream; 060 import java.util.zip.ZipOutputStream; 061 062 import org.opends.server.types.*; 063 import static org.opends.server.loggers.ErrorLogger.logError; 064 import static org.opends.server.loggers.debug.DebugLogger.*; 065 import org.opends.server.loggers.debug.DebugTracer; 066 import static org.opends.messages.JebMessages.*; 067 import static org.opends.server.util.ServerConstants.*; 068 import static org.opends.server.util.StaticUtils.*; 069 070 071 072 /** 073 * A backup manager for JE backends. 074 */ 075 public class BackupManager 076 { 077 /** 078 * The tracer object for the debug logger. 079 */ 080 private static final DebugTracer TRACER = getTracer(); 081 082 /** 083 * The common prefix for archive files. 084 */ 085 public static final String BACKUP_BASE_FILENAME = "backup-"; 086 087 /** 088 * The name of the property that holds the name of the latest log file 089 * at the time the backup was created. 090 */ 091 public static final String PROPERTY_LAST_LOGFILE_NAME = "last_logfile_name"; 092 093 /** 094 * The name of the property that holds the size of the latest log file 095 * at the time the backup was created. 096 */ 097 public static final String PROPERTY_LAST_LOGFILE_SIZE = "last_logfile_size"; 098 099 100 /** 101 * The name of the entry in an incremental backup archive file 102 * containing a list of log files that are unchanged since the 103 * previous backup. 104 */ 105 public static final String ZIPENTRY_UNCHANGED_LOGFILES = "unchanged.txt"; 106 107 /** 108 * The name of a dummy entry in the backup archive file that will act 109 * as a placeholder in case a backup is done on an empty backend. 110 */ 111 public static final String ZIPENTRY_EMPTY_PLACEHOLDER = "empty.placeholder"; 112 113 114 /** 115 * The backend ID. 116 */ 117 private String backendID; 118 119 120 /** 121 * Construct a backup manager for a JE backend. 122 * @param backendID The ID of the backend instance for which a backup 123 * manager is required. 124 */ 125 public BackupManager(String backendID) 126 { 127 this.backendID = backendID; 128 } 129 130 /** 131 * Create a backup of the JE backend. The backup is stored in a single zip 132 * file in the backup directory. If the backup is incremental, then the 133 * first entry in the zip is a text file containing a list of all the JE 134 * log files that are unchanged since the previous backup. The remaining 135 * zip entries are the JE log files themselves, which, for an incremental, 136 * only include those files that have changed. 137 * @param backendDir The directory of the backend instance for 138 * which the backup is required. 139 * @param backupConfig The configuration to use when performing the backup. 140 * @throws DirectoryException If a Directory Server error occurs. 141 */ 142 public void createBackup(File backendDir, BackupConfig backupConfig) 143 throws DirectoryException 144 { 145 // Get the properties to use for the backup. 146 String backupID = backupConfig.getBackupID(); 147 BackupDirectory backupDir = backupConfig.getBackupDirectory(); 148 boolean incremental = backupConfig.isIncremental(); 149 String incrBaseID = backupConfig.getIncrementalBaseID(); 150 boolean compress = backupConfig.compressData(); 151 boolean encrypt = backupConfig.encryptData(); 152 boolean hash = backupConfig.hashData(); 153 boolean signHash = backupConfig.signHash(); 154 155 156 // Create a hash map that will hold the extra backup property information 157 // for this backup. 158 HashMap<String,String> backupProperties = new HashMap<String,String>(); 159 160 161 // Get the crypto manager and use it to obtain references to the message 162 // digest and/or MAC to use for hashing and/or signing. 163 CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); 164 Mac mac = null; 165 MessageDigest digest = null; 166 String digestAlgorithm = null; 167 String macKeyID = null; 168 169 if (hash) 170 { 171 if (signHash) 172 { 173 try 174 { 175 macKeyID = cryptoManager.getMacEngineKeyEntryID(); 176 backupProperties.put(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID); 177 178 mac = cryptoManager.getMacEngine(macKeyID); 179 } 180 catch (Exception e) 181 { 182 if (debugEnabled()) 183 { 184 TRACER.debugCaught(DebugLogLevel.ERROR, e); 185 } 186 187 Message message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get( 188 macKeyID, stackTraceToSingleLineString(e)); 189 throw new DirectoryException( 190 DirectoryServer.getServerErrorResultCode(), message, e); 191 } 192 } 193 else 194 { 195 digestAlgorithm = cryptoManager.getPreferredMessageDigestAlgorithm(); 196 backupProperties.put(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm); 197 198 try 199 { 200 digest = cryptoManager.getPreferredMessageDigest(); 201 } 202 catch (Exception e) 203 { 204 if (debugEnabled()) 205 { 206 TRACER.debugCaught(DebugLogLevel.ERROR, e); 207 } 208 209 Message message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get( 210 digestAlgorithm, stackTraceToSingleLineString(e)); 211 throw new DirectoryException( 212 DirectoryServer.getServerErrorResultCode(), message, e); 213 } 214 } 215 } 216 217 218 // Date the backup. 219 Date backupDate = new Date(); 220 221 // If this is an incremental, determine the base backup for this backup. 222 HashSet<String> dependencies = new HashSet<String>(); 223 BackupInfo baseBackup = null; 224 /* 225 FilenameFilter backupTagFilter = new FilenameFilter() 226 { 227 public boolean accept(File dir, String name) 228 { 229 return name.startsWith(BackupInfo.PROPERTY_BACKUP_ID); 230 } 231 }; 232 */ 233 if (incremental) 234 { 235 if (incrBaseID == null) 236 { 237 // The default is to use the latest backup as base. 238 if (backupDir.getLatestBackup() != null) 239 { 240 incrBaseID = backupDir.getLatestBackup().getBackupID(); 241 } 242 } 243 244 // Get the set of possible base backups from the current database. 245 /* 246 String[] files = backendDir.list(backupTagFilter); 247 if (files == null || files.length == 0) 248 { 249 // Incremental not allowed until after a full. 250 Message msg = ERR_JEB_INCR_BACKUP_REQUIRES_FULL.get(); 251 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 252 msg); 253 } 254 HashSet<String> backups = new HashSet<String>(); 255 int prefixLen = BackupInfo.PROPERTY_BACKUP_ID.length()+1; 256 for (String s : files) 257 { 258 String actualBaseID = s.substring(prefixLen); 259 backups.add(actualBaseID); 260 } 261 262 // Check that it makes sense to do this incremental. 263 if (incrBaseID == null || !backups.contains(incrBaseID)) 264 { 265 Message msg = 266 ERR_JEB_INCR_BACKUP_FROM_WRONG_BASE.get(backups.toString()); 267 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 268 msg); 269 } 270 */ 271 272 baseBackup = getBackupInfo(backupDir, incrBaseID); 273 } 274 275 // Get information about the latest log file from the base backup. 276 String latestFileName = null; 277 long latestFileSize = 0; 278 if (baseBackup != null) 279 { 280 HashMap<String,String> properties = baseBackup.getBackupProperties(); 281 latestFileName = properties.get(PROPERTY_LAST_LOGFILE_NAME); 282 latestFileSize = Long.parseLong( 283 properties.get(PROPERTY_LAST_LOGFILE_SIZE)); 284 } 285 286 // Create an output stream that will be used to write the archive file. At 287 // its core, it will be a file output stream to put a file on the disk. If 288 // we are to encrypt the data, then that file output stream will be wrapped 289 // in a cipher output stream. The resulting output stream will then be 290 // wrapped by a zip output stream (which may or may not actually use 291 // compression). 292 String archiveFilename = null; 293 OutputStream outputStream; 294 File archiveFile; 295 try 296 { 297 archiveFilename = BACKUP_BASE_FILENAME + backendID + "-" + backupID; 298 archiveFile = new File(backupDir.getPath(), archiveFilename); 299 if (archiveFile.exists()) 300 { 301 int i=1; 302 while (true) 303 { 304 archiveFile = new File(backupDir.getPath(), 305 archiveFilename + "." + i); 306 if (archiveFile.exists()) 307 { 308 i++; 309 } 310 else 311 { 312 archiveFilename = archiveFilename + "." + i; 313 break; 314 } 315 } 316 } 317 318 outputStream = new FileOutputStream(archiveFile, false); 319 backupProperties.put(BACKUP_PROPERTY_ARCHIVE_FILENAME, archiveFilename); 320 } 321 catch (Exception e) 322 { 323 if (debugEnabled()) 324 { 325 TRACER.debugCaught(DebugLogLevel.ERROR, e); 326 } 327 328 Message message = ERR_JEB_BACKUP_CANNOT_CREATE_ARCHIVE_FILE. 329 get(String.valueOf(archiveFilename), backupDir.getPath(), 330 stackTraceToSingleLineString(e)); 331 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 332 message, e); 333 } 334 335 336 // If we should encrypt the data, then wrap the output stream in a cipher 337 // output stream. 338 if (encrypt) 339 { 340 try 341 { 342 outputStream 343 = cryptoManager.getCipherOutputStream(outputStream); 344 } 345 catch (CryptoManagerException e) 346 { 347 if (debugEnabled()) 348 { 349 TRACER.debugCaught(DebugLogLevel.ERROR, e); 350 } 351 352 Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( 353 stackTraceToSingleLineString(e)); 354 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 355 message, e); 356 } 357 } 358 359 360 // Wrap the file output stream in a zip output stream. 361 ZipOutputStream zipStream = new ZipOutputStream(outputStream); 362 363 Message message = ERR_JEB_BACKUP_ZIP_COMMENT.get( 364 DynamicConstants.PRODUCT_NAME, 365 backupID, backendID); 366 zipStream.setComment(message.toString()); 367 368 if (compress) 369 { 370 zipStream.setLevel(Deflater.DEFAULT_COMPRESSION); 371 } 372 else 373 { 374 zipStream.setLevel(Deflater.NO_COMPRESSION); 375 } 376 377 // Record this backup in the database itself. 378 /* 379 String backupTag = BackupInfo.PROPERTY_BACKUP_ID + "_" + backupID; 380 File tagFile = new File(backendDir, backupTag); 381 try 382 { 383 tagFile.createNewFile(); 384 } 385 catch (IOException e) 386 { 387 assert debugException(CLASS_NAME, "createBackup", e); 388 Message msg = ERR_JEB_CANNOT_CREATE_BACKUP_TAG_FILE.get( 389 backupTag, backendDir.getPath()); 390 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 391 msg); 392 } 393 */ 394 395 // Get a list of all the log files comprising the database. 396 FilenameFilter filenameFilter = new FilenameFilter() 397 { 398 public boolean accept(File d, String name) 399 { 400 return name.endsWith(".jdb"); 401 } 402 }; 403 404 File[] logFiles; 405 try 406 { 407 logFiles = backendDir.listFiles(filenameFilter); 408 } 409 catch (Exception e) 410 { 411 if (debugEnabled()) 412 { 413 TRACER.debugCaught(DebugLogLevel.ERROR, e); 414 } 415 416 message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get( 417 backendDir.getAbsolutePath(), stackTraceToSingleLineString(e)); 418 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 419 message, e); 420 } 421 422 // Check to see if backend is empty. If so, insert placeholder entry into 423 // archive 424 if(logFiles.length <= 0) 425 { 426 try 427 { 428 ZipEntry emptyPlaceholder = new ZipEntry(ZIPENTRY_EMPTY_PLACEHOLDER); 429 zipStream.putNextEntry(emptyPlaceholder); 430 } 431 catch (IOException e) 432 { 433 if (debugEnabled()) 434 { 435 TRACER.debugCaught(DebugLogLevel.ERROR, e); 436 } 437 message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( 438 ZIPENTRY_EMPTY_PLACEHOLDER, stackTraceToSingleLineString(e)); 439 throw new DirectoryException( 440 DirectoryServer.getServerErrorResultCode(), message, e); 441 } 442 } 443 444 // Sort the log files from oldest to youngest since this is the order 445 // in which they must be copied. 446 // This is easy since the files are created in alphabetical order by JE. 447 Arrays.sort(logFiles); 448 449 try 450 { 451 // Archive the backup tag files. 452 /* 453 File[] tagFiles = backendDir.listFiles(backupTagFilter); 454 if (tagFiles != null) 455 { 456 for (File f : tagFiles) 457 { 458 try 459 { 460 archiveFile(zipStream, mac, digest, f); 461 } 462 catch (IOException e) 463 { 464 assert debugException(CLASS_NAME, "createBackup", e); 465 Message message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( 466 backupTag, stackTraceToSingleLineString(e)); 467 throw new DirectoryException( 468 DirectoryServer.getServerErrorResultCode(), message, e); 469 } 470 } 471 } 472 */ 473 474 // Process log files that are unchanged from the base backup. 475 int indexCurrent = 0; 476 if (latestFileName != null) 477 { 478 ArrayList<String> unchangedList = new ArrayList<String>(); 479 while (indexCurrent < logFiles.length && 480 !backupConfig.isCancelled()) 481 { 482 File logFile = logFiles[indexCurrent]; 483 String logFileName = logFile.getName(); 484 485 // Stop when we get to the first log file that has been 486 // written since the base backup. 487 int compareResult = logFileName.compareTo(latestFileName); 488 if (compareResult > 0 || 489 (compareResult == 0 && logFile.length() != latestFileSize)) 490 { 491 break; 492 } 493 494 message = NOTE_JEB_BACKUP_FILE_UNCHANGED.get(logFileName); 495 logError(message); 496 497 unchangedList.add(logFileName); 498 499 indexCurrent++; 500 } 501 502 // Write a file containing the list of unchanged log files. 503 if (!unchangedList.isEmpty()) 504 { 505 String zipEntryName = ZIPENTRY_UNCHANGED_LOGFILES; 506 try 507 { 508 archiveList(zipStream, mac, digest, zipEntryName, unchangedList); 509 } 510 catch (IOException e) 511 { 512 if (debugEnabled()) 513 { 514 TRACER.debugCaught(DebugLogLevel.ERROR, e); 515 } 516 message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( 517 zipEntryName, stackTraceToSingleLineString(e)); 518 throw new DirectoryException( 519 DirectoryServer.getServerErrorResultCode(), message, e); 520 } 521 522 // Set the dependency. 523 dependencies.add(baseBackup.getBackupID()); 524 } 525 } 526 527 // Write the new log files to the zip file. 528 do 529 { 530 boolean deletedFiles = false; 531 532 while (indexCurrent < logFiles.length && 533 !backupConfig.isCancelled()) 534 { 535 File logFile = logFiles[indexCurrent]; 536 537 try 538 { 539 latestFileSize = archiveFile(zipStream, mac, digest, 540 logFile, backupConfig); 541 latestFileName = logFile.getName(); 542 } 543 catch (FileNotFoundException e) 544 { 545 if (debugEnabled()) 546 { 547 TRACER.debugCaught(DebugLogLevel.ERROR, e); 548 } 549 550 // A log file has been deleted by the cleaner since we started. 551 deletedFiles = true; 552 } 553 catch (IOException e) 554 { 555 if (debugEnabled()) 556 { 557 TRACER.debugCaught(DebugLogLevel.ERROR, e); 558 } 559 message = ERR_JEB_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get( 560 logFile.getName(), stackTraceToSingleLineString(e)); 561 throw new DirectoryException( 562 DirectoryServer.getServerErrorResultCode(), message, e); 563 } 564 565 indexCurrent++; 566 } 567 568 if (deletedFiles) 569 { 570 // The cleaner is active and has deleted one or more of the log files 571 // since we started. The in-use data from those log files will have 572 // been written to new log files, so we must include those new files. 573 final String latest = logFiles[logFiles.length-1].getName(); 574 final long latestSize = latestFileSize; 575 FilenameFilter filter = new FilenameFilter() 576 { 577 public boolean accept(File d, String name) 578 { 579 if (!name.endsWith(".jdb")) return false; 580 int compareTo = name.compareTo(latest); 581 if (compareTo > 0) return true; 582 if (compareTo == 0 && d.length() > latestSize) return true; 583 return false; 584 } 585 }; 586 587 try 588 { 589 logFiles = backendDir.listFiles(filter); 590 indexCurrent = 0; 591 } 592 catch (Exception e) 593 { 594 if (debugEnabled()) 595 { 596 TRACER.debugCaught(DebugLogLevel.ERROR, e); 597 } 598 599 message = ERR_JEB_BACKUP_CANNOT_LIST_LOG_FILES.get( 600 backendDir.getAbsolutePath(), stackTraceToSingleLineString(e)); 601 throw new DirectoryException( 602 DirectoryServer.getServerErrorResultCode(), message, e); 603 } 604 605 if (logFiles == null) 606 { 607 break; 608 } 609 610 Arrays.sort(logFiles); 611 612 message = NOTE_JEB_BACKUP_CLEANER_ACTIVITY.get( 613 String.valueOf(logFiles.length)); 614 logError(message); 615 } 616 else 617 { 618 // We are done. 619 break; 620 } 621 } 622 while (true); 623 624 } 625 catch (DirectoryException e) 626 { 627 if (debugEnabled()) 628 { 629 TRACER.debugCaught(DebugLogLevel.ERROR, e); 630 } 631 632 try 633 { 634 zipStream.close(); 635 } catch (Exception e2) {} 636 } 637 638 // We're done writing the file, so close the zip stream (which should also 639 // close the underlying stream). 640 try 641 { 642 zipStream.close(); 643 } 644 catch (Exception e) 645 { 646 if (debugEnabled()) 647 { 648 TRACER.debugCaught(DebugLogLevel.ERROR, e); 649 } 650 651 message = ERR_JEB_BACKUP_CANNOT_CLOSE_ZIP_STREAM. 652 get(archiveFilename, backupDir.getPath(), 653 stackTraceToSingleLineString(e)); 654 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 655 message, e); 656 } 657 658 659 // Get the digest or MAC bytes if appropriate. 660 byte[] digestBytes = null; 661 byte[] macBytes = null; 662 if (hash) 663 { 664 if (signHash) 665 { 666 macBytes = mac.doFinal(); 667 } 668 else 669 { 670 digestBytes = digest.digest(); 671 } 672 } 673 674 675 // Create a descriptor for this backup. 676 backupProperties.put(PROPERTY_LAST_LOGFILE_NAME, latestFileName); 677 backupProperties.put(PROPERTY_LAST_LOGFILE_SIZE, 678 String.valueOf(latestFileSize)); 679 BackupInfo backupInfo = new BackupInfo(backupDir, backupID, 680 backupDate, incremental, compress, 681 encrypt, digestBytes, macBytes, 682 dependencies, backupProperties); 683 684 try 685 { 686 backupDir.addBackup(backupInfo); 687 backupDir.writeBackupDirectoryDescriptor(); 688 } 689 catch (Exception e) 690 { 691 if (debugEnabled()) 692 { 693 TRACER.debugCaught(DebugLogLevel.ERROR, e); 694 } 695 696 message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( 697 backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); 698 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 699 message, e); 700 } 701 702 // Remove the backup if this operation was cancelled since the 703 // backup may be incomplete 704 if (backupConfig.isCancelled()) 705 { 706 removeBackup(backupDir, backupID); 707 } 708 } 709 710 711 712 /** 713 * Restore a JE backend from backup, or verify the backup. 714 * @param backendDir The configuration of the backend instance to be 715 * restored. 716 * @param restoreConfig The configuration to use when performing the restore. 717 * @throws DirectoryException If a Directory Server error occurs. 718 */ 719 public void restoreBackup(File backendDir, 720 RestoreConfig restoreConfig) 721 throws DirectoryException 722 { 723 // Get the properties to use for the restore. 724 String backupID = restoreConfig.getBackupID(); 725 BackupDirectory backupDir = restoreConfig.getBackupDirectory(); 726 boolean verifyOnly = restoreConfig.verifyOnly(); 727 728 BackupInfo backupInfo = getBackupInfo(backupDir, backupID); 729 730 // Create a restore directory with a different name to the backend 731 // directory. 732 File restoreDir = new File(backendDir.getPath() + "-restore-" + backupID); 733 if (!verifyOnly) 734 { 735 File[] files = restoreDir.listFiles(); 736 if (files != null) 737 { 738 for (File f : files) 739 { 740 f.delete(); 741 } 742 } 743 restoreDir.mkdir(); 744 } 745 746 // Get the set of restore files that are in dependencies. 747 Set<String> includeFiles; 748 try 749 { 750 includeFiles = getUnchanged(backupDir, backupInfo); 751 } 752 catch (IOException e) 753 { 754 if (debugEnabled()) 755 { 756 TRACER.debugCaught(DebugLogLevel.ERROR, e); 757 } 758 Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( 759 backupInfo.getBackupID(), stackTraceToSingleLineString(e)); 760 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 761 message, e); 762 } 763 764 // Restore any dependencies. 765 List<BackupInfo> dependents = getDependents(backupDir, backupInfo); 766 for (BackupInfo dependent : dependents) 767 { 768 try 769 { 770 restoreArchive(restoreDir, restoreConfig, dependent, includeFiles); 771 } 772 catch (IOException e) 773 { 774 if (debugEnabled()) 775 { 776 TRACER.debugCaught(DebugLogLevel.ERROR, e); 777 } 778 Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( 779 dependent.getBackupID(), stackTraceToSingleLineString(e)); 780 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 781 message, e); 782 } 783 } 784 785 // Restore the final archive file. 786 try 787 { 788 restoreArchive(restoreDir, restoreConfig, backupInfo, null); 789 } 790 catch (IOException e) 791 { 792 if (debugEnabled()) 793 { 794 TRACER.debugCaught(DebugLogLevel.ERROR, e); 795 } 796 Message message = ERR_JEB_BACKUP_CANNOT_RESTORE.get( 797 backupInfo.getBackupID(), stackTraceToSingleLineString(e)); 798 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 799 message, e); 800 } 801 802 // Delete the current backend directory and rename the restore directory. 803 if (!verifyOnly) 804 { 805 File[] files = backendDir.listFiles(); 806 if (files != null) 807 { 808 for (File f : files) 809 { 810 f.delete(); 811 } 812 } 813 backendDir.delete(); 814 if (!restoreDir.renameTo(backendDir)) 815 { 816 Message msg = ERR_JEB_CANNOT_RENAME_RESTORE_DIRECTORY.get( 817 restoreDir.getPath(), backendDir.getPath()); 818 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 819 msg); 820 } 821 } 822 } 823 824 /** 825 * Removes the specified backup if it is possible to do so. 826 * 827 * @param backupDir The backup directory structure with which the 828 * specified backup is associated. 829 * @param backupID The backup ID for the backup to be removed. 830 * 831 * @throws DirectoryException If it is not possible to remove the specified 832 * backup for some reason (e.g., no such backup 833 * exists or there are other backups that are 834 * dependent upon it). 835 */ 836 public void removeBackup(BackupDirectory backupDir, 837 String backupID) 838 throws DirectoryException 839 { 840 BackupInfo backupInfo = getBackupInfo(backupDir, backupID); 841 HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); 842 843 String archiveFilename = 844 backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); 845 File archiveFile = new File(backupDir.getPath(), archiveFilename); 846 847 try 848 { 849 backupDir.removeBackup(backupID); 850 } 851 catch (ConfigException e) 852 { 853 if (debugEnabled()) 854 { 855 TRACER.debugCaught(DebugLogLevel.ERROR, e); 856 } 857 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 858 e.getMessageObject()); 859 } 860 861 try 862 { 863 backupDir.writeBackupDirectoryDescriptor(); 864 } 865 catch (Exception e) 866 { 867 if (debugEnabled()) 868 { 869 TRACER.debugCaught(DebugLogLevel.ERROR, e); 870 } 871 872 Message message = ERR_JEB_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( 873 backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); 874 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 875 message, e); 876 } 877 878 // Remove the archive file. 879 archiveFile.delete(); 880 881 } 882 883 884 885 /** 886 * Restore the contents of an archive file. If the archive is being 887 * restored as a dependency, then only files in the specified set 888 * are restored, and the restored files are removed from the set. Otherwise 889 * all files from the archive are restored, and files that are to be found 890 * in dependencies are added to the set. 891 * 892 * @param restoreDir The directory in which files are to be restored. 893 * @param restoreConfig The restore configuration. 894 * @param backupInfo The backup containing the files to be restored. 895 * @param includeFiles The set of files to be restored. If null, then 896 * all files are restored. 897 * @throws DirectoryException If a Directory Server error occurs. 898 * @throws IOException If an I/O exception occurs during the restore. 899 */ 900 private void restoreArchive(File restoreDir, 901 RestoreConfig restoreConfig, 902 BackupInfo backupInfo, 903 Set<String> includeFiles) 904 throws DirectoryException,IOException 905 { 906 BackupDirectory backupDir = restoreConfig.getBackupDirectory(); 907 boolean verifyOnly = restoreConfig.verifyOnly(); 908 909 String backupID = backupInfo.getBackupID(); 910 boolean encrypt = backupInfo.isEncrypted(); 911 byte[] hash = backupInfo.getUnsignedHash(); 912 byte[] signHash = backupInfo.getSignedHash(); 913 914 HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); 915 916 String archiveFilename = 917 backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); 918 File archiveFile = new File(backupDir.getPath(), archiveFilename); 919 920 InputStream inputStream = new FileInputStream(archiveFile); 921 922 // Get the crypto manager and use it to obtain references to the message 923 // digest and/or MAC to use for hashing and/or signing. 924 CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); 925 Mac mac = null; 926 MessageDigest digest = null; 927 String digestAlgorithm = null; 928 String macKeyID = null; 929 930 if (signHash != null) 931 { 932 macKeyID = backupProperties.get(BACKUP_PROPERTY_MAC_KEY_ID); 933 934 try 935 { 936 mac = cryptoManager.getMacEngine(macKeyID); 937 } 938 catch (Exception e) 939 { 940 if (debugEnabled()) 941 { 942 TRACER.debugCaught(DebugLogLevel.ERROR, e); 943 } 944 945 Message message = ERR_JEB_BACKUP_CANNOT_GET_MAC.get( 946 macKeyID, stackTraceToSingleLineString(e)); 947 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 948 message, e); 949 } 950 } 951 952 if (hash != null) 953 { 954 digestAlgorithm = backupProperties.get(BACKUP_PROPERTY_DIGEST_ALGORITHM); 955 956 try 957 { 958 digest = cryptoManager.getMessageDigest(digestAlgorithm); 959 } 960 catch (Exception e) 961 { 962 if (debugEnabled()) 963 { 964 TRACER.debugCaught(DebugLogLevel.ERROR, e); 965 } 966 967 Message message = ERR_JEB_BACKUP_CANNOT_GET_DIGEST.get( 968 digestAlgorithm, stackTraceToSingleLineString(e)); 969 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 970 message, e); 971 } 972 } 973 974 975 // If the data is encrypted, then wrap the input stream in a cipher 976 // input stream. 977 if (encrypt) 978 { 979 try 980 { 981 inputStream = cryptoManager.getCipherInputStream(inputStream); 982 } 983 catch (CryptoManagerException e) 984 { 985 if (debugEnabled()) 986 { 987 TRACER.debugCaught(DebugLogLevel.ERROR, e); 988 } 989 990 Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( 991 stackTraceToSingleLineString(e)); 992 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 993 message, e); 994 } 995 } 996 997 998 // Wrap the file input stream in a zip input stream. 999 ZipInputStream zipStream = new ZipInputStream(inputStream); 1000 1001 // Iterate through the entries in the zip file. 1002 ZipEntry zipEntry = zipStream.getNextEntry(); 1003 while (zipEntry != null && !restoreConfig.isCancelled()) 1004 { 1005 String name = zipEntry.getName(); 1006 1007 if (name.equals(ZIPENTRY_EMPTY_PLACEHOLDER)) 1008 { 1009 // This entry is treated specially to indicate a backup of an empty 1010 // backend was attempted. 1011 1012 zipEntry = zipStream.getNextEntry(); 1013 continue; 1014 } 1015 1016 if (name.equals(ZIPENTRY_UNCHANGED_LOGFILES)) 1017 { 1018 // This entry is treated specially. It is never restored, 1019 // and its hash is computed on the strings, not the bytes. 1020 if (mac != null || digest != null) 1021 { 1022 // The file name is part of the hash. 1023 if (mac != null) 1024 { 1025 mac.update(getBytes(name)); 1026 } 1027 1028 if (digest != null) 1029 { 1030 digest.update(getBytes(name)); 1031 } 1032 1033 InputStreamReader reader = new InputStreamReader(zipStream); 1034 BufferedReader bufferedReader = new BufferedReader(reader); 1035 String line = bufferedReader.readLine(); 1036 while (line != null) 1037 { 1038 if (mac != null) 1039 { 1040 mac.update(getBytes(line)); 1041 } 1042 1043 if (digest != null) 1044 { 1045 digest.update(getBytes(line)); 1046 } 1047 1048 line = bufferedReader.readLine(); 1049 } 1050 } 1051 1052 zipEntry = zipStream.getNextEntry(); 1053 continue; 1054 } 1055 1056 // See if we need to restore the file. 1057 File file = new File(restoreDir, name); 1058 OutputStream outputStream = null; 1059 if (includeFiles == null || includeFiles.contains(zipEntry.getName())) 1060 { 1061 if (!verifyOnly) 1062 { 1063 outputStream = new FileOutputStream(file); 1064 } 1065 } 1066 1067 if (outputStream != null || mac != null || digest != null) 1068 { 1069 if (verifyOnly) 1070 { 1071 Message message = NOTE_JEB_BACKUP_VERIFY_FILE.get(zipEntry.getName()); 1072 logError(message); 1073 } 1074 1075 // The file name is part of the hash. 1076 if (mac != null) 1077 { 1078 mac.update(getBytes(name)); 1079 } 1080 1081 if (digest != null) 1082 { 1083 digest.update(getBytes(name)); 1084 } 1085 1086 // Process the file. 1087 long totalBytesRead = 0; 1088 byte[] buffer = new byte[8192]; 1089 int bytesRead = zipStream.read(buffer); 1090 while (bytesRead > 0 && !restoreConfig.isCancelled()) 1091 { 1092 totalBytesRead += bytesRead; 1093 1094 if (mac != null) 1095 { 1096 mac.update(buffer, 0, bytesRead); 1097 } 1098 1099 if (digest != null) 1100 { 1101 digest.update(buffer, 0, bytesRead); 1102 } 1103 1104 if (outputStream != null) 1105 { 1106 outputStream.write(buffer, 0, bytesRead); 1107 } 1108 1109 bytesRead = zipStream.read(buffer); 1110 } 1111 1112 if (outputStream != null) 1113 { 1114 outputStream.close(); 1115 1116 Message message = NOTE_JEB_BACKUP_RESTORED_FILE.get( 1117 zipEntry.getName(), totalBytesRead); 1118 logError(message); 1119 } 1120 } 1121 1122 zipEntry = zipStream.getNextEntry(); 1123 } 1124 1125 zipStream.close(); 1126 1127 // Check the hash. 1128 if (digest != null) 1129 { 1130 if (!Arrays.equals(digest.digest(), hash)) 1131 { 1132 Message message = ERR_JEB_BACKUP_UNSIGNED_HASH_ERROR.get(backupID); 1133 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1134 message); 1135 } 1136 } 1137 1138 if (mac != null) 1139 { 1140 byte[] computedSignHash = mac.doFinal(); 1141 1142 if (!Arrays.equals(computedSignHash, signHash)) 1143 { 1144 Message message = ERR_JEB_BACKUP_SIGNED_HASH_ERROR.get(backupID); 1145 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1146 message); 1147 } 1148 } 1149 } 1150 1151 1152 1153 /** 1154 * Writes a file to an entry in the archive file. 1155 * @param zipStream The zip output stream to which the file is to be 1156 * written. 1157 * @param mac A message authentication code to be updated, if not null. 1158 * @param digest A message digest to be updated, if not null. 1159 * @param file The file to be written. 1160 * @return The number of bytes written from the file. 1161 * @throws FileNotFoundException If the file to be archived does not exist. 1162 * @throws IOException If an I/O error occurs while archiving the file. 1163 */ 1164 private long archiveFile(ZipOutputStream zipStream, 1165 Mac mac, MessageDigest digest, File file, 1166 BackupConfig backupConfig) 1167 throws IOException, FileNotFoundException 1168 { 1169 ZipEntry zipEntry = new ZipEntry(file.getName()); 1170 1171 // Open the file for reading. 1172 InputStream inputStream = new FileInputStream(file); 1173 1174 // Start the zip entry. 1175 zipStream.putNextEntry(zipEntry); 1176 1177 // Put the name in the hash. 1178 if (mac != null) 1179 { 1180 mac.update(getBytes(file.getName())); 1181 } 1182 1183 if (digest != null) 1184 { 1185 digest.update(getBytes(file.getName())); 1186 } 1187 1188 // Write the file. 1189 long totalBytesRead = 0; 1190 byte[] buffer = new byte[8192]; 1191 int bytesRead = inputStream.read(buffer); 1192 while (bytesRead > 0 && !backupConfig.isCancelled()) 1193 { 1194 if (mac != null) 1195 { 1196 mac.update(buffer, 0, bytesRead); 1197 } 1198 1199 if (digest != null) 1200 { 1201 digest.update(buffer, 0, bytesRead); 1202 } 1203 1204 zipStream.write(buffer, 0, bytesRead); 1205 totalBytesRead += bytesRead; 1206 bytesRead = inputStream.read(buffer); 1207 } 1208 inputStream.close(); 1209 1210 // Finish the zip entry. 1211 zipStream.closeEntry(); 1212 1213 Message message = NOTE_JEB_BACKUP_ARCHIVED_FILE.get(zipEntry.getName()); 1214 logError(message); 1215 1216 return totalBytesRead; 1217 } 1218 1219 /** 1220 * Write a list of strings to an entry in the archive file. 1221 * @param zipStream The zip output stream to which the entry is to be 1222 * written. 1223 * @param mac An optional MAC to be updated. 1224 * @param digest An optional message digest to be updated. 1225 * @param fileName The name of the zip entry to be written. 1226 * @param list A list of strings to be written. The strings must not 1227 * contain newlines. 1228 * @throws IOException If an I/O error occurs while writing the archive entry. 1229 */ 1230 private void archiveList(ZipOutputStream zipStream, 1231 Mac mac, MessageDigest digest, String fileName, 1232 List<String> list) 1233 throws IOException 1234 { 1235 ZipEntry zipEntry = new ZipEntry(fileName); 1236 1237 // Start the zip entry. 1238 zipStream.putNextEntry(zipEntry); 1239 1240 // Put the name in the hash. 1241 if (mac != null) 1242 { 1243 mac.update(getBytes(fileName)); 1244 } 1245 1246 if (digest != null) 1247 { 1248 digest.update(getBytes(fileName)); 1249 } 1250 1251 Writer writer = new OutputStreamWriter(zipStream); 1252 for (String s : list) 1253 { 1254 if (mac != null) 1255 { 1256 mac.update(getBytes(s)); 1257 } 1258 1259 if (digest != null) 1260 { 1261 digest.update(getBytes(s)); 1262 } 1263 1264 writer.write(s); 1265 writer.write(EOL); 1266 } 1267 writer.flush(); 1268 1269 // Finish the zip entry. 1270 zipStream.closeEntry(); 1271 } 1272 1273 /** 1274 * Obtains the set of files in a backup that are unchanged from its 1275 * dependent backup or backups. This list is stored as the first entry 1276 * in the archive file. 1277 * @param backupDir The backup directory. 1278 * @param backupInfo The backup info. 1279 * @return The set of files that were unchanged. 1280 * @throws DirectoryException If an error occurs while trying to get the 1281 * appropriate cipher algorithm for an encrypted backup. 1282 * @throws IOException If an I/O error occurs while reading the backup 1283 * archive file. 1284 */ 1285 private Set<String> getUnchanged(BackupDirectory backupDir, 1286 BackupInfo backupInfo) 1287 throws DirectoryException, IOException 1288 { 1289 HashSet<String> hashSet = new HashSet<String>(); 1290 1291 boolean encrypt = backupInfo.isEncrypted(); 1292 1293 HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); 1294 1295 String archiveFilename = 1296 backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); 1297 File archiveFile = new File(backupDir.getPath(), archiveFilename); 1298 1299 InputStream inputStream = new FileInputStream(archiveFile); 1300 1301 // Get the crypto manager and use it to obtain references to the message 1302 // digest and/or MAC to use for hashing and/or signing. 1303 CryptoManager cryptoManager = DirectoryServer.getCryptoManager(); 1304 1305 // If the data is encrypted, then wrap the input stream in a cipher 1306 // input stream. 1307 if (encrypt) 1308 { 1309 try 1310 { 1311 inputStream = cryptoManager.getCipherInputStream(inputStream); 1312 } 1313 catch (CryptoManagerException e) 1314 { 1315 if (debugEnabled()) 1316 { 1317 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1318 } 1319 1320 Message message = ERR_JEB_BACKUP_CANNOT_GET_CIPHER.get( 1321 stackTraceToSingleLineString(e)); 1322 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1323 message, e); 1324 } 1325 } 1326 1327 1328 // Wrap the file input stream in a zip input stream. 1329 ZipInputStream zipStream = new ZipInputStream(inputStream); 1330 1331 // Iterate through the entries in the zip file. 1332 ZipEntry zipEntry = zipStream.getNextEntry(); 1333 while (zipEntry != null) 1334 { 1335 // We are looking for the entry containing the list of unchanged files. 1336 if (zipEntry.getName().equals(ZIPENTRY_UNCHANGED_LOGFILES)) 1337 { 1338 InputStreamReader reader = new InputStreamReader(zipStream); 1339 BufferedReader bufferedReader = new BufferedReader(reader); 1340 String line = bufferedReader.readLine(); 1341 while (line != null) 1342 { 1343 hashSet.add(line); 1344 line = bufferedReader.readLine(); 1345 } 1346 break; 1347 } 1348 1349 zipEntry = zipStream.getNextEntry(); 1350 } 1351 1352 zipStream.close(); 1353 return hashSet; 1354 } 1355 1356 /** 1357 * Obtains a list of the dependencies of a given backup in order from 1358 * the oldest (the full backup), to the most recent. 1359 * @param backupDir The backup directory. 1360 * @param backupInfo The backup for which dependencies are required. 1361 * @return A list of dependent backups. 1362 * @throws DirectoryException If a Directory Server error occurs. 1363 */ 1364 private ArrayList<BackupInfo> getDependents(BackupDirectory backupDir, 1365 BackupInfo backupInfo) 1366 throws DirectoryException 1367 { 1368 ArrayList<BackupInfo> dependents = new ArrayList<BackupInfo>(); 1369 while (backupInfo != null && !backupInfo.getDependencies().isEmpty()) 1370 { 1371 String backupID = backupInfo.getDependencies().iterator().next(); 1372 backupInfo = getBackupInfo(backupDir, backupID); 1373 if (backupInfo != null) 1374 { 1375 dependents.add(backupInfo); 1376 } 1377 } 1378 Collections.reverse(dependents); 1379 return dependents; 1380 } 1381 1382 /** 1383 * Get the information for a given backup ID from the backup directory. 1384 * @param backupDir The backup directory. 1385 * @param backupID The backup ID. 1386 * @return The backup information, never null. 1387 * @throws DirectoryException If the backup information cannot be found. 1388 */ 1389 private BackupInfo getBackupInfo(BackupDirectory backupDir, 1390 String backupID) throws DirectoryException 1391 { 1392 BackupInfo backupInfo = backupDir.getBackupInfo(backupID); 1393 if (backupInfo == null) 1394 { 1395 Message message = 1396 ERR_JEB_BACKUP_MISSING_BACKUPID.get(backupDir.getPath(), backupID); 1397 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1398 message); 1399 } 1400 return backupInfo; 1401 } 1402 }