post_item(), ['/item', 'GET'] => get_item(), //['/search', 'GET'] => get_search(), default => Json::error('Not implemented') })->die(); function get_item() { $id = $_GET['q'] ?? false; if (!$id) Json::error('ID not specified')->die(); $media_db = new MediaDB(); $item = Item::load($media_db, $id); if ($item === null) { Json::error('No item was found with the given ID')->die(); } return Json::new($item->getUri()); } function post_item() { // The checks are performed internally $item = Item::upload($_FILES['file']); return Json::new([ 'type' => 'success', 'id' => $item->getHash(), 'link' => $item->getUri(), ]); }; class MediaDB { private $handler; private bool $finished = false; private array $lines = []; public function __construct() { // Just initialize the file handler $this->handler = fopen($GLOBALS['path_mediadb'], 'r'); if ($this->handler === false) Json::error('Error opening media DB'); } private function getLine(): ?array { $ret = fgetcsv($this->handler, 0, ' '); if ($ret === false) return null; return $ret; } public function map(callable $func) { // First read from what's already in memory foreach ($this->lines as $i_line) { if ($func($i_line) === false) return; } // REVIEW: Maybe this isn't needed cuz the while would be as eficient // anyway? // If we already got to the end, there's no sense in carrying on if ($this->finished) return; // If we run out, read from the file and append to array so the next lookup // is fast while ($line = $this->getLine()) { $this->lines[] = $line; if ($func($line) === false) return; } // Set the flag so we don't reach this point two times $this->finished = true; } } class Item { // TODO: Change to id here in all occurrences private string $hash; private string $extension; private array $tags = []; public static function load(MediaDB $db, string $id): ?Item { $ret = null; $db->map(function ($line) use (&$ret, $id) { if ($line[0] == $id) { $ret = new Item(); $ret->hash = array_shift($line); $ret->tags = $line; $ret->extensionFromTags(); return true; } return false; }); return $ret; } // ::upload() requires the PHP file upload ARRAY from $_FILES public static function upload(array $php_file): Item { $from_path = $php_file['tmp_name'] ?? null; // --- CHECKS --- if (!is_string($from_path)) Json::error('Passed invalid upload structure')->die(); if (!is_uploaded_file($from_path)) Json::error('Trying to upload illegal or non-existent file')->die(); // --- INITIALIZE --- $ret = new Item(); $ret->hash = hash_file('sha256', $from_path); $ret->extensionFromUpload($php_file); // --- ACTUALLY GRAB FILE --- $new_path = $ret->getPath(); if (file_exists($new_path)) Json::error('File already exists')->die(); if (!move_uploaded_file($from_path, $new_path)) Json::error('Failed to move uploaded file')->die(); return $ret; } private function extensionFromTags() { foreach ($this->tags as $i_tag) { $parts = explode(':', $i_tag); if ($parts[0] == 'format') { $this->extension = $parts[1]; return; } } } // Assumes is_uploaded_file has been done private function extensionFromUpload(array $php_file) { $path = $php_file['tmp_name'] ?? false; $name = $php_file['name'] ?? null; // Try to get extension from mimetype $ext = (new finfo(FILEINFO_EXTENSION))->file($path); // REVIEW: Maybe change it so it doesn't have to be separated like this but // do somethign like ['mime/type', null] be equivalent to this? if (array_search($ext, ['webp', 'png', 'jpeg']) !== false) { $this->extension = $ext; return; } // If it doesn't work, try to figure it out from the original extension if ($ext == '???') { // We mustn't accept everything straight away, so we're only accepting // mime-type and original extension combinations that make sense $whitelist = [ ['application/zip', 'kra'], ['application/zip', 'krz'], ]; // NOTE: The original extension has to be grabbed from the original name $mime = mime_content_type($path); $ext = pathinfo($name, PATHINFO_EXTENSION); if (array_search([$mime, $ext], $whitelist) !== false) { $this->extension = $ext; return; } } Json::error('File mime-type and or extension not allowed.')->die(); } public function getHash(): string { return $this->hash; } public function getExtension(): string { return $this->extension; } public function getPath(bool $absolute = false): string { return sprintf( '%s/%s.%s', $GLOBALS[$absolute ? 'path_media' : 'path_media'], $this->hash, $this->extension ); } public function getUri(): string { // TODO: Implement relative return sprintf( '%s/%s/%s.%s', $GLOBALS['base_root'], $GLOBALS['base_media'], $this->hash, $this->extension, ); } } class Json { function __construct( private mixed $struct ) { $this->struct = $struct; } public static function new(mixed $struct): Json { return new Json($struct); } public static function error(mixed $msg): Json { return new Json([ 'type' => 'error', 'msg' => $msg, ]); } public function die() { print(json_encode($this->struct)); die(); } }