Skip to content

Commit bfa6eea

Browse files
committed
wip
1 parent ed4cfa1 commit bfa6eea

File tree

4 files changed

+472
-1
lines changed

4 files changed

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

0 commit comments

Comments
 (0)