diff --git a/classes/local/store/azure_blob_storage/client.php b/classes/local/store/azure_blob_storage/client.php index e8eb9783..1a9e66d5 100644 --- a/classes/local/store/azure_blob_storage/client.php +++ b/classes/local/store/azure_blob_storage/client.php @@ -20,6 +20,7 @@ use tool_objectfs\local\store\object_client_base; // TODO does this fail when the plugin is not installed ? +// e.g. when just using AWS? use local_azureblobstorage\api; use stdClass; use Throwable; diff --git a/classes/local/store/azure_blob_storage/stream_wrapper.php b/classes/local/store/azure_blob_storage/stream_wrapper.php index aade577a..998536b5 100644 --- a/classes/local/store/azure_blob_storage/stream_wrapper.php +++ b/classes/local/store/azure_blob_storage/stream_wrapper.php @@ -1,12 +1,36 @@ . namespace tool_objectfs\local\store\azure_blob_storage; +use GuzzleHttp\Psr7\CachingStream; use GuzzleHttp\Psr7\Stream; use GuzzleHttp\Psr7\Utils; use local_azureblobstorage\api; use Throwable; +/** + * Stream wrapper. + * Allows you to use built in file function e.g. readfile, copy, unlink, etc... with blob:// file urls. + * + * @package tool_objectfs + * @author Matthew Hilton + * @copyright 2024 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class stream_wrapper { /** @var resource|null Stream context (this is set by PHP) */ public $context; @@ -107,13 +131,40 @@ private function open_read_stream() { $params = $this->getOptions(true); // TODO not sure if this is correct. - $promise = $client->get_blob($params['Key'])->wait(); - $this->body = $promise->body; + $response = $client->get_blob($params['Key'])->wait(); + $this->body = $response->getBody(); // TODO implement checking - // TODO implement CachingStream + // Wrap the body in a caching entity body if seeking is allowed. + if ($this->getOption('seekable') && !$this->body->isSeekable()) { + $this->body = new CachingStream($this->body); + } + + return true; + } + + /** + * Reads from the stream + * @param int $count bytes to read + * @return string + */ + public function stream_read($count) { + // If the file isn't readable, we need to return no content. Azure can emit XML here otherwise. + return $this->readable ? $this->body->read($count) : ''; + } + + /** + * Returns if stream is at end of file. + * @return bool + */ + public function stream_eof() { + return $this->body->eof(); + } + public function unlink($path) { + $info = $this->getcontainerkey($path); + $this->getclient()->delete_blob($info['Key'])->wait(); return true; } @@ -381,12 +432,13 @@ public function url_stat($path, $flags) { try { $params = $this->withPath($path); - $properties = $this->getclient()->get_blob_properties($params['Key'])->wait(); + $res = $this->getclient()->get_blob_properties($params['Key'])->wait(); - $stat['size'] = $stat[7] = $bp->getContentLength(); + $stat['size'] = $stat[7] = current($res->getHeader('Content-Length')); // Set the modification time and last modified to the Last-Modified header. - $lastmodified = $bp->getLastModified()->getTimestamp(); + // Convert from the + $lastmodified = strtotime(current($res->getHeader('Last-Modified'))); $stat['mtime'] = $stat[9] = $lastmodified; $stat['ctime'] = $stat[10] = $lastmodified; @@ -398,7 +450,7 @@ public function url_stat($path, $flags) { // TODO better exception } catch (Throwable $ex) { - // The specified blob does not exist. + // It's most likely the specified blob does not exist. return false; } } @@ -416,4 +468,27 @@ private function withpath($path) { // TODO rewrite return $this->getContainerKey($path) + $params; } + + /** + * Returns the size of the opened object body. + * + * @return int|null + */ + private function getsize() { + $size = $this->body->getSize(); + + return $size !== null ? $size : $this->size; + } + + /** + * stream_stat + * @return array + */ + public function stream_stat() { + $stat = $this->getStatTemplate(); + $stat[7] = $stat['size'] = $this->getSize(); + $stat[2] = $stat['mode'] = $this->mode; + + return $stat; + } } \ No newline at end of file