| 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 |
}
|