001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2006-2009 Sun Microsystems, Inc. 015 * Portions Copyright 2013-2015 ForgeRock AS. 016 */ 017package org.opends.server.util; 018 019import static java.util.Collections.*; 020 021import static org.opends.messages.BackendMessages.*; 022import static org.opends.messages.UtilityMessages.*; 023import static org.opends.server.util.ServerConstants.*; 024import static org.opends.server.util.StaticUtils.*; 025 026import java.io.BufferedReader; 027import java.io.Closeable; 028import java.io.File; 029import java.io.FileFilter; 030import java.io.FileInputStream; 031import java.io.FileNotFoundException; 032import java.io.FileOutputStream; 033import java.io.IOException; 034import java.io.InputStream; 035import java.io.InputStreamReader; 036import java.io.OutputStream; 037import java.io.OutputStreamWriter; 038import java.io.Writer; 039import java.nio.file.Files; 040import java.nio.file.Path; 041import java.nio.file.Paths; 042import java.security.MessageDigest; 043import java.util.ArrayList; 044import java.util.Arrays; 045import java.util.Collections; 046import java.util.Date; 047import java.util.HashMap; 048import java.util.HashSet; 049import java.util.List; 050import java.util.ListIterator; 051import java.util.Map; 052import java.util.Set; 053import java.util.regex.Pattern; 054import java.util.zip.Deflater; 055import java.util.zip.ZipEntry; 056import java.util.zip.ZipInputStream; 057import java.util.zip.ZipOutputStream; 058 059import javax.crypto.Mac; 060 061import org.forgerock.i18n.LocalizableMessage; 062import org.forgerock.i18n.slf4j.LocalizedLogger; 063import org.forgerock.opendj.config.server.ConfigException; 064import org.forgerock.opendj.ldap.ResultCode; 065import org.forgerock.util.Pair; 066import org.opends.server.api.Backupable; 067import org.opends.server.core.DirectoryServer; 068import org.opends.server.types.BackupConfig; 069import org.opends.server.types.BackupDirectory; 070import org.opends.server.types.BackupInfo; 071import org.opends.server.types.CryptoManager; 072import org.opends.server.types.CryptoManagerException; 073import org.opends.server.types.DirectoryException; 074import org.opends.server.types.RestoreConfig; 075 076/** 077 * A backup manager for any entity that is backupable (backend, storage). 078 * 079 * @see {@link Backupable} 080 */ 081public class BackupManager 082{ 083 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 084 085 /** 086 * The common prefix for archive files. 087 */ 088 private static final String BACKUP_BASE_FILENAME = "backup-"; 089 090 /** 091 * The name of the property that holds the name of the latest log file 092 * at the time the backup was created. 093 */ 094 private static final String PROPERTY_LAST_LOGFILE_NAME = "last_logfile_name"; 095 096 /** 097 * The name of the property that holds the size of the latest log file 098 * at the time the backup was created. 099 */ 100 private static final String PROPERTY_LAST_LOGFILE_SIZE = "last_logfile_size"; 101 102 103 /** 104 * The name of the entry in an incremental backup archive file 105 * containing a list of log files that are unchanged since the 106 * previous backup. 107 */ 108 private static final String ZIPENTRY_UNCHANGED_LOGFILES = "unchanged.txt"; 109 110 /** 111 * The name of a dummy entry in the backup archive file that will act 112 * as a placeholder in case a backup is done on an empty backend. 113 */ 114 private static final String ZIPENTRY_EMPTY_PLACEHOLDER = "empty.placeholder"; 115 116 117 /** 118 * The backend ID. 119 */ 120 private final String backendID; 121 122 /** 123 * Construct a backup manager for a backend. 124 * 125 * @param backendID 126 * The ID of the backend instance for which a backup manager is 127 * required. 128 */ 129 public BackupManager(String backendID) 130 { 131 this.backendID = backendID; 132 } 133 134 /** A cryptographic engine to use for backup creation or restore. */ 135 private static abstract class CryptoEngine 136 { 137 final CryptoManager cryptoManager; 138 final boolean shouldEncrypt; 139 140 /** Creates a crypto engine for archive creation. */ 141 static CryptoEngine forCreation(BackupConfig backupConfig, NewBackupParams backupParams) 142 throws DirectoryException { 143 if (backupConfig.hashData()) 144 { 145 if (backupConfig.signHash()) 146 { 147 return new MacCryptoEngine(backupConfig, backupParams); 148 } 149 else 150 { 151 return new DigestCryptoEngine(backupConfig, backupParams); 152 } 153 } 154 else 155 { 156 return new NoHashCryptoEngine(backupConfig.encryptData()); 157 } 158 } 159 160 /** Creates a crypto engine for archive restore. */ 161 static CryptoEngine forRestore(BackupInfo backupInfo) 162 throws DirectoryException { 163 boolean hasSignedHash = backupInfo.getSignedHash() != null; 164 boolean hasHashData = hasSignedHash || backupInfo.getUnsignedHash() != null; 165 if (hasHashData) 166 { 167 if (hasSignedHash) 168 { 169 return new MacCryptoEngine(backupInfo); 170 } 171 else 172 { 173 return new DigestCryptoEngine(backupInfo); 174 } 175 } 176 else 177 { 178 return new NoHashCryptoEngine(backupInfo.isEncrypted()); 179 } 180 } 181 182 CryptoEngine(boolean shouldEncrypt) 183 { 184 cryptoManager = DirectoryServer.getCryptoManager(); 185 this.shouldEncrypt = shouldEncrypt; 186 } 187 188 /** Indicates if data is encrypted. */ 189 final boolean shouldEncrypt() { 190 return shouldEncrypt; 191 } 192 193 /** Indicates if hashed data is signed. */ 194 boolean hasSignedHash() { 195 return false; 196 } 197 198 /** Update the hash with the provided string. */ 199 abstract void updateHashWith(String s); 200 201 /** Update the hash with the provided buffer. */ 202 abstract void updateHashWith(byte[] buffer, int offset, int len); 203 204 /** Generates the hash bytes. */ 205 abstract byte[] generateBytes(); 206 207 /** Returns the error message to use in case of check failure. */ 208 abstract LocalizableMessage getErrorMessageForCheck(String backupID); 209 210 /** Check that generated hash is equal to the provided hash. */ 211 final void check(byte[] hash, String backupID) throws DirectoryException 212 { 213 byte[] bytes = generateBytes(); 214 if (bytes != null && !Arrays.equals(bytes, hash)) 215 { 216 LocalizableMessage message = getErrorMessageForCheck(backupID); 217 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); 218 } 219 } 220 221 /** Wraps an output stream in a cipher output stream if encryption is required. */ 222 final OutputStream encryptOutput(OutputStream output) throws DirectoryException 223 { 224 if (!shouldEncrypt()) 225 { 226 return output; 227 } 228 try 229 { 230 return cryptoManager.getCipherOutputStream(output); 231 } 232 catch (CryptoManagerException e) 233 { 234 logger.traceException(e); 235 StaticUtils.close(output); 236 LocalizableMessage message = ERR_BACKUP_CANNOT_GET_CIPHER.get(stackTraceToSingleLineString(e)); 237 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 238 } 239 } 240 241 /** Wraps an input stream in a cipher input stream if encryption is required. */ 242 final InputStream encryptInput(InputStream inputStream) throws DirectoryException 243 { 244 if (!shouldEncrypt) 245 { 246 return inputStream; 247 } 248 249 try 250 { 251 return cryptoManager.getCipherInputStream(inputStream); 252 } 253 catch (CryptoManagerException e) 254 { 255 logger.traceException(e); 256 StaticUtils.close(inputStream); 257 LocalizableMessage message = ERR_BACKUP_CANNOT_GET_CIPHER.get(stackTraceToSingleLineString(e)); 258 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 259 } 260 } 261 262 } 263 264 /** Represents the cryptographic engine with no hash used for a backup. */ 265 private static final class NoHashCryptoEngine extends CryptoEngine 266 { 267 268 NoHashCryptoEngine(boolean shouldEncrypt) 269 { 270 super(shouldEncrypt); 271 } 272 273 @Override 274 void updateHashWith(String s) 275 { 276 // nothing to do 277 } 278 279 @Override 280 void updateHashWith(byte[] buffer, int offset, int len) 281 { 282 // nothing to do 283 } 284 285 @Override 286 byte[] generateBytes() 287 { 288 return null; 289 } 290 291 @Override 292 LocalizableMessage getErrorMessageForCheck(String backupID) 293 { 294 // check never fails because bytes are always null 295 return null; 296 } 297 298 } 299 300 /** 301 * Represents the cryptographic engine with signed hash. 302 */ 303 private static final class MacCryptoEngine extends CryptoEngine 304 { 305 private Mac mac; 306 307 /** Constructor for backup creation. */ 308 private MacCryptoEngine(BackupConfig backupConfig, NewBackupParams backupParams) throws DirectoryException 309 { 310 super(backupConfig.encryptData()); 311 312 String macKeyID = null; 313 try 314 { 315 macKeyID = cryptoManager.getMacEngineKeyEntryID(); 316 backupParams.putProperty(BACKUP_PROPERTY_MAC_KEY_ID, macKeyID); 317 } 318 catch (CryptoManagerException e) 319 { 320 LocalizableMessage message = ERR_BACKUP_CANNOT_GET_MAC_KEY_ID.get(backupParams.backupID, 321 stackTraceToSingleLineString(e)); 322 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 323 } 324 retrieveMacEngine(macKeyID); 325 } 326 327 /** Constructor for backup restore. */ 328 private MacCryptoEngine(BackupInfo backupInfo) throws DirectoryException 329 { 330 super(backupInfo.isEncrypted()); 331 HashMap<String,String> backupProperties = backupInfo.getBackupProperties(); 332 String macKeyID = backupProperties.get(BACKUP_PROPERTY_MAC_KEY_ID); 333 retrieveMacEngine(macKeyID); 334 } 335 336 private void retrieveMacEngine(String macKeyID) throws DirectoryException 337 { 338 try 339 { 340 mac = cryptoManager.getMacEngine(macKeyID); 341 } 342 catch (Exception e) 343 { 344 LocalizableMessage message = ERR_BACKUP_CANNOT_GET_MAC.get(macKeyID, stackTraceToSingleLineString(e)); 345 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 346 } 347 } 348 349 /** {@inheritDoc} */ 350 @Override 351 void updateHashWith(String s) 352 { 353 mac.update(getBytes(s)); 354 } 355 356 /** {@inheritDoc} */ 357 @Override 358 void updateHashWith(byte[] buffer, int offset, int len) 359 { 360 mac.update(buffer, offset, len); 361 } 362 363 @Override 364 byte[] generateBytes() 365 { 366 return mac.doFinal(); 367 } 368 369 @Override 370 boolean hasSignedHash() 371 { 372 return true; 373 } 374 375 @Override 376 LocalizableMessage getErrorMessageForCheck(String backupID) 377 { 378 return ERR_BACKUP_SIGNED_HASH_ERROR.get(backupID); 379 } 380 381 @Override 382 public String toString() 383 { 384 return "MacCryptoEngine [mac=" + mac + "]"; 385 } 386 387 } 388 389 /** Represents the cryptographic engine with unsigned hash used for a backup. */ 390 private static final class DigestCryptoEngine extends CryptoEngine 391 { 392 private final MessageDigest digest; 393 394 /** Constructor for backup creation. */ 395 private DigestCryptoEngine(BackupConfig backupConfig, NewBackupParams backupParams) throws DirectoryException 396 { 397 super(backupConfig.encryptData()); 398 String digestAlgorithm = cryptoManager.getPreferredMessageDigestAlgorithm(); 399 backupParams.putProperty(BACKUP_PROPERTY_DIGEST_ALGORITHM, digestAlgorithm); 400 digest = retrieveMessageDigest(digestAlgorithm); 401 } 402 403 /** Constructor for backup restore. */ 404 private DigestCryptoEngine(BackupInfo backupInfo) throws DirectoryException 405 { 406 super(backupInfo.isEncrypted()); 407 HashMap<String, String> backupProperties = backupInfo.getBackupProperties(); 408 String digestAlgorithm = backupProperties.get(BACKUP_PROPERTY_DIGEST_ALGORITHM); 409 digest = retrieveMessageDigest(digestAlgorithm); 410 } 411 412 private MessageDigest retrieveMessageDigest(String digestAlgorithm) throws DirectoryException 413 { 414 try 415 { 416 return cryptoManager.getMessageDigest(digestAlgorithm); 417 } 418 catch (Exception e) 419 { 420 LocalizableMessage message = 421 ERR_BACKUP_CANNOT_GET_DIGEST.get(digestAlgorithm, stackTraceToSingleLineString(e)); 422 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 423 } 424 } 425 426 /** {@inheritDoc} */ 427 @Override 428 public void updateHashWith(String s) 429 { 430 digest.update(getBytes(s)); 431 } 432 433 /** {@inheritDoc} */ 434 @Override 435 public void updateHashWith(byte[] buffer, int offset, int len) 436 { 437 digest.update(buffer, offset, len); 438 } 439 440 /** {@inheritDoc} */ 441 @Override 442 public byte[] generateBytes() 443 { 444 return digest.digest(); 445 } 446 447 /** {@inheritDoc} */ 448 @Override 449 LocalizableMessage getErrorMessageForCheck(String backupID) 450 { 451 return ERR_BACKUP_UNSIGNED_HASH_ERROR.get(backupID); 452 } 453 454 @Override 455 public String toString() 456 { 457 return "DigestCryptoEngine [digest=" + digest + "]"; 458 } 459 460 } 461 462 /** 463 * Contains all parameters for creation of a new backup. 464 */ 465 private static final class NewBackupParams 466 { 467 final String backupID; 468 final BackupDirectory backupDir; 469 final HashMap<String,String> backupProperties; 470 471 final boolean shouldCompress; 472 473 final boolean isIncremental; 474 final String incrementalBaseID; 475 final BackupInfo baseBackupInfo; 476 477 NewBackupParams(BackupConfig backupConfig) throws DirectoryException 478 { 479 backupID = backupConfig.getBackupID(); 480 backupDir = backupConfig.getBackupDirectory(); 481 backupProperties = new HashMap<>(); 482 shouldCompress = backupConfig.compressData(); 483 484 incrementalBaseID = retrieveIncrementalBaseID(backupConfig); 485 isIncremental = incrementalBaseID != null; 486 baseBackupInfo = isIncremental ? getBackupInfo(backupDir, incrementalBaseID) : null; 487 } 488 489 private String retrieveIncrementalBaseID(BackupConfig backupConfig) 490 { 491 String id = null; 492 if (backupConfig.isIncremental()) 493 { 494 if (backupConfig.getIncrementalBaseID() == null && backupDir.getLatestBackup() != null) 495 { 496 // The default is to use the latest backup as base. 497 id = backupDir.getLatestBackup().getBackupID(); 498 } 499 else 500 { 501 id = backupConfig.getIncrementalBaseID(); 502 } 503 504 if (id == null) 505 { 506 // No incremental backup ID: log a message informing that a backup 507 // could not be found and that a normal backup will be done. 508 logger.warn(WARN_BACKUPDB_INCREMENTAL_NOT_FOUND_DOING_NORMAL, backupDir.getPath()); 509 } 510 } 511 return id; 512 } 513 514 void putProperty(String name, String value) { 515 backupProperties.put(name, value); 516 } 517 518 @Override 519 public String toString() 520 { 521 return "BackupCreationParams [backupID=" + backupID + ", backupDir=" + backupDir.getPath() + "]"; 522 } 523 524 } 525 526 /** Represents a new backup archive. */ 527 private static final class NewBackupArchive { 528 529 private final String archiveFilename; 530 531 private String latestFileName; 532 private long latestFileSize; 533 534 private final HashSet<String> dependencies; 535 536 private final String backendID; 537 private final NewBackupParams newBackupParams; 538 private final CryptoEngine cryptoEngine; 539 540 NewBackupArchive(String backendID, NewBackupParams backupParams, CryptoEngine crypt) 541 { 542 this.backendID = backendID; 543 this.newBackupParams = backupParams; 544 this.cryptoEngine = crypt; 545 dependencies = new HashSet<>(); 546 if (backupParams.isIncremental) 547 { 548 HashMap<String,String> properties = backupParams.baseBackupInfo.getBackupProperties(); 549 latestFileName = properties.get(PROPERTY_LAST_LOGFILE_NAME); 550 latestFileSize = Long.parseLong(properties.get(PROPERTY_LAST_LOGFILE_SIZE)); 551 } 552 archiveFilename = BACKUP_BASE_FILENAME + backendID + "-" + backupParams.backupID; 553 } 554 555 String getArchiveFilename() 556 { 557 return archiveFilename; 558 } 559 560 String getBackendID() 561 { 562 return backendID; 563 } 564 565 String getBackupID() 566 { 567 return newBackupParams.backupID; 568 } 569 570 String getBackupPath() { 571 return newBackupParams.backupDir.getPath(); 572 } 573 574 void addBaseBackupAsDependency() { 575 dependencies.add(newBackupParams.baseBackupInfo.getBackupID()); 576 } 577 578 void updateBackupDirectory() throws DirectoryException 579 { 580 BackupInfo backupInfo = createDescriptorForBackup(); 581 try 582 { 583 newBackupParams.backupDir.addBackup(backupInfo); 584 newBackupParams.backupDir.writeBackupDirectoryDescriptor(); 585 } 586 catch (Exception e) 587 { 588 logger.traceException(e); 589 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 590 ERR_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( 591 newBackupParams.backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)), 592 e); 593 } 594 } 595 596 /** Create a descriptor for the backup. */ 597 private BackupInfo createDescriptorForBackup() 598 { 599 byte[] bytes = cryptoEngine.generateBytes(); 600 byte[] digestBytes = cryptoEngine.hasSignedHash() ? null : bytes; 601 byte[] macBytes = cryptoEngine.hasSignedHash() ? bytes : null; 602 newBackupParams.putProperty(PROPERTY_LAST_LOGFILE_NAME, latestFileName); 603 newBackupParams.putProperty(PROPERTY_LAST_LOGFILE_SIZE, String.valueOf(latestFileSize)); 604 return new BackupInfo( 605 newBackupParams.backupDir, newBackupParams.backupID, new Date(), newBackupParams.isIncremental, 606 newBackupParams.shouldCompress, cryptoEngine.shouldEncrypt(), digestBytes, macBytes, 607 dependencies, newBackupParams.backupProperties); 608 } 609 610 @Override 611 public String toString() 612 { 613 return "NewArchive [archive file=" + archiveFilename + ", latestFileName=" + latestFileName 614 + ", backendID=" + backendID + "]"; 615 } 616 617 } 618 619 /** Represents an existing backup archive. */ 620 private static final class ExistingBackupArchive { 621 622 private final String backupID; 623 private final BackupDirectory backupDir; 624 private final BackupInfo backupInfo; 625 private final CryptoEngine cryptoEngine; 626 private final File archiveFile; 627 628 ExistingBackupArchive(String backupID, BackupDirectory backupDir) throws DirectoryException 629 { 630 this.backupID = backupID; 631 this.backupDir = backupDir; 632 this.backupInfo = BackupManager.getBackupInfo(backupDir, backupID); 633 this.cryptoEngine = CryptoEngine.forRestore(backupInfo); 634 this.archiveFile = BackupManager.retrieveArchiveFile(backupInfo, backupDir.getPath()); 635 } 636 637 File getArchiveFile() 638 { 639 return archiveFile; 640 } 641 642 BackupInfo getBackupInfo() { 643 return backupInfo; 644 } 645 646 String getBackupID() 647 { 648 return backupID; 649 } 650 651 CryptoEngine getCryptoEngine() 652 { 653 return cryptoEngine; 654 } 655 656 /** 657 * Obtains a list of the dependencies of this backup in order from 658 * the oldest (the full backup), to the most recent. 659 * 660 * @return A list of dependent backups. 661 * @throws DirectoryException If a Directory Server error occurs. 662 */ 663 List<BackupInfo> getBackupDependencies() throws DirectoryException 664 { 665 List<BackupInfo> dependencies = new ArrayList<>(); 666 BackupInfo currentBackupInfo = backupInfo; 667 while (currentBackupInfo != null && !currentBackupInfo.getDependencies().isEmpty()) 668 { 669 String backupID = currentBackupInfo.getDependencies().iterator().next(); 670 currentBackupInfo = backupDir.getBackupInfo(backupID); 671 if (currentBackupInfo != null) 672 { 673 dependencies.add(currentBackupInfo); 674 } 675 } 676 Collections.reverse(dependencies); 677 return dependencies; 678 } 679 680 boolean hasDependencies() 681 { 682 return !backupInfo.getDependencies().isEmpty(); 683 } 684 685 /** Removes the archive from file system. */ 686 boolean removeArchive() throws DirectoryException 687 { 688 try 689 { 690 backupDir.removeBackup(backupID); 691 backupDir.writeBackupDirectoryDescriptor(); 692 } 693 catch (ConfigException e) 694 { 695 logger.traceException(e); 696 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); 697 } 698 catch (Exception e) 699 { 700 logger.traceException(e); 701 LocalizableMessage message = ERR_BACKUP_CANNOT_UPDATE_BACKUP_DESCRIPTOR.get( 702 backupDir.getDescriptorPath(), stackTraceToSingleLineString(e)); 703 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 704 } 705 706 return archiveFile.delete(); 707 } 708 709 } 710 711 /** Represents a writer of a backup archive. */ 712 private static final class BackupArchiveWriter implements Closeable { 713 714 private final ZipOutputStream zipOutputStream; 715 private final NewBackupArchive archive; 716 private final CryptoEngine cryptoEngine; 717 718 BackupArchiveWriter(NewBackupArchive archive) throws DirectoryException 719 { 720 this.archive = archive; 721 this.cryptoEngine = archive.cryptoEngine; 722 this.zipOutputStream = open(archive.getBackupPath(), archive.getArchiveFilename()); 723 } 724 725 @Override 726 public void close() throws IOException 727 { 728 StaticUtils.close(zipOutputStream); 729 } 730 731 /** 732 * Writes the provided file to a new entry in the archive. 733 * 734 * @param file 735 * The file to be written. 736 * @param cryptoMethod 737 * The cryptographic method for the written data. 738 * @param backupConfig 739 * The configuration, used to know if operation is cancelled. 740 * 741 * @return The number of bytes written from the file. 742 * @throws FileNotFoundException If the file to be archived does not exist. 743 * @throws IOException If an I/O error occurs while archiving the file. 744 */ 745 long writeFile(Path file, String relativePath, CryptoEngine cryptoMethod, BackupConfig backupConfig) 746 throws IOException, FileNotFoundException 747 { 748 ZipEntry zipEntry = new ZipEntry(relativePath); 749 zipOutputStream.putNextEntry(zipEntry); 750 751 cryptoMethod.updateHashWith(relativePath); 752 753 InputStream inputStream = null; 754 long totalBytesRead = 0; 755 try { 756 inputStream = new FileInputStream(file.toFile()); 757 byte[] buffer = new byte[8192]; 758 int bytesRead = inputStream.read(buffer); 759 while (bytesRead > 0 && !backupConfig.isCancelled()) 760 { 761 cryptoMethod.updateHashWith(buffer, 0, bytesRead); 762 zipOutputStream.write(buffer, 0, bytesRead); 763 totalBytesRead += bytesRead; 764 bytesRead = inputStream.read(buffer); 765 } 766 } 767 finally { 768 StaticUtils.close(inputStream); 769 } 770 771 zipOutputStream.closeEntry(); 772 logger.info(NOTE_BACKUP_ARCHIVED_FILE, zipEntry.getName()); 773 return totalBytesRead; 774 } 775 776 /** 777 * Write a list of strings to an entry in the archive. 778 * 779 * @param stringList 780 * A list of strings to be written. The strings must not 781 * contain newlines. 782 * @param fileName 783 * The name of the zip entry to be written. 784 * @param cryptoMethod 785 * The cryptographic method for the written data. 786 * @throws IOException 787 * If an I/O error occurs while writing the archive entry. 788 */ 789 void writeStrings(List<String> stringList, String fileName, CryptoEngine cryptoMethod) 790 throws IOException 791 { 792 ZipEntry zipEntry = new ZipEntry(fileName); 793 zipOutputStream.putNextEntry(zipEntry); 794 795 cryptoMethod.updateHashWith(fileName); 796 797 Writer writer = new OutputStreamWriter(zipOutputStream); 798 for (String s : stringList) 799 { 800 cryptoMethod.updateHashWith(s); 801 writer.write(s); 802 writer.write(EOL); 803 } 804 writer.flush(); 805 zipOutputStream.closeEntry(); 806 } 807 808 /** Writes a empty placeholder entry into the archive. */ 809 void writeEmptyPlaceHolder() throws DirectoryException 810 { 811 try 812 { 813 ZipEntry emptyPlaceholder = new ZipEntry(ZIPENTRY_EMPTY_PLACEHOLDER); 814 zipOutputStream.putNextEntry(emptyPlaceholder); 815 } 816 catch (IOException e) 817 { 818 logger.traceException(e); 819 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 820 ERR_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(ZIPENTRY_EMPTY_PLACEHOLDER, archive.getBackupID(), 821 stackTraceToSingleLineString(e)), 822 e); 823 } 824 } 825 826 /** 827 * Writes the files that are unchanged from the base backup (for an 828 * incremental backup only). 829 * <p> 830 * The unchanged files names are listed in the "unchanged.txt" file, which 831 * is put in the archive. 832 * 833 */ 834 void writeUnchangedFiles(Path rootDirectory, ListIterator<Path> files, BackupConfig backupConfig) 835 throws DirectoryException 836 { 837 List<String> unchangedFilenames = new ArrayList<>(); 838 while (files.hasNext() && !backupConfig.isCancelled()) 839 { 840 Path file = files.next(); 841 String relativePath = rootDirectory.relativize(file).toString(); 842 int cmp = relativePath.compareTo(archive.latestFileName); 843 if (cmp > 0 || (cmp == 0 && file.toFile().length() != archive.latestFileSize)) 844 { 845 files.previous(); 846 break; 847 } 848 logger.info(NOTE_BACKUP_FILE_UNCHANGED, relativePath); 849 unchangedFilenames.add(relativePath); 850 } 851 852 if (!unchangedFilenames.isEmpty()) 853 { 854 writeUnchangedFilenames(unchangedFilenames); 855 } 856 } 857 858 /** Writes the list of unchanged files names in a file as new entry in the archive. */ 859 private void writeUnchangedFilenames(List<String> unchangedList) throws DirectoryException 860 { 861 String zipEntryName = ZIPENTRY_UNCHANGED_LOGFILES; 862 try 863 { 864 writeStrings(unchangedList, zipEntryName, archive.cryptoEngine); 865 } 866 catch (IOException e) 867 { 868 logger.traceException(e); 869 throw new DirectoryException( 870 DirectoryServer.getServerErrorResultCode(), 871 ERR_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(zipEntryName, archive.getBackupID(), 872 stackTraceToSingleLineString(e)), e); 873 } 874 archive.addBaseBackupAsDependency(); 875 } 876 877 /** 878 * Writes the new files in the archive. 879 */ 880 void writeChangedFiles(Path rootDirectory, ListIterator<Path> files, BackupConfig backupConfig) 881 throws DirectoryException 882 { 883 while (files.hasNext() && !backupConfig.isCancelled()) 884 { 885 Path file = files.next(); 886 String relativePath = rootDirectory.relativize(file).toString(); 887 try 888 { 889 archive.latestFileSize = writeFile(file, relativePath, archive.cryptoEngine, backupConfig); 890 archive.latestFileName = relativePath; 891 } 892 catch (FileNotFoundException e) 893 { 894 // The file may have been deleted by a cleaner (i.e. for JE storage) since we started. 895 // The backupable entity is responsible for handling the changes through the files list iterator 896 logger.traceException(e); 897 } 898 catch (IOException e) 899 { 900 logger.traceException(e); 901 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 902 ERR_BACKUP_CANNOT_WRITE_ARCHIVE_FILE.get(relativePath, archive.getBackupID(), 903 stackTraceToSingleLineString(e)), e); 904 } 905 } 906 } 907 908 private ZipOutputStream open(String backupPath, String archiveFilename) throws DirectoryException 909 { 910 OutputStream output = openStream(backupPath, archiveFilename); 911 output = cryptoEngine.encryptOutput(output); 912 return openZipStream(output); 913 } 914 915 private OutputStream openStream(String backupPath, String archiveFilename) throws DirectoryException { 916 OutputStream output = null; 917 try 918 { 919 File archiveFile = new File(backupPath, archiveFilename); 920 int i = 1; 921 while (archiveFile.exists()) 922 { 923 archiveFile = new File(backupPath, archiveFilename + "." + i); 924 i++; 925 } 926 output = new FileOutputStream(archiveFile, false); 927 archive.newBackupParams.putProperty(BACKUP_PROPERTY_ARCHIVE_FILENAME, archiveFilename); 928 return output; 929 } 930 catch (Exception e) 931 { 932 logger.traceException(e); 933 StaticUtils.close(output); 934 LocalizableMessage message = ERR_BACKUP_CANNOT_CREATE_ARCHIVE_FILE. 935 get(archiveFilename, backupPath, archive.getBackupID(), stackTraceToSingleLineString(e)); 936 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 937 } 938 } 939 940 /** Wraps the file output stream in a zip output stream. */ 941 private ZipOutputStream openZipStream(OutputStream outputStream) 942 { 943 ZipOutputStream zipStream = new ZipOutputStream(outputStream); 944 945 zipStream.setComment(ERR_BACKUP_ZIP_COMMENT.get(DynamicConstants.PRODUCT_NAME, archive.getBackupID()) 946 .toString()); 947 948 if (archive.newBackupParams.shouldCompress) 949 { 950 zipStream.setLevel(Deflater.DEFAULT_COMPRESSION); 951 } 952 else 953 { 954 zipStream.setLevel(Deflater.NO_COMPRESSION); 955 } 956 return zipStream; 957 } 958 959 @Override 960 public String toString() 961 { 962 return "BackupArchiveWriter [archive file=" + archive.getArchiveFilename() + ", backendId=" 963 + archive.getBackendID() + "]"; 964 } 965 966 } 967 968 /** Represents a reader of a backup archive. */ 969 private static final class BackupArchiveReader { 970 971 private final CryptoEngine cryptoEngine; 972 private final File archiveFile; 973 private final String identifier; 974 private final BackupInfo backupInfo; 975 976 BackupArchiveReader(String identifier, ExistingBackupArchive archive) 977 { 978 this.identifier = identifier; 979 this.backupInfo = archive.getBackupInfo(); 980 this.archiveFile = archive.getArchiveFile(); 981 this.cryptoEngine = archive.getCryptoEngine(); 982 } 983 984 BackupArchiveReader(String identifier, BackupInfo backupInfo, String backupDirectoryPath) throws DirectoryException 985 { 986 this.identifier = identifier; 987 this.backupInfo = backupInfo; 988 this.archiveFile = BackupManager.retrieveArchiveFile(backupInfo, backupDirectoryPath); 989 this.cryptoEngine = CryptoEngine.forRestore(backupInfo); 990 } 991 992 /** 993 * Obtains the set of files in a backup that are unchanged from its 994 * dependent backup or backups. 995 * <p> 996 * The file set is stored as as the first entry in the archive file. 997 * 998 * @return The set of files that are listed in "unchanged.txt" file 999 * of the archive. 1000 * @throws DirectoryException 1001 * If an error occurs. 1002 */ 1003 Set<String> readUnchangedDependentFiles() throws DirectoryException 1004 { 1005 Set<String> hashSet = new HashSet<>(); 1006 ZipInputStream zipStream = null; 1007 try 1008 { 1009 zipStream = openZipStream(); 1010 1011 // Iterate through the entries in the zip file. 1012 ZipEntry zipEntry = zipStream.getNextEntry(); 1013 while (zipEntry != null) 1014 { 1015 // We are looking for the entry containing the list of unchanged files. 1016 if (ZIPENTRY_UNCHANGED_LOGFILES.equals(zipEntry.getName())) 1017 { 1018 hashSet.addAll(readAllLines(zipStream)); 1019 break; 1020 } 1021 zipEntry = zipStream.getNextEntry(); 1022 } 1023 return hashSet; 1024 } 1025 catch (IOException e) 1026 { 1027 logger.traceException(e); 1028 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_BACKUP_CANNOT_RESTORE.get( 1029 identifier, stackTraceToSingleLineString(e)), e); 1030 } 1031 finally { 1032 StaticUtils.close(zipStream); 1033 } 1034 } 1035 1036 /** 1037 * Restore the provided list of files from the provided restore directory. 1038 * @param restoreDir 1039 * The target directory for restored files. 1040 * @param filesToRestore 1041 * The set of files to restore. If empty, all files in the archive 1042 * are restored. 1043 * @param restoreConfig 1044 * The restore configuration, used to check for cancellation of 1045 * this restore operation. 1046 * @throws DirectoryException 1047 * If an error occurs. 1048 */ 1049 void restoreArchive(Path restoreDir, Set<String> filesToRestore, RestoreConfig restoreConfig, Backupable backupable) 1050 throws DirectoryException 1051 { 1052 try 1053 { 1054 restoreArchive0(restoreDir, filesToRestore, restoreConfig, backupable); 1055 } 1056 catch (IOException e) 1057 { 1058 logger.traceException(e); 1059 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1060 ERR_BACKUP_CANNOT_RESTORE.get(identifier, stackTraceToSingleLineString(e)), e); 1061 } 1062 1063 // check the hash 1064 byte[] hash = backupInfo.getUnsignedHash() != null ? backupInfo.getUnsignedHash() : backupInfo.getSignedHash(); 1065 cryptoEngine.check(hash, backupInfo.getBackupID()); 1066 } 1067 1068 private void restoreArchive0(Path restoreDir, Set<String> filesToRestore, RestoreConfig restoreConfig, 1069 Backupable backupable) throws DirectoryException, IOException { 1070 1071 ZipInputStream zipStream = null; 1072 try { 1073 zipStream = openZipStream(); 1074 1075 ZipEntry zipEntry = zipStream.getNextEntry(); 1076 while (zipEntry != null && !restoreConfig.isCancelled()) 1077 { 1078 String zipEntryName = zipEntry.getName(); 1079 1080 Pair<Boolean, ZipEntry> result = handleSpecialEntries(zipStream, zipEntryName); 1081 if (result.getFirst()) { 1082 zipEntry = result.getSecond(); 1083 continue; 1084 } 1085 1086 boolean mustRestoreOnDisk = !restoreConfig.verifyOnly() 1087 && (filesToRestore.isEmpty() || filesToRestore.contains(zipEntryName)); 1088 1089 if (mustRestoreOnDisk) 1090 { 1091 restoreZipEntry(zipEntryName, zipStream, restoreDir, restoreConfig); 1092 } 1093 else 1094 { 1095 restoreZipEntryVirtual(zipEntryName, zipStream, restoreConfig); 1096 } 1097 1098 zipEntry = zipStream.getNextEntry(); 1099 } 1100 } 1101 finally { 1102 StaticUtils.close(zipStream); 1103 } 1104 } 1105 1106 /** 1107 * Handle any special entry in the archive. 1108 * 1109 * @return the pair (true, zipEntry) if next entry was read, (false, null) otherwise 1110 */ 1111 private Pair<Boolean, ZipEntry> handleSpecialEntries(ZipInputStream zipStream, String zipEntryName) 1112 throws IOException 1113 { 1114 if (ZIPENTRY_EMPTY_PLACEHOLDER.equals(zipEntryName)) 1115 { 1116 // the backup contains no files 1117 return Pair.of(true, zipStream.getNextEntry()); 1118 } 1119 1120 if (ZIPENTRY_UNCHANGED_LOGFILES.equals(zipEntryName)) 1121 { 1122 // This entry is treated specially. It is never restored, 1123 // and its hash is computed on the strings, not the bytes. 1124 cryptoEngine.updateHashWith(zipEntryName); 1125 List<String> lines = readAllLines(zipStream); 1126 for (String line : lines) 1127 { 1128 cryptoEngine.updateHashWith(line); 1129 } 1130 return Pair.of(true, zipStream.getNextEntry()); 1131 } 1132 return Pair.of(false, null); 1133 } 1134 1135 /** 1136 * Restores a zip entry virtually (no actual write on disk). 1137 */ 1138 private void restoreZipEntryVirtual(String zipEntryName, ZipInputStream zipStream, RestoreConfig restoreConfig) 1139 throws FileNotFoundException, IOException 1140 { 1141 if (restoreConfig.verifyOnly()) 1142 { 1143 logger.info(NOTE_BACKUP_VERIFY_FILE, zipEntryName); 1144 } 1145 cryptoEngine.updateHashWith(zipEntryName); 1146 restoreFile(zipStream, null, restoreConfig); 1147 } 1148 1149 /** 1150 * Restores a zip entry with actual write on disk. 1151 */ 1152 private void restoreZipEntry(String zipEntryName, ZipInputStream zipStream, Path restoreDir, 1153 RestoreConfig restoreConfig) throws IOException, DirectoryException 1154 { 1155 OutputStream outputStream = null; 1156 long totalBytesRead = 0; 1157 try 1158 { 1159 Path fileToRestore = restoreDir.resolve(zipEntryName); 1160 ensureFileCanBeRestored(fileToRestore); 1161 outputStream = new FileOutputStream(fileToRestore.toFile()); 1162 cryptoEngine.updateHashWith(zipEntryName); 1163 totalBytesRead = restoreFile(zipStream, outputStream, restoreConfig); 1164 logger.info(NOTE_BACKUP_RESTORED_FILE, zipEntryName, totalBytesRead); 1165 } 1166 finally 1167 { 1168 StaticUtils.close(outputStream); 1169 } 1170 } 1171 1172 private void ensureFileCanBeRestored(Path fileToRestore) throws DirectoryException 1173 { 1174 Path parent = fileToRestore.getParent(); 1175 if (!Files.exists(parent)) 1176 { 1177 try 1178 { 1179 Files.createDirectories(parent); 1180 } 1181 catch (IOException e) 1182 { 1183 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1184 ERR_BACKUP_CANNOT_CREATE_DIRECTORY_TO_RESTORE_FILE.get(fileToRestore, identifier)); 1185 } 1186 } 1187 } 1188 1189 /** 1190 * Restores the file provided by the zip input stream. 1191 * <p> 1192 * The restore can be virtual: if the outputStream is {@code null}, the file 1193 * is not actually restored on disk. 1194 */ 1195 private long restoreFile(ZipInputStream zipInputStream, OutputStream outputStream, RestoreConfig restoreConfig) 1196 throws IOException 1197 { 1198 long totalBytesRead = 0; 1199 byte[] buffer = new byte[8192]; 1200 int bytesRead = zipInputStream.read(buffer); 1201 while (bytesRead > 0 && !restoreConfig.isCancelled()) 1202 { 1203 totalBytesRead += bytesRead; 1204 1205 cryptoEngine.updateHashWith(buffer, 0, bytesRead); 1206 1207 if (outputStream != null) 1208 { 1209 outputStream.write(buffer, 0, bytesRead); 1210 } 1211 1212 bytesRead = zipInputStream.read(buffer); 1213 } 1214 return totalBytesRead; 1215 } 1216 1217 private InputStream openStream() throws DirectoryException 1218 { 1219 try 1220 { 1221 return new FileInputStream(archiveFile); 1222 } 1223 catch (FileNotFoundException e) 1224 { 1225 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1226 ERR_BACKUP_CANNOT_RESTORE.get(identifier, stackTraceToSingleLineString(e)), e); 1227 } 1228 } 1229 1230 private ZipInputStream openZipStream() throws DirectoryException 1231 { 1232 InputStream inputStream = openStream(); 1233 inputStream = cryptoEngine.encryptInput(inputStream); 1234 return new ZipInputStream(inputStream); 1235 } 1236 1237 private List<String> readAllLines(ZipInputStream zipStream) throws IOException 1238 { 1239 final ArrayList<String> results = new ArrayList<>(); 1240 String line; 1241 BufferedReader reader = new BufferedReader(new InputStreamReader(zipStream)); 1242 while ((line = reader.readLine()) != null) 1243 { 1244 results.add(line); 1245 } 1246 return results; 1247 } 1248 } 1249 1250 /** 1251 * Creates a backup of the provided backupable entity. 1252 * <p> 1253 * The backup is stored in a single zip file in the backup directory. 1254 * <p> 1255 * If the backup is incremental, then the first entry in the zip is a text 1256 * file containing a list of all the log files that are unchanged since the 1257 * previous backup. The remaining zip entries are the log files themselves, 1258 * which, for an incremental, only include those files that have changed. 1259 * 1260 * @param backupable 1261 * The underlying entity (storage, backend) to be backed up. 1262 * @param backupConfig 1263 * The configuration to use when performing the backup. 1264 * @throws DirectoryException 1265 * If a Directory Server error occurs. 1266 */ 1267 public void createBackup(final Backupable backupable, final BackupConfig backupConfig) throws DirectoryException 1268 { 1269 final NewBackupParams backupParams = new NewBackupParams(backupConfig); 1270 final CryptoEngine cryptoEngine = CryptoEngine.forCreation(backupConfig, backupParams); 1271 final NewBackupArchive newArchive = new NewBackupArchive(backendID, backupParams, cryptoEngine); 1272 1273 BackupArchiveWriter archiveWriter = null; 1274 try 1275 { 1276 final ListIterator<Path> files = backupable.getFilesToBackup(); 1277 final Path rootDirectory = backupable.getDirectory().toPath(); 1278 archiveWriter = new BackupArchiveWriter(newArchive); 1279 1280 if (files.hasNext()) 1281 { 1282 if (backupParams.isIncremental) { 1283 archiveWriter.writeUnchangedFiles(rootDirectory, files, backupConfig); 1284 } 1285 archiveWriter.writeChangedFiles(rootDirectory, files, backupConfig); 1286 } 1287 else { 1288 archiveWriter.writeEmptyPlaceHolder(); 1289 } 1290 } 1291 finally 1292 { 1293 closeArchiveWriter(archiveWriter, newArchive.getArchiveFilename(), backupParams.backupDir.getPath()); 1294 } 1295 1296 newArchive.updateBackupDirectory(); 1297 1298 if (backupConfig.isCancelled()) 1299 { 1300 // Remove the backup since it may be incomplete 1301 removeBackup(backupParams.backupDir, backupParams.backupID); 1302 } 1303 } 1304 1305 /** 1306 * Restores a backupable entity from its backup, or verify the backup. 1307 * 1308 * @param backupable 1309 * The underlying entity (storage, backend) to be backed up. 1310 * @param restoreConfig 1311 * The configuration to use when performing the restore. 1312 * @throws DirectoryException 1313 * If a Directory Server error occurs. 1314 */ 1315 public void restoreBackup(Backupable backupable, RestoreConfig restoreConfig) throws DirectoryException 1316 { 1317 Path saveDirectory = null; 1318 if (!restoreConfig.verifyOnly()) 1319 { 1320 saveDirectory = backupable.beforeRestore(); 1321 } 1322 1323 final String backupID = restoreConfig.getBackupID(); 1324 final ExistingBackupArchive existingArchive = 1325 new ExistingBackupArchive(backupID, restoreConfig.getBackupDirectory()); 1326 final Path restoreDirectory = getRestoreDirectory(backupable, backupID); 1327 1328 if (existingArchive.hasDependencies()) 1329 { 1330 final BackupArchiveReader zipArchiveReader = new BackupArchiveReader(backupID, existingArchive); 1331 final Set<String> unchangedFilesToRestore = zipArchiveReader.readUnchangedDependentFiles(); 1332 final List<BackupInfo> dependencies = existingArchive.getBackupDependencies(); 1333 for (BackupInfo dependencyBackupInfo : dependencies) 1334 { 1335 restoreArchive(restoreDirectory, unchangedFilesToRestore, restoreConfig, backupable, dependencyBackupInfo); 1336 } 1337 } 1338 1339 // Restore the final archive file. 1340 Set<String> filesToRestore = emptySet(); 1341 restoreArchive(restoreDirectory, filesToRestore, restoreConfig, backupable, existingArchive.getBackupInfo()); 1342 1343 if (!restoreConfig.verifyOnly()) 1344 { 1345 backupable.afterRestore(restoreDirectory, saveDirectory); 1346 } 1347 } 1348 1349 /** 1350 * Removes the specified backup if it is possible to do so. 1351 * 1352 * @param backupDir The backup directory structure with which the 1353 * specified backup is associated. 1354 * @param backupID The backup ID for the backup to be removed. 1355 * 1356 * @throws DirectoryException If it is not possible to remove the specified 1357 * backup for some reason (e.g., no such backup 1358 * exists or there are other backups that are 1359 * dependent upon it). 1360 */ 1361 public void removeBackup(BackupDirectory backupDir, String backupID) throws DirectoryException 1362 { 1363 ExistingBackupArchive archive = new ExistingBackupArchive(backupID, backupDir); 1364 archive.removeArchive(); 1365 } 1366 1367 private Path getRestoreDirectory(Backupable backupable, String backupID) 1368 { 1369 File restoreDirectory = backupable.getDirectory(); 1370 if (!backupable.isDirectRestore()) 1371 { 1372 restoreDirectory = new File(restoreDirectory.getAbsoluteFile() + "-restore-" + backupID); 1373 } 1374 return restoreDirectory.toPath(); 1375 } 1376 1377 private void closeArchiveWriter(BackupArchiveWriter archiveWriter, String backupFile, String backupPath) 1378 throws DirectoryException 1379 { 1380 if (archiveWriter != null) 1381 { 1382 try 1383 { 1384 archiveWriter.close(); 1385 } 1386 catch (Exception e) 1387 { 1388 logger.traceException(e); 1389 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1390 ERR_BACKUP_CANNOT_CLOSE_ZIP_STREAM.get(backupFile, backupPath, stackTraceToSingleLineString(e)), e); 1391 } 1392 } 1393 } 1394 1395 /** 1396 * Restores the content of an archive file. 1397 * <p> 1398 * If set of files is not empty, only the specified files are restored. 1399 * If set of files is empty, all files are restored. 1400 * 1401 * If the archive is being restored as a dependency, then only files in the 1402 * specified set are restored, and the restored files are removed from the 1403 * set. Otherwise all files from the archive are restored, and files that are 1404 * to be found in dependencies are added to the set. 1405 * @param restoreDir 1406 * The directory in which files are to be restored. 1407 * @param filesToRestore 1408 * The set of files to restore. If empty, then all files are 1409 * restored. 1410 * @param restoreConfig 1411 * The restore configuration. 1412 * @param backupInfo 1413 * The backup containing the files to be restored. 1414 * 1415 * @throws DirectoryException 1416 * If a Directory Server error occurs. 1417 * @throws IOException 1418 * If an I/O exception occurs during the restore. 1419 */ 1420 private void restoreArchive(Path restoreDir, 1421 Set<String> filesToRestore, 1422 RestoreConfig restoreConfig, 1423 Backupable backupable, 1424 BackupInfo backupInfo) throws DirectoryException 1425 { 1426 String backupID = backupInfo.getBackupID(); 1427 String backupDirectoryPath = restoreConfig.getBackupDirectory().getPath(); 1428 1429 BackupArchiveReader zipArchiveReader = new BackupArchiveReader(backupID, backupInfo, backupDirectoryPath); 1430 zipArchiveReader.restoreArchive(restoreDir, filesToRestore, restoreConfig, backupable); 1431 } 1432 1433 /** Retrieves the full path of the archive file. */ 1434 private static File retrieveArchiveFile(BackupInfo backupInfo, String backupDirectoryPath) 1435 { 1436 Map<String,String> backupProperties = backupInfo.getBackupProperties(); 1437 String archiveFilename = backupProperties.get(BACKUP_PROPERTY_ARCHIVE_FILENAME); 1438 return new File(backupDirectoryPath, archiveFilename); 1439 } 1440 1441 /** 1442 * Get the information for a given backup ID from the backup directory. 1443 * 1444 * @param backupDir The backup directory. 1445 * @param backupID The backup ID. 1446 * @return The backup information, never null. 1447 * @throws DirectoryException If the backup information cannot be found. 1448 */ 1449 private static BackupInfo getBackupInfo(BackupDirectory backupDir, String backupID) throws DirectoryException 1450 { 1451 BackupInfo backupInfo = backupDir.getBackupInfo(backupID); 1452 if (backupInfo == null) 1453 { 1454 LocalizableMessage message = ERR_BACKUP_MISSING_BACKUPID.get(backupID, backupDir.getPath()); 1455 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); 1456 } 1457 return backupInfo; 1458 } 1459 1460 /** 1461 * Helper method to build a list of files to backup, in the simple case where all files are located 1462 * under the provided directory. 1463 * 1464 * @param directory 1465 * The directory containing files to backup. 1466 * @param filter 1467 * The filter to select files to backup. 1468 * @param identifier 1469 * Identifier of the backed-up entity 1470 * @return the files to backup, which may be empty but never {@code null} 1471 * @throws DirectoryException 1472 * if an error occurs. 1473 */ 1474 public static List<Path> getFiles(File directory, FileFilter filter, String identifier) 1475 throws DirectoryException 1476 { 1477 File[] files = null; 1478 try 1479 { 1480 files = directory.listFiles(filter); 1481 } 1482 catch (Exception e) 1483 { 1484 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1485 ERR_BACKUP_CANNOT_LIST_LOG_FILES.get(directory.getAbsolutePath(), identifier), e); 1486 } 1487 if (files == null) 1488 { 1489 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1490 ERR_BACKUP_CANNOT_LIST_LOG_FILES.get(directory.getAbsolutePath(), identifier)); 1491 } 1492 1493 List<Path> paths = new ArrayList<>(); 1494 for (File file : files) 1495 { 1496 paths.add(file.toPath()); 1497 } 1498 return paths; 1499 } 1500 1501 /** 1502 * Helper method to save all current files of the provided backupable entity, using 1503 * default behavior. 1504 * 1505 * @param backupable 1506 * The entity to backup. 1507 * @param identifier 1508 * Identifier of the backup 1509 * @return the directory where all files are saved. 1510 * @throws DirectoryException 1511 * If a problem occurs. 1512 */ 1513 public static Path saveCurrentFilesToDirectory(Backupable backupable, String identifier) throws DirectoryException 1514 { 1515 ListIterator<Path> filesToBackup = backupable.getFilesToBackup(); 1516 File rootDirectory = backupable.getDirectory(); 1517 String saveDirectory = rootDirectory.getAbsolutePath() + ".save"; 1518 BackupManager.saveFilesToDirectory(rootDirectory.toPath(), filesToBackup, saveDirectory, identifier); 1519 return Paths.get(saveDirectory); 1520 } 1521 1522 /** 1523 * Helper method to move all provided files in a target directory created from 1524 * provided target base path, keeping relative path information relative to 1525 * root directory. 1526 * 1527 * @param rootDirectory 1528 * A directory which is an ancestor of all provided files. 1529 * @param files 1530 * The files to move. 1531 * @param targetBasePath 1532 * Base path of the target directory. Actual directory is built by 1533 * adding ".save" and a number, always ensuring that the directory is new. 1534 * @param identifier 1535 * Identifier of the backup 1536 * @return the actual directory where all files are saved. 1537 * @throws DirectoryException 1538 * If a problem occurs. 1539 */ 1540 public static Path saveFilesToDirectory(Path rootDirectory, ListIterator<Path> files, String targetBasePath, 1541 String identifier) throws DirectoryException 1542 { 1543 Path targetDirectory = null; 1544 try 1545 { 1546 targetDirectory = createDirectoryWithNumericSuffix(targetBasePath, identifier); 1547 while (files.hasNext()) 1548 { 1549 Path file = files.next(); 1550 Path relativeFilePath = rootDirectory.relativize(file); 1551 Path targetFile = targetDirectory.resolve(relativeFilePath); 1552 Files.createDirectories(targetFile.getParent()); 1553 Files.move(file, targetFile); 1554 } 1555 return targetDirectory; 1556 } 1557 catch (IOException e) 1558 { 1559 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1560 ERR_BACKUP_CANNOT_SAVE_FILES_BEFORE_RESTORE.get(rootDirectory, targetDirectory, identifier, 1561 stackTraceToSingleLineString(e)), e); 1562 } 1563 } 1564 1565 /** 1566 * Creates a new directory based on the provided directory path, by adding a 1567 * suffix number that is guaranteed to be the highest. 1568 */ 1569 static Path createDirectoryWithNumericSuffix(final String baseDirectoryPath, String identifier) 1570 throws DirectoryException 1571 { 1572 try 1573 { 1574 int number = getHighestSuffixNumberForPath(baseDirectoryPath); 1575 String path = baseDirectoryPath + (number + 1); 1576 Path directory = Paths.get(path); 1577 Files.createDirectories(directory); 1578 return directory; 1579 } 1580 catch (IOException e) 1581 { 1582 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1583 ERR_BACKUP_CANNOT_CREATE_SAVE_DIRECTORY.get(baseDirectoryPath, identifier, 1584 stackTraceToSingleLineString(e)), e); 1585 } 1586 } 1587 1588 /** 1589 * Returns a number that correspond to the highest suffix number existing for the provided base path. 1590 * <p> 1591 * Example: given the following directory structure 1592 * <pre> 1593 * +--- someDir 1594 * | \--- directory 1595 * | \--- directory1 1596 * | \--- directory2 1597 * | \--- directory10 1598 * </pre> 1599 * getHighestSuffixNumberForPath("directory") returns 10. 1600 * 1601 * @param basePath 1602 * A base path to a file or directory, without any suffix number. 1603 * @return the highest suffix number, or 0 if no suffix number exists 1604 * @throws IOException 1605 * if an error occurs. 1606 */ 1607 private static int getHighestSuffixNumberForPath(final String basePath) throws IOException 1608 { 1609 final File baseFile = new File(basePath).getCanonicalFile(); 1610 final File[] existingFiles = baseFile.getParentFile().listFiles(); 1611 final Pattern pattern = Pattern.compile(baseFile + "\\d*"); 1612 int highestNumber = 0; 1613 for (File file : existingFiles) 1614 { 1615 final String name = file.getCanonicalPath(); 1616 if (pattern.matcher(name).matches()) 1617 { 1618 String numberAsString = name.substring(baseFile.getPath().length()); 1619 int number = numberAsString.isEmpty() ? 0 : Integer.valueOf(numberAsString); 1620 highestNumber = number > highestNumber ? number : highestNumber; 1621 } 1622 } 1623 return highestNumber; 1624 } 1625 1626}