WritableStream.php 7.16 KB
Newer Older
1 2 3 4 5 6 7
<?php

namespace MongoDB\GridFS;

use MongoDB\BSON\Binary;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
8
use MongoDB\Driver\Exception\Exception as DriverException;
9
use MongoDB\Exception\InvalidArgumentException;
10 11

/**
12
 * WritableStream abstracts the process of writing a GridFS file.
13 14 15
 *
 * @internal
 */
16
class WritableStream
17
{
18 19
    private static $defaultChunkSizeBytes = 261120;

20 21 22 23
    private $buffer;
    private $bufferLength = 0;
    private $chunkOffset = 0;
    private $chunkSize;
24
    private $collectionWrapper;
25 26 27 28 29 30
    private $ctx;
    private $file;
    private $isClosed = false;
    private $length = 0;

    /**
31
     * Constructs a writable GridFS stream.
32 33 34
     *
     * Supported options:
     *
35 36
     *  * _id (mixed): File document identifier. Defaults to a new ObjectId.
     *
37 38 39 40 41 42 43 44 45 46 47 48 49
     *  * aliases (array of strings): DEPRECATED An array of aliases. 
     *    Applications wishing to store aliases should add an aliases field to
     *    the metadata document instead.
     *
     *  * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
     *    261120 (i.e. 255 KiB).
     *
     *  * contentType (string): DEPRECATED content type to be stored with the
     *    file. This information should now be added to the metadata.
     *
     *  * metadata (document): User data for the "metadata" field of the files
     *    collection document.
     *
50
     * @param CollectionWrapper $collectionWrapper GridFS collection wrapper
51
     * @param string            $filename          Filename
52
     * @param array             $options           Upload options
53
     * @throws InvalidArgumentException
54
     */
55
    public function __construct(CollectionWrapper $collectionWrapper, $filename, array $options = [])
56
    {
57 58 59 60
        $options += [
            '_id' => new ObjectId,
            'chunkSizeBytes' => self::$defaultChunkSizeBytes,
        ];
61 62

        if (isset($options['aliases']) && ! \MongoDB\is_string_array($options['aliases'])) {
63
            throw InvalidArgumentException::invalidType('"aliases" option', $options['aliases'], 'array of strings');
64 65
        }

66 67 68 69
        if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
            throw InvalidArgumentException::invalidType('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
        }

70
        if (isset($options['contentType']) && ! is_string($options['contentType'])) {
71
            throw InvalidArgumentException::invalidType('"contentType" option', $options['contentType'], 'string');
72 73 74
        }

        if (isset($options['metadata']) && ! is_array($options['metadata']) && ! is_object($options['metadata'])) {
75
            throw InvalidArgumentException::invalidType('"metadata" option', $options['metadata'], 'array or object');
76 77 78
        }

        $this->chunkSize = $options['chunkSizeBytes'];
79
        $this->collectionWrapper = $collectionWrapper;
80 81 82 83
        $this->buffer = fopen('php://temp', 'w+');
        $this->ctx = hash_init('md5');

        $this->file = [
84
            '_id' => $options['_id'],
85 86
            'chunkSize' => $this->chunkSize,
            'filename' => (string) $filename,
87
            // TODO: This is necessary until PHPC-536 is implemented
88
            'uploadDate' => new UTCDateTime((int) floor(microtime(true) * 1000)),
89 90 91
        ] + array_intersect_key($options, ['aliases' => 1, 'contentType' => 1, 'metadata' => 1]);
    }

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
    /**
     * Return internal properties for debugging purposes.
     *
     * @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
     * @return array
     */
    public function __debugInfo()
    {
        return [
            'bucketName' => $this->collectionWrapper->getBucketName(),
            'databaseName' => $this->collectionWrapper->getDatabaseName(),
            'file' => $this->file,
        ];
    }

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
    /**
     * Closes an active stream and flushes all buffered data to GridFS.
     */
    public function close()
    {
        if ($this->isClosed) {
            // TODO: Should this be an error condition? e.g. BadMethodCallException
            return;
        }

        rewind($this->buffer);
        $cached = stream_get_contents($this->buffer);

        if (strlen($cached) > 0) {
            $this->insertChunk($cached);
        }

        fclose($this->buffer);
        $this->fileCollectionInsert();
        $this->isClosed = true;
    }

129
    /**
130
     * Return the stream's file document.
131
     *
132
     * @return stdClass
133
     */
134
    public function getFile()
135
    {
136
        return (object) $this->file;
137 138
    }

139 140 141 142 143 144 145
    /**
     * Return the stream's size in bytes.
     *
     * Note: this value will increase as more data is written to the stream.
     *
     * @return integer
     */
146 147 148 149 150 151 152 153 154 155 156 157 158
    public function getSize()
    {
        return $this->length;
    }

    /**
     * Inserts binary data into GridFS via chunks.
     *
     * Data will be buffered internally until chunkSizeBytes are accumulated, at
     * which point a chunk's worth of data will be inserted and the buffer
     * reset.
     *
     * @param string $toWrite Binary data to write
159
     * @return integer
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
     */
    public function insertChunks($toWrite)
    {
        if ($this->isClosed) {
            // TODO: Should this be an error condition? e.g. BadMethodCallException
            return;
        }

        $readBytes = 0;

        while ($readBytes != strlen($toWrite)) {
            $addToBuffer = substr($toWrite, $readBytes, $this->chunkSize - $this->bufferLength);
            fwrite($this->buffer, $addToBuffer);
            $readBytes += strlen($addToBuffer);
            $this->bufferLength += strlen($addToBuffer);

            if ($this->bufferLength == $this->chunkSize) {
                rewind($this->buffer);
                $this->insertChunk(stream_get_contents($this->buffer));
                ftruncate($this->buffer, 0);
                $this->bufferLength = 0;
            }
        }

        return $readBytes;
    }

    private function abort()
    {
189
        $this->collectionWrapper->deleteChunksByFilesId($this->file['_id']);
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
        $this->isClosed = true;
    }

    private function fileCollectionInsert()
    {
        if ($this->isClosed) {
            // TODO: Should this be an error condition? e.g. BadMethodCallException
            return;
        }

        $md5 = hash_final($this->ctx);

        $this->file['length'] = $this->length;
        $this->file['md5'] = $md5;

205
        $this->collectionWrapper->insertFile($this->file);
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224

        return $this->file['_id'];
    }

    private function insertChunk($data)
    {
        if ($this->isClosed) {
            // TODO: Should this be an error condition? e.g. BadMethodCallException
            return;
        }

        $toUpload = [
            'files_id' => $this->file['_id'],
            'n' => $this->chunkOffset,
            'data' => new Binary($data, Binary::TYPE_GENERIC),
        ];

        hash_update($this->ctx, $data);

225
        $this->collectionWrapper->insertChunk($toUpload);
226 227 228 229 230 231 232 233
        $this->length += strlen($data);
        $this->chunkOffset++;
    }

    private function readChunk($source)
    {
        try {
            $data = fread($source, $this->chunkSize);
234
        } catch (DriverException $e) {
235 236 237 238 239 240 241
            $this->abort();
            throw $e;
        }

        return $data;
    }
}