/**
* Copyright 2012 multibit.org
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.multibit.file;
import com.google.bitcoin.core.BlockChain;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.crypto.KeyCrypterException;
import org.multibit.ApplicationDataDirectoryLocator;
import org.multibit.controller.Controller;
import org.multibit.controller.bitcoin.BitcoinController;
import org.multibit.message.Message;
import org.multibit.message.MessageManager;
import org.multibit.model.bitcoin.BitcoinModel;
import org.multibit.model.bitcoin.WalletData;
import org.multibit.model.bitcoin.WalletInfoData;
import org.multibit.model.core.CoreModel;
import org.multibit.network.MultiBitService;
import org.multibit.store.MultiBitWalletProtobufSerializer;
import org.multibit.store.MultiBitWalletVersion;
import org.multibit.store.WalletVersionException;
import org.multibit.viewsystem.View;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.channels.FileChannel;
import java.util.*;
/**
* Class consolidating the File IO in MultiBit for wallets and wallet infos.
*
* @author jim
*
*/
public class FileHandler {
private static Logger log = LoggerFactory.getLogger(FileHandler.class);
public static final String USER_PROPERTIES_FILE_NAME = "multibit.properties";
public static final String USER_PROPERTIES_HEADER_TEXT = "multibit";
private final Controller controller;
private final BitcoinController bitcoinController;
private static final int MAX_FILE_SIZE = 1024 * 1024 * 1024; // Dont read files greater than 1 gigabyte.
private MultiBitWalletProtobufSerializer walletProtobufSerializer;
public FileHandler(BitcoinController bitcoinController) {
this.bitcoinController = bitcoinController;
this.controller = this.bitcoinController;
walletProtobufSerializer = new MultiBitWalletProtobufSerializer();
}
/**
* Load up a WalletData from a specified wallet file.
* If the main wallet cannot be loaded, the most recent backup is tried,
* followed by the next recent.
*
* @param walletFile the file of the wallet to load
* @return WalletData - the walletData for the created wallet
* @throws WalletLoadException
* @throws WalletVersionException
*/
public WalletData loadFromFile(File walletFile) throws WalletLoadException, WalletVersionException {
if (walletFile == null) {
return null;
}
String walletFilenameToUseInModel = walletFile.getAbsolutePath();
try {
// See if the wallet is serialized or protobuf.
WalletInfoData walletInfo;
if (isWalletSerialised(walletFile)) {
// Serialised wallets are no longer supported.
throw new WalletLoadException("Could not load wallet '" + walletFilenameToUseInModel
+ "'. Serialized wallets are no longer supported.");
} else {
walletInfo = new WalletInfoData(walletFilenameToUseInModel, null, MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
}
// If the wallet file is missing or empty but the backup file exists
// load that instead. This indicates that the write was interrupted
// (e.g. power loss).
boolean useBackupWallets = ( !walletFile.exists() || walletFile.length() == 0 );
boolean walletWasLoadedSuccessfully = false;
Collection<String> errorMessages = new ArrayList<String>();
Wallet wallet = null;
// Try the main wallet first unless it is obviously broken.
if (!useBackupWallets) {
FileInputStream fileInputStream = new FileInputStream(walletFile);
InputStream stream = null;
try {
stream = new BufferedInputStream(fileInputStream);
wallet = Wallet.loadFromFileStream(stream);
walletWasLoadedSuccessfully = true;
} catch (WalletVersionException wve) {
// We want this exception to propagate out.
throw wve;
} catch (Exception e) {
e.printStackTrace();
String description = e.getClass().getCanonicalName() + " " + e.getMessage();
log.error(description);
errorMessages.add(description);
} finally {
if (stream != null) {
stream.close();
}
fileInputStream.close();
}
}
if (!walletWasLoadedSuccessfully) {
// If the main wallet was not loaded successfully, work out the best backup
// wallets to try and load them.
useBackupWallets = true;
Collection<String> backupWalletsToTry = BackupManager.INSTANCE.calculateBestWalletBackups(walletFile, walletInfo);
Iterator<String> iterator = backupWalletsToTry.iterator();
while (!walletWasLoadedSuccessfully && iterator.hasNext()) {
String walletToTry = iterator.next();
FileInputStream fileInputStream = new FileInputStream(new File(walletToTry));
InputStream stream = null;
try {
stream = new BufferedInputStream(fileInputStream);
wallet = Wallet.loadFromFileStream(stream);
walletWasLoadedSuccessfully = true;
// Mention to user that backup is being used.
// Report failure to user.
MessageManager.INSTANCE.addMessage(new Message(bitcoinController.getLocaliser().getString("fileHandler.walletCannotLoadUsingBackup",
new String[]{walletFilenameToUseInModel, walletToTry})));
} catch (Exception e) {
e.printStackTrace();
String description = e.getClass().getCanonicalName() + " " + e.getMessage();
log.error(description);
errorMessages.add(description);
} finally {
if (stream != null) {
stream.close();
}
fileInputStream.close();
}
}
}
WalletData perWalletModelData = null;
if (walletWasLoadedSuccessfully) {
if (walletInfo != null) {
// If wallet description is only in the wallet, copy it to
// the wallet info
// (perhaps the user deleted/ did not copy the info file).
String walletDescriptionInInfo = walletInfo.getProperty(WalletInfoData.DESCRIPTION_PROPERTY);
if ((walletDescriptionInInfo == null || walletDescriptionInInfo.length() == 0)
&& wallet.getDescription() != null) {
walletInfo.put(WalletInfoData.DESCRIPTION_PROPERTY, wallet.getDescription());
}
// Check that only receiving addresses that appear in a key
// appear in the wallet info.
walletInfo.checkAllReceivingAddressesAppearInWallet(wallet);
// Make sure the version type in the info file matches what was actually loaded.
// (A backup with a different encryption type might have been used).
walletInfo.setWalletVersion(wallet.getVersion());
}
// Ensure that the directories for the backups of the private
// keys, rolling backups and regular backups exist.
BackupManager.INSTANCE.createBackupDirectories(walletFile);
// Add the new wallet into the model.
wallet.setNetworkParameters(bitcoinController.getModel().getNetworkParameters());
perWalletModelData = bitcoinController.getModel().addWallet(this.bitcoinController, wallet,
walletFilenameToUseInModel);
perWalletModelData.setWalletInfo(walletInfo);
// If the backup files were used save them immediately and don't
// delete any rolling backups.
if (useBackupWallets) {
// Wipe the wallet backup property so that the rolling
// backup file will not be overwritten
walletInfo.put(BitcoinModel.WALLET_BACKUP_FILE, "");
// Save the wallet immediately just to be on the safe side.
savePerWalletModelData(perWalletModelData, true);
}
synchronized (walletInfo) {
perWalletModelData.setDirty(false);
}
} else {
// No wallet was loaded successfully.
// Wipe the rolling backup property to ensure that file wont be deleted.
if (walletInfo != null) {
walletInfo.put(BitcoinModel.WALLET_BACKUP_FILE, "");
}
// Report failure to user.
String messageText = bitcoinController.getLocaliser().getString("fileHandler.unableToLoadWalletOrBackups", new String[] {walletFilenameToUseInModel});
if (!errorMessages.isEmpty()) {
StringBuilder errorMessagesAsString = new StringBuilder();
for (String errorText : errorMessages) {
if (errorMessagesAsString.length()>0) {
errorMessagesAsString.append("\n");
}
errorMessagesAsString.append(errorText);
}
messageText = messageText + "\n" + bitcoinController.getLocaliser().getString("deleteWalletConfirmDialog.walletDeleteError2", new String[]{errorMessagesAsString.toString()});
}
MessageManager.INSTANCE.addMessage(new Message(messageText));
}
return perWalletModelData;
} catch (WalletVersionException wve) {
// We want this to propagate out.
throw wve;
} catch (Exception e) {
e.printStackTrace();
log.error(e.getClass().getCanonicalName() + " " + e.getMessage());
throw new WalletLoadException(e.getClass().getCanonicalName() + " " + e.getMessage(), e);
}
}
private boolean isWalletSerialised(File walletFile) {
boolean isWalletSerialised = false;
InputStream stream = null;
try {
// Determine what kind of wallet stream this is: Java Serialization
// or protobuf format.
stream = new BufferedInputStream(new FileInputStream(walletFile));
isWalletSerialised = stream.read() == 0xac && stream.read() == 0xed;
} catch (IOException e) {
log.error(e.getClass().getCanonicalName() + " " + e.getMessage());
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
log.error(e.getClass().getCanonicalName() + " " + e.getMessage());
}
}
}
return isWalletSerialised;
}
/**
* Save the perWalletModelData to file.
*
* @param perWalletModelData the wallet data
* @param forceWrite
* force the write of the perWalletModelData
*/
public void savePerWalletModelData(WalletData perWalletModelData, boolean forceWrite) {
if (perWalletModelData == null || perWalletModelData.getWalletFilename() == null) {
return;
}
WalletInfoData walletInfo = perWalletModelData.getWalletInfo();
if (walletInfo != null) {
synchronized (walletInfo) {
// Save the perWalletModelData if it is dirty or if forceWrite is true.
if (perWalletModelData.isDirty() || forceWrite) {
// Normal write of data.
String walletInfoFilename = WalletInfoData.createWalletInfoFilename(perWalletModelData.getWalletFilename());
saveWalletAndWalletInfo(perWalletModelData, perWalletModelData.getWalletFilename(), walletInfoFilename);
// The perWalletModelData is no longer dirty.
perWalletModelData.setDirty(false);
}
}
}
}
/**
* Simply save the wallet and wallet info files.
* Used for backup writes.
*
* @param perWalletModelData the wallet data
* @param walletFilename the wallet filename
* @param walletInfoFilename the wallet info filename
*/
public void saveWalletAndWalletInfoSimple(WalletData perWalletModelData, String walletFilename, String walletInfoFilename) {
File walletFile = new File(walletFilename);
WalletInfoData walletInfo = perWalletModelData.getWalletInfo();
FileOutputStream fileOutputStream = null;
// Save the wallet file
try {
if (perWalletModelData.getWallet() != null) {
// Wallet description is currently stored in the wallet info
// file but is now available on the wallet itself.
// Store the description from the wallet info in the wallet - in
// the future the wallet value will be primary
// and wallet infos can be deprecated.
if (walletInfo != null) {
String walletDescriptionInInfoFile = walletInfo.getProperty(WalletInfoData.DESCRIPTION_PROPERTY);
if (walletDescriptionInInfoFile != null) {
perWalletModelData.getWallet().setDescription(walletDescriptionInInfoFile);
}
}
log.debug("Saving wallet file '" + walletFile.getAbsolutePath() + "' ...");
if (MultiBitWalletVersion.SERIALIZED == walletInfo.getWalletVersion()) {
throw new WalletSaveException("Cannot save wallet '" + walletFile.getAbsolutePath() + "'. Serialized wallets are no longer supported.");
} else {
// See if there are any encrypted private keys - if there
// are the wallet will be saved
// as encrypted and the version set to PROTOBUF_ENCRYPTED.
boolean walletIsActuallyEncrypted = false;
Wallet wallet = perWalletModelData.getWallet();
// Check all the keys individually.
for (ECKey key : wallet.getKeychain()) {
if (key.isEncrypted()) {
walletIsActuallyEncrypted = true;
break;
}
}
if (walletIsActuallyEncrypted) {
walletInfo.setWalletVersion(MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
}
if (MultiBitWalletVersion.PROTOBUF == walletInfo.getWalletVersion()) {
// Save as a Wallet message.
perWalletModelData.getWallet().saveToFile(walletFile);
} else if (MultiBitWalletVersion.PROTOBUF_ENCRYPTED == walletInfo.getWalletVersion()) {
fileOutputStream = new FileOutputStream(walletFile);
// Save as a Wallet message with a mandatory extension
// to prevent loading by older versions of multibit.
walletProtobufSerializer.writeWallet(perWalletModelData.getWallet(), fileOutputStream);
} else {
throw new WalletVersionException("Cannot save wallet '" + perWalletModelData.getWalletFilename()
+ "'. Its wallet version is '" + walletInfo.getWalletVersion().toString()
+ "' but this version of MultiBit does not understand that format.");
}
}
log.debug("... done saving wallet file.");
}
} catch (IOException ioe) {
throw new WalletSaveException("Cannot save wallet '" + perWalletModelData.getWalletFilename(), ioe);
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.flush();
fileOutputStream.close();
} catch (IOException e) {
throw new WalletSaveException("Cannot save wallet '" + perWalletModelData.getWalletFilename(), e);
}
}
}
// Write wallet info.
walletInfo.writeToFile(walletInfoFilename, walletInfo.getWalletVersion());
}
/**
* To protect the wallet data, the write is in steps: 1) Create a backup
* file called <wallet file name>-<yyyymmddhhmmss>.wallet and copy the
* original wallet to that 2) Write the new wallet to the walletFilename 3)
* Delete the old backup file 4) Make the backup file in step 1) the new
* backup file
*
**/
private void saveWalletAndWalletInfo(WalletData perWalletModelData, String walletFilename, String walletInfoFilename) {
File walletFile = new File(walletFilename);
WalletInfoData walletInfo = perWalletModelData.getWalletInfo();
FileOutputStream fileOutputStream = null;
// Save the wallet file
try {
if (perWalletModelData.getWallet() != null) {
// Wallet description is currently stored in the wallet info
// file but is now available on the wallet itself.
// Store the description from the wallet info in the wallet - in
// the future the wallet value will be primary
// and wallet infos can be deprecated.
if (walletInfo != null) {
String walletDescriptionInInfoFile = walletInfo.getProperty(WalletInfoData.DESCRIPTION_PROPERTY);
if (walletDescriptionInInfoFile != null) {
perWalletModelData.getWallet().setDescription(walletDescriptionInInfoFile);
}
}
String oldBackupFilename = perWalletModelData.getWalletInfo().getProperty(BitcoinModel.WALLET_BACKUP_FILE);
File oldBackupFile = null;
String newBackupFilename = null;
if (null != oldBackupFilename && !"".equals(oldBackupFilename)) {
oldBackupFile = new File(oldBackupFilename);
}
if (walletInfo != null && (MultiBitWalletVersion.PROTOBUF == walletInfo.getWalletVersion()
|| MultiBitWalletVersion.PROTOBUF_ENCRYPTED == walletInfo.getWalletVersion())) {
newBackupFilename = BackupManager.INSTANCE.createBackupFilename(walletFile, BackupManager.ROLLING_WALLET_BACKUP_DIRECTORY_NAME, false, false, BitcoinModel.WALLET_FILE_EXTENSION);
File newWalletBackupFile = new File(newBackupFilename);
// Rename the existing wallet to the newWalletBackupFile
boolean result = rename(walletFile, newWalletBackupFile);
log.debug("Result of the wallet rename to backup file was " + result);
}
log.debug("Saving wallet file '" + walletFile.getAbsolutePath() + "' ...");
if (walletInfo != null && MultiBitWalletVersion.SERIALIZED == walletInfo.getWalletVersion()) {
throw new WalletSaveException("Cannot save wallet '" + walletFile.getAbsolutePath() + "'. Serialized wallets are no longer supported.");
} else {
// See if there are any encrypted private keys - if there
// are the wallet will be saved
// as encrypted and the version set to PROTOBUF_ENCRYPTED.
boolean walletIsActuallyEncrypted = false;
Wallet wallet = perWalletModelData.getWallet();
// Check all the keys individually.
for (ECKey key : wallet.getKeychain()) {
if (key.isEncrypted()) {
walletIsActuallyEncrypted = true;
break;
}
}
if (walletIsActuallyEncrypted) {
if (walletInfo == null) {
walletInfo = new WalletInfoData(walletFilename, wallet, MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
perWalletModelData.setWalletInfo(walletInfo);
} else {
walletInfo.setWalletVersion(MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
}
}
if (walletInfo != null && MultiBitWalletVersion.PROTOBUF == walletInfo.getWalletVersion()) {
// Save as a Wallet message.
perWalletModelData.getWallet().saveToFile(walletFile);
} else if (walletInfo != null && MultiBitWalletVersion.PROTOBUF_ENCRYPTED == walletInfo.getWalletVersion()) {
// Create wallet output file if it does not exist
if (!walletFile.exists()) {
boolean createdOk = walletFile.createNewFile();
log.debug("Result of empty wallet create for file {} was {}", walletFile, createdOk);
} else {
log.debug("Wallet file {} already exists - does not need creating", walletFile);
}
fileOutputStream = new FileOutputStream(walletFile);
// Save as a Wallet message with a mandatory extension
// to prevent loading by older versions of multibit.
walletProtobufSerializer.writeWallet(perWalletModelData.getWallet(), fileOutputStream);
} else {
throw new WalletVersionException("Cannot save wallet '" + perWalletModelData.getWalletFilename()
+ "'. Its wallet version is '" + (walletInfo == null ? "UNKNOWN" : walletInfo.getWalletVersion().toString())
+ "' but this version of MultiBit does not understand that format.");
}
}
log.debug("... done saving wallet file. Wallet file size is " + walletFile.length() + " bytes.");
if (MultiBitWalletVersion.PROTOBUF == walletInfo.getWalletVersion()
|| MultiBitWalletVersion.PROTOBUF_ENCRYPTED == walletInfo.getWalletVersion()) {
if (newBackupFilename != null) {
perWalletModelData.getWalletInfo().put(BitcoinModel.WALLET_BACKUP_FILE, newBackupFilename);
}
// Delete the oldBackupFile unless the user has manually opened it.
boolean userHasOpenedBackupFile = false;
List<WalletData> perWalletModelDataList = this.bitcoinController.getModel().getPerWalletModelDataList();
if (perWalletModelDataList != null) {
for (WalletData perWalletModelDataLoop : perWalletModelDataList) {
if ((oldBackupFilename != null && oldBackupFilename.equals(perWalletModelDataLoop.getWalletFilename()))
|| (newBackupFilename != null && newBackupFilename.equals(perWalletModelDataLoop
.getWalletFilename()))) {
userHasOpenedBackupFile = true;
break;
}
}
}
if (!userHasOpenedBackupFile) {
SecureFiles.secureDelete(oldBackupFile);
}
}
}
} catch (IOException ioe) {
String message = "Cannot save wallet '" + perWalletModelData.getWalletFilename();
log.error(message + " (1) " + ioe.getClass().getCanonicalName() + " " + ioe.getMessage());
throw new WalletSaveException(message, ioe);
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.flush();
fileOutputStream.close();
} catch (IOException e) {
String message = "Cannot save wallet '" + perWalletModelData.getWalletFilename();
log.error(message + " (2) " + e.getClass().getCanonicalName() + " " + e.getMessage());
throw new WalletSaveException(message, e);
}
}
}
// Write wallet info.
walletInfo.writeToFile(walletInfoFilename, walletInfo.getWalletVersion());
}
/**
* Backup the private keys of the active wallet to a file with name <wallet-name>-data/key-backup/<wallet
* name>-yyyymmddhhmmss.key
*
* @param passwordToUse The password to use
* @return File to which keys were backed up, or null if they were not.
* @throws KeyCrypterException
*/
public File backupPrivateKeys(CharSequence passwordToUse) throws IOException, KeyCrypterException {
File privateKeysBackupFile = null;
// Only encrypted files are backed up, and they must have a non blank password.
if (passwordToUse != null && passwordToUse.length() > 0) {
if (controller.getModel() != null
&& this.bitcoinController.getModel().getActiveWalletWalletInfo() != null
&& this.bitcoinController.getModel().getActiveWalletWalletInfo().getWalletVersion() == MultiBitWalletVersion.PROTOBUF_ENCRYPTED) {
// Save a backup copy of the private keys, encrypted with the passwordToUse.
PrivateKeysHandler privateKeysHandler = new PrivateKeysHandler(this.bitcoinController.getModel()
.getNetworkParameters());
String privateKeysBackupFilename = BackupManager.INSTANCE.createBackupFilename(new File(this.bitcoinController.getModel()
.getActiveWalletFilename()), BackupManager.PRIVATE_KEY_BACKUP_DIRECTORY_NAME, false, false, BitcoinModel.PRIVATE_KEY_FILE_EXTENSION);
privateKeysBackupFile = new File(privateKeysBackupFilename);
BlockChain blockChain = null;
if (this.bitcoinController.getMultiBitService() != null) {
blockChain = this.bitcoinController.getMultiBitService().getChain();
}
privateKeysHandler.exportPrivateKeys(privateKeysBackupFile, this.bitcoinController.getModel().getActiveWallet(),
blockChain, true, passwordToUse, passwordToUse);
} else {
log.debug("Wallet '" + this.bitcoinController.getModel().getActiveWalletFilename()
+ "' private keys not backed up as not PROTOBUF_ENCRYPTED");
}
} else {
log.debug("Wallet '" + this.bitcoinController.getModel().getActiveWalletFilename()
+ "' private keys not backed up password was blank or of zero length");
}
return privateKeysBackupFile;
}
/**
* Copy an existing wallet to a backup file and delete the original.
* Used in rolling backups
*
* @param walletFile the wallet file
* @return the new wallet backup name
* @throws IOException
*/
private String copyExistingWalletToBackupAndDeleteOriginal(File walletFile, boolean secureDelete) throws IOException {
String newWalletBackupFilename = BackupManager.INSTANCE.createBackupFilename(walletFile, BackupManager.ROLLING_WALLET_BACKUP_DIRECTORY_NAME, false, false, BitcoinModel.WALLET_FILE_EXTENSION);
File newWalletBackupFile = new File(newWalletBackupFilename);
if (walletFile != null && walletFile.exists()) {
FileHandler.copyFile(walletFile, newWalletBackupFile);
if (walletFile.length() != newWalletBackupFile.length()) {
throw new IOException("Failed to copy the existing wallet from '" + walletFile.getAbsolutePath() + "' to '"
+ newWalletBackupFilename + "'");
}
if (!walletFile.getAbsolutePath().equals(newWalletBackupFile.getAbsolutePath())) {
if (secureDelete) {
log.debug("Using secure delete to delete wallet file {}", walletFile);
SecureFiles.secureDelete(walletFile);
} else {
log.debug("Result of wallet delete was {}", walletFile.delete());
}
}
}
return newWalletBackupFilename;
}
/**
Rename a file from the original to the new File
**/
private boolean rename(File originalFile, File newFile) {
try {
if (Utils.isWindows()) {
// Work around an issue on Windows whereby you can't rename over existing files.
File canonical = newFile.getCanonicalFile();
if (canonical.exists() && !canonical.delete())
throw new IOException("Failed to delete canonical destination file");
if (originalFile.renameTo(canonical))
return true; // else fall through.
throw new IOException("Failed to rename " + originalFile + " to " + canonical);
} else if (!originalFile.renameTo(newFile)) {
throw new IOException("Failed to rename " + originalFile + " to " + newFile);
}
return true;
} catch (IOException ioe) {
log.error("Error in renaming file " + originalFile + " to " + newFile, ioe);
return false;
}
}
/**
* Secure delete the wallet and the wallet info file.
*
* @param perWalletModelData the wallet data
*/
public void deleteWalletAndWalletInfo(WalletData perWalletModelData) {
if (perWalletModelData == null) {
return;
}
File walletFile = new File(perWalletModelData.getWalletFilename());
WalletInfoData walletInfo = perWalletModelData.getWalletInfo();
String walletInfoFilenameAsString = WalletInfoData.createWalletInfoFilename(perWalletModelData.getWalletFilename());
File walletInfoFile = new File(walletInfoFilenameAsString);
synchronized (walletInfo) {
// See if either of the files are readonly - abort.
if (!walletFile.canWrite() || !walletInfoFile.canWrite()) {
throw new DeleteWalletException(controller.getLocaliser().getString("deleteWalletException.walletWasReadonly"));
}
// Delete the wallet info file first, then the wallet.
try {
SecureFiles.secureDelete(walletInfoFile);
SecureFiles.secureDelete(walletFile);
walletInfo.setDeleted(true);
} catch (IOException ioe) {
log.error(ioe.getClass().getCanonicalName() + " " + ioe.getMessage());
throw new DeleteWalletException(controller.getLocaliser().getString("deleteWalletException.genericCouldNotDelete",
new String[] { perWalletModelData.getWalletFilename() }));
}
}
// If the wallet was deleted, delete the model data.
if (walletInfo.isDeleted()) {
this.bitcoinController.getModel().remove(perWalletModelData);
}
}
public static void writeUserPreferences(BitcoinController bitcoinController) {
final Controller controller = bitcoinController;
// Save all the wallets' filenames in the user preferences.
if (bitcoinController.getModel().getPerWalletModelDataList() != null) {
List<WalletData> perWalletModelDataList = bitcoinController.getModel().getPerWalletModelDataList();
List<String> orderList = new ArrayList<String>();
List<String> earlyList = new ArrayList<String>();
List<String> protobuf3List = new ArrayList<String>();
for (WalletData perWalletModelData : perWalletModelDataList) {
// Check if this is the initial empty WalletData
if ("".equals(perWalletModelData.getWalletFilename()) || perWalletModelData.getWalletFilename() == null
|| perWalletModelData.getWalletInfo() == null) {
continue;
}
// Do not save deleted wallets
if (perWalletModelData.getWalletInfo().isDeleted()) {
log.debug("Not writing out info about wallet '" + perWalletModelData.getWalletFilename()
+ "' as it has been deleted");
continue;
}
if (!orderList.contains(perWalletModelData.getWalletFilename())) {
orderList.add(perWalletModelData.getWalletFilename());
}
if (perWalletModelData.getWalletInfo().getWalletVersion() == MultiBitWalletVersion.PROTOBUF_ENCRYPTED) {
if (!protobuf3List.contains(perWalletModelData.getWalletFilename())) {
protobuf3List.add(perWalletModelData.getWalletFilename());
}
} else if (perWalletModelData.getWalletInfo().getWalletVersion() == null
|| perWalletModelData.getWalletInfo().getWalletVersion() == MultiBitWalletVersion.SERIALIZED
|| perWalletModelData.getWalletInfo().getWalletVersion() == MultiBitWalletVersion.PROTOBUF) {
if (!earlyList.contains(perWalletModelData.getWalletFilename())) {
earlyList.add(perWalletModelData.getWalletFilename());
}
}
}
int orderCount = 1;
for (String walletFilename : orderList) {
controller.getModel().setUserPreference(BitcoinModel.WALLET_ORDER_PREFIX + orderCount, walletFilename);
orderCount++;
}
controller.getModel().setUserPreference(BitcoinModel.WALLET_ORDER_TOTAL, "" + orderList.size());
int earlyCount = 1;
for (String walletFilename : earlyList) {
controller.getModel().setUserPreference(BitcoinModel.EARLY_WALLET_FILENAME_PREFIX + earlyCount, walletFilename);
earlyCount++;
}
controller.getModel().setUserPreference(BitcoinModel.NUMBER_OF_EARLY_WALLETS, "" + earlyList.size());
int protobuf3Count = 1;
for (String walletFilename : protobuf3List) {
controller.getModel().setUserPreference(BitcoinModel.PROTOBUF3_WALLET_FILENAME_PREFIX + protobuf3Count,
walletFilename);
protobuf3Count++;
}
controller.getModel().setUserPreference(BitcoinModel.NUMBER_OF_PROTOBUF3_WALLETS, "" + protobuf3List.size());
controller.getModel().setUserPreference(BitcoinModel.ACTIVE_WALLET_FILENAME,
bitcoinController.getModel().getActiveWalletFilename());
}
Properties userPreferences = controller.getModel().getAllUserPreferences();
// If the view is marked as also requiring a backwards compatible
// numeric field write that.
@SuppressWarnings("deprecation")
int currentViewNumericFormat = View.toOldViewNumeric(controller.getCurrentView());
if (currentViewNumericFormat != 0) {
userPreferences.put(CoreModel.SELECTED_VIEW, "" + currentViewNumericFormat);
} else {
// Make sure the old numeric value for a view is not in the user
// properties.
userPreferences.remove(CoreModel.SELECTED_VIEW);
}
// Write the user preference properties.
OutputStream outputStream = null;
try {
String userPropertiesFilename;
if ("".equals(controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory())) {
userPropertiesFilename = USER_PROPERTIES_FILE_NAME;
} else {
userPropertiesFilename = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory()
+ File.separator + USER_PROPERTIES_FILE_NAME;
}
outputStream = new FileOutputStream(userPropertiesFilename);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(bufferedOutputStream, "UTF8");
userPreferences.store(outputStreamWriter, USER_PROPERTIES_HEADER_TEXT);
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error(e.getClass().getCanonicalName() + " " + e.getMessage());
} finally {
outputStream = null;
}
}
}
}
public static Properties loadUserPreferences(ApplicationDataDirectoryLocator applicationDataDirectoryLocator) {
Properties userPreferences = new Properties();
try {
String userPropertiesFilename;
if ("".equals(applicationDataDirectoryLocator.getApplicationDataDirectory())) {
userPropertiesFilename = USER_PROPERTIES_FILE_NAME;
} else {
userPropertiesFilename = applicationDataDirectoryLocator.getApplicationDataDirectory() + File.separator
+ USER_PROPERTIES_FILE_NAME;
}
InputStream inputStream = new FileInputStream(userPropertiesFilename);
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF8");
userPreferences.load(inputStreamReader);
} catch (IOException e) {
// Ok - may not have been created yet.
}
return userPreferences;
}
/**
* To support multiple users on the same machine, the checkpoints file is
* installed into the program installation directory and is then copied to
* the user's application data directory when MultiBit is first used.
*
* Thus each user has their own copy of the blockchain.
*/
public void copyCheckpointsFromInstallationDirectory(String destinationCheckpointsFilename) throws IOException {
if (destinationCheckpointsFilename == null) {
return;
}
// See if the block chain in the user's application data directory
// exists.
File destinationCheckpoints = new File(destinationCheckpointsFilename);
if (!destinationCheckpoints.exists()) {
// Work out the source checkpoints (put into the program
// installation directory by the installer).
File directory = new File(".");
String currentWorkingDirectory = directory.getCanonicalPath();
String filePrefix = MultiBitService.getFilePrefix();
String checkpointsFilename = filePrefix + MultiBitService.CHECKPOINTS_SUFFIX;
String sourceCheckpointsFilename = currentWorkingDirectory + File.separator + checkpointsFilename;
File sourceBlockcheckpoints = new File(sourceCheckpointsFilename);
if (sourceBlockcheckpoints.exists() && !destinationCheckpointsFilename.equals(sourceCheckpointsFilename)) {
// It should exist since installer puts them in.
log.info("Copying checkpoints from '" + sourceCheckpointsFilename + "' to '" + destinationCheckpointsFilename + "'");
copyFile(sourceBlockcheckpoints, destinationCheckpoints);
// Check all the data was copied.
long sourceLength = sourceBlockcheckpoints.length();
long destinationLength = destinationCheckpoints.length();
if (sourceLength != destinationLength) {
String errorText = "Checkpoints were not copied to user's application data directory correctly.\nThe source checkpoints '"
+ sourceCheckpointsFilename
+ "' is of length "
+ sourceLength
+ "\nbut the destination checkpoints '"
+ destinationCheckpointsFilename + "' is of length " + destinationLength;
log.error(errorText);
throw new FileHandlerException(errorText);
}
}
}
}
public static void copyFile(File sourceFile, File destinationFile) throws IOException {
if (!destinationFile.exists()) {
destinationFile.createNewFile();
}
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
FileChannel source = null;
FileChannel destination = null;
try {
fileInputStream = new FileInputStream(sourceFile);
source = fileInputStream.getChannel();
fileOutputStream = new FileOutputStream(destinationFile);
destination = fileOutputStream.getChannel();
long transfered = 0;
long bytes = source.size();
while (transfered < bytes) {
transfered += destination.transferFrom(source, 0, source.size());
destination.position(transfered);
}
} finally {
if (source != null) {
source.close();
source = null;
} else if (fileInputStream != null) {
fileInputStream.close();
fileInputStream = null;
}
if (destination != null) {
destination.close();
destination = null;
} else if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
}
}
public static void writeFile(byte[] sourceBytes, File destinationFile) throws IOException {
if (!destinationFile.exists()) {
destinationFile.createNewFile();
}
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(destinationFile);
fileOutputStream.write(sourceBytes);
} finally {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
}
}
public static File createTempDirectory(String filePrefix) throws IOException {
final File temp;
temp = File.createTempFile(filePrefix, Long.toString(System.currentTimeMillis()));
if (!(temp.delete())) {
throw new IOException("Could not delete temp file: " + temp.getAbsolutePath());
}
if (!(temp.mkdir())) {
throw new IOException("Could not create temp directory: " + temp.getAbsolutePath());
}
temp.deleteOnExit();
return temp;
}
public static byte[] read(File file) throws IOException {
if (file == null) {
throw new IllegalArgumentException("File must be provided");
}
if ( file.length() > MAX_FILE_SIZE ) {
throw new IOException("File '" + file.getAbsolutePath() + "' is too large to input");
}
byte []buffer = new byte[(int) file.length()];
InputStream ios = null;
try {
ios = new FileInputStream(file);
if ( ios.read(buffer) == -1 ) {
throw new IOException("EOF reached while trying to read the whole file");
}
} finally {
try {
if ( ios != null ) {
ios.close();
}
} catch ( IOException e) {
log.error(e.getClass().getName() + " " + e.getMessage());
}
}
return buffer;
}
}