MultiBit HDTHE TECHNICAL RECOVERY LIBRARYmail@keychainx.io ↗

MultiBit HD / Technical library

INDEPENDENT KEYCHAINX RESOURCE

BackupManager: rolling and archive backups

Recognizes backup names, builds rolling and encrypted ZIP backups, and prunes retained copies. Read creation and retention together: normal operation is not an evidence-preservation workflow. The constants help explain directory and filename conventions.

By KeychainX · 20 September 2026 · Source revision 24a12b199a01

Source inspection only. Historical Java code; not executed or security-audited for this publication.

mbhd-core/src/main/java/org/multibit/hd/core/managers/BackupManager.java

View the exact upstream file · Read raw source · License notices

SHA-256: 4a2f285244a707b486cef4ed02ed14729134607390f4f60467cff8bc4cba0e10

1package org.multibit.hd.core.managers;23import com.google.common.base.Optional;4import com.google.common.base.Preconditions;5import com.google.common.collect.Lists;6import com.google.common.collect.Maps;7import com.google.common.io.ByteStreams;8import com.google.common.io.Files;9import org.bitcoinj.core.Wallet;10import org.joda.time.DateTime;11import org.multibit.commons.crypto.AESUtils;12import org.multibit.commons.utils.Dates;13import org.multibit.hd.brit.core.seed_phrase.Bip39SeedPhraseGenerator;14import org.multibit.hd.brit.core.seed_phrase.SeedPhraseGenerator;15import org.multibit.hd.core.crypto.EncryptedFileReaderWriter;16import org.multibit.hd.core.dto.BackupSummary;17import org.multibit.hd.core.dto.CoreMessageKey;18import org.multibit.hd.core.dto.WalletId;19import org.multibit.hd.core.dto.WalletSummary;20import org.multibit.hd.core.error_reporting.ExceptionHandler;21import org.multibit.hd.core.events.CoreEvents;22import org.multibit.hd.core.events.WalletLoadEvent;23import org.multibit.hd.core.exceptions.EncryptedFileReaderWriterException;24import org.multibit.hd.core.exceptions.WalletLoadException;25import org.multibit.commons.files.SecureFiles;26import org.multibit.hd.core.files.ZipFiles;27import org.slf4j.Logger;28import org.slf4j.LoggerFactory;29import org.spongycastle.crypto.params.KeyParameter;30import org.multibit.hd.core.files.EncryptedWalletFile;31import java.io.ByteArrayInputStream;32import java.io.File;33import java.io.FileOutputStream;34import java.io.IOException;35import java.text.DateFormat;36import java.text.ParseException;37import java.text.SimpleDateFormat;38import java.util.*;3940import static org.multibit.hd.core.dto.WalletId.LENGTH_OF_FORMATTED_WALLET_ID;41import static org.multibit.hd.core.dto.WalletId.WALLET_ID_SEPARATOR;424344/**45 * Class to manage creation and reading back of the wallet backups.46 */47public enum BackupManager {4849  INSTANCE;5051  public static final String BACKUP_ZIP_FILE_EXTENSION = ".zip";52  public static final String ENCRYPTED_BACKUP_FILE_EXTENSION = ".zip.aes";53  public static final String ENCRYPTED_BACKUP_ZIP_FILE_EXTENSION_REGEX = "\\.zip\\.aes";5455  public static final String ROLLING_BACKUP_DIRECTORY_NAME = "rolling-backup";56  public static final int MAXIMUM_NUMBER_OF_ROLLING_BACKUPS = 4;5758  public static final String REGEX_FOR_TIMESTAMP_AND_WALLET_AND_AES_SUFFIX = ".*-\\d{14}\\.wallet\\.aes$";5960  public static final String LOCAL_ZIP_BACKUP_DIRECTORY_NAME = "zip-backup";61  public static final int MAXIMUM_NUMBER_OF_ZIP_BACKUPS = 60; // Chosen so that you will have about weekly backups for a year, fortnightly over two years.62  public static final int NUMBER_OF_FIRST_WALLET_ZIP_BACKUPS_TO_ALWAYS_KEEP = 2;63  public static final int NUMBER_OF_LAST_WALLET_ZIP_BACKUPS_TO_ALWAYS_KEEP = 8; // Must be at least 1.6465  public static final String BACKUP_TIMESTAMP_SUFFIX_FORMAT = "yyyyMMddHHmmss";66  private DateFormat dateFormat;6768  private static final Logger log = LoggerFactory.getLogger(BackupManager.class);6970  // Where wallets are stored71  private File applicationDataDirectory = null;7273  // Where the cloud backups are stored (this is typically specified by the user and is a SpiderOak etc sync directory)74  private Optional<File> cloudBackupDirectory;7576  /**77   * Initialise the backup manager to use the specified cloudBackupDirectory.78   * All the cloud backups will be written and read from this directory.79   * Note that each wallet also have a local copy of the zip backups.80   */81  public void initialise(File applicationDataDirectory, Optional<File> cloudBackupDirectory) {8283    Preconditions.checkNotNull(applicationDataDirectory, "'applicationDataDirectory' must be present");84    Preconditions.checkNotNull(cloudBackupDirectory, "'cloudBackupDirectory' must not be null");8586    this.applicationDataDirectory = applicationDataDirectory;87    this.cloudBackupDirectory = cloudBackupDirectory;88  }8990  /**91   */92  public void shutdownNow() {93    this.applicationDataDirectory = null;94    this.cloudBackupDirectory = Optional.absent();95  }9697  /**98   * Get all the backups available in the cloud backup directory for the wallet id specified.99   */100  public List<BackupSummary> getCloudBackups(WalletId walletId, File cloudBackupDirectory) {101    return getWalletBackups(walletId, cloudBackupDirectory);102  }103104  /**105   * Get all the backups available in the local zip backup directory for the wallet id specified.106   */107  public List<BackupSummary> getLocalZipBackups(WalletId walletId) {108    createApplicationDataDirectoryIfNotSet();109110    // Find the wallet root directory for this wallet id111    File walletRootDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, WalletManager.createWalletRoot(walletId));112113    if (!walletRootDirectory.exists()) {114      // No directory - no backups115      return Lists.newArrayList();116    }117118    // Find the zip-backups directory containing the local backups119    File zipBackupsDirectory = new File(walletRootDirectory.getAbsoluteFile() + File.separator + LOCAL_ZIP_BACKUP_DIRECTORY_NAME);120121    return getWalletBackups(walletId, zipBackupsDirectory);122  }123124  /**125   * Find the wallet backups in a directory.126   * Wallet backups are called mbhd-[formatted wallet id]-timestamp.zip and the specified wallet id is used to subset all backups127   *128   * @param walletId      The walletId to subset on129   * @param directoryName The directory to look in130   *131   * @return The wallet backups available132   */133  public List<BackupSummary> getWalletBackups(WalletId walletId, File directoryName) {134    List<BackupSummary> walletBackups = Lists.newArrayList();135136    if (directoryName == null || !directoryName.exists()) {137      // No directory - no backups138      return walletBackups;139    }140141    File[] files = directoryName.listFiles();142143    // Look for filenames with format "mbhd-" + [formatted wallet id ] + "-YYYYMMDDHHMMSS.aes"144    String backupRegex = WalletManager.WALLET_DIRECTORY_PREFIX145      + WALLET_ID_SEPARATOR146      + walletId.toFormattedString()147      + WALLET_ID_SEPARATOR148      + "\\d{14}"149      + ENCRYPTED_BACKUP_ZIP_FILE_EXTENSION_REGEX;150151    if (files != null) {152      for (File file : files) {153        if (file.isFile()) {154          if (file.getName().matches(backupRegex)) {155            if (file.length() > 0) {156              BackupSummary backupSummary = new BackupSummary(walletId, file.getName(), file);157              // Work out timestamp158              int start = (WalletManager.MBHD_WALLET_PREFIX + WALLET_ID_SEPARATOR + WALLET_ID_SEPARATOR).length() + LENGTH_OF_FORMATTED_WALLET_ID;159              int stop = start + 14;160              String timeStampString = file.getName().substring(start, stop);161              try {162                DateTime timestamp = Dates.parseBackupDate(timeStampString);163                backupSummary.setCreated(timestamp);164              } catch (IllegalArgumentException e) {165                // Serious problem if the backup format has failed166                ExceptionHandler.handleThrowable(e);167              }168              walletBackups.add(backupSummary);169            }170          }171        }172      }173    }174175    log.debug("For the walletId {}, looking in directory {}, there were {} backups", walletId, directoryName, walletBackups.size());176    return walletBackups;177  }178179  /**180   * Get all the available rolling backups181   * These are ordered in the order of timestamp i.e  the oldest one is first, the newest one is last182   *183   * @param walletId the wallet id of the wallet to search for rolling backups for184   *185   * @return a list of filenames of the rolling backups, oldest first186   */187  public List<File> getRollingBackups(WalletId walletId) {188    Preconditions.checkNotNull(walletId);189    createApplicationDataDirectoryIfNotSet();190191    // Calculate the directory the rolling backups are stored in for this wallet id192    String rollingBackupDirectoryName = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, WalletManager.createWalletRoot(walletId)) +193      File.separator + ROLLING_BACKUP_DIRECTORY_NAME;194    log.debug("Application data directory\n'{}'", applicationDataDirectory);195    log.debug("Rolling backup directory\n'{}'", rollingBackupDirectoryName);196    File rollingBackupDirectory = new File(rollingBackupDirectoryName);197198    if (!rollingBackupDirectory.exists()) {199      // no directory - no backups200      return Lists.newArrayList();201    }202203    // See if there are any wallet rolling backups.204    File[] files = rollingBackupDirectory.listFiles();205206    Map<Long, File> mapOfTimeToFile = Maps.newTreeMap(); // Note that this is sorted by long207208    // Look for file names with format "text"-YYYYMMDDHHMMSS.wallet.aes<eol> and are not empty.209    if (files != null) {210      for (File file : files) {211        if (file.isFile()) {212          if (file.getName().matches(REGEX_FOR_TIMESTAMP_AND_WALLET_AND_AES_SUFFIX)) {213            if (file.length() > 0) {214              // Work out timestamp215              int start = (WalletManager.MBHD_WALLET_PREFIX + WALLET_ID_SEPARATOR).length();216              int stop = start + 14;217              String timeStampString = file.getName().substring(start, stop);218              try {219                long timestamp = Dates.parseBackupDate(timeStampString).getMillis();220                mapOfTimeToFile.put(timestamp, file);221              } catch (IllegalArgumentException e) {222                // Serious problem if the backup format has failed223                ExceptionHandler.handleThrowable(new IllegalArgumentException("Rolling backup files are in the wrong format. Error = '" + e.getMessage() + "'"));224              }225            }226          }227        }228      }229    }230231    List<File> walletBackups = Lists.newArrayList();232233    // Iterate over entry set for efficiency234    for (Map.Entry<Long, File> entry : mapOfTimeToFile.entrySet()) {235      // Note that these are added in order of creation time, oldest first (tree map)236      walletBackups.add(entry.getValue());237    }238239    return walletBackups;240  }241242  /**243   * Create a rolling backup of the wallet, specified by the walletId.244   * <p/>245   * This is a copy of the supplied wallet file, timestamped and copied to the rolling-backup directory246   * There is a maximum number of rolling backups, removals are done using a first in - first out rule.247   *248   * @param walletSummary The wallet data with the wallet to backup249   *250   * @return the File of the created rolling wallet backup251   *252   * @throws java.io.IOException if the wallet backup could not be created253   */254  public File createRollingBackup(WalletSummary walletSummary, CharSequence password) throws IOException {255    Preconditions.checkNotNull(walletSummary, "'walletSummary' must be present");256    Preconditions.checkNotNull(walletSummary.getWallet(), "'wallet' must be present");257    Preconditions.checkNotNull(walletSummary.getWalletId(), "'walletId' must be present");258    createApplicationDataDirectoryIfNotSet();259260    // Find the wallet root directory for this wallet id261    File walletRootDirectory = WalletManager.getOrCreateWalletDirectory(262      applicationDataDirectory, WalletManager.createWalletRoot(walletSummary.getWalletId())263    );264265    if (!walletRootDirectory.exists()) {266      throw new IOException("Directory " + walletRootDirectory + " does not exist. Cannot create rolling backup.");267    }268269    String rollingBackupDirectoryName = walletRootDirectory270      + File.separator271      + BackupManager.ROLLING_BACKUP_DIRECTORY_NAME;272    SecureFiles.verifyOrCreateDirectory(new File(rollingBackupDirectoryName));273274    String walletBackupFilename = rollingBackupDirectoryName275      + File.separator276      + WalletManager.MBHD_WALLET_PREFIX277      + WALLET_ID_SEPARATOR278      + Dates.formatBackupDate(Dates.nowUtc())279      + WalletManager.MBHD_WALLET_SUFFIX;280281    File walletBackupFile = new File(walletBackupFilename);282    log.debug("Creating rolling-backup\n'{}'", walletBackupFilename);283    walletSummary.getWallet().saveToFile(walletBackupFile);284    log.debug("Created rolling-backup successfully. Size = {}", walletBackupFile.length());285286    File encryptedAESCopy = EncryptedFileReaderWriter.makeAESEncryptedCopyAndDeleteOriginal(walletBackupFile, password);287    log.debug("Created rolling-backup AES copy successfully as file:\n'{}'", encryptedAESCopy == null ? "" : encryptedAESCopy.getAbsolutePath());288289    List<File> rollingBackups = getRollingBackups(walletSummary.getWalletId());290291    // If there are more than the maximum number of rolling backups, secure delete the eldest292    if (rollingBackups.size() > MAXIMUM_NUMBER_OF_ROLLING_BACKUPS) {293      // Delete the eldest294      SecureFiles.secureDelete(rollingBackups.get(0));295    }296297    // If there are even more than that trim off another one - over time this will gently reduce the number to the maximum298    if (rollingBackups.size() > MAXIMUM_NUMBER_OF_ROLLING_BACKUPS + 1) {299      // Delete the second eldest300      SecureFiles.secureDelete(rollingBackups.get(1));301    }302    return walletBackupFile;303  }304305  /**306   * Create a local zip backup of the specified wallet id.307   * The wallet manager is interrogated to find the physical directory where the wallet is stored.308   * The whole directory (except the zip-backups) is then copied and zipped into a timestamped backup file309   * This is then written to the local backup directories310   *311   * @return The created local backup as a file312   */313  public File createLocalBackup(WalletId walletId, CharSequence password) throws IOException {314    Preconditions.checkNotNull(walletId);315    createApplicationDataDirectoryIfNotSet();316317    // Find the wallet root directory for this wallet id318    File walletRootDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, WalletManager.createWalletRoot(walletId));319320    if (!walletRootDirectory.exists()) {321      throw new IOException("Directory " + walletRootDirectory + " does not exist. Cannot backup.");322    }323324    WalletSummary walletSummary = WalletManager.getOrCreateWalletSummary(walletRootDirectory, walletId);325326    File localBackupDirectory = new File(walletRootDirectory.getAbsoluteFile() + File.separator + LOCAL_ZIP_BACKUP_DIRECTORY_NAME);327    SecureFiles.verifyOrCreateDirectory(localBackupDirectory);328329    String backupFilename = WalletManager.WALLET_DIRECTORY_PREFIX330      + WALLET_ID_SEPARATOR331      + walletId.toFormattedString()332      + WALLET_ID_SEPARATOR333      + Dates.formatBackupDate(Dates.nowUtc())334      + BACKUP_ZIP_FILE_EXTENSION;335    String localBackupFilename = localBackupDirectory.getAbsolutePath() + File.separator + backupFilename;336337    log.debug("Creating local zip-backup\n'{}'", localBackupFilename);338    ZipFiles.zipFolder(walletRootDirectory.getAbsolutePath(), localBackupFilename, false);339    File localBackupEncryptedFilename = EncryptedFileReaderWriter.makeBackupAESEncryptedCopyAndDeleteOriginal(340      new File(localBackupFilename),341      (String) password,342      walletSummary);343    log.debug("Created encrypted local zip-backup successfully. Size = {} bytes", localBackupEncryptedFilename.length());344345    // Thin the local backup directory346    thinBackupDirectory(walletId, localBackupDirectory);347348    return localBackupEncryptedFilename;349  }350351  /**352   * Create a cloud backup of the specified wallet id.353   * The wallet manager is interrogated to find the physical directory where the wallet is stored.354   * The whole directory (except the zip-backups) is then copied and zipped into a timestamped backup file355   * This is then written to the cloud backup directories356   *357   * @return The created cloud backup as a file or null if nothing was generated358   */359  public File createCloudBackup(WalletId walletId, CharSequence password) throws IOException {360    Preconditions.checkNotNull(walletId);361    createApplicationDataDirectoryIfNotSet();362363    // Find the wallet root directory for this wallet id364    File walletRootDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, WalletManager.createWalletRoot(walletId));365366    if (!walletRootDirectory.exists()) {367      throw new IOException("Directory " + walletRootDirectory + " does not exist. Cannot backup.");368    }369370    WalletSummary walletSummary = WalletManager.getAndChangeWalletSummary(walletRootDirectory, walletId,password);371372373    String backupFilename = WalletManager.WALLET_DIRECTORY_PREFIX374      + WALLET_ID_SEPARATOR375      + walletId.toFormattedString()376      + WALLET_ID_SEPARATOR377      + Dates.formatBackupDate(Dates.nowUtc())378      + BACKUP_ZIP_FILE_EXTENSION;379380    if (cloudBackupDirectory.isPresent() && cloudBackupDirectory.get().exists()) {381      String cloudBackupFilename = cloudBackupDirectory.get().getAbsolutePath() + File.separator + backupFilename;382      log.debug("Creating cloud zip-backup '" + cloudBackupFilename + "'");383      ZipFiles.zipFolder(walletRootDirectory.getAbsolutePath(), cloudBackupFilename, false);384      File cloudBackupEncryptedFilename = EncryptedFileReaderWriter.makeBackupAESEncryptedCopyAndDeleteOriginal(385        new File(cloudBackupFilename),386        (String) password,387        walletSummary);388389      log.debug("Created encrypted cloud zip-backup successfully. Size = " + (cloudBackupEncryptedFilename).length() + " bytes");390391      // Thin the local backup directory392      thinBackupDirectory(walletId, cloudBackupDirectory.get());393394      return cloudBackupEncryptedFilename;395    } else {396      log.debug("No cloud backup made for wallet '" + walletId + "' as no cloudBackupDirectory is set.");397      return null;398    }399  }400401  /**402   * Load a rolling backup file.403   * A BackupWalletLoadedEvent is emitted404   *405   * @param walletId The walletId of the wallet406   * @param password The credentials used to decrypt the encrypted wallet backup407   *408   * @throws WalletLoadException if no rolling backup could be loaded successfully, or none are available409   */410  public Wallet loadRollingBackup(final WalletId walletId, CharSequence password) throws WalletLoadException {411    // Get the available rolling backups412    List<File> rollingBackupFiles = getRollingBackups(walletId);413414    if (rollingBackupFiles.isEmpty()) {415      // Throw WalletLoadException - no wallet could be loaded416      throw new WalletLoadException("No rolling backup to load");417    } else {418      Wallet wallet = null;419      File fileLoaded = null;420421      // Try loading each rolling backup in turn, newest first422      for (int i = rollingBackupFiles.size(); i > 0; i--) {423        try {424          wallet = WalletManager.INSTANCE.loadWalletFromFile(rollingBackupFiles.get(i - 1), password);425          log.debug("Wallet read in from rolling backup file:\n'{}'", wallet.toString());426          fileLoaded = rollingBackupFiles.get(i - 1);427          break;428        } catch (Exception e) {429          // Log the initial error (and then carry on to the next rolling backup430          log.error("Could not load rolling backup:\n'{}', error was: {}", rollingBackupFiles.get(i - 1).getAbsolutePath(), e.getClass().getCanonicalName() + " " + e.getMessage());431        }432      }433434      if (wallet == null) {435        // No rolling backup was successfully loaded436        throw new WalletLoadException("Could not load any rolling backup successfully.");437      } else {438        // Emit WalletLoadedEvent for notification on GUI439        if (fileLoaded != null) {440          log.debug("Loaded backup wallet file:\n'{}'", fileLoaded.getAbsolutePath());441          CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), false, CoreMessageKey.BACKUP_WALLET_WAS_LOADED, null, Optional.of(fileLoaded)));442        }443        return wallet;444      }445    }446  }447448  /**449   * Load a zip backup file, copying all the backup files to the appropriate wallet root directory450   *451   * @param backupFileToLoad The encrypted backup file to load452   * @param seedPhrase       The seed phrase to use to decrypt the backup file453   */454  public WalletId loadZipBackup(File backupFileToLoad, List<String> seedPhrase) throws IOException {455    try {456      SeedPhraseGenerator seedPhraseGenerator = new Bip39SeedPhraseGenerator();457      byte[] seed = seedPhraseGenerator.convertToSeed(seedPhrase);458459      KeyParameter seedDerivedAESKey = org.multibit.commons.crypto.AESUtils.createAESKey(seed, WalletManager.scryptSalt());460461      return loadZipBackup(backupFileToLoad, seedDerivedAESKey);462    } catch (Exception e) {463      throw new EncryptedFileReaderWriterException("Cannot read and decrypt the backup file '" + backupFileToLoad.getAbsolutePath() + "'", e);464    }465  }466467  /**468   * Load a zip backup file, copying all the backup files to the appropriate wallet root directory469   *470   * @param backupFileToLoad The encrypted backup file to load471   * @param backupAESKey     The AES key to use to decrypt the backup file472   */473  public WalletId loadZipBackup(File backupFileToLoad, KeyParameter backupAESKey) throws IOException {474    File temporaryFile = null;475    try {476      // Work out the walletId of the backup file being loaded477      String backupFilename = backupFileToLoad.getName();478479      // Remove "mbhd-" prefix480      String walletRoot = backupFilename.replace(WalletManager.WALLET_DIRECTORY_PREFIX + WALLET_ID_SEPARATOR, "");481482      // Remove  ".zip.aes" suffix483      walletRoot = walletRoot.replace(ENCRYPTED_BACKUP_FILE_EXTENSION, "");484485      // Remove the timestamp486      if (walletRoot.length() > LENGTH_OF_FORMATTED_WALLET_ID) {487        walletRoot = walletRoot.substring(0, LENGTH_OF_FORMATTED_WALLET_ID);488      }489      WalletId walletId = new WalletId(walletRoot);490491      File walletRootDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, WalletManager.createWalletRoot(walletId));492493      // Read the encrypted file in.494      byte[] fileBytes = Files.toByteArray(new File(backupFileToLoad.getAbsolutePath()));495      byte[] ivBytes = Arrays.copyOfRange(fileBytes, 0, 16);496      byte[] encryptedWalletBytes = Arrays.copyOfRange(fileBytes, 16, fileBytes.length);497      // Decrypt the backup bytes498      byte[] decryptedBytes = AESUtils.decrypt(encryptedWalletBytes, backupAESKey, ivBytes);499      if(!EncryptedWalletFile.isParseable(decryptedBytes)){500        decryptedBytes = AESUtils.decrypt(fileBytes, backupAESKey, WalletManager.deprecatedFixedAesInitializationVector());501      }502503      File tempDirectory = Files.createTempDir();504      temporaryFile = File.createTempFile("backup", "zip", tempDirectory);505      try (FileOutputStream outputFileStream = new FileOutputStream(temporaryFile)) {506        ByteStreams.copy(new ByteArrayInputStream(decryptedBytes), outputFileStream);507      }508509      // Unzip the backup into the wallet root directory - this overwrites files if already present (hence the backup just done)510      ZipFiles.unzip(temporaryFile.getAbsolutePath(), walletRootDirectory.getAbsolutePath());511512      return walletId;513    } catch (Exception e) {514      throw new EncryptedFileReaderWriterException("Cannot read and decrypt the backup file '" + backupFileToLoad.getAbsolutePath() + "'", e);515    } finally {516      if (temporaryFile != null) {517        SecureFiles.secureDelete(temporaryFile);518      }519    }520  }521522  /**523   * Thin the wallet backups when they reach the MAXIMUM_NUMBER_OF_BACKUPS setting.524   * Thinning is done by removing the most quickly replaced backup, except for the first and last few525   * (as they are considered to be more valuable backups).526   *527   * @param walletId        the wallet id of wallet backups to thin528   * @param backupDirectory the directory to thin529   */530  private void thinBackupDirectory(WalletId walletId, File backupDirectory) {531    if (dateFormat == null) {532      dateFormat = new SimpleDateFormat(BACKUP_TIMESTAMP_SUFFIX_FORMAT);533    }534535    if (walletId == null || backupDirectory == null) {536      return;537    }538539    // Find out how many wallet backups there are.540    List<BackupSummary> backups = getWalletBackups(walletId, backupDirectory);541542    if (backups.size() < MAXIMUM_NUMBER_OF_ZIP_BACKUPS) {543      // No thinning required.544      return;545    }546547    // Work out the date the backup was made for each of the wallet.548    // This is done using the timestamp rather than the write time of the file.549    // A typical backup filename is: mbhd-0da4e1dc-3a1726d7-5456cb44-f474e117-285799bb-20140520110455.zip.aes550    // Constructed of :551    // 5 chars "mbhd-"552    // 44 chars of walletId553    // 1 char separator554    // 14 chars of timestamp555    // 8 chars of file type suffix556    Map<File, Date> mapOfFileToBackupTimes = new HashMap<>();557    for (BackupSummary backup : backups) {558      String filename = backup.getName();559      if (filename.length() > 71) {560        int startOfTimestamp = filename.length() - BACKUP_TIMESTAMP_SUFFIX_FORMAT.length() - ENCRYPTED_BACKUP_FILE_EXTENSION.length();561        String timestampText = filename.substring(startOfTimestamp, startOfTimestamp + BACKUP_TIMESTAMP_SUFFIX_FORMAT.length());562        try {563          Date parsedTimestamp = dateFormat.parse(timestampText);564          mapOfFileToBackupTimes.put(backup.getFile(), parsedTimestamp);565        } catch (ParseException pe) {566          // Cannot parse text - may be some other type of file the user has put in the directory.567          log.debug("For wallet '" + filename + " could not parse the timestamp of '" + timestampText + "'.");568        }569      }570    }571572    // See which wallet is most quickly replaced by another backup - this will be thinned.573    int walletBackupToDeleteIndex = -1; // Not set yet.574    long walletBackupToDeleteReplacementTimeMillis = Integer.MAX_VALUE; // How quickly the wallet was replaced by a later one.575576    for (int i = 0; i < backups.size(); i++) {577      if ((i < NUMBER_OF_FIRST_WALLET_ZIP_BACKUPS_TO_ALWAYS_KEEP)578        || (i >= backups.size() - NUMBER_OF_LAST_WALLET_ZIP_BACKUPS_TO_ALWAYS_KEEP)) {579        // Keep the very first and last wallets always.580      } else {581        // Work out how quickly the wallet is replaced by the next backup.582        Date thisWalletTimestamp = mapOfFileToBackupTimes.get(backups.get(i).getFile());583        Date nextWalletTimestamp = mapOfFileToBackupTimes.get(backups.get(i + 1).getFile());584        if (thisWalletTimestamp != null && nextWalletTimestamp != null) {585          long deltaTimeMillis = nextWalletTimestamp.getTime() - thisWalletTimestamp.getTime();586          if (deltaTimeMillis < walletBackupToDeleteReplacementTimeMillis) {587            // This is the best candidate for deletion so far.588            walletBackupToDeleteIndex = i;589            walletBackupToDeleteReplacementTimeMillis = deltaTimeMillis;590          }591        }592      }593    }594595    if (walletBackupToDeleteIndex > -1) {596      try {597        // Secure delete the chosen backup wallet.598        log.debug(599          "To save space, secure deleting backup wallet\n'{}'", backups600            .get(walletBackupToDeleteIndex)601            .getFile()602            .getAbsolutePath()603        );604        SecureFiles.secureDelete(backups.get(walletBackupToDeleteIndex).getFile());605      } catch (IOException ioe) {606        log.error(ioe.getClass().getName() + " " + ioe.getMessage());607      }608    }609  }610611  public void setApplicationDataDirectory(File applicationDataDirectory) {612    this.applicationDataDirectory = applicationDataDirectory;613  }614615  private void createApplicationDataDirectoryIfNotSet() {616    if (applicationDataDirectory == null) {617      // Locate the standard installation directory618      applicationDataDirectory = InstallationManager.getOrCreateApplicationDataDirectory();619      log.debug("Setting the application data directory\n'{}'", applicationDataDirectory);620    }621  }622623  public void setCloudBackupDirectory(Optional<File> cloudBackupDirectory) {624    Preconditions.checkNotNull(cloudBackupDirectory, "'cloudBackupDirectory' must not be null");625    this.cloudBackupDirectory = cloudBackupDirectory;626  }627}

Read in context

Recognizes backup names, builds rolling and encrypted ZIP backups, and prunes retained copies. Read creation and retention together: normal operation is not an evidence-preservation workflow. The constants help explain directory and filename conventions.

Compare the callers, dependency versions and corresponding tests. The pinned file is one part of a larger application. Do not execute write, prune or migration routines against your only copy of a wallet.

Sources at the reviewed revision