/* * (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 * Florent Guillaume */ package org.nuxeo.ecm.core.storage.mongodb; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static org.nuxeo.ecm.core.blob.BlobProviderDescriptor.PREVENT_USER_UPDATE; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; 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.blob.BlobInfo; import org.nuxeo.ecm.core.blob.BlobProvider; import org.nuxeo.ecm.core.blob.binary.AbstractBinaryManager; import org.nuxeo.ecm.core.blob.binary.Binary; import org.nuxeo.ecm.core.blob.binary.BinaryBlobProvider; import org.nuxeo.ecm.core.blob.binary.BinaryGarbageCollector; import org.nuxeo.ecm.core.blob.binary.BinaryManager; import org.nuxeo.ecm.core.blob.binary.BinaryManagerStatus; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.mongodb.MongoClient; import com.mongodb.MongoClientURI; import com.mongodb.ServerAddress; import com.mongodb.gridfs.GridFS; import com.mongodb.gridfs.GridFSDBFile; import com.mongodb.gridfs.GridFSInputFile; /** * Implements the {@link BinaryManager} and {@link BlobProvider} interface using MongoDB GridFS. * <p> * This implementation does not use local caching. * <p> * This implementation may not always be ideal regarding streaming because of the usage of {@link Binary} interface that * exposes a {@link File}. * * @since 7.10 */ public class GridFSBinaryManager extends AbstractBinaryManager implements BlobProvider { public static final String SERVER_PROPERTY = "server"; public static final String DBNAME_PROPERTY = "dbname"; public static final String BUCKET_PROPERTY = "bucket"; protected Map<String, String> properties; protected MongoClient client; protected GridFS gridFS; @Override public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { super.initialize(blobProviderId, properties); this.properties = properties; String server = properties.get(SERVER_PROPERTY); if (StringUtils.isBlank(server)) { throw new NuxeoException("Missing server property in GridFS Binary Manager descriptor: " + blobProviderId); } String dbname = properties.get(DBNAME_PROPERTY); if (StringUtils.isBlank(dbname)) { throw new NuxeoException("Missing dbname property in GridFS Binary Manager descriptor: " + blobProviderId); } String bucket = properties.get(BUCKET_PROPERTY); if (StringUtils.isBlank(bucket)) { bucket = blobProviderId + ".fs"; } if (server.startsWith("mongodb://")) { client = new MongoClient(new MongoClientURI(server)); } else { client = new MongoClient(new ServerAddress(server)); } gridFS = new GridFS(client.getDB(dbname), bucket); garbageCollector = new GridFSBinaryGarbageCollector(); } @Override public void close() { if (client != null) { client.close(); client = null; } } @Override public BinaryManager getBinaryManager() { return this; } public GridFS getGridFS() { return gridFS; } /** * A binary backed by GridFS. */ protected class GridFSBinary extends Binary { private static final long serialVersionUID = 1L; protected GridFSBinary(String digest, String blobProviderId) { super(digest, blobProviderId); } @Override public InputStream getStream() { GridFSDBFile dbFile = gridFS.findOne(digest); return dbFile == null ? null : dbFile.getInputStream(); } } @Override public Binary getBinary(Blob blob) throws IOException { if (!(blob instanceof FileBlob)) { return super.getBinary(blob); // just open the stream and call getBinary(InputStream) } // we already have a file so can compute the length and digest efficiently File file = ((FileBlob) blob).getFile(); String digest; try (InputStream in = new FileInputStream(file)) { digest = DigestUtils.md5Hex(in); } // if the digest is not already known then save to GridFS GridFSDBFile dbFile = gridFS.findOne(digest); if (dbFile == null) { try (InputStream in = new FileInputStream(file)) { GridFSInputFile inputFile = gridFS.createFile(in, digest); inputFile.save(); } } return new GridFSBinary(digest, blobProviderId); } @Override protected Binary getBinary(InputStream in) throws IOException { // save the file to GridFS GridFSInputFile inputFile = gridFS.createFile(in, true); inputFile.save(); // now we know length and digest String digest = inputFile.getMD5(); // if the digest is already known then reuse it instead GridFSDBFile dbFile = gridFS.findOne(digest); if (dbFile == null) { // no existing file, set its filename as the digest inputFile.setFilename(digest); inputFile.save(); } else { // file already existed, no need for the temporary one gridFS.remove(inputFile); } return new GridFSBinary(digest, blobProviderId); } @Override public Binary getBinary(String digest) { GridFSDBFile dbFile = gridFS.findOne(digest); if (dbFile != null) { return new GridFSBinary(digest, blobProviderId); } return null; } @Override public Blob readBlob(BlobInfo blobInfo) throws IOException { // just delegate to avoid copy/pasting code return new BinaryBlobProvider(this).readBlob(blobInfo); } @Override public String writeBlob(Blob blob) throws IOException { // just delegate to avoid copy/pasting code return new BinaryBlobProvider(this).writeBlob(blob); } @Override public boolean supportsUserUpdate() { return !Boolean.parseBoolean(properties.get(PREVENT_USER_UPDATE)); } public class GridFSBinaryGarbageCollector implements BinaryGarbageCollector { protected BinaryManagerStatus status; protected volatile long startTime; protected static final String MARK_KEY_PREFIX = "gc-mark-key-"; protected String msKey; @Override public String getId() { return "gridfs:" + getGridFS().getBucketName(); } @Override public BinaryManagerStatus getStatus() { return status; } @Override public boolean isInProgress() { return startTime != 0; } @Override public void mark(String digest) { GridFSDBFile dbFile = gridFS.findOne(digest); if (dbFile != null) { dbFile.setMetaData(new BasicDBObject(msKey, TRUE)); dbFile.save(); status.numBinaries += 1; status.sizeBinaries += dbFile.getLength(); } } @Override public void start() { if (startTime != 0) { throw new NuxeoException("Already started"); } startTime = System.currentTimeMillis(); status = new BinaryManagerStatus(); msKey = MARK_KEY_PREFIX + System.currentTimeMillis(); } @Override public void stop(boolean delete) { DBObject query = new BasicDBObject("metadata." + msKey, new BasicDBObject("$exists", FALSE)); List<GridFSDBFile> files = gridFS.find(query); for (GridFSDBFile file : files) { status.numBinariesGC += 1; status.sizeBinariesGC += file.getLength(); if (delete) { gridFS.remove(file); } } startTime = 0; } } }