From 0a4479fe9e905384390fe0cf308751337ba959ed Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 28 Sep 2021 20:17:19 +0100 Subject: [PATCH] Initial implementation of automatic bootstrapping Currently supports block archive (i.e. full) bootstraps only. Still need to add support for top-only bootstraps. --- pom.xml | 18 + .../api/resource/BootstrapResource.java | 84 ++++ .../java/org/qortal/block/BlockChain.java | 48 ++- .../java/org/qortal/repository/Bootstrap.java | 362 ++++++++++++++++++ .../qortal/repository/RepositoryManager.java | 4 +- .../java/org/qortal/settings/Settings.java | 9 + src/main/java/org/qortal/utils/SevenZ.java | 77 ++++ .../test-settings-v2-bitcoin-regtest.json | 1 + .../test-settings-v2-block-archive.json | 1 + .../test-settings-v2-founder-rewards.json | 1 + .../test-settings-v2-leftover-reward.json | 1 + .../resources/test-settings-v2-minting.json | 1 + ...test-settings-v2-qora-holder-extremes.json | 1 + .../test-settings-v2-qora-holder.json | 1 + .../test-settings-v2-reward-levels.json | 1 + .../test-settings-v2-reward-scaling.json | 1 + src/test/resources/test-settings-v2.json | 1 + 17 files changed, 590 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/qortal/api/resource/BootstrapResource.java create mode 100644 src/main/java/org/qortal/repository/Bootstrap.java create mode 100644 src/main/java/org/qortal/utils/SevenZ.java diff --git a/pom.xml b/pom.xml index 4aeb5182..be4b89ae 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,9 @@ 1.3.8 3.6 1.8 + 2.6 + 1.21 + 1.9 1.2.2 28.1-jre 2.5.1 @@ -449,6 +452,21 @@ commons-text ${commons-text.version} + + commons-io + commons-io + ${commons-io.version} + + + org.apache.commons + commons-compress + ${commons-compress.version} + + + org.tukaani + xz + ${xz.version} + io.druid diff --git a/src/main/java/org/qortal/api/resource/BootstrapResource.java b/src/main/java/org/qortal/api/resource/BootstrapResource.java new file mode 100644 index 00000000..fe2ed378 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/BootstrapResource.java @@ -0,0 +1,84 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.api.ApiError; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.api.Security; +import org.qortal.repository.Bootstrap; +import org.qortal.repository.DataException; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import java.io.IOException; + + +@Path("/bootstrap") +@Tag(name = "Bootstrap") +public class BootstrapResource { + + private static final Logger LOGGER = LogManager.getLogger(BootstrapResource.class); + + @Context + HttpServletRequest request; + + @POST + @Path("/create") + @Operation( + summary = "Create bootstrap", + description = "Builds a bootstrap file for distribution", + responses = { + @ApiResponse( + description = "path to file on success, an exception on failure", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + public String createBootstrap() { + Security.checkApiCallAllowed(request); + + Bootstrap bootstrap = new Bootstrap(); + if (!bootstrap.canBootstrap()) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + + boolean isBlockchainValid = bootstrap.validateBlockchain(); + if (!isBlockchainValid) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); + } + + try { + return bootstrap.create(); + + } catch (DataException | InterruptedException | IOException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); + } + } + + @GET + @Path("/validate") + @Operation( + summary = "Validate blockchain", + description = "Useful to check database integrity prior to creating or after installing a bootstrap. " + + "This process is intensive and can take over an hour to run.", + responses = { + @ApiResponse( + description = "true if valid, false if invalid", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")) + ) + } + ) + public boolean validateBootstrap() { + Security.checkApiCallAllowed(request); + + Bootstrap bootstrap = new Bootstrap(); + return bootstrap.validateCompleteBlockchain(); + } +} diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index aee85131..98b8d4fd 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -4,10 +4,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.locks.ReentrantLock; import javax.xml.bind.JAXBContext; @@ -27,11 +24,9 @@ import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qortal.controller.Controller; import org.qortal.data.block.BlockData; import org.qortal.network.Network; -import org.qortal.repository.BlockRepository; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryManager; +import org.qortal.repository.*; import org.qortal.settings.Settings; +import org.qortal.utils.Base58; import org.qortal.utils.StringLongMapXmlAdapter; /** @@ -506,23 +501,28 @@ public class BlockChain { * @throws SQLException */ public static void validate() throws DataException { + + BlockData chainTip; try (final Repository repository = RepositoryManager.getRepository()) { + chainTip = repository.getBlockRepository().getLastBlock(); + } - boolean pruningEnabled = Settings.getInstance().isPruningEnabled(); - BlockData chainTip = repository.getBlockRepository().getLastBlock(); - boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1); + boolean pruningEnabled = Settings.getInstance().isPruningEnabled(); + boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1); - if (pruningEnabled && hasBlocks) { - // Pruning is enabled and we have blocks, so it's possible that the genesis block has been pruned - // It's best not to validate it, and there's no real need to - } - else { - // Check first block is Genesis Block - if (!isGenesisBlockValid()) { - rebuildBlockchain(); - } + if (pruningEnabled && hasBlocks) { + // Pruning is enabled and we have blocks, so it's possible that the genesis block has been pruned + // It's best not to validate it, and there's no real need to + } else { + // Check first block is Genesis Block + if (!isGenesisBlockValid()) { + rebuildBlockchain(); } + } + // We need to create a new connection, as the previous repository and its connections may be been + // closed by rebuildBlockchain() if a bootstrap was applied + try (final Repository repository = RepositoryManager.getRepository()) { repository.checkConsistency(); // Set the number of blocks to validate based on the pruned state of the chain @@ -615,6 +615,14 @@ public class BlockChain { } private static void rebuildBlockchain() throws DataException { + boolean shouldBootstrap = Settings.getInstance().getBootstrap(); + if (shouldBootstrap) { + // Settings indicate that we should apply a bootstrap rather than rebuilding and syncing from genesis + Bootstrap bootstrap = new Bootstrap(); + bootstrap.startImport(); + return; + } + // (Re)build repository if (!RepositoryManager.wasPristineAtOpen()) RepositoryManager.rebuild(); diff --git a/src/main/java/org/qortal/repository/Bootstrap.java b/src/main/java/org/qortal/repository/Bootstrap.java new file mode 100644 index 00000000..2289db5e --- /dev/null +++ b/src/main/java/org/qortal/repository/Bootstrap.java @@ -0,0 +1,362 @@ +package org.qortal.repository; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.block.BlockChain; +import org.qortal.controller.Controller; +import org.qortal.data.account.MintingAccountData; +import org.qortal.data.block.BlockData; +import org.qortal.data.crosschain.TradeBotData; +import org.qortal.repository.hsqldb.HSQLDBImportExport; +import org.qortal.repository.hsqldb.HSQLDBRepository; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qortal.settings.Settings; +import org.qortal.utils.NTP; +import org.qortal.utils.SevenZ; + +import java.io.BufferedInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + + +public class Bootstrap { + + private static final Logger LOGGER = LogManager.getLogger(Bootstrap.class); + + /** The maximum number of untrimmed blocks allowed to be included in a bootstrap, beyond the trim threshold */ + private static final int MAXIMUM_UNTRIMMED_BLOCKS = 100; + + /** The maximum number of unpruned blocks allowed to be included in a bootstrap, beyond the prune threshold */ + private static final int MAXIMUM_UNPRUNED_BLOCKS = 100; + + + public Bootstrap() { + + } + + /** + * canBootstrap() + * Performs basic initial checks to ensure everything is in order + * @return true if ready for bootstrap creation, or false if not + * All failure reasons are logged + */ + public boolean canBootstrap() { + LOGGER.info("Checking repository state..."); + + try (final Repository repository = RepositoryManager.getRepository()) { + + final boolean pruningEnabled = Settings.getInstance().isPruningEnabled(); + final boolean archiveEnabled = Settings.getInstance().isArchiveEnabled(); + + // Avoid creating bootstraps from pruned nodes until officially supported + if (pruningEnabled) { + LOGGER.info("Creating bootstraps from top-only nodes isn't yet supported."); + // TODO: add support for top-only bootstraps + return false; + } + + // Require that a block archive has been built + if (!archiveEnabled) { + LOGGER.info("Unable to bootstrap because the block archive isn't enabled. " + + "Set {\"archivedEnabled\": true} in settings.json to fix."); + return false; + } + + // Make sure that the block archiver is up to date + boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository); + if (!upToDate) { + LOGGER.info("Unable to bootstrap because the block archive isn't fully built yet."); + return false; + } + + // Ensure that this database contains the ATStatesHeightIndex which was missing in some cases + boolean hasAtStatesHeightIndex = repository.getATRepository().hasAtStatesHeightIndex(); + if (!hasAtStatesHeightIndex) { + LOGGER.info("Unable to bootstrap due to missing ATStatesHeightIndex. A re-sync from genesis is needed."); + return false; + } + + // Ensure we have synced NTP time + if (NTP.getTime() == null) { + LOGGER.info("Unable to bootstrap because the node hasn't synced its time yet."); + return false; + } + + // Ensure the chain is synced + final BlockData chainTip = Controller.getInstance().getChainTip(); + final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp(); + if (minLatestBlockTimestamp == null || chainTip.getTimestamp() < minLatestBlockTimestamp) { + LOGGER.info("Unable to bootstrap because the blockchain isn't fully synced."); + return false; + } + + // FUTURE: ensure trim and prune settings are using default values + + // Ensure that the online account signatures have been fully trimmed + final int accountsTrimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight(); + final long accountsUpperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime(); + final int accountsUpperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(accountsUpperTrimmableTimestamp); + final int accountsBlocksRemaining = accountsUpperTrimmableHeight - accountsTrimStartHeight; + if (accountsBlocksRemaining > MAXIMUM_UNTRIMMED_BLOCKS) { + LOGGER.info("Blockchain is not fully trimmed. Please allow the node to run for longer, " + + "then try again. Blocks remaining (online accounts signatures): {}", accountsBlocksRemaining); + return false; + } + + // Ensure that the AT states data has been fully trimmed + final int atTrimStartHeight = repository.getATRepository().getAtTrimHeight(); + final long atUpperTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime(); + final int atUpperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(atUpperTrimmableTimestamp); + final int atBlocksRemaining = atUpperTrimmableHeight - atTrimStartHeight; + if (atBlocksRemaining > MAXIMUM_UNTRIMMED_BLOCKS) { + LOGGER.info("Blockchain is not fully trimmed. Please allow the node to run for longer, " + + "then try again. Blocks remaining (AT states): {}", atBlocksRemaining); + return false; + } + + // Ensure that blocks have been fully pruned + final int blockPruneStartHeight = repository.getBlockRepository().getBlockPruneHeight(); + int blockUpperPrunableHeight = chainTip.getHeight() - Settings.getInstance().getPruneBlockLimit(); + if (archiveEnabled) { + blockUpperPrunableHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1; + } + final int blocksPruneRemaining = blockUpperPrunableHeight - blockPruneStartHeight; + if (blocksPruneRemaining > MAXIMUM_UNPRUNED_BLOCKS) { + LOGGER.info("Blockchain is not fully pruned. Please allow the node to run for longer, " + + "then try again. Blocks remaining: {}", blocksPruneRemaining); + return false; + } + + // Ensure that AT states have been fully pruned + final int atPruneStartHeight = repository.getATRepository().getAtPruneHeight(); + int atUpperPrunableHeight = chainTip.getHeight() - Settings.getInstance().getPruneBlockLimit(); + if (archiveEnabled) { + atUpperPrunableHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1; + } + final int atPruneRemaining = atUpperPrunableHeight - atPruneStartHeight; + if (atPruneRemaining > MAXIMUM_UNPRUNED_BLOCKS) { + LOGGER.info("Blockchain is not fully pruned. Please allow the node to run for longer, " + + "then try again. Blocks remaining (AT states): {}", atPruneRemaining); + return false; + } + + LOGGER.info("Repository state checks passed"); + return true; + } + catch (DataException e) { + LOGGER.info("Unable to create bootstrap: {}", e.getMessage()); + return false; + } + } + + /** + * validateBlockchain + * Performs quick validation of recent blocks in blockchain, prior to creating a bootstrap + * @return true if valid, false if not + */ + public boolean validateBlockchain() { + LOGGER.info("Validating blockchain..."); + + try { + BlockChain.validate(); + + LOGGER.info("Blockchain is valid"); + + return true; + } catch (DataException e) { + LOGGER.info("Blockchain validation failed: {}", e.getMessage()); + return false; + } + } + + /** + * validateCompleteBlockchain + * Performs intensive validation of all blocks in blockchain + * @return true if valid, false if not + */ + public boolean validateCompleteBlockchain() { + LOGGER.info("Validating blockchain..."); + + try { + // Perform basic startup validation + BlockChain.validate(); + + // Perform more intensive full-chain validation + BlockChain.validateAllBlocks(); + + LOGGER.info("Blockchain is valid"); + + return true; + } catch (DataException e) { + LOGGER.info("Blockchain validation failed: {}", e.getMessage()); + return false; + } + } + + public String create() throws DataException, InterruptedException, IOException { + try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) { + + LOGGER.info("Acquiring blockchain lock..."); + ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); + blockchainLock.lockInterruptibly(); + + Path inputPath = null; + + try { + + LOGGER.info("Exporting local data..."); + repository.exportNodeLocalData(); + + LOGGER.info("Deleting trade bot states..."); + List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData(); + for (TradeBotData tradeBotData : allTradeBotData) { + repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey()); + } + + LOGGER.info("Deleting minting accounts..."); + List mintingAccounts = repository.getAccountRepository().getMintingAccounts(); + for (MintingAccountData mintingAccount : mintingAccounts) { + repository.getAccountRepository().delete(mintingAccount.getPrivateKey()); + } + + repository.saveChanges(); + + LOGGER.info("Performing repository maintenance..."); + repository.performPeriodicMaintenance(); + + LOGGER.info("Creating bootstrap..."); + repository.backup(true, "bootstrap"); + + LOGGER.info("Moving files to output directory..."); + inputPath = Paths.get(Settings.getInstance().getRepositoryPath(), "bootstrap"); + Path outputPath = Paths.get("bootstrap"); + FileUtils.deleteDirectory(outputPath.toFile()); + + // Move the db backup to a "bootstrap" folder in the root directory + Files.move(inputPath, outputPath); + + // Copy the archive folder to inside the bootstrap folder + FileUtils.copyDirectory( + Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toFile(), + Paths.get(outputPath.toString(), "archive").toFile() + ); + + LOGGER.info("Compressing..."); + String fileName = "bootstrap.7z"; + SevenZ.compress(fileName, outputPath.toFile()); + + // Return the path to the compressed bootstrap file + Path finalPath = Paths.get(outputPath.toString(), fileName); + return finalPath.toAbsolutePath().toString(); + + } finally { + LOGGER.info("Re-importing local data..."); + Path exportPath = HSQLDBImportExport.getExportDirectory(false); + repository.importDataFromFile(Paths.get(exportPath.toString(), "TradeBotStates.json").toString()); + repository.importDataFromFile(Paths.get(exportPath.toString(), "MintingAccounts.json").toString()); + + blockchainLock.unlock(); + + // Cleanup + if (inputPath != null) { + FileUtils.deleteDirectory(inputPath.toFile()); + } + } + } + } + + public void startImport() throws DataException { + Path path = null; + try { + Path tempDir = Files.createTempDirectory("qortal-bootstrap"); + path = Paths.get(tempDir.toString(), "bootstrap.7z"); + + this.downloadToPath(path); + this.importFromPath(path); + + } catch (InterruptedException | DataException | IOException e) { + throw new DataException(String.format("Unable to import bootstrap: %s", e.getMessage())); + } + finally { + if (path != null) { + try { + FileUtils.deleteDirectory(path.toFile()); + + } catch (IOException e) { + // Temp folder will be cleaned up by system anyway, so ignore this failure + } + } + } + } + + private void downloadToPath(Path path) throws DataException { + String bootstrapUrl = "http://bootstrap.qortal.org/bootstrap.7z"; + + while (!Controller.isStopping()) { + try { + LOGGER.info("Downloading bootstrap..."); + InputStream in = new URL(bootstrapUrl).openStream(); + Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING); + break; + + } catch (IOException e) { + LOGGER.info("Unable to download bootstrap: {}", e.getMessage()); + LOGGER.info("Retrying in 5 minutes"); + + try { + Thread.sleep(5 * 60 * 1000L); + } catch (InterruptedException e2) { + break; + } + } + } + + // It's best to throw an exception on all failures, even though we're most likely just stopping + throw new DataException("Unable to download bootstrap"); + } + + private void importFromPath(Path path) throws InterruptedException, DataException, IOException { + + ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); + blockchainLock.lockInterruptibly(); + + try { + LOGGER.info("Extracting bootstrap..."); + Path input = path.toAbsolutePath(); + Path output = path.getParent().toAbsolutePath(); + SevenZ.decompress(input.toString(), output.toFile()); + + LOGGER.info("Stopping repository..."); + RepositoryManager.closeRepositoryFactory(); + + Path inputPath = Paths.get("bootstrap"); + Path outputPath = Paths.get(Settings.getInstance().getRepositoryPath()); + if (!inputPath.toFile().exists()) { + throw new DataException("Extracted bootstrap doesn't exist"); + } + + // Move the "bootstrap" folder in place of the "db" folder + LOGGER.info("Moving files to output directory..."); + FileUtils.deleteDirectory(outputPath.toFile()); + Files.move(inputPath, outputPath); + + LOGGER.info("Starting repository from bootstrap..."); + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + + } + finally { + blockchainLock.unlock(); + } + } + +} diff --git a/src/main/java/org/qortal/repository/RepositoryManager.java b/src/main/java/org/qortal/repository/RepositoryManager.java index c392d213..480edc59 100644 --- a/src/main/java/org/qortal/repository/RepositoryManager.java +++ b/src/main/java/org/qortal/repository/RepositoryManager.java @@ -50,9 +50,9 @@ public abstract class RepositoryManager { repositoryFactory = null; } - public static void backup(boolean quick) { + public static void backup(boolean quick, String name) { try (final Repository repository = getRepository()) { - repository.backup(quick); + repository.backup(quick, name); } catch (DataException e) { // Backup is best-effort so don't complain } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index d284d59d..8e1ed51b 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -138,6 +138,10 @@ public class Settings { private long archiveInterval = 7171L; // milliseconds + /** Whether to automatically bootstrap instead of syncing from genesis */ + private boolean bootstrap = true; + + // Peer-to-peer related private boolean isTestNet = false; /** Port number for inbound peer-to-peer connections. */ @@ -603,4 +607,9 @@ public class Settings { return this.archiveInterval; } + + public boolean getBootstrap() { + return this.bootstrap; + } + } diff --git a/src/main/java/org/qortal/utils/SevenZ.java b/src/main/java/org/qortal/utils/SevenZ.java new file mode 100644 index 00000000..7af7ffc0 --- /dev/null +++ b/src/main/java/org/qortal/utils/SevenZ.java @@ -0,0 +1,77 @@ +// +// Code originally written by memorynotfound +// https://memorynotfound.com/java-7z-seven-zip-example-compress-decompress-file/ +// Modified Sept 2021 by Qortal Core dev team +// + +package org.qortal.utils; + +import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; +import org.apache.commons.compress.archivers.sevenz.SevenZFile; +import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; + +import java.io.*; + +public class SevenZ { + + private SevenZ() { + + } + + public static void compress(String name, File... files) throws IOException { + try (SevenZOutputFile out = new SevenZOutputFile(new File(name))){ + for (File file : files){ + addToArchiveCompression(out, file, "."); + } + } + } + + public static void decompress(String in, File destination) throws IOException { + SevenZFile sevenZFile = new SevenZFile(new File(in)); + SevenZArchiveEntry entry; + while ((entry = sevenZFile.getNextEntry()) != null){ + if (entry.isDirectory()){ + continue; + } + File curfile = new File(destination, entry.getName()); + File parent = curfile.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + } + + FileOutputStream out = new FileOutputStream(curfile); + byte[] b = new byte[8192]; + int count = 0; + while ((count = sevenZFile.read(b)) > 0) { + out.write(b, 0, count); + } + out.close(); + } + } + + private static void addToArchiveCompression(SevenZOutputFile out, File file, String dir) throws IOException { + String name = dir + File.separator + file.getName(); + if (file.isFile()){ + SevenZArchiveEntry entry = out.createArchiveEntry(file, name); + out.putArchiveEntry(entry); + + FileInputStream in = new FileInputStream(file); + byte[] b = new byte[8192]; + int count = 0; + while ((count = in.read(b)) > 0) { + out.write(b, 0, count); + } + out.closeArchiveEntry(); + + } else if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null){ + for (File child : children){ + addToArchiveCompression(out, child, name); + } + } + } else { + System.out.println(file.getName() + " is not supported"); + } + } +} diff --git a/src/test/resources/test-settings-v2-bitcoin-regtest.json b/src/test/resources/test-settings-v2-bitcoin-regtest.json index f0a993e2..7f03b447 100644 --- a/src/test/resources/test-settings-v2-bitcoin-regtest.json +++ b/src/test/resources/test-settings-v2-bitcoin-regtest.json @@ -4,6 +4,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-block-archive.json b/src/test/resources/test-settings-v2-block-archive.json index b71b2679..7cac32b6 100644 --- a/src/test/resources/test-settings-v2-block-archive.json +++ b/src/test/resources/test-settings-v2-block-archive.json @@ -4,6 +4,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0, diff --git a/src/test/resources/test-settings-v2-founder-rewards.json b/src/test/resources/test-settings-v2-founder-rewards.json index b73544ea..fedd5de4 100644 --- a/src/test/resources/test-settings-v2-founder-rewards.json +++ b/src/test/resources/test-settings-v2-founder-rewards.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-leftover-reward.json b/src/test/resources/test-settings-v2-leftover-reward.json index 5c87cc94..45f86ff3 100644 --- a/src/test/resources/test-settings-v2-leftover-reward.json +++ b/src/test/resources/test-settings-v2-leftover-reward.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-leftover-reward.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-minting.json b/src/test/resources/test-settings-v2-minting.json index abff27e3..c2522774 100644 --- a/src/test/resources/test-settings-v2-minting.json +++ b/src/test/resources/test-settings-v2-minting.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-minting.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-qora-holder-extremes.json b/src/test/resources/test-settings-v2-qora-holder-extremes.json index dbf55170..a4422562 100644 --- a/src/test/resources/test-settings-v2-qora-holder-extremes.json +++ b/src/test/resources/test-settings-v2-qora-holder-extremes.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-qora-holder-extremes.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-qora-holder.json b/src/test/resources/test-settings-v2-qora-holder.json index c9b995a6..f8777ca1 100644 --- a/src/test/resources/test-settings-v2-qora-holder.json +++ b/src/test/resources/test-settings-v2-qora-holder.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-reward-levels.json b/src/test/resources/test-settings-v2-reward-levels.json index 4cc8de14..02a91d28 100644 --- a/src/test/resources/test-settings-v2-reward-levels.json +++ b/src/test/resources/test-settings-v2-reward-levels.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-reward-levels.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2-reward-scaling.json b/src/test/resources/test-settings-v2-reward-scaling.json index e1958d63..87f77d44 100644 --- a/src/test/resources/test-settings-v2-reward-scaling.json +++ b/src/test/resources/test-settings-v2-reward-scaling.json @@ -2,6 +2,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-reward-scaling.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0 diff --git a/src/test/resources/test-settings-v2.json b/src/test/resources/test-settings-v2.json index 13c0a60f..4dfaeac1 100644 --- a/src/test/resources/test-settings-v2.json +++ b/src/test/resources/test-settings-v2.json @@ -4,6 +4,7 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2.json", "exportPath": "qortal-backup-test", + "bootstrap": false, "wipeUnconfirmedOnStart": false, "testNtpOffset": 0, "minPeers": 0