diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataWriter.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataWriter.java index 8b1d00c3..2cddfc0f 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataWriter.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataWriter.java @@ -1,5 +1,7 @@ package org.qortal.arbitrary; +import com.j256.simplemagic.ContentInfo; +import com.j256.simplemagic.ContentInfoUtil; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,6 +25,8 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import java.io.File; import java.io.IOException; +import java.net.FileNameMap; +import java.net.URLConnection; import java.nio.file.*; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -48,6 +52,7 @@ public class ArbitraryDataWriter { private final List tags; private final Category category; private List files; + private String mimeType; private int chunkSize = ArbitraryDataFile.CHUNK_SIZE; @@ -79,6 +84,7 @@ public class ArbitraryDataWriter { this.tags = ArbitraryDataTransactionMetadata.limitTags(tags); this.category = category; this.files = new ArrayList<>(); // Populated in buildFileList() + this.mimeType = null; // Populated in buildFileList() } public void save() throws IOException, DataException, InterruptedException, MissingDataException { @@ -144,20 +150,41 @@ public class ArbitraryDataWriter { } private void buildFileList() throws IOException { - // Single file resources consist of a single element in the file list + // Check if the path already points to a single file boolean isSingleFile = this.filePath.toFile().isFile(); + Path singleFilePath = null; if (isSingleFile) { this.files.add(this.filePath.getFileName().toString()); - return; + singleFilePath = this.filePath; + } + else { + // Multi file resources (or a single file in a directory) require a walk through the directory tree + try (Stream stream = Files.walk(this.filePath)) { + this.files = stream + .filter(Files::isRegularFile) + .map(p -> this.filePath.relativize(p).toString()) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + + if (this.files.size() == 1) { + singleFilePath = Paths.get(this.filePath.toString(), this.files.get(0)); + } + } } - // Multi file resources require a walk through the directory tree - try (Stream stream = Files.walk(this.filePath)) { - this.files = stream - .filter(Files::isRegularFile) - .map(p -> this.filePath.relativize(p).toString()) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList()); + if (singleFilePath != null) { + // Single file resource, so try and determine the MIME type + ContentInfoUtil util = new ContentInfoUtil(); + ContentInfo info = util.findMatch(singleFilePath.toFile()); + if (info != null) { + // Attempt to extract MIME type from file contents + this.mimeType = info.getMimeType(); + } + else { + // Fall back to using the filename + FileNameMap fileNameMap = URLConnection.getFileNameMap(); + this.mimeType = fileNameMap.getContentTypeFor(singleFilePath.toFile().getName()); + } } } @@ -304,6 +331,7 @@ public class ArbitraryDataWriter { metadata.setCategory(this.category); metadata.setChunks(this.arbitraryDataFile.chunkHashList()); metadata.setFiles(this.files); + metadata.setMimeType(this.mimeType); metadata.write(); // Create an ArbitraryDataFile from the JSON file (we don't have a signature yet) diff --git a/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java b/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java index 33da343c..447e9901 100644 --- a/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java +++ b/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java @@ -20,6 +20,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { private List tags; private Category category; private List files; + private String mimeType; private static int MAX_TITLE_LENGTH = 80; private static int MAX_DESCRIPTION_LENGTH = 500; @@ -92,6 +93,10 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { } this.files = filesList; } + + if (metadata.has("mimeType")) { + this.mimeType = metadata.getString("mimeType"); + } } @Override @@ -134,6 +139,10 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { } outer.put("files", files); + if (this.mimeType != null && !this.mimeType.isEmpty()) { + outer.put("mimeType", this.mimeType); + } + this.jsonString = outer.toString(2); LOGGER.trace("Transaction metadata: {}", this.jsonString); } @@ -187,6 +196,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { return this.files; } + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getMimeType() { + return this.mimeType; + } + public boolean containsChunk(byte[] chunk) { for (byte[] c : this.chunks) { if (Arrays.equals(c, chunk)) { diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java index 497e214f..a6aa6e26 100644 --- a/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java +++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java @@ -16,16 +16,18 @@ public class ArbitraryResourceMetadata { private Category category; private String categoryName; private List files; + private String mimeType; public ArbitraryResourceMetadata() { } - public ArbitraryResourceMetadata(String title, String description, List tags, Category category, List files) { + public ArbitraryResourceMetadata(String title, String description, List tags, Category category, List files, String mimeType) { this.title = title; this.description = description; this.tags = tags; this.category = category; this.files = files; + this.mimeType = mimeType; if (category != null) { this.categoryName = category.getName(); @@ -40,6 +42,7 @@ public class ArbitraryResourceMetadata { String description = transactionMetadata.getDescription(); List tags = transactionMetadata.getTags(); Category category = transactionMetadata.getCategory(); + String mimeType = transactionMetadata.getMimeType(); // We don't always want to include the file list as it can be too verbose List files = null; @@ -47,11 +50,11 @@ public class ArbitraryResourceMetadata { files = transactionMetadata.getFiles(); } - if (title == null && description == null && tags == null && category == null && files == null) { + if (title == null && description == null && tags == null && category == null && files == null && mimeType == null) { return null; } - return new ArbitraryResourceMetadata(title, description, tags, category, files); + return new ArbitraryResourceMetadata(title, description, tags, category, files, mimeType); } public List getFiles() { diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java index bf4f0a70..922f6e1d 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java @@ -118,6 +118,7 @@ public class ArbitraryTransactionMetadataTests extends Common { assertEquals(description, arbitraryDataFile.getMetadata().getDescription()); assertEquals(tags, arbitraryDataFile.getMetadata().getTags()); assertEquals(category, arbitraryDataFile.getMetadata().getCategory()); + assertEquals("text/plain", arbitraryDataFile.getMetadata().getMimeType()); // Now build the latest data state for this name ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ResourceIdType.NAME, service, identifier); @@ -168,6 +169,7 @@ public class ArbitraryTransactionMetadataTests extends Common { assertEquals(description, arbitraryDataFile.getMetadata().getDescription()); assertEquals(tags, arbitraryDataFile.getMetadata().getTags()); assertEquals(category, arbitraryDataFile.getMetadata().getCategory()); + assertEquals("text/plain", arbitraryDataFile.getMetadata().getMimeType()); // Delete the file, to simulate that it hasn't been fetched from the network yet arbitraryDataFile.delete(); @@ -230,6 +232,7 @@ public class ArbitraryTransactionMetadataTests extends Common { assertEquals(description, arbitraryDataFile.getMetadata().getDescription()); assertEquals(tags, arbitraryDataFile.getMetadata().getTags()); assertEquals(category, arbitraryDataFile.getMetadata().getCategory()); + assertEquals("text/plain", arbitraryDataFile.getMetadata().getMimeType()); // Now build the latest data state for this name ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ResourceIdType.NAME, service, identifier); @@ -318,9 +321,11 @@ public class ArbitraryTransactionMetadataTests extends Common { assertTrue(resourceMetadata.getFiles().contains("file.txt")); // Ensure it's not returned when specified to be excluded - // The entire object will be null because there is no metadata ArbitraryResourceMetadata resourceMetadataSimple = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), false); - assertNull(resourceMetadataSimple); + assertNull(resourceMetadataSimple.getFiles()); + + // Single-file resources should have a MIME type + assertEquals("text/plain", arbitraryDataFile.getMetadata().getMimeType()); } } @@ -369,6 +374,9 @@ public class ArbitraryTransactionMetadataTests extends Common { // The entire object will be null because there is no metadata ArbitraryResourceMetadata resourceMetadataSimple = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), false); assertNull(resourceMetadataSimple); + + // Multi-file resources won't have a MIME type + assertEquals(null, arbitraryDataFile.getMetadata().getMimeType()); } }