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