/* * (C) Copyright 2015 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: * Thierry Delprat <tdelprat@nuxeo.com> * Antoine Taillefer <ataillefer@nuxeo.com> */ package org.nuxeo.ecm.core.transientstore; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.Environment; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.impl.blob.FileBlob; import org.nuxeo.ecm.core.transientstore.api.MaximumTransientSpaceExceeded; import org.nuxeo.ecm.core.transientstore.api.TransientStore; import org.nuxeo.ecm.core.transientstore.api.TransientStoreConfig; /** * Base class for a {@link TransientStore} implementation. * * @since 7.2 */ public abstract class AbstractTransientStore implements TransientStore { protected static final Log log = LogFactory.getLog(AbstractTransientStore.class); protected TransientStoreConfig config; protected File cacheDir; @Override public void init(TransientStoreConfig config) { this.config = config; File data = getDataDir(config); data.mkdirs(); cacheDir = data.getAbsoluteFile(); } private File getDataDir(TransientStoreConfig config) { String dataDirPath = config.getDataDir(); if (StringUtils.isBlank(dataDirPath)) { File transienStoreHome = new File(Environment.getDefault().getData(), "transientstores"); return new File(transienStoreHome, config.getName()); } else { return new File(dataDirPath); } } @Override public abstract void shutdown(); @Override public abstract boolean exists(String key); @Override public abstract Set<String> keySet(); @Override public abstract void putParameter(String key, String parameter, Serializable value); @Override public abstract Serializable getParameter(String key, String parameter); @Override public abstract void putParameters(String key, Map<String, Serializable> parameters); @Override public abstract Map<String, Serializable> getParameters(String key); @Override public abstract List<Blob> getBlobs(String key); @Override public abstract long getSize(String key); @Override public abstract boolean isCompleted(String key); @Override public abstract void setCompleted(String key, boolean completed); @Override public abstract void remove(String key); @Override public abstract void release(String key); /** * Updates the total storage size and the storage size of the entry with the given {@code key} according to * {@code sizeOfBlobs} and stores the blob information in this entry. */ protected abstract void persistBlobs(String key, long sizeOfBlobs, List<Map<String, String>> blobInfos); /** * Returns the size of the disk storage in bytes. */ public abstract long getStorageSize(); /** * Sets the size of the disk storage in bytes. */ protected abstract void setStorageSize(long newSize); protected abstract long incrementStorageSize(long size); protected abstract long decrementStorageSize(long size); protected abstract void removeAllEntries(); @Override public void putBlobs(String key, List<Blob> blobs) { if (config.getAbsoluteMaxSizeMB() < 0 || getStorageSize() < config.getAbsoluteMaxSizeMB() * (1024 * 1024)) { // Store blobs on the file system List<Map<String, String>> blobInfos = storeBlobs(key, blobs); // Persist blob information in the store persistBlobs(key, getSizeOfBlobs(blobs), blobInfos); } else { throw new MaximumTransientSpaceExceeded(); } } protected List<Map<String, String>> storeBlobs(String key, List<Blob> blobs) { if (blobs == null) { return null; } // Store blobs on the file system and compute blob information List<Map<String, String>> blobInfos = new ArrayList<>(); for (Blob blob : blobs) { Map<String, String> blobInfo = new HashMap<>(); File cachingDir = getCachingDirectory(key); String uuid = UUID.randomUUID().toString(); File cachedFile = new File(cachingDir, uuid); try { if (blob instanceof FileBlob && ((FileBlob) blob).isTemporary()) { ((FileBlob) blob).moveTo(cachedFile); } else { blob.transferTo(cachedFile); } } catch (IOException e) { throw new NuxeoException(e); } Path cachedFileRelativePath = Paths.get(cachingDir.getName(), uuid); blobInfo.put("file", cachedFileRelativePath.toString()); // Redis doesn't support null values if (blob.getFilename() != null) { blobInfo.put("filename", blob.getFilename()); } if (blob.getEncoding() != null) { blobInfo.put("encoding", blob.getEncoding()); } if (blob.getMimeType() != null) { blobInfo.put("mimetype", blob.getMimeType()); } if (blob.getDigest() != null) { blobInfo.put("digest", blob.getDigest()); } blobInfos.add(blobInfo); } log.debug("Stored blobs on the file system: " + blobInfos); return blobInfos; } public File getCachingDirectory(String key) { String cachingDirName = getCachingDirName(key); try { File cachingDir = new File(cacheDir.getCanonicalFile(), cachingDirName); if (!cachingDir.getCanonicalPath().startsWith(cacheDir.getCanonicalPath())) { throw new NuxeoException("Trying to traverse illegal path: " + cachingDir + " for key: " + key); } if (!cachingDir.exists()) { cachingDir.mkdir(); } return cachingDir; } catch (IOException e) { throw new NuxeoException("Error when trying to access cache directory: " + cacheDir + "/" + cachingDirName + " for key: " + key, e); } } protected String getCachingDirName(String key) { String dirName = Base64.encodeBase64String(key.getBytes()); dirName = dirName.replaceAll("/", "_"); return dirName; } protected long getSizeOfBlobs(List<Blob> blobs) { int size = 0; if (blobs != null) { for (Blob blob : blobs) { long blobLength = blob.getLength(); if (blobLength > -1) { size += blobLength; } } } return size; } protected List<Blob> loadBlobs(List<Map<String, String>> blobInfos) { log.debug("Loading blobs from the file system: " + blobInfos); List<Blob> blobs = new ArrayList<>(); for (Map<String, String> info : blobInfos) { File blobFile = new File(cacheDir, info.get("file")); Blob blob = new FileBlob(blobFile); blob.setEncoding(info.get("encoding")); blob.setMimeType(info.get("mimetype")); blob.setFilename(info.get("filename")); blob.setDigest(info.get("digest")); blobs.add(blob); } return blobs; } @Override public int getStorageSizeMB() { return (int) getStorageSize() / (1024 * 1024); } @Override public void doGC() { log.debug(String.format("Performing GC for TransientStore %s", config.getName())); long newSize = 0; try { try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(cacheDir.getAbsolutePath()))) { for (Path entry : stream) { String key = getKeyCachingDirName(entry.getFileName().toString()); if (exists(key)) { newSize += getFilePathSize(entry); continue; } FileUtils.deleteQuietly(entry.toFile()); } } } catch (IOException e) { log.error("Error while performing GC", e); } setStorageSize(newSize); } protected String getKeyCachingDirName(String dir) { String key = dir.replaceAll("_", "/"); return new String(Base64.decodeBase64(key)); } protected long getFilePathSize(Path entry) { long size = 0; for (File file : entry.toFile().listFiles()) { size += file.length(); } return size; } @Override public void removeAll() { log.debug("Removing all entries from TransientStore " + config.getName()); removeAllEntries(); doGC(); } public File getCacheDir() { return cacheDir; } }