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