/* * Copyright (c) 2008-2014 MongoDB, Inc. * * 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. */ package com.mongodb.gridfs; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.mongodb.MongoException; import com.mongodb.util.Util; import org.bson.types.ObjectId; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; /** * <p>This class represents a GridFS file to be written to the database. Operations include:</p> * * <ul> * <li>Writing data obtained from an InputStream</li> * <li>Getting an OutputStream to stream the data out to</li> * </ul> * * @mongodb.driver.manual core/gridfs/ GridFS */ public class GridFSInputFile extends GridFSFile { private final InputStream inputStream; private final boolean closeStreamOnPersist; private boolean savedChunks = false; private byte[] buffer = null; private int currentChunkNumber = 0; private int currentBufferPosition = 0; private long totalBytes = 0; private OutputStream outputStream = null; private MessageDigest messageDigester = null; /** * Default constructor setting the GridFS file name and providing an input stream containing data to be written to the file. * * @param gridFS The GridFS connection handle. * @param inputStream Stream used for reading data from. * @param filename Name of the file to be created. * @param closeStreamOnPersist indicate the passed in input stream should be closed once the data chunk persisted */ protected GridFSInputFile(final GridFS gridFS, final InputStream inputStream, final String filename, final boolean closeStreamOnPersist) { this.fs = gridFS; this.inputStream = inputStream; this.filename = filename; this.closeStreamOnPersist = closeStreamOnPersist; this.id = new ObjectId(); this.chunkSize = GridFS.DEFAULT_CHUNKSIZE; this.uploadDate = new Date(); try { this.messageDigester = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("No MD5!"); } this.messageDigester.reset(); this.buffer = new byte[(int) chunkSize]; } /** * Default constructor setting the GridFS file name and providing an input stream containing data to be written to the file. * * @param gridFS The GridFS connection handle. * @param inputStream Stream used for reading data from. * @param filename Name of the file to be created. */ protected GridFSInputFile(final GridFS gridFS, final InputStream inputStream, final String filename) { this(gridFS, inputStream, filename, false); } /** * Constructor that only provides a file name, but does not rely on the presence of an {@link java.io.InputStream}. An {@link * java.io.OutputStream} can later be obtained for writing using the {@link #getOutputStream()} method. * * @param gridFS The GridFS connection handle. * @param filename Name of the file to be created. */ protected GridFSInputFile(final GridFS gridFS, final String filename) { this(gridFS, null, filename); } /** * Minimal constructor that does not rely on the presence of an {@link java.io.InputStream}. An {@link java.io.OutputStream} can later * be obtained for writing using the {@link #getOutputStream()} method. * * @param gridFS The GridFS connection handle. */ protected GridFSInputFile(final GridFS gridFS) { this(gridFS, null, null); } /** * Sets the ID of this GridFS file. * * @param id the file's ID. */ public void setId(final Object id) { this.id = id; } /** * Sets the file name on the GridFS entry. * * @param filename File name. */ public void setFilename(final String filename) { this.filename = filename; } /** * Sets the content type (MIME type) on the GridFS entry. * * @param contentType Content type. */ public void setContentType(final String contentType) { this.contentType = contentType; } /** * Set the chunk size. This must be called before saving any data. * * @param chunkSize The size in bytes. */ public void setChunkSize(final long chunkSize) { if (outputStream != null || savedChunks) { return; } this.chunkSize = chunkSize; buffer = new byte[(int) this.chunkSize]; } /** * Calls {@link GridFSInputFile#save(long)} with the existing chunk size. * * @throws MongoException if there's a problem saving the file. */ @Override public void save() { save(chunkSize); } /** * This method first calls saveChunks(long) if the file data has not been saved yet. Then it persists the file entry to GridFS. * * @param chunkSize Size of chunks for file in bytes. * @throws MongoException if there's a problem saving the file. */ public void save(final long chunkSize) { if (outputStream != null) { throw new MongoException("cannot mix OutputStream and regular save()"); } // note that chunkSize only changes chunkSize in case we actually save chunks // otherwise there is a risk file and chunks are not compatible if (!savedChunks) { try { saveChunks(chunkSize); } catch (IOException ioe) { throw new MongoException("couldn't save chunks", ioe); } } super.save(); } /** * Saves all data into chunks from configured {@link java.io.InputStream} input stream to GridFS. * * @return Number of the next chunk. * @throws IOException on problems reading the new entry's {@link java.io.InputStream}. * @throws MongoException if there's a failure * @see com.mongodb.gridfs.GridFSInputFile#saveChunks(long) */ public int saveChunks() throws IOException { return saveChunks(chunkSize); } /** * Saves all data into chunks from configured {@link java.io.InputStream} input stream to GridFS. A non-default chunk size can be * specified. This method does NOT save the file object itself, one must call save() to do so. * * @param chunkSize Size of chunks for file in bytes. * @return Number of the next chunk. * @throws IOException on problems reading the new entry's {@link java.io.InputStream}. * @throws MongoException if there's a failure */ public int saveChunks(final long chunkSize) throws IOException { if (outputStream != null) { throw new MongoException("Cannot mix OutputStream and regular save()"); } if (savedChunks) { throw new MongoException("Chunks already saved!"); } if (chunkSize <= 0) { throw new MongoException("chunkSize must be greater than zero"); } if (this.chunkSize != chunkSize) { this.chunkSize = chunkSize; buffer = new byte[(int) this.chunkSize]; } int bytesRead = 0; while (bytesRead >= 0) { currentBufferPosition = 0; bytesRead = _readStream2Buffer(); dumpBuffer(true); } // only finish data, do not write file, in case one wants to change metadata finishData(); return currentChunkNumber; } /** * After retrieving this {@link java.io.OutputStream}, this object will be capable of accepting successively written data to the output * stream. To completely persist this GridFS object, you must finally call the {@link java.io.OutputStream#close()} method on the output * stream. Note that calling the save() and saveChunks() methods will throw Exceptions once you obtained the OutputStream. * * @return Writable stream object. */ public OutputStream getOutputStream() { if (outputStream == null) { outputStream = new GridFSOutputStream(); } return outputStream; } /** * Dumps a new chunk into the chunks collection. Depending on the flag, also partial buffers (at the end) are going to be written * immediately. * * @param writePartial Write also partial buffers full. * @throws MongoException if there's a failure */ private void dumpBuffer(final boolean writePartial) { if ((currentBufferPosition < chunkSize) && !writePartial) { // Bail out, chunk not complete yet return; } if (currentBufferPosition == 0) { // chunk is empty, may be last chunk return; } byte[] writeBuffer = buffer; if (currentBufferPosition != chunkSize) { writeBuffer = new byte[currentBufferPosition]; System.arraycopy(buffer, 0, writeBuffer, 0, currentBufferPosition); } DBObject chunk = createChunk(id, currentChunkNumber, writeBuffer); fs.getChunksCollection().save(chunk); currentChunkNumber++; totalBytes += writeBuffer.length; messageDigester.update(writeBuffer); currentBufferPosition = 0; } /** * Creates a new chunk of this file. Can be over-ridden, if input files need to be split into chunks using a different mechanism. * * @param id the file ID * @param currentChunkNumber the unique id for this chunk * @param writeBuffer the byte array containing the data for this chunk * @return a DBObject representing this chunk. */ protected DBObject createChunk(final Object id, final int currentChunkNumber, final byte[] writeBuffer) { return new BasicDBObject("files_id", id) .append("n", currentChunkNumber) .append("data", writeBuffer); } /** * Reads a buffer full from the {@link java.io.InputStream}. * * @return Number of bytes read from stream. * @throws IOException if the reading from the stream fails. */ private int _readStream2Buffer() throws IOException { int bytesRead = 0; while (currentBufferPosition < chunkSize && bytesRead >= 0) { bytesRead = inputStream.read(buffer, currentBufferPosition, (int) chunkSize - currentBufferPosition); if (bytesRead > 0) { currentBufferPosition += bytesRead; } else if (bytesRead == 0) { throw new RuntimeException("i'm doing something wrong"); } } return bytesRead; } /** * Marks the data as fully written. This needs to be called before super.save() */ private void finishData() { if (!savedChunks) { md5 = Util.toHex(messageDigester.digest()); messageDigester = null; length = totalBytes; savedChunks = true; try { if (inputStream != null && closeStreamOnPersist) { inputStream.close(); } } catch (IOException e) { //ignore } } } /** * An output stream implementation that can be used to successively write to a GridFS file. */ private class GridFSOutputStream extends OutputStream { @Override public void write(final int b) throws IOException { byte[] byteArray = new byte[1]; byteArray[0] = (byte) (b & 0xff); write(byteArray, 0, 1); } @Override public void write(final byte[] b, final int off, final int len) throws IOException { int offset = off; int length = len; int toCopy = 0; while (length > 0) { toCopy = length; if (toCopy > chunkSize - currentBufferPosition) { toCopy = (int) chunkSize - currentBufferPosition; } System.arraycopy(b, offset, buffer, currentBufferPosition, toCopy); currentBufferPosition += toCopy; offset += toCopy; length -= toCopy; if (currentBufferPosition == chunkSize) { dumpBuffer(false); } } } /** * Processes/saves all data from {@link java.io.InputStream} and closes the potentially present {@link java.io.OutputStream}. The * GridFS file will be persisted afterwards. */ @Override public void close() { // write last buffer if needed dumpBuffer(true); // finish stream finishData(); // save file obj GridFSInputFile.super.save(); } } }