forked from Qortal/qortal
Started abstracting the file processing code away from the API handlers, and making it more modular.
This commit is contained in:
parent
e259a09b89
commit
c790ea07dd
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
DataFileReader dataFileReader = new DataFileReader(resourceId, resourceIdType);
|
||||
dataFileReader.setSecret58(secret58); // Optional, used for loading encrypted file hashes only
|
||||
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) {
|
||||
dataFileReader.load(false);
|
||||
} catch (Exception e) {
|
||||
return this.get404Response();
|
||||
}
|
||||
java.nio.file.Path path = dataFileReader.getFilePath();
|
||||
if (path == null) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
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<String> indexFiles() {
|
||||
List<String> indexFiles = new ArrayList<>();
|
||||
indexFiles.add("index.html");
|
||||
|
@ -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);
|
||||
|
213
src/main/java/org/qortal/storage/DataFileReader.java
Normal file
213
src/main/java/org/qortal/storage/DataFileReader.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
190
src/main/java/org/qortal/storage/DataFileWriter.java
Normal file
190
src/main/java/org/qortal/storage/DataFileWriter.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user