/* * (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Florent Guillaume, Mathieu Guillaume, jcarsique */ package org.nuxeo.ecm.core.blob.binary; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.Environment; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.trackers.files.FileEventTracker; /** * A simple filesystem-based binary manager. It stores the binaries according to their digest (hash), which means that * no transactional behavior needs to be implemented. * <p> * A garbage collection is needed to purge unused binaries. * <p> * The format of the <em>binaries</em> directory is: * <ul> * <li><em>data/</em> hierarchy with the actual binaries in subdirectories,</li> * <li><em>tmp/</em> temporary storage during creation,</li> * <li><em>config.xml</em> a file containing the configuration used.</li> * </ul> * * @author Florent Guillaume * @since 5.6 */ public class LocalBinaryManager extends AbstractBinaryManager { private static final Log log = LogFactory.getLog(LocalBinaryManager.class); public static final Pattern WINDOWS_ABSOLUTE_PATH = Pattern.compile("[a-zA-Z]:[/\\\\].*"); public static final String DEFAULT_PATH = "binaries"; public static final String DATA = "data"; public static final String TMP = "tmp"; public static final String CONFIG_FILE = "config.xml"; protected File storageDir; protected File tmpDir; @Override public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { super.initialize(blobProviderId, properties); String path = properties.get(BinaryManager.PROP_PATH); if (StringUtils.isBlank(path)) { path = DEFAULT_PATH; } path = Framework.expandVars(path); path = path.trim(); File base; if (path.startsWith("/") || path.startsWith("\\") || path.contains("://") || path.contains(":\\") || WINDOWS_ABSOLUTE_PATH.matcher(path).matches()) { // absolute base = new File(path); } else { // relative File home = Environment.getDefault().getData(); base = new File(home, path); // Backward compliance with versions before 5.4 (NXP-5370) File oldBase = new File(Framework.getRuntime().getHome().getPath(), path); if (oldBase.exists()) { log.warn("Old binaries path used (NXP-5370). Please move " + oldBase + " to " + base); base = oldBase; } } log.info("Registering binary manager '" + blobProviderId + "' using " + (this.getClass().equals(LocalBinaryManager.class) ? "" : (this.getClass().getSimpleName() + " and ")) + "binary store: " + base); storageDir = new File(base, DATA); tmpDir = new File(base, TMP); storageDir.mkdirs(); tmpDir.mkdirs(); descriptor = getDescriptor(new File(base, CONFIG_FILE)); createGarbageCollector(); // be sure FileTracker won't steal our files ! FileEventTracker.registerProtectedPath(storageDir.getAbsolutePath()); } @Override public void close() { if (tmpDir != null) { try { FileUtils.cleanDirectory(tmpDir); } catch (IOException e) { throw new NuxeoException(e); } } } public File getStorageDir() { return storageDir; } @Override protected Binary getBinary(InputStream in) throws IOException { String digest = storeAndDigest(in); File file = getFileForDigest(digest, false); /* * Now we can build the Binary. */ return new Binary(file, digest, blobProviderId); } @Override public Binary getBinary(String digest) { File file = getFileForDigest(digest, false); if (file == null) { // invalid digest return null; } if (!file.exists()) { log.warn("cannot fetch content at " + file.getPath() + " (file does not exist), check your configuration"); return null; } return new Binary(file, digest, blobProviderId); } /** * Gets a file representing the storage for a given digest. * * @param digest the digest * @param createDir {@code true} if the directory containing the file itself must be created * @return the file for this digest */ public File getFileForDigest(String digest, boolean createDir) { int depth = descriptor.depth; if (digest.length() < 2 * depth) { return null; } StringBuilder buf = new StringBuilder(3 * depth - 1); for (int i = 0; i < depth; i++) { if (i != 0) { buf.append(File.separatorChar); } buf.append(digest.substring(2 * i, 2 * i + 2)); } File dir = new File(storageDir, buf.toString()); if (createDir) { dir.mkdirs(); } return new File(dir, digest); } protected String storeAndDigest(InputStream in) throws IOException { File tmp = File.createTempFile("create_", ".tmp", tmpDir); OutputStream out = new FileOutputStream(tmp); /* * First, write the input stream to a temporary file, while computing a digest. */ try { String digest; try { digest = storeAndDigest(in, out); } finally { in.close(); out.close(); } /* * Move the tmp file to its destination. */ File file = getFileForDigest(digest, true); atomicMove(tmp, file); return digest; } finally { tmp.delete(); } } /** * Does an atomic move of the tmp (or source) file to the final file. * <p> * Tries to work well with NFS mounts and different filesystems. */ protected void atomicMove(File source, File dest) throws IOException { if (dest.exists()) { // The file with the proper digest is already there so don't do // anything. This is to avoid "Stale NFS File Handle" problems // which would occur if we tried to overwrite it anyway. // Note that this doesn't try to protect from the case where // two identical files are uploaded at the same time. // Update date for the GC. dest.setLastModified(source.lastModified()); return; } if (!source.renameTo(dest)) { // Failed to rename, probably a different filesystem. // Do *NOT* use Apache Commons IO's FileUtils.moveFile() // because it rewrites the destination file so is not atomic. // Do a copy through a tmp file on the same filesystem then // atomic rename. File tmp = File.createTempFile(dest.getName(), ".tmp", dest.getParentFile()); try { try (InputStream in = new FileInputStream(source); // OutputStream out = new FileOutputStream(tmp)) { IOUtils.copy(in, out); } // then do the atomic rename tmp.renameTo(dest); } finally { tmp.delete(); } // finally remove the original source source.delete(); } if (!dest.exists()) { throw new IOException("Could not create file: " + dest); } } protected void createGarbageCollector() { garbageCollector = new DefaultBinaryGarbageCollector(this); } public static class DefaultBinaryGarbageCollector implements BinaryGarbageCollector { /** * Windows FAT filesystems have a time resolution of 2s. Other common filesystems have 1s. */ public static int TIME_RESOLUTION = 2000; protected final LocalBinaryManager binaryManager; protected volatile long startTime; protected BinaryManagerStatus status; public DefaultBinaryGarbageCollector(LocalBinaryManager binaryManager) { this.binaryManager = binaryManager; } @Override public String getId() { return binaryManager.getStorageDir().toURI().toString(); } @Override public BinaryManagerStatus getStatus() { return status; } @Override public boolean isInProgress() { // volatile as this is designed to be called from another thread return startTime != 0; } @Override public void start() { if (startTime != 0) { throw new RuntimeException("Alread started"); } startTime = System.currentTimeMillis(); status = new BinaryManagerStatus(); } @Override public void mark(String digest) { File file = binaryManager.getFileForDigest(digest, false); if (!file.exists()) { log.error("Unknown file digest: " + digest); return; } touch(file); } @Override public void stop(boolean delete) { if (startTime == 0) { throw new RuntimeException("Not started"); } deleteOld(binaryManager.getStorageDir(), startTime - TIME_RESOLUTION, 0, delete); status.gcDuration = System.currentTimeMillis() - startTime; startTime = 0; } protected void deleteOld(File file, long minTime, int depth, boolean delete) { if (file.isDirectory()) { for (File f : file.listFiles()) { deleteOld(f, minTime, depth + 1, delete); } if (depth > 0 && file.list().length == 0) { // empty directory file.delete(); } } else if (file.isFile() && file.canWrite()) { long lastModified = file.lastModified(); long length = file.length(); if (lastModified == 0) { log.error("Cannot read last modified for file: " + file); } else if (lastModified < minTime) { status.sizeBinariesGC += length; status.numBinariesGC++; if (delete && !file.delete()) { log.warn("Cannot gc file: " + file); } } else { status.sizeBinaries += length; status.numBinaries++; } } } } /** * Sets the last modification date to now on a file * * @param file the file */ public static void touch(File file) { long time = System.currentTimeMillis(); if (file.setLastModified(time)) { // ok return; } if (!file.canWrite()) { // cannot write -> stop won't be able to delete anyway return; } try { // Windows: the file may be open for reading // workaround found by Thomas Mueller, see JCR-2872 RandomAccessFile r = new RandomAccessFile(file, "rw"); try { r.setLength(r.length()); } finally { r.close(); } } catch (IOException e) { log.error("Cannot set last modified for file: " + file, e); } } }