diff --git a/pom.xml b/pom.xml index 5634925..920fee8 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,11 @@ junit-jupiter-engine test + + org.junit.jupiter + junit-jupiter-params + test + diff --git a/src/main/java/io/mkr/archivefs/archive/Archive.java b/src/main/java/io/mkr/archivefs/archive/Archive.java index 7928c0e..277a0ce 100644 --- a/src/main/java/io/mkr/archivefs/archive/Archive.java +++ b/src/main/java/io/mkr/archivefs/archive/Archive.java @@ -1,5 +1,6 @@ package io.mkr.archivefs.archive; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.util.Collection; @@ -8,7 +9,7 @@ * An archive is package of directories and files and their associated metadata. Classes implementing this interface * provide non-sequential ("random") access to the contents of an archive. */ -public interface Archive { +public interface Archive extends Closeable { /** * Returns metadata items for all entries in the archive diff --git a/src/main/java/io/mkr/archivefs/archive/ArchiveFactory.java b/src/main/java/io/mkr/archivefs/archive/ArchiveFactory.java index 94b683c..11ee89c 100644 --- a/src/main/java/io/mkr/archivefs/archive/ArchiveFactory.java +++ b/src/main/java/io/mkr/archivefs/archive/ArchiveFactory.java @@ -14,7 +14,6 @@ import org.apache.commons.compress.archivers.jar.JarArchiveEntry; import org.apache.commons.compress.archivers.jar.JarArchiveInputStream; import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; -import org.apache.commons.compress.archivers.sevenz.SevenZFile; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; @@ -66,8 +65,9 @@ public static boolean supportedFromStream(String mime) { * @param mimeType The mime type to create the archive for * @param file the {@link File} * @return the {@link Archive} or null if the type is not supported + * @throws IOException If creating the archive fails */ - public Archive archiveForFile(String mimeType, final File file) { + public Archive archiveForFile(String mimeType, final File file) throws IOException { switch (mimeType) { case MIME_ZIP: case MIME_JAR: @@ -75,17 +75,7 @@ public Archive archiveForFile(String mimeType, final File file) { case MIME_RAR: return new RarFileArchive(file); case MIME_7Z: - // The 7Z file implementation does not allow random-access, map it to an in-memory archive - return archiveForInputStream(mimeType, new Creator() { - @Override - public CCSeven7ArchiveInputStream create() { - try { - return new CCSeven7ArchiveInputStream(new SevenZFile(file)); - } catch (IOException e) { - throw new IllegalArgumentException("Error accessing file", e); - } - } - }); + return new CCSevenZArchive(file); case MIME_LZH1: case MIME_LZH2: case MIME_LZH3: diff --git a/src/main/java/io/mkr/archivefs/archive/MemoryMappedArchive.java b/src/main/java/io/mkr/archivefs/archive/MemoryMappedArchive.java index 726b3e8..394bdb2 100644 --- a/src/main/java/io/mkr/archivefs/archive/MemoryMappedArchive.java +++ b/src/main/java/io/mkr/archivefs/archive/MemoryMappedArchive.java @@ -85,4 +85,8 @@ public Exception getReadException() { return readException; } + @Override + public void close() throws IOException { + + } } diff --git a/src/main/java/io/mkr/archivefs/archive/StreamingArchive.java b/src/main/java/io/mkr/archivefs/archive/StreamingArchive.java index 7558e5d..93b75a8 100644 --- a/src/main/java/io/mkr/archivefs/archive/StreamingArchive.java +++ b/src/main/java/io/mkr/archivefs/archive/StreamingArchive.java @@ -68,4 +68,8 @@ public InputStream contentFor(ArchiveItem entry) throws IOException { return null; } + @Override + public void close() throws IOException { + + } } diff --git a/src/main/java/io/mkr/archivefs/archive/cc/CCSevenZArchive.java b/src/main/java/io/mkr/archivefs/archive/cc/CCSevenZArchive.java new file mode 100644 index 0000000..0e2b588 --- /dev/null +++ b/src/main/java/io/mkr/archivefs/archive/cc/CCSevenZArchive.java @@ -0,0 +1,85 @@ +package io.mkr.archivefs.archive.cc; + +import io.mkr.archivefs.archive.Archive; +import io.mkr.archivefs.archive.ArchiveItem; +import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; +import org.apache.commons.compress.archivers.sevenz.SevenZFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Wrapper for 7z archives. Commons-compress does support random acces on 7z archives since 1.20. + */ +public class CCSevenZArchive implements Archive { + + private SevenZFile sevenZFile; + + private boolean closed; + + /** + * Constructor for a new 7z archive. + * @param file File which is a 7z archive. + */ + public CCSevenZArchive(final File file) throws IOException { + this(file.toPath()); + } + + /** + * Constructor for a new 7z archive. + * @param file Path to file which is a 7z archive. + */ + public CCSevenZArchive(final Path file) throws IOException { + this.sevenZFile = new SevenZFile(Files.newByteChannel(file)); + } + + /** + * {@inheritDoc} + */ + @Override + public Collection getItems() throws IOException { + checkArchiveClosed(); + final List entriesMeta = new ArrayList<>(); + for (final SevenZArchiveEntry entry : sevenZFile.getEntries()) { + entriesMeta.add(new CCArchiveEntryItem(entry)); + } + return entriesMeta; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream contentFor(ArchiveItem entry) throws IOException { + checkArchiveClosed(); + return sevenZFile.getInputStream((SevenZArchiveEntry) ((CCArchiveEntryItem) entry).getDelegate()); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + sevenZFile.close(); + sevenZFile = null; + } + } + + /** + * Checks if the archive is already closed. An {@link IOException} is thrown if the archive is already closed. + * @throws IOException if the archive is already closed + */ + private void checkArchiveClosed() throws IOException { + if (closed) { + throw new IOException("Archive is already closed!"); + } + } +} diff --git a/src/test/java/io/mkr/archivefs/archive/cc/FlatArchiveTest.java b/src/test/java/io/mkr/archivefs/archive/cc/FlatArchiveTest.java new file mode 100644 index 0000000..1c5dd44 --- /dev/null +++ b/src/test/java/io/mkr/archivefs/archive/cc/FlatArchiveTest.java @@ -0,0 +1,73 @@ +package io.mkr.archivefs.archive.cc; + +import io.mkr.archivefs.archive.util.ArchiveConstants; +import io.mkr.archivefs.archive.util.ArchiveCreator; +import io.mkr.archivefs.archive.util.SevenZArchiveCreator; +import io.mkr.archivefs.fs.ArchiveFilesystemProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FlatArchiveTest { + + private static final ArchiveFilesystemProvider afsp = new ArchiveFilesystemProvider(); + + @TempDir + Path tempDir; + + private Path testFile; + + private static Stream archiveCreators() { + return Stream.of( + new SevenZArchiveCreator() + ); + } + + @BeforeEach + void createTempDirectory() { + testFile = tempDir.resolve("testfile"); + } + + @ParameterizedTest + @MethodSource("archiveCreators") + void readContentFromFlatArchive(final ArchiveCreator archiveCreator) throws IOException { + archiveCreator.createFlatArchive(testFile); + assertTrue(Files.exists(testFile)); + + try (final FileSystem afs = afsp.newFileSystem(testFile, new HashMap<>())) { + final Path testFileInArchive = afs.getPath("/" + ArchiveConstants.TEST_FILE_NAME); + assertAll(() -> assertTrue(Files.exists(testFileInArchive)), + () -> assertArrayEquals(ArchiveConstants.TEST_FILE_CONTENT.getBytes(StandardCharsets.UTF_8), + Files.readAllBytes(testFileInArchive))); + } + } + + @ParameterizedTest + @MethodSource("archiveCreators") + void accessFileSystemAfterClose(final ArchiveCreator archiveCreator) throws IOException { + archiveCreator.createFlatArchive(testFile); + assertTrue(Files.exists(testFile)); + + final FileSystem afs = afsp.newFileSystem(testFile, new HashMap<>()); + final Path testFileInArchive = afs.getPath("/" + ArchiveConstants.TEST_FILE_NAME); + afs.close(); + + assertNotNull(afs.getPath("/" + ArchiveConstants.TEST_FILE_NAME)); + assertThrows(IOException.class, () -> Files.readAllBytes(testFileInArchive)); + } +} diff --git a/src/test/java/io/mkr/archivefs/archive/util/ArchiveConstants.java b/src/test/java/io/mkr/archivefs/archive/util/ArchiveConstants.java new file mode 100644 index 0000000..c42aa3f --- /dev/null +++ b/src/test/java/io/mkr/archivefs/archive/util/ArchiveConstants.java @@ -0,0 +1,8 @@ +package io.mkr.archivefs.archive.util; + +public class ArchiveConstants { + + public static final String TEST_FILE_CONTENT = "Test"; + + public static final String TEST_FILE_NAME = "test.txt"; +} diff --git a/src/test/java/io/mkr/archivefs/archive/util/ArchiveCreationHelper.java b/src/test/java/io/mkr/archivefs/archive/util/ArchiveCreationHelper.java new file mode 100644 index 0000000..cae6477 --- /dev/null +++ b/src/test/java/io/mkr/archivefs/archive/util/ArchiveCreationHelper.java @@ -0,0 +1,31 @@ +package io.mkr.archivefs.archive.util; + +import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; +import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ArchiveCreationHelper { + + /** + * Create a 7z file. + * @param path Path where the 7z file should be created + * @throws IOException If creating the 7z file fails + */ + public static void createSevenZArchive(final Path path) throws IOException { + try (final SevenZOutputFile out = new SevenZOutputFile(path.toFile())) { + final File inputFile = Files.createTempFile("SevenZBaseTemp", "").toFile(); + + SevenZArchiveEntry entry = out.createArchiveEntry(inputFile, ArchiveConstants.TEST_FILE_NAME); + out.putArchiveEntry(entry); + out.write(ArchiveConstants.TEST_FILE_CONTENT.getBytes(StandardCharsets.UTF_8)); + out.closeArchiveEntry(); + + Files.deleteIfExists(inputFile.toPath()); + } + } +} diff --git a/src/test/java/io/mkr/archivefs/archive/util/ArchiveCreator.java b/src/test/java/io/mkr/archivefs/archive/util/ArchiveCreator.java new file mode 100644 index 0000000..5315baa --- /dev/null +++ b/src/test/java/io/mkr/archivefs/archive/util/ArchiveCreator.java @@ -0,0 +1,10 @@ +package io.mkr.archivefs.archive.util; + +import java.io.IOException; +import java.nio.file.Path; + +public interface ArchiveCreator { + + void createFlatArchive(Path path) throws IOException; + +} diff --git a/src/test/java/io/mkr/archivefs/archive/util/SevenZArchiveCreator.java b/src/test/java/io/mkr/archivefs/archive/util/SevenZArchiveCreator.java new file mode 100644 index 0000000..2031e3c --- /dev/null +++ b/src/test/java/io/mkr/archivefs/archive/util/SevenZArchiveCreator.java @@ -0,0 +1,12 @@ +package io.mkr.archivefs.archive.util; + +import java.io.IOException; +import java.nio.file.Path; + +public class SevenZArchiveCreator implements ArchiveCreator { + + @Override + public void createFlatArchive(final Path path) throws IOException { + ArchiveCreationHelper.createSevenZArchive(path); + } +}