/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr.value.binary; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.modeshape.common.FixFor; import org.modeshape.common.statistic.Stopwatch; import org.modeshape.common.util.FileUtil; import org.modeshape.common.util.IoUtil; import org.modeshape.common.util.SecureHash; import org.modeshape.common.util.SecureHash.Algorithm; import org.modeshape.jcr.api.Binary; import org.modeshape.jcr.value.BinaryKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FileSystemBinaryStoreTest extends AbstractBinaryStoreTest { protected static final int MIN_BINARY_SIZE = 20; public static final String[] CONTENT = new String[] { "Lorem ipsum" + UUID.randomUUID().toString(), "Lorem ipsum pulvinar" + UUID.randomUUID().toString(), "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vel felis tellus, at pellentesque sem. " + UUID.randomUUID().toString(), "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tortor nunc, blandit in tempor ut, venenatis ac magna. Vestibulum gravida." + UUID.randomUUID().toString(), "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam volutpat tortor non eros bibendum vitae consectetur lacus eleifend. Mauris tempus." + UUID.randomUUID().toString(), "Morbi pulvinar volutpat sem id sagittis. Vestibulum ornare urna at massa iaculis vitae tincidunt nisi volutpat. Suspendisse auctor gravida viverra." + UUID.randomUUID().toString()}; public static final String[] CONTENT_HASHES; private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemBinaryStoreTest.class); static { String[] sha1s = new String[CONTENT.length]; int index = 0; for (String content : CONTENT) { sha1s[index++] = sha1(content); } CONTENT_HASHES = sha1s; } private static String sha1( String content ) { try { byte[] bytes = SecureHash.getHash(Algorithm.SHA_1, new ByteArrayInputStream(content.getBytes())); return SecureHash.asHexString(bytes); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } protected static File directory; protected static File trash; protected static FileSystemBinaryStore store; protected static boolean print = false; @Before public void beforeClass() { directory = new File("target/fsbs/"); FileUtil.delete(directory); directory.mkdirs(); trash = new File(directory, FileSystemBinaryStore.TRASH_DIRECTORY_NAME); store = new FileSystemBinaryStore(directory, trash); store.setMinimumBinarySizeInBytes(MIN_BINARY_SIZE); store.setMimeTypeDetector(DEFAULT_DETECTOR); print = false; } @After public void afterClass() { FileUtil.delete(directory); } @Override protected BinaryStore getBinaryStore() { return store; } @Override @Test( expected = BinaryStoreException.class ) public void shouldStoreZeroLengthBinary() throws BinaryStoreException, IOException { // the file system binary store will not store a 0 byte size content super.shouldStoreZeroLengthBinary(); } @Test public void shouldCreateTrashFilesForUnusedBinaries() throws Exception { Set<String> storedSha1s = new HashSet<String>(); for (int i = 0; i != CONTENT.length; ++i) { Binary binary = storeAndCheck(i); if (binary instanceof StoredBinaryValue) storedSha1s.add(binary.getHexHash()); } // Make sure there are files for all stored values ... assertThat(countStoredFiles(), is(storedSha1s.size())); assertThat(countTrashFiles(), is(0)); // Mark one of the files as being unused ... String unused = storedSha1s.iterator().next(); store.markAsUnused(Collections.singleton(new BinaryKey(unused))); // Make sure the trash file was created assertThat(countStoredFiles(), is(storedSha1s.size())); assertThat(countTrashFiles(), is(1)); // Check that the name of the trash file is the SHA1 File trashFile = collectFiles(trash).get(0); assertNotNull(trashFile); assertEquals(unused, trashFile.getName()); Thread.sleep(1100L); // Sleep more than a second, since modified times may only be accurate to nearest second ... store.removeValuesUnusedLongerThan(1, TimeUnit.SECONDS); // Make sure the file was removed from the trash ... assertThat(countStoredFiles(), is(storedSha1s.size() - 1)); assertThat(countTrashFiles(), is(0)); // And that all directories in the trash were removed (since they should be empty) ... assertThat(trash.listFiles().length, is(0)); } @Test public void shouldCleanTrashFilesWhenFilesBecomeUsed() throws Exception { Set<Binary> binaries = new HashSet<Binary>(); int storedCount = 0; for (int i = 0; i != CONTENT.length; ++i) { Binary binary = storeAndCheck(i); assertThat(binary, is(notNullValue())); binaries.add(binary); if (binary instanceof StoredBinaryValue) storedCount++; } // Make sure there are files for all stored values ... assertThat(countStoredFiles(), is(storedCount)); assertThat(countTrashFiles(), is(0)); // Mark one of the files as being unused ... String unused = binaries.iterator().next().getHexHash(); BinaryKey unusedKey = new BinaryKey(unused); store.markAsUnused(Collections.singleton(unusedKey)); // Make sure the file was moved to the trash ... assertThat(countStoredFiles(), is(storedCount)); assertThat(countTrashFiles(), is(1)); // Now access all the binary files which will not change there used/unused state for (Binary binary : binaries) { InputStream stream = binary.getStream(); String content = IoUtil.read(stream); assertThat(content.length() != 0, is(true)); } // Make sure there are files for all stored values ... assertThat(countStoredFiles(), is(storedCount)); assertThat(countTrashFiles(), is(1)); // Now mark the file explicitly as used and check that the file from the trash was removed store.markAsUsed(Collections.singleton(unusedKey)); assertThat(countTrashFiles(), is(0)); } @Test public void shouldStoreLargeFile() throws Exception { print = true; storeAndCheckResource("docs/postgresql-8.4.1-US.pdf", "3d4d11208cd130d92075e1111423667c76e61819", "17MB file", 17714435L); } @Test public void shouldCreateFileLock() throws IOException { File tmpFile = File.createTempFile("foo", "bar"); File tmpDir = tmpFile.getParentFile(); tmpFile.delete(); assertThat(tmpDir.exists(), is(true)); assertThat(tmpDir.canRead(), is(true)); assertThat(tmpDir.canWrite(), is(true)); assertThat(tmpDir.isDirectory(), is(true)); File lockFile = new File(tmpDir, "lock"); lockFile.createNewFile(); RandomAccessFile raf = new RandomAccessFile(lockFile, "rw"); FileLock fileLock = raf.getChannel().lock(); fileLock.release(); assertThat(lockFile.exists(), is(true)); lockFile.delete(); } @Test public void shouldKeepLockWhileMovingLockedFile() throws IOException { File tmpDir = new File("target"); System.out.println("Temporary directory for tests: " + tmpDir.getAbsolutePath()); assertThat(tmpDir.exists(), is(true)); assertThat(tmpDir.canRead(), is(true)); assertThat(tmpDir.canWrite(), is(true)); assertThat(tmpDir.isDirectory(), is(true)); File file1 = new File(tmpDir, "lockFile"); // file1.createNewFile(); // Lock the file ... RandomAccessFile raf = new RandomAccessFile(file1, "rw"); FileLock fileLock = raf.getChannel().lock(); // Now try moving our locked file ... File file2 = new File(tmpDir, "afterMove"); if (!file1.renameTo(file2)) { LOGGER.warn("RenameTo not successful. Will be ignored if on Windows"); if (System.getProperty("os.name").toLowerCase().contains("windows")) { fileLock.release(); return; } } fileLock.release(); assertThat(file1.exists(), is(false)); assertThat(file2.exists(), is(true)); } @FixFor( "MODE-1358" ) @Test public void shouldCopyFilesUsingStreams() throws Exception { // Copy a large file into a temporary file ... File tempFile = File.createTempFile("copytest", "pdf"); RandomAccessFile destinationRaf = null; RandomAccessFile originalRaf = null; try { URL sourceUrl = getClass().getResource("/docs/postgresql-8.4.1-US.pdf"); assertThat(sourceUrl, is(notNullValue())); File sourceFile = new File(sourceUrl.toURI()); assertThat(sourceFile.exists(), is(true)); assertThat(sourceFile.canRead(), is(true)); assertThat(sourceFile.isFile(), is(true)); boolean useBufferedStream = true; final int bufferSize = AbstractBinaryStore.bestBufferSize(sourceFile.length()); destinationRaf = new RandomAccessFile(tempFile, "rw"); originalRaf = new RandomAccessFile(sourceFile, "r"); FileChannel destinationChannel = destinationRaf.getChannel(); OutputStream output = Channels.newOutputStream(destinationChannel); if (useBufferedStream) output = new BufferedOutputStream(output, bufferSize); // Create an input stream to the original file ... FileChannel originalChannel = originalRaf.getChannel(); InputStream input = Channels.newInputStream(originalChannel); if (useBufferedStream) input = new BufferedInputStream(input, bufferSize); // Copy the content ... Stopwatch sw = new Stopwatch(); sw.start(); IoUtil.write(input, output, bufferSize); sw.stop(); System.out.println("Time to copy \"" + sourceFile.getName() + "\" (" + sourceFile.length() + " bytes): " + sw.getTotalDuration()); } finally { tempFile.delete(); if (destinationRaf != null) destinationRaf.close(); if (originalRaf != null) originalRaf.close(); } } @Test public void multipleThreadsShouldReadTheSameFile() throws Exception { final String textBase = "The quick brown fox jumps over the lazy dog"; StringBuilder builder = new StringBuilder(); Random rand = new Random(); while (builder.length() <= MIN_BINARY_SIZE) { builder.append(textBase.substring(0, rand.nextInt(textBase.length()))); } final String text = builder.toString(); final Binary storedValue = store.storeValue(new ByteArrayInputStream(text.getBytes()), false); ExecutorService executor = Executors.newFixedThreadPool(3); try { Callable<String> readingTask = () -> { File tempFile = File.createTempFile("test-binary-store", "bin"); try { FileOutputStream fos = new FileOutputStream(tempFile); InputStream is = storedValue.getStream(); byte[] buff = new byte[100]; int available; while ((available = is.read(buff)) != -1) { fos.write(buff, 0, available); } fos.close(); return IoUtil.read(tempFile); } finally { tempFile.delete(); } }; List<Callable<String>> tasks = Arrays.asList(readingTask, readingTask, readingTask); List<Future<String>> futures = executor.invokeAll(tasks, 5, TimeUnit.SECONDS); for (Future<String> future : futures) { assertEquals(text, future.get()); } } finally { executor.shutdownNow(); } } protected Binary storeAndCheck( int contentIndex ) throws Exception { return storeAndCheck(contentIndex, null); } protected Binary storeAndCheck( int contentIndex, Class<? extends Binary> valueClass ) throws Exception { String content = CONTENT[contentIndex]; String sha1 = CONTENT_HASHES[contentIndex]; InputStream stream = new ByteArrayInputStream(content.getBytes()); Stopwatch sw = new Stopwatch(); sw.start(); Binary binary = store.storeValue(stream, false); sw.stop(); if (print) System.out.println("Time to store 18MB file: " + sw.getTotalDuration()); if (valueClass != null) { assertThat(binary, is(instanceOf(valueClass))); } if (content.length() == 0) { assertThat(binary, is(instanceOf(EmptyBinaryValue.class))); } else if (content.length() < MIN_BINARY_SIZE) { assertThat(binary, is(instanceOf(InMemoryBinaryValue.class))); } else { assertThat(binary, is(instanceOf(StoredBinaryValue.class))); } assertThat(binary.getHexHash(), is(sha1)); String binaryContent = IoUtil.read(binary.getStream()); assertThat(binaryContent, is(content)); return binary; } protected void storeAndCheckResource( String resourcePath, String expectedSha1, String desc, long numBytes ) throws Exception { InputStream content = getClass().getClassLoader().getResourceAsStream(resourcePath); assertThat(content, is(notNullValue())); Stopwatch sw = new Stopwatch(); sw.start(); Binary binary = store.storeValue(content, false); sw.stop(); if (print) System.out.println("Time to store " + desc + ": " + sw.getTotalDuration()); if (numBytes == 0) { assertThat(binary, is(instanceOf(EmptyBinaryValue.class))); } else if (numBytes < MIN_BINARY_SIZE) { assertThat(binary, is(instanceOf(InMemoryBinaryValue.class))); } else { assertThat(binary, is(instanceOf(StoredBinaryValue.class))); } assertThat(binary.getHexHash(), is(expectedSha1)); assertThat(binary.getSize(), is(numBytes)); // Now try reading and comparing the two streams ... InputStream expected = getClass().getClassLoader().getResourceAsStream(resourcePath); InputStream actual = binary.getStream(); byte[] buffer1 = new byte[1024]; byte[] buffer2 = new byte[1024]; int numRead = 0; while ((numRead = expected.read(buffer1)) == actual.read(buffer2)) { if (numRead == -1) break; for (int i = 0; i != numRead; ++i) { assertThat(buffer1[i], is(buffer2[i])); } } if (print) { // And try measuring how fast we can read the file ... sw = new Stopwatch(); sw.start(); while (-1 != actual.read(buffer2)) { } sw.stop(); System.out.println("Time to read " + desc + ": " + sw.getTotalDuration()); } } protected int countStoredFiles() throws IOException { return countFiles(directory, trash); } protected int countTrashFiles() throws IOException { return countFiles(trash); } protected int countFiles( File fileOrDir, File... excluding ) throws IOException { if (excluding == null || excluding.length == 0) { return countFiles(fileOrDir, Collections.<File>emptySet()); } return countFiles(fileOrDir, new HashSet<File>(Arrays.asList(excluding))); } protected int countFiles( File fileOrDir, Set<File> excluding ) throws IOException { if (excluding.contains(fileOrDir)) return 0; int result = 0; if (fileOrDir.isDirectory() && !fileOrDir.isHidden()) { for (File child : fileOrDir.listFiles()) { result += countFiles(child, excluding); } } else if (fileOrDir.isFile() && !fileOrDir.isHidden()) { result++; } return result; } protected List<File> collectFiles(File dir) { List<File> result = new ArrayList<File>(); File[] files = dir.listFiles(); if (files == null) { return result; } for (File file : files) { if (file.isFile() && file.canRead()) { result.add(file); } else if (file.isDirectory() && file.canRead()) { result.addAll(collectFiles(file)); } } return result; } }