001/* 002 * (C) Copyright 2011-2014 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Stephane Lacoin 016 * Florent Guillaume 017 */ 018package org.nuxeo.ecm.core.blob.binary; 019 020import java.io.File; 021import java.io.FileInputStream; 022import java.io.FileOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.io.OutputStreamWriter; 027import java.io.Writer; 028import java.util.Map; 029 030import org.apache.commons.io.FileUtils; 031import org.apache.commons.io.IOUtils; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.nuxeo.common.file.FileCache; 035import org.nuxeo.common.file.LRUFileCache; 036import org.nuxeo.common.utils.SizeUtils; 037import org.nuxeo.ecm.core.api.NuxeoException; 038import org.nuxeo.runtime.api.Framework; 039import org.nuxeo.runtime.trackers.files.FileEventTracker; 040 041/** 042 * Abstract class for a {@link BinaryManager} that uses a cache for its files because fetching them is expensive. 043 * <p> 044 * Initialization of the {@link BinaryManager} must call {@link #initializeCache} from the {@link #initialize} method. 045 * 046 * @since 5.7 047 */ 048public abstract class CachingBinaryManager extends AbstractBinaryManager { 049 050 private static final Log log = LogFactory.getLog(CachingBinaryManager.class); 051 052 protected static final String LEN_DIGEST_SUFFIX = "-len"; 053 054 protected File cachedir; 055 056 public FileCache fileCache; 057 058 protected FileStorage fileStorage; 059 060 @Override 061 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 062 super.initialize(blobProviderId, properties); 063 descriptor = new BinaryManagerRootDescriptor(); 064 descriptor.digest = getDefaultDigestAlgorithm(); 065 log.info("Registering binary manager '" + blobProviderId + "' using " + getClass().getSimpleName()); 066 } 067 068 /** 069 * Initialize the cache. 070 * 071 * @param dir the directory to use to store cached files 072 * @param maxSize the maximum size of the cache (in bytes) 073 * @param fileStorage the file storage mechanism to use to store and fetch files 074 * @since 5.9.2 075 */ 076 public void initializeCache(File dir, long maxSize, @SuppressWarnings("hiding") FileStorage fileStorage) { 077 fileCache = new LRUFileCache(dir, maxSize); 078 this.fileStorage = fileStorage; 079 } 080 081 /** 082 * Initialize the cache. 083 * 084 * @param cacheSizeStr the maximum size of the cache (as a String) 085 * @param fileStorage the file storage mechanism to use to store and fetch files 086 * @since 6.0 087 * @see #initializeCache(File, long, FileStorage) 088 * @see SizeUtils#parseSizeInBytes(String) 089 */ 090 public void initializeCache(String cacheSizeStr, @SuppressWarnings("hiding") FileStorage fileStorage) 091 throws IOException { 092 cachedir = File.createTempFile("nxbincache.", "", null); 093 cachedir.delete(); 094 cachedir.mkdir(); 095 long cacheSize = SizeUtils.parseSizeInBytes(cacheSizeStr); 096 initializeCache(cachedir, cacheSize, fileStorage); 097 log.info("Using binary cache directory: " + cachedir.getPath() + " size: " + cacheSizeStr); 098 099 // be sure FileTracker won't steal our files ! 100 FileEventTracker.registerProtectedPath(cachedir.getAbsolutePath()); 101 } 102 103 @Override 104 public void close() { 105 fileCache.clear(); 106 if (cachedir != null) { 107 try { 108 FileUtils.deleteDirectory(cachedir); 109 } catch (IOException e) { 110 throw new NuxeoException(e); 111 } 112 } 113 } 114 115 @Override 116 protected Binary getBinary(InputStream in) throws IOException { 117 // write the input stream to a temporary file, while computing a digest 118 File tmp = fileCache.getTempFile(); 119 OutputStream out = new FileOutputStream(tmp); 120 String digest; 121 try { 122 digest = storeAndDigest(in, out); 123 } finally { 124 in.close(); 125 out.close(); 126 } 127 128 File cachedFile = fileCache.getFile(digest); 129 if (cachedFile != null) { 130 // file already in cache 131 if (Framework.isTestModeSet()) { 132 Framework.getProperties().setProperty("cachedBinary", digest); 133 } 134 // delete tmp file, not needed anymore 135 tmp.delete(); 136 return new Binary(cachedFile, digest, blobProviderId); 137 } 138 139 // send the file to storage 140 fileStorage.storeFile(digest, tmp); 141 142 // register the file in the file cache if all went well 143 File file = fileCache.putFile(digest, tmp); 144 145 return new Binary(file, digest, blobProviderId); 146 } 147 148 @Override 149 public Binary getBinary(String digest) { 150 // Check in the cache 151 File file = fileCache.getFile(digest); 152 if (file == null) { 153 return new LazyBinary(digest, blobProviderId, this); 154 } else { 155 return new Binary(file, digest, blobProviderId); 156 } 157 } 158 159 /* =============== Methods used by LazyBinary =============== */ 160 161 /** 162 * Gets a file from cache or storage. 163 * <p> 164 * Used by {@link LazyBinary}. 165 */ 166 public File getFile(String digest) throws IOException { 167 // get file from cache 168 File file = fileCache.getFile(digest); 169 if (file != null) { 170 return file; 171 } 172 // fetch file from storage 173 File tmp = fileCache.getTempFile(); 174 if (fileStorage.fetchFile(digest, tmp)) { 175 // put file in cache 176 file = fileCache.putFile(digest, tmp); 177 return file; 178 } else { 179 // file not in storage 180 tmp.delete(); 181 return null; 182 } 183 } 184 185 /** 186 * Gets a file length from cache or storage. 187 * <p> 188 * Use by {@link LazyBinary}. 189 */ 190 public Long getLength(String digest) throws IOException { 191 // get length from cache 192 Long length = getLengthFromCache(digest); 193 if (length != null) { 194 return length; 195 } 196 // fetch length from storage 197 length = fileStorage.fetchLength(digest); 198 // put length in cache 199 putLengthInCache(digest, length); 200 return length; 201 } 202 203 protected Long getLengthFromCache(String digest) throws IOException { 204 File f = fileCache.getFile(digest + LEN_DIGEST_SUFFIX); 205 if (f == null) { 206 return null; 207 } 208 // read decimal length from file 209 InputStream in = null; 210 try { 211 in = new FileInputStream(f); 212 String len = IOUtils.toString(in); 213 return Long.valueOf(len); 214 } catch (NumberFormatException e) { 215 throw new IOException("Invalid length in " + f, e); 216 } finally { 217 IOUtils.closeQuietly(in); 218 } 219 } 220 221 protected void putLengthInCache(String digest, Long len) throws IOException { 222 // write decimal length in file 223 OutputStream out = null; 224 try { 225 File tmp = fileCache.getTempFile(); 226 out = new FileOutputStream(tmp); 227 Writer writer = new OutputStreamWriter(out); 228 writer.write(len.toString()); 229 writer.flush(); 230 writer.close(); 231 fileCache.putFile(digest + LEN_DIGEST_SUFFIX, tmp); 232 } finally { 233 IOUtils.closeQuietly(out); 234 } 235 } 236 237}