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