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