/** * Copyright (c) 2011-2012, Thilo Planz. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package v7db.files.mongodb; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import org.apache.commons.io.IOUtils; import org.bson.BSONObject; import org.bson.types.ObjectId; import v7db.files.ContentStorageFacade; import v7db.files.spi.Content; import v7db.files.spi.ContentPointer; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.WriteConcern; import com.mongodb.WriteResult; public class V7GridFS { private final DBCollection files; private final ContentStorageFacade storage; public static final String COLLECTION_NAME_FILES = "v7files.files"; public V7GridFS(DB db) { files = db.getCollection(COLLECTION_NAME_FILES); storage = new ContentStorageFacade(new MongoContentStorage(db), new MongoReferenceTracking(db)); } public V7File getFile(String... path) { // the filesystem root V7File parentFile = V7File.lazy(this, path[0], null); if (path.length == 1) { return parentFile; } DBObject metaData; // directly under the root if (path.length == 2) { metaData = files.findOne(new BasicDBObject("parent", path[0]) .append("filename", path[1])); } else { List<String> filenames = Arrays.asList(path) .subList(1, path.length); List<DBObject> candidates = files.find( new BasicDBObject("filename", new BasicDBObject("$in", filenames))).toArray(); // we need to have at least one candidate for every path component if (candidates.size() < filenames.size()) return null; Object parent = path[0]; metaData = null; path: for (String fileName : filenames) { for (DBObject c : candidates) { if (parent.equals(c.get("parent")) && fileName.equals(c.get("filename"))) { parent = c.get("_id"); parentFile = new V7File(this, metaData, parentFile); metaData = c; continue path; } } return null; } } if (metaData == null) return null; return new V7File(this, metaData, parentFile); } /** * @param data * can be null, for a file without content (e.g. a folder) * @param parentFileId * @param filename * @return * @throws IOException */ public ObjectId addFolder(Object parentFileId, String filename) throws IOException { return addFile(null, 0, 0, parentFileId, filename, null); } /** * @param data * can be null, for a file without content (e.g. a folder) * @param parentFileId * @param filename * @return * @throws IOException */ public ObjectId addFile(byte[] data, Object parentFileId, String filename, String contentType) throws IOException { if (data == null) return addFile(null, 0, 0, parentFileId, filename, contentType); return addFile(data, 0, data.length, parentFileId, filename, contentType); } public ObjectId addFile(ContentPointer data, Object parentFileId, String filename, String contentType) throws IOException { if (data == null) return addFile(null, 0, 0, parentFileId, filename, contentType); ObjectId fileId = new ObjectId(); BasicDBObject metaData = new BasicDBObject("parent", parentFileId) .append("_id", fileId); metaData.putAll(storage.updateBackRefs(data, fileId, filename, contentType)); insertMetaData(metaData); return fileId; } public ObjectId addFile(byte[] data, int offset, int len, Object parentFileId, String filename, String contentType) throws IOException { ObjectId fileId = new ObjectId(); BasicDBObject metaData = new BasicDBObject("parent", parentFileId) .append("_id", fileId); metaData.putAll(storage.inlineOrInsertContentsAndBackRefs(100, data, offset, len, fileId, filename, contentType)); insertMetaData(metaData); return fileId; } /** * will close the InputStream before returning */ public ObjectId addFile(InputStream data, Object parentFileId, String filename, String contentType) throws IOException { ObjectId fileId = new ObjectId(); BasicDBObject metaData = new BasicDBObject("parent", parentFileId) .append("_id", fileId); metaData.putAll(storage.insertContentsAndBackRefs(data, fileId, filename, contentType)); insertMetaData(metaData); return fileId; } public List<V7File> getChildren(V7File parent) { List<V7File> children = new ArrayList<V7File>(); for (DBObject child : files.find(new BasicDBObject("parent", parent .getId()))) { children.add(new V7File(this, child, parent)); } return children; } private void insertMetaData(DBObject metaData) throws IOException { metaData.put("_version", 1); metaData.put("created_at", new Date()); WriteResult result = files.insert(WriteConcern.SAFE, metaData); String error = result.getError(); if (error != null) throw new IOException(error); } void updateMetaData(DBObject metaData) throws IOException { metaData.put("updated_at", new Date()); try { Vermongo.update(files, metaData); } catch (UpdateConflictException e) { throw new IOException(e); } } void updateContents(DBObject metaData, byte[] contents) throws IOException { updateContents(metaData, contents, 0, contents == null ? 0 : contents.length); } void updateContents(DBObject metaData, ContentPointer newContents) throws IOException { ContentPointer oldContents = getContentPointer(metaData); if (newContents.contentEquals(oldContents)) return; String filename = (String) metaData.get("filename"); String contentType = (String) metaData.get("contentType"); Object fileId = metaData.get("_id"); BSONObject newContent = storage.updateBackRefs(newContents, fileId, filename, contentType); metaData.removeField("sha"); metaData.removeField("length"); metaData.removeField("in"); metaData.putAll(newContent); updateMetaData(metaData); } void insertContents(DBObject metaData, ContentPointer newContents) throws IOException { String filename = (String) metaData.get("filename"); String contentType = (String) metaData.get("contentType"); Object fileId = metaData.get("_id"); if (newContents != null) { BSONObject newContent = storage.updateBackRefs(newContents, fileId, filename, contentType); metaData.removeField("sha"); metaData.removeField("length"); metaData.removeField("in"); metaData.putAll(newContent); } insertMetaData(metaData); } /** * read into the buffer, continuing until the stream is finished or the * buffer is full. * * @return the number of bytes read, which could be 0 (not -1) * @throws IOException */ static int readFully(InputStream data, byte[] buffer) throws IOException { int read = data.read(buffer); if (read == -1) { return 0; } while (read < buffer.length) { int added = data.read(buffer, read, buffer.length - read); if (added == -1) return read; read += added; } return read; } void updateContents(DBObject metaData, InputStream contents, Long size) throws IOException { if (contents == null) { updateContents(metaData, (byte[]) null); return; } if (size != null) { if (size <= 1024 * 1024) { updateContents(metaData, IOUtils.toByteArray(contents, size)); return; } } updateContents(metaData, contents); } private void updateContents(DBObject metaData, InputStream contents) throws IOException { Object fileId = metaData.get("_id"); ContentPointer oldContents = getContentPointer(metaData); String filename = (String) metaData.get("filename"); String contentType = (String) metaData.get("contentType"); BSONObject newContent = storage.insertContentsAndBackRefs(contents, fileId, filename, contentType); // check if it has changed ContentPointer newContents = getContentPointer(newContent); if (newContents.contentEquals(oldContents)) return; metaData.removeField("sha"); metaData.removeField("length"); metaData.removeField("in"); metaData.putAll(newContent); updateMetaData(metaData); } private void updateContents(DBObject metaData, byte[] contents, int offset, int len) throws IOException { Object fileId = metaData.get("_id"); ContentPointer oldContents = getContentPointer(metaData); String filename = (String) metaData.get("filename"); String contentType = (String) metaData.get("contentType"); // for up to 55 bytes, storing the complete file inline // takes less space than just storing the SHA-1 and length // 20 (SHA-1) + 1 (sha - in) + 6 (length) + 4 (int32) + 2*12 // (ObjectId back-references) BSONObject newContent = storage.inlineOrInsertContentsAndBackRefs(55, contents, offset, len, fileId, filename, contentType); // check if it has changed ContentPointer newContents = getContentPointer(newContent); if (newContents.contentEquals(oldContents)) return; metaData.removeField("sha"); metaData.removeField("length"); metaData.removeField("in"); metaData.putAll(newContent); updateMetaData(metaData); } public V7File getChild(V7File parentFile, String childName) { DBObject child = files.findOne(new BasicDBObject("parent", parentFile .getId()).append("filename", childName)); if (child == null) return null; return new V7File(this, child, parentFile); } void delete(V7File file) throws IOException { // TODO: should check the version present in the db Vermongo.remove(files, file.getId(), new BasicDBObject("deleted_at", new Date())); storage.insertContentsAndBackRefs(null, file.getId(), null, null); } ContentPointer getContentPointer(BSONObject metaData) { return storage.getContentPointer(metaData); } Content getContent(BSONObject metaData) throws IOException { ContentPointer pointer = storage.getContentPointer(metaData); return storage.getContent(pointer); } }