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.
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.