id; } public function getPath(): string { return PRM_UPLOADS . '/' . $this->id; } public function getThumbPath(): string { return PRM_THUMBS . '/' . $this->id; } public function getRemotePath(): string { return '/uploads/' . $this->id; } public function getPublicUrl(bool $forceReal = false): string { if(!$forceReal && Config::has('Uploads', 'short_domain')) return '//' . Config::get('Uploads', 'short_domain') . '/' . $this->id; return '//' . $_SERVER['HTTP_HOST'] . $this->getRemotePath(); } public function getPublicThumbUrl(bool $forceReal = false): string { return $this->getPublicUrl($forceReal) . '.t'; } public function getUserId(): int { return $this->userId; } public function getApplicationId(): int { return $this->appId; } public function getType(): string { return $this->type; } public function isImage(): bool { return str_starts_with($this->type, 'image/'); } public function isVideo(): bool { return str_starts_with($this->type, 'video/'); } public function isAudio(): bool { return str_starts_with($this->type, 'audio/'); } public function getName(): string { return $this->name; } public function getSize(): int { return $this->size; } public function getHash(): string { return $this->hash; } public function getCreated(): int { return $this->created; } public function getLastAccessed(): int { return $this->accessed; } public function bumpAccess(IDbConnection $conn): void { if(empty($this->id)) return; $this->accessed = time(); $bump = $conn->prepare('UPDATE `prm_uploads` SET `upload_accessed` = NOW() WHERE `upload_id` = ?'); $bump->addParameter(1, $this->id, DbType::STRING); $bump->execute(); } public function getExpires(): int { return $this->expires; } public function hasExpired(): bool { return $this->expires > 1 && $this->expires <= time(); } public function getDeleted(): int { return $this->deleted; } public function isDeleted(): bool { return $this->deleted; } public function getDMCA(): int { return $this->dmca; } public function isDMCA(): bool { return $this->dmca > 0; } public function getExpiryBump(): int { return $this->bump; } public function bumpExpiry(IDbConnection $conn): void { if(empty($this->id) || $this->expires < 1) return; if($this->bump < 1) return; $this->upload_expires = time() + $this->bump; $bump = $conn->prepare('UPDATE `prm_uploads` SET `upload_expires` = NOW() + INTERVAL ? SECOND WHERE `upload_id` = ?'); $bump->addParameter(1, $this->bump, DbType::INTEGER); $bump->addParameter(2, $this->id, DbType::STRING); $bump->execute(); } public function restore(IDbConnection $conn): void { $this->upload_deleted = null; $restore = $conn->prepare('UPDATE `prm_uploads` SET `upload_deleted` = NULL WHERE `upload_id` = ?'); $restore->addParameter(1, $this->id, DbType::STRING); $restore->execute(); } public function delete(IDbConnection $conn, bool $hard): void { $this->upload_deleted = time(); if($hard) { if(is_file($this->getPath())) unlink($this->getPath()); if(is_file($this->getThumbPath())) unlink($this->getThumbPath()); if($this->dmca < 1) { $delete = $conn->prepare('DELETE FROM `prm_uploads` WHERE `upload_id` = ?'); $delete->addParameter(1, $this->id, DbType::STRING); $delete->execute(); } } else { $delete = $conn->prepare('UPDATE `prm_uploads` SET `upload_deleted` = NOW() WHERE `upload_id` = ?'); $delete->addParameter(1, $this->id, DbType::STRING); $delete->execute(); } } public function jsonSerialize(): mixed { return [ 'id' => $this->id, 'url' => $this->getPublicUrl(), 'urlf' => $this->getPublicUrl(true), 'thumb' => $this->getPublicThumbUrl(), 'name' => $this->name, 'type' => $this->type, 'size' => $this->size, 'user' => $this->userId, 'appl' => $this->appId, 'hash' => $this->hash, 'created' => date('c', $this->created), 'accessed' => $this->accessed < 1 ? null : date('c', $this->accessed), 'expires' => $this->expires < 1 ? null : date('c', $this->expires), 'deleted' => $this->deleted < 1 ? null : date('c', $this->deleted), 'dmca' => $this->dmca < 1 ? null : date('c', $this->dmca), ]; } public static function generateId(int $length = 32): string { $bytes = str_repeat("\0", $length); $chars = strlen(self::ID_CHARS); for($i = 0; $i < $length; ++$i) $bytes[$i] = self::ID_CHARS[random_int(0, $chars - 1)]; return $bytes; } public static function create( IDbConnection $conn, Application $app, User $user, string $fileName, string $fileType, string $fileSize, string $fileHash, int $fileExpiry, bool $bumpExpiry ): self { $appId = $app->getId(); $userId = $user->getId(); if(strpos($fileType, '/') === false || $fileSize < 1 || strlen($fileHash) !== 64 || $fileExpiry < 0) throw new UploadCreationFailedException('Bad args.'); $id = self::generateId(); $create = $conn->prepare( 'INSERT INTO `prm_uploads` (' . ' `upload_id`, `app_id`, `user_id`, `upload_name`,' . ' `upload_type`, `upload_size`, `upload_hash`, `upload_ip`,' . ' `upload_expires`, `upload_bump`' . ') VALUES (?, ?, ?, ?, ?, ?, UNHEX(?), INET6_ATON(?), FROM_UNIXTIME(?), ?)' ); $create->addParameter(1, $id, DbType::STRING); $create->addParameter(2, $appId < 1 ? null : $appId, DbType::INTEGER); $create->addParameter(3, $userId < 1 ? null : $userId, DbType::INTEGER); $create->addParameter(4, $fileName, DbType::STRING); $create->addParameter(5, $fileType, DbType::STRING); $create->addParameter(6, $fileSize, DbType::INTEGER); $create->addParameter(7, $fileHash, DbType::STRING); $create->addParameter(8, $_SERVER['REMOTE_ADDR'], DbType::STRING); $create->addParameter(9, $fileExpiry > 0 ? (time() + $fileExpiry) : 0, DbType::INTEGER); $create->addParameter(10, $bumpExpiry ? $fileExpiry : 0, DbType::INTEGER); $create->execute(); try { return self::byId($conn, $id); } catch(UploadNotFoundException $ex) { throw new UploadCreationFailedException; } } private static function constructDb(IDbResult $result): self { return new static( $result->getString(0), $result->getInteger(2), $result->getInteger(1), $result->getString(4), $result->getString(3), $result->getInteger(5), $result->getString(13), $result->getInteger(7), $result->getInteger(8), $result->getInteger(9), $result->getInteger(10), $result->getInteger(11), $result->getInteger(6), $result->getString(12), ); } public static function byId(IDbConnection $conn, string $id): self { $get = $conn->prepare( 'SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,' . ' UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,' . ' UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,' . ' UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,' . ' UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,' . ' UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,' . ' INET6_NTOA(`upload_ip`) AS `upload_ip`,' . ' LOWER(HEX(`upload_hash`)) AS `upload_hash`' . ' FROM `prm_uploads` WHERE `upload_id` = ? AND `upload_deleted` IS NULL' ); $get->addParameter(1, $id, DbType::STRING); $get->execute(); $result = $get->getResult(); if(!$result->next()) throw new UploadNotFoundException; return self::constructDb($result); } public static function byHash(IDbConnection $conn, string $hash): ?self { $get = $conn->prepare( 'SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,' . ' UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,' . ' UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,' . ' UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,' . ' UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,' . ' UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,' . ' INET6_NTOA(`upload_ip`) AS `upload_ip`,' . ' LOWER(HEX(`upload_hash`)) AS `upload_hash`' . ' FROM `prm_uploads` WHERE `upload_hash` = UNHEX(?)' ); $get->addParameter(1, $hash, DbType::STRING); $get->execute(); $result = $get->getResult(); if(!$result->next()) return null; return self::constructDb($result); } public static function deleted(IDbConnection $conn): array { $result = $conn->query( 'SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,' . ' UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,' . ' UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,' . ' UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,' . ' UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,' . ' UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,' . ' INET6_NTOA(`upload_ip`) AS `upload_ip`,' . ' LOWER(HEX(`upload_hash`)) AS `upload_hash`' . ' FROM `prm_uploads`' . ' WHERE `upload_deleted` IS NOT NULL' . ' OR `upload_dmca` IS NOT NULL' . ' OR `user_id` IS NULL' . ' OR `app_id` IS NULL' ); $deleted = []; while($result->next()) $deleted[] = self::constructDb($result); return $deleted; } public static function expired(IDbConnection $conn): array { $result = $conn->query( 'SELECT `upload_id`, `app_id`, `user_id`, `upload_name`, `upload_type`, `upload_size`, `upload_bump`,' . ' UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,' . ' UNIX_TIMESTAMP(`upload_accessed`) AS `upload_accessed`,' . ' UNIX_TIMESTAMP(`upload_expires`) AS `upload_expires`,' . ' UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,' . ' UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,' . ' INET6_NTOA(`upload_ip`) AS `upload_ip`,' . ' LOWER(HEX(`upload_hash`)) AS `upload_hash`' . ' FROM `prm_uploads`' . ' WHERE `upload_expires` IS NOT NULL' . ' AND `upload_expires` <= NOW()' . ' AND `upload_dmca` IS NULL' ); $expired = []; while($result->next()) $expired[] = self::constructDb($result); return $expired; } public function supportsThumbnail(): bool { return $this->isImage() || $this->isAudio() || $this->isVideo(); } public function createThumbnail(): void { $tmpFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'eeprom' . bin2hex(random_bytes(10)) . '.jpg'; try { if($this->isImage()) $imagick = new Imagick($this->getPath()); elseif($this->isAudio()) $imagick = $this->getCoverFromAudio($tmpFile); elseif($this->isVideo()) $imagick = $this->getFrameFromVideo($tmpFile); if(!isset($imagick)) return; $imagick->setImageFormat('jpg'); $imagick->setImageCompressionQuality(40); $thumbRes = 100; $width = $imagick->getImageWidth(); $height = $imagick->getImageHeight(); if ($width > $height) { $resizeWidth = $width * $thumbRes / $height; $resizeHeight = $thumbRes; } else { $resizeWidth = $thumbRes; $resizeHeight = $height * $thumbRes / $width; } $imagick->resizeImage( $resizeWidth, $resizeHeight, Imagick::FILTER_GAUSSIAN, 0.7 ); $imagick->cropImage( $thumbRes, $thumbRes, ($resizeWidth - $thumbRes) / 2, ($resizeHeight - $thumbRes) / 2 ); $imagick->writeImage($this->getThumbPath()); } catch(Exception $ex) {} if(is_file($tmpFile)) unlink($tmpFile); } private function getFrameFromVideo(string $path): Imagick { shell_exec(sprintf('ffmpeg -i %s -ss 00:00:01.000 -vframes 1 %s', $this->getPath(), $path)); return new Imagick($path); } private function getCoverFromAudio(string $path): Imagick { shell_exec(sprintf('ffmpeg -i %s -an -vcodec copy %s', $this->getPath(), $path)); return new Imagick($path); } }