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        descriptor = new BinaryManagerRootDescriptor();
060        descriptor.digest = getDefaultDigestAlgorithm();
061        log.info("Registering binary manager '" + blobProviderId + "' using " + getClass().getSimpleName());
062    }
063
064    /**
065     * Initialize the cache.
066     *
067     * @param dir the directory to use to store cached files
068     * @param maxSize the maximum size of the cache (in bytes)
069     * @param maxCount the maximum number of files in the cache
070     * @param minAge the minimum age of a file in the cache to be eligible for removal (in seconds)
071     * @param fileStorage the file storage mechanism to use to store and fetch files
072     * @since 5.9.2
073     */
074    protected void initializeCache(File dir, long maxSize, long maxCount, long minAge, FileStorage fileStorage) {
075        fileCache = new LRUFileCache(dir, maxSize, maxCount, minAge);
076        this.fileStorage = fileStorage;
077    }
078
079    /**
080     * Initialize the cache.
081     *
082     * @param maxSizeStr the maximum size of the cache (as a String)
083     * @param fileStorage the file storage mechanism to use to store and fetch files
084     * @see #initializeCache(String, String, String, FileStorage)
085     * @since 6.0
086     */
087    public void initializeCache(String maxSizeStr, FileStorage fileStorage) throws IOException {
088        String maxCountStr = "10000"; // default for legacy code
089        String minAgeStr = "3600"; // default for legacy code
090        initializeCache(maxSizeStr, maxCountStr, minAgeStr, fileStorage);
091    }
092
093    /**
094     * Initializes the cache.
095     *
096     * @param maxSizeStr the maximum size of the cache (as a String)
097     * @param maxCountStr the maximum number of files in the cache
098     * @param minAgeStr the minimum age of a file in the cache to be eligible for removal (in seconds)
099     * @param fileStorage the file storage mechanism to use to store and fetch files
100     * @see SizeUtils#parseSizeInBytes(String)
101     * @since 7.10-HF03, 8.1
102     */
103    public void initializeCache(String maxSizeStr, String maxCountStr, String minAgeStr, FileStorage fileStorage)
104            throws IOException {
105        cachedir = Framework.createTempFile("nxbincache.", "");
106        cachedir.delete();
107        cachedir.mkdir();
108        long maxSize = SizeUtils.parseSizeInBytes(maxSizeStr);
109        long maxCount = Long.parseLong(maxCountStr);
110        long minAge = Long.parseLong(minAgeStr);
111        initializeCache(cachedir, maxSize, maxCount, minAge, fileStorage);
112        log.info("Using binary cache directory: " + cachedir.getPath() + " size: " + maxSizeStr + " maxCount: "
113                + maxCount + " minAge: " + minAge);
114
115        // be sure FileTracker won't steal our files !
116        FileEventTracker.registerProtectedPath(cachedir.getAbsolutePath());
117    }
118
119    @Override
120    public void close() {
121        fileCache.clear();
122        if (cachedir != null) {
123            try {
124                FileUtils.deleteDirectory(cachedir);
125            } catch (IOException e) {
126                throw new NuxeoException(e);
127            }
128        }
129    }
130
131    @Override
132    protected Binary getBinary(InputStream in) throws IOException {
133        // write the input stream to a temporary file, while computing a digest
134        File tmp = fileCache.getTempFile();
135        OutputStream out = new FileOutputStream(tmp);
136        String digest;
137        try {
138            digest = storeAndDigest(in, out);
139        } finally {
140            in.close();
141            out.close();
142        }
143
144        File cachedFile = fileCache.getFile(digest);
145        if (cachedFile != null) {
146            // file already in cache
147            if (Framework.isTestModeSet()) {
148                Framework.getProperties().setProperty("cachedBinary", digest);
149            }
150            // delete tmp file, not needed anymore
151            tmp.delete();
152        } else {
153            // send the file to storage
154            fileStorage.storeFile(digest, tmp);
155            // register the file in the file cache
156            fileCache.putFile(digest, tmp);
157        }
158        return getBinary(digest);
159    }
160
161    @Override
162    public Binary getBinary(String digest) {
163        return new LazyBinary(digest, blobProviderId, this);
164    }
165
166    /* =============== Methods used by LazyBinary =============== */
167
168    /**
169     * Gets a file from cache or storage.
170     * <p>
171     * Used by {@link LazyBinary}.
172     */
173    public File getFile(String digest) throws IOException {
174        // get file from cache
175        File file = fileCache.getFile(digest);
176        if (file != null) {
177            return file;
178        }
179        // fetch file from storage
180        File tmp = fileCache.getTempFile();
181        if (fileStorage.fetchFile(digest, tmp)) {
182            // put file in cache
183            file = fileCache.putFile(digest, tmp);
184            return file;
185        } else {
186            // file not in storage
187            tmp.delete();
188            return null;
189        }
190    }
191
192}