aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade
diff options
context:
space:
mode:
Diffstat (limited to 'Digitigrade')
-rw-r--r--Digitigrade/Model/StorageObject.php107
-rw-r--r--Digitigrade/StorageProvider.php62
-rw-r--r--Digitigrade/StorageProvider/FilesystemStorage.php52
3 files changed, 221 insertions, 0 deletions
diff --git a/Digitigrade/Model/StorageObject.php b/Digitigrade/Model/StorageObject.php
new file mode 100644
index 0000000..333b5b1
--- /dev/null
+++ b/Digitigrade/Model/StorageObject.php
@@ -0,0 +1,107 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\Model;
+use Digitigrade\StorageProvider;
+
+class StorageObject extends Model {
+ public string $oid;
+ public string $provider;
+ public string $token;
+ public string $filename;
+ public string $mimetype;
+ public \DateTimeImmutable $created;
+
+ /**
+ * Creates a new storage object on the given provider by consuming a local file
+ * @param string $provider class name of the storage provider to use
+ * @param string $path path to local file (will be deleted)
+ * @param bool $checkIsUpload if true, ensures that the local file was uploaded via HTTP
+ * @return self
+ */
+ public static function createFromFile(string $provider, string $path, string $filename, bool $checkIsUpload = false): self {
+ if (!is_file($path)) {
+ throw new \RuntimeException('local file does not exist');
+ }
+ $mimetype = mime_content_type($path);
+ $p = new $provider();
+ assert($p instanceof StorageProvider);
+ $token = $p->storeFile($path, $checkIsUpload);
+
+ $obj = new self();
+ $obj->oid = random_printable_bytes();
+ $obj->provider = $p::class;
+ $obj->token = $token;
+ $obj->filename = $filename;
+ $obj->mimetype = $mimetype;
+ $obj->created = new \DateTimeImmutable();
+
+ $obj->save();
+ return $obj;
+ }
+
+ public static function findByOid(string $oid): ?self {
+ return self::findWhere('oid = ?', [$oid]);
+ }
+
+ public static function findByProviderTokenPair(string $provider, string $token): ?self {
+ return self::findWhere('provider = ? AND token = ?', [$provider, $token]);
+ }
+
+ protected function getUpdateWhereClause(\PDO $db): ?string {
+ if (isset($this->oid) && self::findByOid($this->oid) != null) {
+ return 'oid = ' . $db->quote($this->oid);
+ }
+ if (self::findByProviderTokenPair($this->provider, $this->token) != null) {
+ return 'provider = ' . $db->quote($this->provider) . ' AND token = ' . $db->quote($this->token);
+ }
+ return null;
+ }
+
+ private function getProvider(): StorageProvider {
+ return new $this->provider();
+ }
+
+ /**
+ * Deletes this object from the storage backend and removes it from the database.
+ * @return void
+ */
+ public function delete() {
+ $this->getProvider()->delete($this->token);
+ $this->remove();
+ }
+
+ /**
+ * Gets the entire content of this object.
+ * @return string
+ */
+ public function retrieve(): string {
+ return $this->getProvider()->retrieve($this->token);
+ }
+
+ /**
+ * Streams the content of this object to the web client (stdout).
+ * @return void
+ */
+ public function passthrough() {
+ $this->getProvider()->passthrough($this->token, $this->mimetype);
+ }
+
+ /**
+ * Gets a direct public URL to this object, if one exists.
+ * @return ?string the URL or null if none is available
+ */
+ public function getDirectUrl(): ?string {
+ return $this->getProvider()->directUrl($this->token);
+ }
+
+ public function getUrl(bool $allowDirect = true): string {
+ if ($allowDirect) {
+ $url = $this->getDirectUrl();
+ if ($url != null) {
+ return $url;
+ }
+ }
+ return path_to_uri("/storage/$this->oid");
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/StorageProvider.php b/Digitigrade/StorageProvider.php
new file mode 100644
index 0000000..53b3549
--- /dev/null
+++ b/Digitigrade/StorageProvider.php
@@ -0,0 +1,62 @@
+<?php
+namespace Digitigrade;
+
+abstract class StorageProvider {
+ public function __construct() {
+ }
+
+ /**
+ * Moves a file from the local filesystem to the storage backend. Deletes
+ * the file from the local filesystem.
+ * @param string $path the path to the file to store
+ * @param bool $checkIsUpload if true, verifies that the local file was
+ * uploaded via PHP's HHTP upload mechanism before doing anything with it
+ * @return ?string a token that uniquely identifies the new object on this
+ * provider, or null if it could not be stored for any reason
+ */
+ public function storeFile(string $path, bool $checkIsUpload = false): ?string {
+ if ($checkIsUpload && !is_uploaded_file($path)) {
+ return null;
+ }
+ return $this->storeFileInternal($path);
+ }
+
+ abstract protected function storeFileInternal(string $path): ?string;
+
+ /**
+ * @param string $token the token of the object to look for
+ * @return bool whether the requested object exists or not
+ */
+ abstract public function exists(string $token): bool;
+
+ /**
+ * Retrieves an object's contents from the storage backend.
+ * @param string $token the token of the object to retrieve
+ * @return ?string the entire content, or null if not found
+ */
+ abstract public function retrieve(string $token): ?string;
+
+ /**
+ * Gets a direct, public URL to the given object, if possible.
+ * @param string $token the token of the object to look up
+ * @return ?string the URL or null if one is not available
+ */
+ abstract public function directUrl(string $token): ?string;
+
+ /**
+ * Streams the data of an object to the web client (stdout).
+ * @param string $token the token of the object to retrieve
+ * @param ?string $mimetype value of the Content-Type header to send (if
+ * null, will try to figure it out anyway)
+ * @return void
+ */
+ abstract public function passthrough(string $token, ?string $mimetype = null);
+
+ /**
+ * Deletes an object from the backend. If the object doesn't exist, does
+ * nothing.
+ * @param string $token the token of the object to delete
+ * @return void
+ */
+ abstract public function delete(string $token);
+} \ No newline at end of file
diff --git a/Digitigrade/StorageProvider/FilesystemStorage.php b/Digitigrade/StorageProvider/FilesystemStorage.php
new file mode 100644
index 0000000..8f67653
--- /dev/null
+++ b/Digitigrade/StorageProvider/FilesystemStorage.php
@@ -0,0 +1,52 @@
+<?php
+namespace Digitigrade\StorageProvider;
+
+use Digitigrade\StorageProvider;
+
+class FilesystemStorage extends StorageProvider {
+ private const BASE_PATH = __DIR__ . '/../../upload/';
+
+ private static function tokenToPath(string $token): string {
+ return self::BASE_PATH . $token;
+ }
+
+ protected function storeFileInternal(string $path): ?string {
+ $token = hash_file('sha256', $path);
+ rename($path, self::tokenToPath($token));
+ return $token;
+ }
+
+ public function exists(string $token): bool {
+ return is_file(self::tokenToPath($token));
+ }
+
+ public function retrieve(string $token): ?string {
+ $path = self::tokenToPath($token);
+ if (!is_readable($path)) {
+ return null;
+ }
+ return file_get_contents($path);
+ }
+
+ public function directUrl(string $token): ?string {
+ return path_to_uri("/upload/$token");
+ }
+
+ public function passthrough(string $token, ?string $mimetype = null) {
+ if (!$this->exists($token)) {
+ throw new \RuntimeException('requested object does not exist');
+ }
+ $path = self::tokenToPath($token);
+ if (!headers_sent()) {
+ header('Content-Type: ' . ($mimetype ?? (mime_content_type($path) ?: 'application/octet-stream')));
+ }
+ readfile($path);
+ }
+
+ public function delete(string $token) {
+ $path = self::tokenToPath($token);
+ if (is_file($path)) {
+ unlink($path);
+ }
+ }
+} \ No newline at end of file