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