001/*
002 * (C) Copyright 2011-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Stephane Lacoin
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.core.blob.binary;
021
022import java.io.File;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.util.Map;
028
029import org.apache.commons.io.FileUtils;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.common.file.FileCache;
033import org.nuxeo.common.file.LRUFileCache;
034import org.nuxeo.common.utils.SizeUtils;
035import org.nuxeo.ecm.core.api.NuxeoException;
036import org.nuxeo.runtime.api.Framework;
037import org.nuxeo.runtime.trackers.files.FileEventTracker;
038
039/**
040 * Abstract class for a {@link BinaryManager} that uses a cache for its files because fetching them is expensive.
041 * <p>
042 * Initialization of the {@link BinaryManager} must call {@link #initializeCache} from the {@link #initialize} method.
043 *
044 * @since 5.7
045 */
046public abstract class CachingBinaryManager extends AbstractBinaryManager {
047
048    private static final Log log = LogFactory.getLog(CachingBinaryManager.class);
049
050    protected File cachedir;
051
052    public FileCache fileCache;
053
054    protected FileStorage fileStorage;
055
056    @Override
057    public void initialize(String blobProviderId, Map<String, String> properties) throws IOException {
058        super.initialize(blobProviderId, properties);
059        BinaryManagerRootDescriptor descriptor = new BinaryManagerRootDescriptor();
060        descriptor.digest = getDefaultDigestAlgorithm();
061        setDescriptor(descriptor);
062        log.info("Registering binary manager '" + blobProviderId + "' using " + getClass().getSimpleName());
063    }
064
065    /**
066     * Initialize the cache.
067     *
068     * @param dir the directory to use to store cached files
069     * @param maxSize the maximum size of the cache (in bytes)
070     * @param maxCount the maximum number of files in the cache
071     * @param minAge the minimum age of a file in the cache to be eligible for removal (in seconds)
072     * @param fileStorage the file storage mechanism to use to store and fetch files
073     * @since 5.9.2
074     */
075    protected void initializeCache(File dir, long maxSize, long maxCount, long minAge, FileStorage fileStorage) {
076        fileCache = new LRUFileCache(dir, maxSize, maxCount, minAge);
077        this.fileStorage = fileStorage;
078    }
079
080    /**
081     * Initialize the cache.
082     *
083     * @param maxSizeStr the maximum size of the cache (as a String)
084     * @param fileStorage the file storage mechanism to use to store and fetch files
085     * @see #initializeCache(String, String, String, FileStorage)
086     * @since 6.0
087     */
088    public void initializeCache(String maxSizeStr, FileStorage fileStorage) throws IOException {
089        String maxCountStr = "10000"; // default for legacy code
090        String minAgeStr = "3600"; // default for legacy code
091        initializeCache(maxSizeStr, maxCountStr, minAgeStr, fileStorage);
092    }
093
094    /**
095     * Initializes the cache.
096     *
097     * @param maxSizeStr the maximum size of the cache (as a String)
098     * @param maxCountStr the maximum number of files in the cache
099     * @param minAgeStr the minimum age of a file in the cache to be eligible for removal (in seconds)
100     * @param fileStorage the file storage mechanism to use to store and fetch files
101     * @see SizeUtils#parseSizeInBytes(String)
102     * @since 7.10-HF03, 8.1
103     */
104    public void initializeCache(String maxSizeStr, String maxCountStr, String minAgeStr, FileStorage fileStorage)
105            throws IOException {
106        cachedir = Framework.createTempFile("nxbincache.", "");
107        cachedir.delete();
108        cachedir.mkdir();
109        long maxSize = SizeUtils.parseSizeInBytes(maxSizeStr);
110        long maxCount = Long.parseLong(maxCountStr);
111        long minAge = Long.parseLong(minAgeStr);
112        initializeCache(cachedir, maxSize, maxCount, minAge, fileStorage);
113        log.info("Using binary cache directory: " + cachedir.getPath() + " size: " + maxSizeStr + " maxCount: "
114                + maxCount + " minAge: " + minAge);
115
116        // be sure FileTracker won't steal our files !
117        FileEventTracker.registerProtectedPath(cachedir.getAbsolutePath());
118    }
119
120    @Override
121    public void close() {
122        fileCache.clear();
123        if (cachedir != null) {
124            try {
125                FileUtils.deleteDirectory(cachedir);
126            } catch (IOException e) {
127                throw new NuxeoException(e);
128            }
129        }
130    }
131
132    @Override
133    protected Binary getBinary(InputStream in) throws IOException {
134        // write the input stream to a temporary file, while computing a digest
135        File tmp = fileCache.getTempFile();
136        OutputStream out = new FileOutputStream(tmp);
137        String digest;
138        try {
139            digest = storeAndDigest(in, out);
140        } finally {
141            in.close();
142            out.close();
143        }
144
145        File cachedFile = fileCache.getFile(digest);
146        if (cachedFile != null) {
147            // file already in cache
148            if (Framework.isTestModeSet()) {
149                Framework.getProperties().setProperty("cachedBinary", digest);
150            }
151            // delete tmp file, not needed anymore
152            tmp.delete();
153        } else {
154            // send the file to storage
155            fileStorage.storeFile(digest, tmp);
156            // register the file in the file cache
157            fileCache.putFile(digest, tmp);
158        }
159        return getBinary(digest);
160    }
161
162    @Override
163    public Binary getBinary(String digest) {
164        return new LazyBinary(digest, blobProviderId, this);
165    }
166
167    /* =============== Methods used by LazyBinary =============== */
168
169    /**
170     * Gets a file from cache or storage.
171     * <p>
172     * Used by {@link LazyBinary}.
173     */
174    public File getFile(String digest) throws IOException {
175        // get file from cache
176        File file = fileCache.getFile(digest);
177        if (file != null) {
178            return file;
179        }
180        // fetch file from storage
181        File tmp = fileCache.getTempFile();
182        if (fileStorage.fetchFile(digest, tmp)) {
183            // put file in cache
184            file = fileCache.putFile(digest, tmp);
185            return file;
186        } else {
187            // file not in storage
188            tmp.delete();
189            return null;
190        }
191    }
192
193}