/[drupal]/contributions/modules/bitcache/bitcache.inc
ViewVC logotype

Contents of /contributions/modules/bitcache/bitcache.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.21 - (show annotations) (download) (as text)
Thu Oct 1 03:48:28 2009 UTC (7 weeks, 6 days ago) by arto
Branch: MAIN
CVS Tags: HEAD
Changes since 1.20: +1 -0 lines
File MIME type: text/x-php
#432986: Ensure proc_close() is always explicitly called after proc_open().
1 <?php
2 // $Id$
3
4 //////////////////////////////////////////////////////////////////////////////
5 // Constants
6
7 @define('BITCACHE_ID_FORMAT', '/^[a-z0-9]{40}$/');
8 @define('BITCACHE_LAST_MODIFIED', TRUE);
9 @define('BITCACHE_POST_MAX_SIZE', (int)substr(ini_get('post_max_size'), 0, -1) * pow(2, array_search(strtolower(substr(ini_get('post_max_size'), -1)), array(1 => '', 10 => 'k', 20 => 'm', 30 => 'g'))));
10
11 //////////////////////////////////////////////////////////////////////////////
12 // Bitstream implementation
13
14 /**
15 * Implements a wrapper for accessing individual bitstreams.
16 */
17 class Bitcache_Stream {
18 public $id, $data, $size, $type;
19 public $compressed = FALSE;
20 public $encrypted = FALSE;
21
22 function __construct($id, $data = NULL, array $options = array()) {
23 $this->id = $id;
24 $this->data = $data;
25 foreach ($options as $k => $v) {
26 $this->$k = $v;
27 }
28 if ($this->data !== NULL) {
29 $this->stat();
30 }
31 }
32
33 /**
34 * Permits access to the bitstream's contents.
35 *
36 * @param string $mode the access type
37 * @see fopen()
38 */
39 function open($mode = 'rb') {
40 return $this->stream = NULL; // TODO
41 }
42
43 /**
44 * Closes access to the bitstream's contents.
45 *
46 * @see fclose()
47 */
48 function close() {
49 if (!empty($this->stream)) {
50 $result = FALSE; // TODO
51 unset($this->stream);
52 return $result;
53 }
54 }
55
56 function path() {
57 // TODO: register 'bitcache://' stream wrapper
58 }
59
60 /**
61 * Returns the bitstream's fingerprint.
62 */
63 function id() {
64 return $this->id;
65 }
66
67 /**
68 * Returns the bitstream's contents.
69 *
70 * @see file_get_contents()
71 */
72 function data() {
73 return $this->data;
74 }
75
76 /**
77 * Makes more information about the bitstream available.
78 */
79 function stat() {
80 if (is_null($this->size)) {
81 $this->size = $this->size();
82 }
83 if (is_null($this->type)) {
84 $this->type = $this->type();
85 }
86 }
87
88 /**
89 * Returns the bitstream's byte length.
90 */
91 function size() {
92 return !is_null($this->size) ? $this->size : $this->size = strlen($this->data);
93 }
94
95 /**
96 * Attempts to determine the bitstream's MIME content type.
97 *
98 * @see finfo_buffer()
99 */
100 function type() {
101 if (extension_loaded('fileinfo') && function_exists('finfo_buffer') && ($finfo = finfo_open(FILEINFO_MIME))) {
102 if (($type = finfo_buffer($finfo, $this->data()))) { // @since PHP 5.3.0, fileinfo 0.1.0
103 finfo_close($finfo);
104 return $type;
105 }
106 }
107
108 // Attempts to detect a file's MIME type using the Unix `file' utility.
109 // MIME content type format defined in http://www.ietf.org/rfc/rfc1521.txt
110 if (substr(PHP_OS, 0, 3) != 'WIN') { // only on Unix systems
111 if (($proc = proc_open('file -bi -', array(0 => array('pipe', 'r'), 1 => array('pipe', 'w')), $pipes))) {
112 list($stdin, $stdout, $stderr) = $pipes;
113 fwrite($stdin, $this->data());
114 fclose($stdin);
115 $output = stream_get_contents($stdout);
116 fclose($stdout);
117 proc_close($proc);
118 if (preg_match('!([\w-]+/[\w\d_+-]+)!', $output, $matches)) {
119 return $matches[1];
120 }
121 }
122 }
123
124 return 'application/octet-stream'; // default to binary
125 }
126 }
127
128 //////////////////////////////////////////////////////////////////////////////
129 // Repository implementation
130
131 /**
132 * Specifies the API contract for repository-like objects.
133 */
134 abstract class Bitcache_Repository implements Countable {
135 public $name, $uri, $options;
136
137 function create() {}
138 function rename($old_name, $new_name, array $options = array()) {}
139 function destroy() {}
140
141 function open() {}
142 function close() {}
143
144 function exists($id) {
145 return FALSE;
146 }
147
148 abstract function get($id);
149 abstract function put($id, $data);
150
151 function put_file($id, $filepath, $move = FALSE) {
152 if (($id = $this->put($id, file_get_contents($filepath))) && $move) {
153 unlink($filepath);
154 }
155 return $id;
156 }
157
158 abstract function delete($id);
159
160 function count() {
161 return iterator_count($this);
162 }
163
164 function size() {
165 $size = 0;
166 foreach ($this as $id => $stream) {
167 $size += $stream->size();
168 }
169 return $size;
170 }
171
172 protected function created($id, $data = NULL) {
173 $this->update_index($id, TRUE, !is_null($data) ? strlen($data) : NULL);
174 if (function_exists('module_invoke_all')) { // Drupal-specific
175 module_invoke_all('bitcache', 'insert', $id, is_object($data) ? $data : new Bitcache_Stream($id, $data));
176 }
177 }
178
179 protected function deleted($id) {
180 $this->update_index($id, FALSE);
181 if (function_exists('module_invoke_all')) { // Drupal-specific
182 module_invoke_all('bitcache', 'delete', $id);
183 }
184 }
185
186 protected function is_indexed() {
187 static $is_indexed = NULL;
188 if (is_null($is_indexed)) {
189 if (function_exists('db_column_exists')) { // Drupal-specific
190 $is_indexed = db_column_exists('bitcache_index', 'in_' . db_escape_string($this->name));
191 }
192 }
193 return $is_indexed;
194 }
195
196 protected function update_index($id, $available = TRUE, $size = NULL) {
197 if ($this->is_indexed()) { // Drupal-specific
198 $column = 'in_' . db_escape_string($this->name);
199 if ($available) {
200 if (!@db_query("INSERT INTO {bitcache_index} (id, size, $column) VALUES ('%s', %d, 1)", $id, $size)) {
201 @db_query("UPDATE {bitcache_index} SET $column = 1 WHERE id = '%s'", $id);
202 }
203 }
204 else {
205 @db_query("UPDATE {bitcache_index} SET $column = 0 WHERE id = '%s'", $id);
206 }
207 }
208 }
209 }
210
211 /**
212 * Implements an aggregating multi-repository proxy.
213 */
214 class Bitcache_AggregateRepository extends Bitcache_Repository implements IteratorAggregate {
215 public $repos = array();
216
217 function __construct(array $repos, array $options = array()) {
218 if (empty($repos)) {
219 trigger_error('Must be given at least one repository object', E_USER_ERROR);
220 }
221 foreach ($repos as $repo) {
222 if ($repo instanceof Bitcache_Repository) {
223 $this->repos[] = $repo;
224 }
225 }
226 }
227
228 /**
229 * Returns the sum total of the repositories' total byte sizes.
230 */
231 function size() {
232 $total = 0;
233 foreach ($this->repos as $repo) {
234 $total += $repo->size();
235 }
236 return $total;
237 }
238
239 /**
240 * Returns the sum total of the repositories' bitstream counts.
241 *
242 * @see Countable
243 */
244 function count() {
245 return iterator_count($this->getIterator());
246 }
247
248 /**
249 * Returns an AppendIterator that provides seamless iteration over all
250 * bitstreams in all constituent repositories, one after the other.
251 *
252 * @see IteratorAggregate
253 */
254 function getIterator() {
255 $iterator = new AppendIterator();
256 foreach ($this->repos as $repo) {
257 $iterator->append(method_exists($repo, 'getIterator') ? $repo->getIterator() : $repo);
258 }
259 return $iterator;
260 }
261
262 /**
263 * Checks for the specified bitstream in any and all repositories.
264 *
265 * @param string $id the bitstream identifier
266 */
267 function exists($id) {
268 return $this->op('exists', FALSE, $id);
269 }
270
271 /**
272 * Returns the specified bitstream from the first repository that has it
273 * and from which it is actually retrievable (i.e. no error occurs).
274 *
275 * @param string $id the bitstream identifier
276 * @see Bitcache_Stream
277 */
278 function get($id) {
279 return $this->op('get', array(FALSE, NULL), $id);
280 }
281
282 /**
283 * Stores the new bitstream into the first repository that accepts it.
284 *
285 * @param string $id the bitstream identifier
286 */
287 function put($id, $data) {
288 return $this->op('put', FALSE, !$id ? sha1($data) : $id, $data);
289 }
290
291 /**
292 * Stores the new bitstream into the first repository that accepts it.
293 *
294 * @param string $id the bitstream identifier
295 */
296 function put_file($id, $filepath, $move = FALSE) {
297 return $this->op('put_file', FALSE, !$id ? sha1_file($filepath) : $id, $filepath, $move);
298 }
299
300 /**
301 * Deletes the specified bitstream from any and all repositories.
302 *
303 * @param string $id the bitstream identifier
304 */
305 function delete($id) {
306 return $this->op('delete', array(TRUE, FALSE, NULL), $id);
307 }
308
309 /**
310 * Performs the same API operation on all constituent repositories until a
311 * stop condition (i.e. an affirmative result) is reached.
312 */
313 protected function op($op, $continue_while = NULL) {
314 $args = array_slice(func_get_args(), 2);
315 $continue_while = is_array($continue_while) ? $continue_while : array($continue_while);
316
317 foreach ($this->repos as $repo) {
318 $result = call_user_func_array(array($repo, $op), $args);
319 if (!in_array($result, $continue_while, TRUE)) {
320 break;
321 }
322 }
323 return $result;
324 }
325 }
326
327 //////////////////////////////////////////////////////////////////////////////
328 // Server implementation
329
330 /**
331 * Implements a Bitcache REST API-compliant HTTP server.
332 */
333 class Bitcache_Server {
334 function __construct($repo, array $options = array()) {
335 $this->repo = is_object($repo) ? $repo : new Bitcache_FileRepository(array('location' => (string)$repo)); // FIXME
336 $this->options = $options;
337 }
338
339 /**
340 * Handles OPTIONS requests. Determines the set of valid HTTP methods for
341 * the bitstream index or a bitstream resource.
342 *
343 * @param string $request the request URI
344 * @param array $query associative array of query string arguments
345 * @link <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.2>
346 */
347 function options($request, array $query = array()) {
348 $this->header('Content-Type: text/plain; charset=ascii');
349
350 if (!($id = $this->resolve('OPTIONS', $request))) {
351 // Options for the bitstream index
352 $methods = array_filter(array('OPTIONS', 'GET', 'POST'), array($this, 'allow'));
353 }
354 else {
355 // Options for a bitstream resource
356 $methods = array('OPTIONS', 'GET', 'HEAD', 'PUT', 'DELETE');
357 foreach ($methods as $key => $method) {
358 if (!$this->allow($method, $id))
359 unset($methods[$key]);
360 }
361 }
362 $this->header('Accept: ' . implode(', ', $methods));
363
364 return TRUE;
365 }
366
367 /**
368 * Handles INDEX requests. Outputs the full index of available bitstreams.
369 *
370 * @param string $request the request URI
371 * @param array $query associative array of query string arguments
372 */
373 function index($request, array $query = array(), $body = TRUE) {
374 $this->header('Content-Type: text/plain; charset=ascii');
375
376 if ($body) {
377 $this->index_text();
378 }
379
380 return TRUE;
381 }
382
383 /**
384 * Handles HEAD requests.
385 *
386 * @param string $request the request URI
387 * @param array $query associative array of query string arguments
388 * @link <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4>
389 */
390 function head($request, array $query = array()) {
391 return $this->get($request, $query, FALSE);
392 }
393
394 /**
395 * Handles GET requests.
396 *
397 * @param string $request the request URI
398 * @param array $query associative array of query string arguments
399 * @link <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3>
400 */
401 function get($request, array $query = array(), $body = TRUE) {
402 if (!($id = $this->resolve('GET', $request))) {
403 return $this->index($request, $query, $body);
404 }
405
406 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5>
407 if (!($stream = $this->repo->get($id))) {
408 return $this->abort(404, 'Not Found');
409 }
410
411 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4>
412 if ((($data = $stream->open()) || ($data = $stream->data())) === NULL) {
413 return $this->abort(503, 'Service Unavailable');
414 }
415
416 $this->headers($id, $stream);
417
418 if ($body) {
419 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13>
420 $this->header('Content-Length: ' . $stream->size());
421
422 if (is_string($data)) {
423 print $data;
424 }
425 else { // stream resource
426 $this->passthru($id, $stream);
427 }
428 }
429
430 $stream->close();
431
432 return TRUE;
433 }
434
435 /**
436 * Handles POST requests.
437 *
438 * @param string $request the request URI
439 * @param array $query associative array of query string arguments
440 * @link <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5>
441 */
442 function post($request, array $query = array()) {
443 // We only support POST requests to the bitstream index.
444 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1>
445 if ($request != '/')
446 return $this->abort(400, 'Bad Request');
447
448 $this->upload(NULL);
449
450 return TRUE;
451 }
452
453 /**
454 * Handles PUT requests.
455 *
456 * @param string $request the request URI
457 * @param array $query associative array of query string arguments
458 * @link <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>
459 */
460 function put($request, array $query = array()) {
461 $this->upload($this->resolve('PUT', $request));
462
463 return TRUE;
464 }
465
466 /**
467 * Handles DELETE requests.
468 *
469 * @param string $request the request URI
470 * @param array $query associative array of query string arguments
471 * @link <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>
472 */
473 function delete($request, array $query = array()) {
474 $id = $this->resolve('DELETE', $request);
475
476 if (!$this->repo->delete($id))
477 return $this->abort(503, 'Service Unavailable');
478
479 $this->deleted($id);
480
481 return TRUE;
482 }
483
484 protected function resolve($method, $request) {
485 if ($request[0] != '/')
486 return $this->abort(400, 'Bad Request');
487
488 $id = substr($request, 1); // strip off the leading '/'
489
490 // We don't support requests for anything else except the root
491 // collection '/' and for bitstreams identified by fingerprints of the
492 // correct length and written in all lowercase hexadecimal letters
493 // '/[a-z0-9]+'. Note that the reason for returning 400 Bad Request
494 // instead of 404 Not Found is to differentiate actually invalid
495 // requests from valid requests for an unknown bitstream.
496 if (!empty($id) && !preg_match(BITCACHE_ID_FORMAT, $id))
497 return $this->abort(400, 'Bad Request');
498
499 if (!$this->allow($method, $id))
500 return $this->abort(403, 'Forbidden');
501
502 return $id;
503 }
504
505 protected function allow($method, $id = NULL) {
506 return TRUE; // Can be overridden in subclasses
507 }
508
509 protected function upload($id = NULL) {
510 // TODO: $_SERVER['CONTENT_TYPE'] != 'application/x-www-form-urlencoded'
511
512 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.12>
513 if (!isset($_SERVER['CONTENT_LENGTH']) || !ctype_digit($_SERVER['CONTENT_LENGTH']))
514 return $this->abort(411, 'Length Required');
515
516 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.14>
517 if (BITCACHE_POST_MAX_SIZE && (int)$_SERVER['CONTENT_LENGTH'] > BITCACHE_POST_MAX_SIZE)
518 return $this->abort(413, 'Request Entity Too Large');
519
520 $tmpfile = $this->receive();
521 $sha1 = sha1_file($tmpfile);
522
523 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10>
524 // If the actual fingerprint does not match the fingerprint provided us
525 // by the client's PUT request, we are dealing either with in-transit
526 // data corruption or a buggy/malicious client. In any case, time to
527 // call it quits.
528 if (!empty($id) && $sha1 != $id)
529 return $this->abort(409, 'Conflict');
530
531 if (!$this->repo->put_file($sha1, $tmpfile, TRUE))
532 return $this->abort(503, 'Service Unavailable');
533
534 $this->created($sha1, $stream);
535
536 return TRUE;
537 }
538
539 protected function created($id, $stream) {
540 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.2>
541 $this->status(201, 'Created');
542
543 $this->header('Content-Type: application/octet-stream'); // FIXME?
544 $this->header('ETag: "' . $id . '"');
545 $this->redirect('/' . $id);
546 }
547
548 protected function deleted($id) {
549 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5>
550 $this->status(204, 'No Content');
551 }
552
553 protected function abort($code, $msg = '') {
554 $this->status($code, $msg);
555
556 $this->header('Content-Type: text/plain; charset=ascii');
557 die(trim("$code $msg") . "\n");
558 }
559
560 protected function redirect($path = '/') {
561 $this->header('Location: ' . $path);
562 }
563
564 protected function status($code, $msg = '') {
565 $this->header(trim("HTTP/1.1 $code $msg"));
566 }
567
568 protected function header($text, $replace = TRUE) {
569 header($text, $replace);
570 }
571
572 protected function headers($id, $stream) {
573 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
574 //$this->header('Accept-Ranges: bytes'); // TODO: support partial downloads
575
576 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11>
577 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19>
578 $this->header('ETag: "' . $id . '"');
579
580 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.15>
581 // A Content-SHA1 header is not specified in HTTP/1.1, but it does
582 // specify Content-MD5; this is a straightforward, if non-standard,
583 // extension and modernization of that same concept.
584 $this->header('Content-SHA1: ' . $id);
585
586 // <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17>
587 $this->header('Content-Type: ' . $stream->type());
588 }
589
590 protected function passthru($id, $stream) {
591 fpassthru($stream->stream);
592 }
593
594 protected function receive() {
595 $input = fopen('php://input', 'rb');
596 $tmpdir = function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp';
597 $tmpfile = tempnam($tmpdir, 'bitcache');
598 $output = fopen($tmpfile, 'wb');
599 stream_copy_to_stream($input, $output);
600 fclose($input);
601 fclose($output);
602 return $tmpfile;
603 }
604
605 protected function index_text() {
606 foreach ($this->repo as $id => $stream) {
607 printf("%s\t%d\n", $id, $stream->size());
608 }
609 }
610 }

  ViewVC Help
Powered by ViewVC 1.1.2