diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index 3b6c2b6d..12deed16 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -9,9 +9,10 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -29,6 +30,7 @@ import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.PaymentData; import org.qortal.data.transaction.ArbitraryTransactionData; +import org.qortal.data.transaction.ArbitraryTransactionData.*; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.ArbitraryTransactionData.DataType; @@ -45,6 +47,7 @@ import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; import org.qortal.storage.DataFile; import org.qortal.storage.DataFileChunk; +import org.qortal.storage.DataFileWriter; import org.qortal.transaction.ArbitraryTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.TransactionType; @@ -265,18 +268,20 @@ public class ArbitraryResource { String name = null; byte[] secret = null; - ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.PUT; - ArbitraryTransactionData.Service service = ArbitraryTransactionData.Service.ARBITRARY_DATA; - ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.NONE; + Method method = Method.PUT; + Service service = Service.ARBITRARY_DATA; + Compression compression = Compression.NONE; - // Check if a file or directory has been supplied - File file = new File(path); - if (!file.isFile()) { - LOGGER.info("Not a file: {}", path); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + DataFileWriter dataFileWriter = new DataFileWriter(Paths.get(path), method, compression); + try { + dataFileWriter.save(); + } catch (IOException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); + } catch (IllegalStateException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); } - DataFile dataFile = DataFile.fromPath(path); + DataFile dataFile = dataFileWriter.getDataFile(); if (dataFile == null) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); } diff --git a/src/main/java/org/qortal/api/resource/WebsiteResource.java b/src/main/java/org/qortal/api/resource/WebsiteResource.java index 4761228b..c4773333 100644 --- a/src/main/java/org/qortal/api/resource/WebsiteResource.java +++ b/src/main/java/org/qortal/api/resource/WebsiteResource.java @@ -1,10 +1,5 @@ package org.qortal.api.resource; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -13,12 +8,9 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import java.io.*; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Paths; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -36,10 +28,10 @@ import org.qortal.api.ApiExceptionFactory; import org.qortal.api.HTMLParser; import org.qortal.api.Security; import org.qortal.block.BlockChain; -import org.qortal.crypto.AES; import org.qortal.crypto.Crypto; import org.qortal.data.PaymentData; import org.qortal.data.transaction.ArbitraryTransactionData; +import org.qortal.data.transaction.ArbitraryTransactionData.*; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.group.Group; import org.qortal.repository.DataException; @@ -47,14 +39,15 @@ import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; import org.qortal.storage.DataFile; +import org.qortal.storage.DataFile.*; +import org.qortal.storage.DataFileReader; +import org.qortal.storage.DataFileWriter; import org.qortal.transaction.ArbitraryTransaction; import org.qortal.transaction.Transaction; import org.qortal.transform.TransformationException; -import org.qortal.transform.Transformer; import org.qortal.transform.transaction.ArbitraryTransactionTransformer; import org.qortal.utils.Base58; import org.qortal.utils.NTP; -import org.qortal.utils.ZipUtils; @Path("/site") @@ -63,11 +56,6 @@ public class WebsiteResource { private static final Logger LOGGER = LogManager.getLogger(WebsiteResource.class); - public enum ResourceIdType { - SIGNATURE, - FILE_HASH - }; - @Context HttpServletRequest request; @Context HttpServletResponse response; @Context ServletContext context; @@ -115,7 +103,16 @@ public class WebsiteResource { ArbitraryTransactionData.Service service = ArbitraryTransactionData.Service.WEBSITE; ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP; - DataFile dataFile = this.hostWebsite(path); + DataFileWriter dataFileWriter = new DataFileWriter(Paths.get(path), method, compression); + try { + dataFileWriter.save(); + } catch (IOException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); + } catch (IllegalStateException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + + DataFile dataFile = dataFileWriter.getDataFile(); if (dataFile == null) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); } @@ -200,7 +197,19 @@ public class WebsiteResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); } - DataFile dataFile = this.hostWebsite(directoryPath); + Method method = Method.PUT; + Compression compression = Compression.ZIP; + + DataFileWriter dataFileWriter = new DataFileWriter(Paths.get(directoryPath), method, compression); + try { + dataFileWriter.save(); + } catch (IOException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); + } catch (IllegalStateException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + } + + DataFile dataFile = dataFileWriter.getDataFile(); if (dataFile != null) { String digest58 = dataFile.digest58(); if (digest58 != null) { @@ -210,77 +219,6 @@ public class WebsiteResource { return "Unable to generate preview URL"; } - private DataFile hostWebsite(String directoryPath) { - - // Check if a file or directory has been supplied - File file = new File(directoryPath); - if (!file.isDirectory()) { - LOGGER.info("Not a directory: {}", directoryPath); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - } - - // Ensure temp folder exists - java.nio.file.Path tempDir = null; - try { - tempDir = Files.createTempDirectory("qortal-zip"); - } catch (IOException e) { - LOGGER.error("Unable to create temp directory"); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE); - } - - // Firstly zip up the directory - String zipOutputFilePath = tempDir.toString() + File.separator + "zipped.zip"; - try { - ZipUtils.zip(directoryPath, zipOutputFilePath, "data"); - } catch (IOException e) { - LOGGER.info("Unable to zip directory", e); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - - // Next, encrypt the file with AES - String encryptedFilePath = tempDir.toString() + File.separator + "zipped_encrypted.zip"; - SecretKey aesKey; - try { - aesKey = AES.generateKey(256); - AES.encryptFile("AES", aesKey, zipOutputFilePath, encryptedFilePath); - Files.delete(Paths.get(zipOutputFilePath)); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException - | BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR); - } - - try { - DataFile dataFile = DataFile.fromPath(encryptedFilePath); - dataFile.setSecret(aesKey.getEncoded()); - DataFile.ValidationResult validationResult = dataFile.isValid(); - if (validationResult != DataFile.ValidationResult.OK) { - LOGGER.error("Invalid file: {}", validationResult); - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - } - LOGGER.info("Whole file digest: {}", dataFile.digest58()); - - int chunkCount = dataFile.split(DataFile.CHUNK_SIZE); - if (chunkCount > 0) { - LOGGER.info(String.format("Successfully split into %d chunk%s:", chunkCount, (chunkCount == 1 ? "" : "s"))); - LOGGER.info("{}", dataFile.printChunks()); - return dataFile; - } - - return null; - } - finally { - // Clean up - File zippedFile = new File(zipOutputFilePath); - if (zippedFile.exists()) { - zippedFile.delete(); - } - File encryptedFile = new File(encryptedFilePath); - if (encryptedFile.exists()) { - encryptedFile.delete(); - } - } - } - @GET @Path("{signature}") public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature) { @@ -331,94 +269,21 @@ public class WebsiteResource { inPath = File.separator + inPath; } - String tempDirectory = System.getProperty("java.io.tmpdir"); - String destPath = tempDirectory + File.separator + "qortal-sites" + File.separator + resourceId; - String unencryptedPath = destPath + File.separator + "zipped.zip"; - String unzippedPath = destPath + File.separator + "data"; - - if (!Files.exists(Paths.get(unzippedPath))) { - - // Load the full transaction data so we can access the file hashes - try (final Repository repository = RepositoryManager.getRepository()) { - DataFile dataFile = null; - byte[] digest = null; - byte[] secret = null; - - if (resourceIdType == ResourceIdType.SIGNATURE) { - ArbitraryTransactionData transactionData = (ArbitraryTransactionData) repository.getTransactionRepository().fromSignature(Base58.decode(resourceId)); - if (!(transactionData instanceof ArbitraryTransactionData)) { - return this.get404Response(); - } - - // Load hashes - digest = transactionData.getData(); - byte[] chunkHashes = transactionData.getChunkHashes(); - - // Load secret - secret = transactionData.getSecret(); - - // Load data file(s) - dataFile = DataFile.fromHash(digest); - if (!dataFile.exists()) { - if (!dataFile.allChunksExist(chunkHashes)) { - // TODO: fetch them? - return this.get404Response(); - } - // We have all the chunks but not the complete file, so join them - dataFile.addChunkHashes(chunkHashes); - dataFile.join(); - } - - - } - else if (resourceIdType == ResourceIdType.FILE_HASH) { - dataFile = DataFile.fromHash58(resourceId); - digest = Base58.decode(resourceId); - secret = secret58 != null ? Base58.decode(secret58) : null; - } - - // If the complete file still doesn't exist then something went wrong - if (!dataFile.exists()) { - return this.get404Response(); - } - - if (!Arrays.equals(dataFile.digest(), digest)) { - LOGGER.info("Unable to validate complete file hash"); - return this.get404Response(); - } - - // Decrypt if we have the secret key. - if (secret != null && secret.length == Transformer.AES256_LENGTH) { - try { - SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES"); - AES.decryptFile("AES", aesKey, dataFile.getFilePath(), unencryptedPath); - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException - | BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) { - return this.get404Response(); - } - } - else { - // Assume it is unencrypted. We may block this. - unencryptedPath = dataFile.getFilePath(); - } - - // Unzip - try { - // TODO: compression types - //if (transactionData.getCompression() == ArbitraryTransactionData.Compression.ZIP) { - ZipUtils.unzip(unencryptedPath, destPath); - //} - } catch (IOException e) { - LOGGER.info("Unable to unzip file"); - } - - } catch (DataException e) { - return this.get500Response(); - } + DataFileReader dataFileReader = new DataFileReader(resourceId, resourceIdType); + dataFileReader.setSecret58(secret58); // Optional, used for loading encrypted file hashes only + try { + dataFileReader.load(false); + } catch (Exception e) { + return this.get404Response(); } + java.nio.file.Path path = dataFileReader.getFilePath(); + if (path == null) { + return this.get404Response(); + } + String unzippedPath = path.toString(); try { - String filename = this.getFilename(unzippedPath, inPath); + String filename = this.getFilename(unzippedPath.toString(), inPath); String filePath = unzippedPath + File.separator + filename; if (HTMLParser.isHtmlFile(filename)) { @@ -445,7 +310,7 @@ public class WebsiteResource { inputStream.close(); } return response; - } catch (FileNotFoundException e) { + } catch (FileNotFoundException | NoSuchFileException e) { LOGGER.info("File not found at path: {}", unzippedPath); if (inPath.equals("/")) { // Delete the unzipped folder if no index file was found @@ -455,7 +320,6 @@ public class WebsiteResource { LOGGER.info("Unable to delete directory: {}", unzippedPath, e); } } - } catch (IOException e) { LOGGER.info("Unable to serve file at path: {}", inPath, e); } @@ -490,19 +354,6 @@ public class WebsiteResource { return response; } - private HttpServletResponse get500Response() { - try { - String responseString = "500: Internal Server Error"; - byte[] responseData = responseString.getBytes(); - response.setStatus(500); - response.setContentLength(responseData.length); - response.getOutputStream().write(responseData); - } catch (IOException e) { - LOGGER.info("Error writing 500 response"); - } - return response; - } - private List indexFiles() { List indexFiles = new ArrayList<>(); indexFiles.add("index.html"); diff --git a/src/main/java/org/qortal/storage/DataFile.java b/src/main/java/org/qortal/storage/DataFile.java index a6a17385..568674d6 100644 --- a/src/main/java/org/qortal/storage/DataFile.java +++ b/src/main/java/org/qortal/storage/DataFile.java @@ -41,6 +41,12 @@ public class DataFile { } } + // Resource ID types + public enum ResourceIdType { + SIGNATURE, + FILE_HASH + }; + private static final Logger LOGGER = LogManager.getLogger(DataFile.class); public static final long MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MiB @@ -69,7 +75,7 @@ public class DataFile { } this.hash58 = Base58.encode(Crypto.digest(fileContent)); - LOGGER.debug(String.format("File digest: %s, size: %d bytes", this.hash58, fileContent.length)); + LOGGER.trace(String.format("File digest: %s, size: %d bytes", this.hash58, fileContent.length)); String outputFilePath = getOutputFilePath(this.hash58, true); File outputFile = new File(outputFilePath); diff --git a/src/main/java/org/qortal/storage/DataFileReader.java b/src/main/java/org/qortal/storage/DataFileReader.java new file mode 100644 index 00000000..a344c997 --- /dev/null +++ b/src/main/java/org/qortal/storage/DataFileReader.java @@ -0,0 +1,213 @@ +package org.qortal.storage; + +import org.qortal.crypto.AES; +import org.qortal.data.transaction.ArbitraryTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.storage.DataFile.*; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; +import org.qortal.utils.ZipUtils; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +public class DataFileReader { + + private String resourceId; + private ResourceIdType resourceIdType; + private String secret58; + private Path filePath; + private DataFile dataFile; + + // Intermediate paths + private Path workingPath; + private Path uncompressedPath; + private Path unencryptedPath; + + public DataFileReader(String resourceId, ResourceIdType resourceIdType) { + this.resourceId = resourceId; + this.resourceIdType = resourceIdType; + } + + public void load(boolean overwrite) throws IllegalStateException, IOException, DataException { + + try { + this.preExecute(); + + // Do nothing if files already exist and overwrite is set to false + if (Files.exists(this.uncompressedPath) && !overwrite) { + this.filePath = this.uncompressedPath; + return; + } + + this.fetch(); + this.decrypt(); + this.uncompress(); + + } finally { + this.postExecute(); + } + } + + private void preExecute() { + this.createWorkingDirectory(); + // Initialize unzipped path as it's used in a few places + this.uncompressedPath = Paths.get(this.workingPath.toString() + File.separator + "data"); + } + + private void postExecute() throws IOException { + this.cleanupFilesystem(); + } + + private void createWorkingDirectory() { + // Use the system tmpdir as our base, as it is deterministic + String baseDir = System.getProperty("java.io.tmpdir"); + Path tempDir = Paths.get(baseDir + File.separator + "qortal" + File.separator + this.resourceId); + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + throw new IllegalStateException("Unable to create temp directory"); + } + this.workingPath = tempDir; + } + + private void fetch() throws IllegalStateException, IOException, DataException { + switch (resourceIdType) { + + case SIGNATURE: + this.fetchFromSignature(); + break; + + case FILE_HASH: + this.fetchFromFileHash(); + break; + + default: + throw new IllegalStateException(String.format("Unknown resource ID type specified: %s", resourceIdType.toString())); + } + } + + private void fetchFromSignature() throws IllegalStateException, IOException, DataException { + + // Load the full transaction data so we can access the file hashes + ArbitraryTransactionData transactionData; + try (final Repository repository = RepositoryManager.getRepository()) { + transactionData = (ArbitraryTransactionData) repository.getTransactionRepository().fromSignature(Base58.decode(resourceId)); + } + if (!(transactionData instanceof ArbitraryTransactionData)) { + throw new IllegalStateException(String.format("Transaction data not found for signature %s", this.resourceId)); + } + + // Load hashes + byte[] digest = transactionData.getData(); + byte[] chunkHashes = transactionData.getChunkHashes(); + + // Load secret + byte[] secret = transactionData.getSecret(); + if (secret != null) { + this.secret58 = Base58.encode(secret); + } + + // Load data file(s) + this.dataFile = DataFile.fromHash(digest); + if (!this.dataFile.exists()) { + if (!this.dataFile.allChunksExist(chunkHashes)) { + // TODO: fetch them? + throw new IllegalStateException(String.format("Missing chunks for file {}", dataFile)); + } + // We have all the chunks but not the complete file, so join them + this.dataFile.addChunkHashes(chunkHashes); + this.dataFile.join(); + } + + // If the complete file still doesn't exist then something went wrong + if (!this.dataFile.exists()) { + throw new IOException(String.format("File doesn't exist: %s", dataFile)); + } + // Ensure the complete hash matches the joined chunks + if (!Arrays.equals(dataFile.digest(), digest)) { + throw new IllegalStateException("Unable to validate complete file hash"); + } + // Set filePath to the location of the DataFile + this.filePath = Paths.get(dataFile.getFilePath()); + } + + private void fetchFromFileHash() { + // Load data file directly from the hash + this.dataFile = DataFile.fromHash58(resourceId); + // Set filePath to the location of the DataFile + this.filePath = Paths.get(dataFile.getFilePath()); + } + + private void decrypt() { + // Decrypt if we have the secret key. + byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null; + if (secret != null && secret.length == Transformer.AES256_LENGTH) { + try { + this.unencryptedPath = Paths.get(this.workingPath.toString() + File.separator + "zipped.zip"); + SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES"); + AES.decryptFile("AES", aesKey, this.filePath.toString(), this.unencryptedPath.toString()); + + // Replace filePath pointer with the encrypted file path + // Don't delete the original DataFile, as this is handled in the cleanup phase + this.filePath = this.unencryptedPath; + + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException + | BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) { + throw new IllegalStateException(String.format("Unable to decrypt file %s: %s", dataFile, e.getMessage())); + } + } else { + // Assume it is unencrypted. We may block this in the future. + this.filePath = Paths.get(this.dataFile.getFilePath()); + } + } + + private void uncompress() throws IOException { + try { + // TODO: compression types + //if (transactionData.getCompression() == ArbitraryTransactionData.Compression.ZIP) { + ZipUtils.unzip(this.filePath.toString(), this.uncompressedPath.getParent().toString()); + //} + } catch (IOException e) { + throw new IllegalStateException(String.format("Unable to unzip file: %s", e.getMessage())); + } + + // Replace filePath pointer with the uncompressed file path + Files.delete(this.filePath); + this.filePath = this.uncompressedPath; + } + + private void cleanupFilesystem() throws IOException { + // Clean up + if (this.uncompressedPath != null) { + File unzippedFile = new File(this.uncompressedPath.toString()); + if (unzippedFile.exists()) { + unzippedFile.delete(); + } + } + } + + + public void setSecret58(String secret58) { + this.secret58 = secret58; + } + + public Path getFilePath() { + return this.filePath; + } + +} diff --git a/src/main/java/org/qortal/storage/DataFileWriter.java b/src/main/java/org/qortal/storage/DataFileWriter.java new file mode 100644 index 00000000..b0ad0a75 --- /dev/null +++ b/src/main/java/org/qortal/storage/DataFileWriter.java @@ -0,0 +1,190 @@ +package org.qortal.storage; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.data.transaction.ArbitraryTransactionData.*; +import org.qortal.crypto.AES; +import org.qortal.storage.DataFile.*; +import org.qortal.utils.ZipUtils; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public class DataFileWriter { + + private static final Logger LOGGER = LogManager.getLogger(DataFileWriter.class); + + private Path filePath; + private Method method; + private Compression compression; + + private SecretKey aesKey; + private DataFile dataFile; + + // Intermediate paths to cleanup + private Path workingPath; + private Path compressedPath; + private Path encryptedPath; + + public DataFileWriter(Path filePath, Method method, Compression compression) { + this.filePath = filePath; + this.method = method; + this.compression = compression; + } + + public void save() throws IllegalStateException, IOException { + try { + this.preExecute(); + this.compress(); + this.encrypt(); + this.split(); + this.validate(); + + } finally { + this.postExecute(); + } + } + + private void preExecute() { + // Enforce compression when uploading a directory + File file = new File(this.filePath.toString()); + if (file.isDirectory() && compression == Compression.NONE) { + throw new IllegalStateException("Unable to upload a directory without compression"); + } + + // Create temporary working directory + this.createWorkingDirectory(); + } + + private void postExecute() throws IOException { + this.cleanupFilesystem(); + } + + private void createWorkingDirectory() { + // Ensure temp folder exists + Path tempDir; + try { + tempDir = Files.createTempDirectory("qortal"); + } catch (IOException e) { + throw new IllegalStateException("Unable to create temp directory"); + } + this.workingPath = tempDir; + } + + private void compress() { + // Compress the data if requested + if (this.compression != Compression.NONE) { + this.compressedPath = Paths.get(this.workingPath.toString() + File.separator + "zipped.zip"); + try { + + if (this.compression == Compression.ZIP) { + ZipUtils.zip(this.filePath.toString(), this.compressedPath.toString(), "data"); + } + else { + throw new IllegalStateException(String.format("Unknown compression type specified: %s", compression.toString())); + } + // FUTURE: other compression types + + // Replace filePath pointer with the zipped file path + // Don't delete the original file/directory, since this may be outside of our directory scope + this.filePath = this.compressedPath; + + } catch (IOException e) { + throw new IllegalStateException("Unable to zip directory", e); + } + } + } + + private void encrypt() { + this.encryptedPath = Paths.get(this.workingPath.toString() + File.separator + "zipped_encrypted.zip"); + try { + // Encrypt the file with AES + this.aesKey = AES.generateKey(256); + AES.encryptFile("AES", this.aesKey, this.filePath.toString(), this.encryptedPath.toString()); + + // Replace filePath pointer with the encrypted file path + Files.delete(this.filePath); + this.filePath = this.encryptedPath; + + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException + | BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) { + throw new IllegalStateException(String.format("Unable to encrypt file %s: %s", this.filePath, e.getMessage())); + } + } + + private void validate() throws IOException { + this.dataFile = DataFile.fromPath(this.filePath.toString()); + if (this.dataFile == null) { + throw new IOException("No file available when validating"); + } + this.dataFile.setSecret(this.aesKey.getEncoded()); + + // Validate the file + ValidationResult validationResult = this.dataFile.isValid(); + if (validationResult != ValidationResult.OK) { + throw new IllegalStateException(String.format("File %s failed validation: %s", this.dataFile, validationResult)); + } + LOGGER.info("Whole file hash is valid: {}", this.dataFile.digest58()); + + // Validate each chunk + for (DataFileChunk chunk : this.dataFile.getChunks()) { + validationResult = chunk.isValid(); + if (validationResult != ValidationResult.OK) { + throw new IllegalStateException(String.format("Chunk %s failed validation: %s", chunk, validationResult)); + } + } + LOGGER.info("Chunk hashes are valid"); + + } + + private void split() throws IOException { + this.dataFile = DataFile.fromPath(this.filePath.toString()); + if (this.dataFile == null) { + throw new IOException("No file available when trying to split"); + } + + int chunkCount = this.dataFile.split(DataFile.CHUNK_SIZE); + if (chunkCount > 0) { + LOGGER.info(String.format("Successfully split into %d chunk%s", chunkCount, (chunkCount == 1 ? "" : "s"))); + } + else { + throw new IllegalStateException("Unable to split file into chunks"); + } + } + + private void cleanupFilesystem() throws IOException { + // Clean up + if (this.compressedPath != null) { + File zippedFile = new File(this.compressedPath.toString()); + if (zippedFile.exists()) { + zippedFile.delete(); + } + } + if (this.encryptedPath != null) { + File encryptedFile = new File(this.encryptedPath.toString()); + if (encryptedFile.exists()) { + encryptedFile.delete(); + } + } + if (this.workingPath != null) { + FileUtils.deleteDirectory(new File(this.workingPath.toString())); + } + } + + + public DataFile getDataFile() { + return this.dataFile; + } + +}