Skip to content

Commit 4278a32

Browse files
committed
wip
1 parent ed4cfa1 commit 4278a32

File tree

4 files changed

+455
-1
lines changed

4 files changed

+455
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
],
1212
"require": {
1313
"php": ">=5.4",
14-
"ext-mongodb": "^1.0.0"
14+
"ext-mongodb": "^1.0.0",
15+
"ext-hash": "*"
1516
},
1617
"autoload": {
1718
"psr-4": { "MongoDB\\": "src/" },

src/GridFS/Bucket.php

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
namespace MongoDB\GridFS;
4+
5+
use MongoDB\Collection;
6+
use MongoDB\Database;
7+
use MongoDB\BSON\ObjectId;
8+
use MongoDB\Driver\ReadPreference;
9+
use MongoDB\Driver\WriteConcern;
10+
use MongoDB\Exception\InvalidArgumentException;
11+
use MongoDB\Exception\InvalidArgumentTypeException;
12+
use MongoDB\Exception\RuntimeException;
13+
use MongoDB\Exception\UnexpectedValueException;
14+
15+
/**
16+
* Bucket abstracts the GridFS files and chunks collections.
17+
*
18+
* @api
19+
*/
20+
class Bucket
21+
{
22+
private $manager;
23+
private $databaseName;
24+
private $options;
25+
26+
private $filesCollection;
27+
private $chunksCollection;
28+
29+
private $ensuredIndexes = false;
30+
31+
/**
32+
* Constructs a GridFS bucket.
33+
*
34+
* Supported options:
35+
*
36+
* * bucketName (string): The bucket name, which will be used as a prefix
37+
* for the files and chunks collections. Defaults to "fs".
38+
*
39+
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to
40+
* 261120 (i.e. 255 KiB).
41+
*
42+
* * readPreference (MongoDB\Driver\ReadPreference): Read preference.
43+
*
44+
* * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
45+
*
46+
* @param Manager $manager Manager instance from the driver
47+
* @param string $databaseName Database name
48+
* @param array $options Bucket options
49+
* @throws InvalidArgumentException
50+
*/
51+
public function __construct(Manager $manager, $databaseName, array $options = [])
52+
{
53+
$options += [
54+
'bucketName' => 'fs',
55+
'chunkSizeBytes' => 261120,
56+
];
57+
58+
if (isset($options['bucketName']) && ! is_string($options['bucketName'])) {
59+
throw new InvalidArgumentTypeException('"bucketName" option', $options['bucketName'], 'string');
60+
}
61+
62+
if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
63+
throw new InvalidArgumentTypeException('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
64+
}
65+
66+
if (isset($options['readPreference']) && ! $options['readPreference'] instanceof ReadPreference) {
67+
throw new InvalidArgumentTypeException('"readPreference" option', $options['readPreference'], 'MongoDB\Driver\ReadPreference');
68+
}
69+
70+
if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
71+
throw new InvalidArgumentTypeException('"writeConcern" option', $options['writeConcern'], 'MongoDB\Driver\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+
isset($options['writeConcern']) ? $options['writeConcern'] : null,
82+
isset($options['readPreference']) ? $options['readPreference'] : null,
83+
);
84+
85+
$this->chunksCollection = new Collection(
86+
$manager,
87+
sprintf('%s.%s.chunks', $this->databaseName, $options['bucketName']),
88+
isset($options['writeConcern']) ? $options['writeConcern'] : null,
89+
isset($options['readPreference']) ? $options['readPreference'] : null,
90+
);
91+
}
92+
93+
/**
94+
* Opens a Stream for reading the contents of a file specified by ID.
95+
*
96+
* @param ObjectId $id
97+
* @return Stream
98+
*/
99+
public function openDownloadStream(ObjectId $id)
100+
{
101+
102+
}
103+
104+
/**
105+
* Return the chunkSizeBytes option for this Bucket.
106+
*
107+
* @return integer
108+
*/
109+
public function getChunkSizeBytes()
110+
{
111+
return $this->options['chunkSizeBytes'];
112+
}
113+
114+
/**
115+
* Opens a Stream for writing the contents of a file.
116+
*
117+
* @param ObjectId $id
118+
* @return Stream
119+
*/
120+
public function openUploadStream($filename, array $options = [])
121+
{
122+
123+
}
124+
125+
private function ensureIndexes()
126+
{
127+
// Indexes should only be ensured once before the first write operation
128+
if ($this->ensuredIndexes) {
129+
return;
130+
}
131+
132+
if ( ! $this->isFilesCollectionEmpty()) {
133+
return;
134+
}
135+
136+
$this->ensureFilesIndex();
137+
$this->ensureChunksIndex();
138+
139+
$this->ensuredIndexes = true;
140+
}
141+
142+
private function ensureChunksIndex()
143+
{
144+
foreach ($this->chunksCollection->listIndexes() as $index) {
145+
if ($index->isUnique() && $index->getKey() === ['files_id' => 1, 'n' => 1]) {
146+
return;
147+
}
148+
}
149+
150+
$this->chunksCollection->createIndex(['files_id' => 1, 'n' => 1], ['unique' => true]);
151+
}
152+
153+
private function ensureFilesIndex()
154+
{
155+
foreach ($this->filesCollection->listIndexes() as $index) {
156+
if ($index->getKey() === ['filename' => 1, 'uploadDate' => 1]) {
157+
return;
158+
}
159+
}
160+
161+
$this->filesCollection->createIndex(['filename' => 1, 'uploadDate' => 1]);
162+
}
163+
164+
private function isFilesCollectionEmpty()
165+
{
166+
return null === $this->filesCollection->findOne([], [
167+
'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
168+
'projection' => ['_id' => 1],
169+
]);
170+
}
171+
}

src/GridFS/StreamWrapper.php

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
namespace MongoDB\GridFS;
4+
5+
use MongoDB\Driver\Server;
6+
use MongoDB\Exception\InvalidArgumentException;
7+
use MongoDB\Exception\InvalidArgumentTypeException;
8+
use MongoDB\Exception\RuntimeException;
9+
use MongoDB\Exception\UnexpectedValueException;
10+
11+
/**
12+
* Stream wrapper for reading and writing a GridFS file.
13+
*
14+
* @internal
15+
* @see MongoDB\GridFS\Bucket::openUploadStream()
16+
*/
17+
class StreamWrapper
18+
{
19+
public $context;
20+
21+
private $bucket;
22+
private $filename;
23+
private $options;
24+
25+
private $protocol = 'gridfs';
26+
27+
private $at;
28+
private $hashContext;
29+
30+
/**
31+
* Constructs a writable upload stream.
32+
*
33+
* Supported options:
34+
*
35+
* * chunkSizeBytes (integer): The number of bytes per chunk of this file.
36+
* Defaults to the chunkSizeBytes of the Bucket.
37+
*
38+
* * metadata (document): User data for the "metadata" field of the files
39+
* collection document.
40+
*
41+
* The following options are deprecated:
42+
*
43+
* * aliases (string[]): An array of aliases (i.e. filenames). Applications
44+
* wishing to store aliases should add an aliases field to the metadata
45+
* document instead.
46+
*
47+
* * contentType (string): A valid MIME type. Applications wishing to store
48+
* a contentType should add a contentType field to the metadata document
49+
* instead.
50+
*
51+
* @param Bucket $bucket Database name
52+
* @param string $filename Filename
53+
* @param array $options Upload options
54+
* @throws InvalidArgumentException
55+
*/
56+
public function __construct(Bucket $bucket, $filename, array $options = [])
57+
{
58+
$options += [
59+
'chunkSizeBytes' => $bucket->getChunkSizeBytes(),
60+
];
61+
62+
if (isset($options['chunkSizeBytes']) && ! is_integer($options['chunkSizeBytes'])) {
63+
throw new InvalidArgumentTypeException('"chunkSizeBytes" option', $options['chunkSizeBytes'], 'integer');
64+
}
65+
66+
if (isset($options['metadata']) && ! is_array($options['metadata']) && ! is_object($options['metadata'])) {
67+
throw new InvalidArgumentTypeException('"metadata" option', $options['metadata'], 'array or object');
68+
}
69+
70+
if (isset($options['aliases'])) {
71+
if ( ! is_array($options['aliases'])) {
72+
throw new InvalidArgumentTypeException('"aliases" option', $options['aliases'], 'array or object');
73+
}
74+
75+
$expectedIndex = 0;
76+
77+
foreach ($options['aliases'] as $i => $alias) {
78+
if ($i !== $expectedIndex) {
79+
throw new InvalidArgumentException(sprintf('"aliases" option is not a list (unexpected index: "%s")', $i));
80+
}
81+
82+
if ( ! is_string($alias)) {
83+
throw new InvalidArgumentTypeException(sprintf('$options["aliases"][%d]', $i), $alias, 'string');
84+
}
85+
86+
$expectedIndex += 1;
87+
}
88+
}
89+
90+
if (isset($options['contentType']) && ! is_string($options['contentType'])) {
91+
throw new InvalidArgumentTypeException('"contentType" option', $options['contentType'], 'string');
92+
}
93+
94+
$this->bucket = $bucket;
95+
$this->filename = (string) $filename;
96+
$this->options = $options;
97+
$this->hashContext = hash_init('md5');
98+
}
99+
100+
public function stream_write($data)
101+
{
102+
hash_update($this->hashContext, $data);
103+
104+
//fopen('php://memory', )
105+
}
106+
107+
/**
108+
* Register the GridFS stream wrapper.
109+
*
110+
* @param Manager $manager Manager instance from the driver
111+
* @param string $protocol Protocol to register
112+
*/
113+
public static function register(Manager $manager, $protocol = 'gridfs')
114+
{
115+
if (in_array($protocol, stream_get_wrappers())) {
116+
stream_wrapper_unregister($protocol);
117+
}
118+
119+
// Set the client passed in as the default stream context client
120+
stream_wrapper_register($protocol, get_called_class(), STREAM_IS_URL);
121+
$default = stream_context_get_options(stream_context_get_default());
122+
$default[$protocol]['manager'] = $manager;
123+
stream_context_set_default($default);
124+
}
125+
126+
public function stream_open($path, $mode, $options, &$openedPath)
127+
{
128+
$this->initProtocol($path);
129+
$this->params = $this->getDatabase($path);
130+
$this->mode = rtrim($mode, 'bt');
131+
132+
if ($errors = $this->validate($path, $this->mode)) {
133+
return $this->triggerError($errors);
134+
}
135+
136+
return $this->boolCall(function() use ($path) {
137+
switch ($this->mode) {
138+
case 'r': return $this->openReadStream($path);
139+
case 'a': return $this->openAppendStream($path);
140+
default: return $this->openWriteStream($path);
141+
}
142+
});
143+
}
144+
145+
private function validate($path, $mode)
146+
{
147+
$errors = [];
148+
149+
if (!in_array($mode, ['r', 'w', 'a', 'x'])) {
150+
$errors[] = "Mode not supported: {$mode}. "
151+
. "Use one 'r', 'w', 'a', or 'x'.";
152+
}
153+
154+
return $errors;
155+
}
156+
157+
/**
158+
* Trigger one or more errors
159+
*
160+
* @param string|array $errors Errors to trigger
161+
* @param mixed $flags If set to STREAM_URL_STAT_QUIET, then no
162+
* error or exception occurs
163+
*
164+
* @return bool Returns false
165+
* @throws \RuntimeException if throw_errors is true
166+
*/
167+
private function triggerError($errors, $flags = null)
168+
{
169+
// This is triggered with things like file_exists()
170+
if ($flags & STREAM_URL_STAT_QUIET) {
171+
return $flags & STREAM_URL_STAT_LINK
172+
// This is triggered for things like is_link()
173+
? $this->formatUrlStat(false)
174+
: false;
175+
}
176+
// This is triggered when doing things like lstat() or stat()
177+
trigger_error(implode("\n", (array) $errors), E_USER_WARNING);
178+
return false;
179+
}
180+
}

0 commit comments

Comments
 (0)