Skip to content

Commit d36e9bd

Browse files
author
Will Banfield
committed
WIP for GridFS Spec
1 parent 6faa366 commit d36e9bd

File tree

4 files changed

+463
-3
lines changed

4 files changed

+463
-3
lines changed

src/GridFS/Bucket.php

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<?php
2+
namespace MongoDB\GridFS;
3+
4+
use MongoDB\Collection;
5+
use MongoDB\Database;
6+
use MongoDB\BSON\ObjectId;
7+
use MongoDB\Driver\ReadPreference;
8+
use MongoDB\Driver\WriteConcern;
9+
use MongoDB\Driver\Manager;
10+
use MongoDB\Exception\InvalidArgumentException;
11+
use MongoDB\Exception\InvalidArgumentTypeException;
12+
use MongoDB\Exception\RuntimeException;
13+
use MongoDB\Exception\UnexpectedValueException;
14+
/**
15+
* Bucket abstracts the GridFS files and chunks collections.
16+
*
17+
* @api
18+
*/
19+
class Bucket
20+
{
21+
private $manager;
22+
private $databaseName;
23+
private $options;
24+
private $filesCollection;
25+
private $chunksCollection;
26+
private $ensuredIndexes = false;
27+
/**
28+
* Constructs a GridFS bucket.
29+
*
30+
* Supported options:
31+
*
32+
* * bucketName (string): The bucket name, which will be used as a prefix
33+
* for the files and chunks collections. Defaults to "fs".
34+
*
35+
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
36+
* 261120 (i.e. 255 KiB).
37+
*
38+
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
39+
*
40+
* * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
41+
*
42+
* @param Manager $manager Manager instance from the driver
43+
* @param string $databaseName Database name
44+
* @param array $options Bucket options
45+
* @throws InvalidArgumentException
46+
*/
47+
public function __construct(Manager $manager, $databaseName, array $options = [])
48+
{
49+
$collectionOpts = array();
50+
$options += [
51+
'bucketName' => 'fs',
52+
'chunkSizeBytes' => 261120,
53+
];
54+
if (isset($options['bucketName']) && ! is_string($options['bucketName'])) {
55+
throw new InvalidArgumentTypeException('"bucketName" option', $options['bucketName'], 'string');
56+
}
57+
if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
58+
throw new InvalidArgumentTypeException('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
59+
}
60+
if (isset($options['readPreference'])) {
61+
if (! $options['readPreference'] instanceof ReadPreference) {
62+
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
63+
} else {
64+
$collectionOpts['readPreference'] = $options['readPreference'];
65+
}
66+
}
67+
if (isset($options['writeConcern'])) {
68+
if (! $options['writeConcern'] instanceof WriteConcern) {
69+
throw new InvalidArgumentTypeException('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\WriteConcern');
70+
} else {
71+
$collectionOpts['writeConcern'] = $options['writeConcern'];
72+
}
73+
}
74+
$this->manager = $manager;
75+
$this->databaseName = (string) $databaseName;
76+
$this->options = $options;
77+
78+
$this->filesCollection = new Collection(
79+
$manager,
80+
sprintf('%s.%s.files', $this->databaseName, $options['bucketName']),
81+
$collectionOpts
82+
);
83+
$this->chunksCollection = new Collection(
84+
$manager,
85+
sprintf('%s.%s.chunks', $this->databaseName, $options['bucketName']),
86+
$collectionOpts
87+
);
88+
}
89+
/**
90+
* Opens a Stream for reading the contents of a file specified by ID.
91+
*
92+
* @param ObjectId $id
93+
* @return Stream
94+
*/
95+
public function openDownloadStream(ObjectId $id)
96+
{
97+
fopen("gridfs://$this->databaseName/$id", "r");
98+
}
99+
/**
100+
* Downloads the contents of the stored file specified by id and writes
101+
* the contents to the destination Stream.
102+
* @param ObjectId $id
103+
* @param Stream destination
104+
*/
105+
public function downloadToStream(ObjectId $id, $destination)
106+
{
107+
$result = $this->filesCollection->findOne(["_id" => $id]);
108+
if($result == null) {
109+
return;
110+
}
111+
if($result->length == 0){
112+
return;
113+
}
114+
115+
$n=0;
116+
$results = $this->chunksCollection->find(["files_id" => $result->_id]);
117+
foreach($results as $chunk) {
118+
if($chunk->n != $n){
119+
return;
120+
}
121+
fwrite($destination, $chunk->data);
122+
$n++;
123+
}
124+
}
125+
126+
/**
127+
* Return the chunkSizeBytes option for this Bucket.
128+
*
129+
* @return integer
130+
*/
131+
public function getChunkSizeBytes()
132+
{
133+
return $this->options['chunkSizeBytes'];
134+
}
135+
/**
136+
* Opens a Stream for writing the contents of a file.
137+
*
138+
* @param string $filename file to up
139+
* @param array $options options for the stream
140+
* @return Stream uploadStream what stream to upload from
141+
*/
142+
public function openUploadStream($filename, array $options = [])
143+
{
144+
return fopen("gridfs://$this->databaseName/$filename", "w");
145+
}
146+
/**
147+
* Upload a file to this bucket by specifying the source stream file
148+
*
149+
* @param String fileName
150+
* @param Stream source
151+
* @param array Options
152+
* @return ObjectId
153+
*/
154+
155+
public function uploadFromStream($filename, $source, array $options = []) {
156+
$id = \MongoDB\BSON\ObjectId();
157+
$ctx = hash_init('md5');
158+
159+
$file_options = [];
160+
if (isset($options['contentType'])) {
161+
if(is_string($options['contentType'])) {
162+
$file_options['contentType'] = $options['contentType'];
163+
} else {
164+
throw new InvalidArgumentTypeException('"contentType" option', $options['contentType'], 'string');
165+
}
166+
}
167+
if (isset($options['aliases'])) {
168+
if(isStringArray($options['aliases'])) {
169+
$file_options['aliases'] = $options['aliases'];
170+
} else {
171+
throw new InvalidArgumentTypeException('"aliases" option', $options['aliases'], 'array of strings');
172+
}
173+
}
174+
175+
if (isset($options['metadata'])) {
176+
if(is_array($options['metadata']) || is_object($options['metadata'])) {
177+
$file_options['metadata'] = $options['metadata'];
178+
} else {
179+
throw new InvalidArgumentTypeException('"metadata" option', $options['metadata'], 'object or array');
180+
}
181+
}
182+
183+
$this->ensureIndexes();
184+
185+
$n=0;
186+
while ($data = fread($source, $this->getChunkSizeBytes())) {
187+
$toUpload = ["files_id" => $id, "n" => $n, "data" => $data];
188+
$this->chunksCollection->insertOne($toUpload);
189+
hash_update($ctx, $data);
190+
$n++;
191+
}
192+
$uploadDate = time();
193+
$md5 = hash_final($ctx, true);
194+
$file = ["chunkSize" => $this->getChunkSizeBytes(), "filename" => $filename, "uploadDate" => $uploadDate,
195+
"md5" => $md5, "length" => $this->n];
196+
$file = array_merge($file, $file_options);
197+
$doc = $this->filesCollection->insertOne($file);
198+
return $id;
199+
}
200+
201+
private function isStringArray($input) {
202+
if (!is_array($input)){
203+
return false;
204+
}
205+
foreach($input as $item) {
206+
if (!is_string($item)) {
207+
return false;
208+
}
209+
}
210+
return true;
211+
}
212+
213+
private function ensureIndexes()
214+
{
215+
// Indexes should only be ensured once before the first write operation
216+
if ($this->ensuredIndexes) {
217+
return;
218+
}
219+
if ( ! $this->isFilesCollectionEmpty()) {
220+
return;
221+
}
222+
$this->ensureFilesIndex();
223+
$this->ensureChunksIndex();
224+
$this->ensuredIndexes = true;
225+
}
226+
private function ensureChunksIndex()
227+
{
228+
foreach ($this->chunksCollection->listIndexes() as $index) {
229+
if ($index->isUnique() && $index->getKey() === ['files_id' => 1, 'n' => 1]) {
230+
return;
231+
}
232+
}
233+
$this->chunksCollection->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
234+
}
235+
private function ensureFilesIndex()
236+
{
237+
foreach ($this->filesCollection->listIndexes() as $index) {
238+
if ($index->getKey() === ['filename' => 1, 'uploadDate' => 1]) {
239+
return;
240+
}
241+
}
242+
$this->filesCollection->createIndex(['filename' => 1, 'uploadDate' => 1]);
243+
}
244+
private function isFilesCollectionEmpty()
245+
{
246+
return null === $this->filesCollection->findOne([], [
247+
'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
248+
'projection' => ['_id' => 1],
249+
]);
250+
}
251+
public function delete(ObjectId $id)
252+
{
253+
$options = array("writeConcern" => $this->writeConcern);
254+
$this->chunksCollection->deleteMany(["file_id" => $id], $options);
255+
256+
$this->filesCollection->deleteOne(["_id" => $id], $options);
257+
}
258+
}

0 commit comments

Comments
 (0)