element.dispatchEvent(event);
if (response.endpointThumbnails !== "") {
- // TODO: Dispatch the request to generate thumbnails.
+ void (await prepareRequest(response.endpointThumbnails).get().fetchAsResponse());
+ // TODO: Handle errors and notify about the new thumbnails.
}
}
} catch (e) {
try {
const response = (await (0, Backend_1.prepareRequest)(endpoint.toString()).post(chunk).fetchAsJson());
if (response.completed) {
- console.log(response);
+ const event = new CustomEvent("uploadCompleted", {
+ detail: {
+ data: response.data,
+ endpointThumbnails: response.endpointThumbnails,
+ fileID: response.fileID,
+ typeName: response.typeName,
+ },
+ });
+ element.dispatchEvent(event);
+ if (response.endpointThumbnails !== "") {
+ void (await (0, Backend_1.prepareRequest)(response.endpointThumbnails).get().fetchAsResponse());
+ // TODO: Handle errors and notify about the new thumbnails.
+ }
}
}
catch (e) {
--- /dev/null
+<?php
+
+namespace wcf\action;
+
+use Laminas\Diactoros\Response\EmptyResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\data\file\File;
+use wcf\http\Helper;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\file\processor\FileProcessor;
+
+final class FileGenerateThumbnailsAction implements RequestHandlerInterface
+{
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $parameters = Helper::mapQueryParameters(
+ $request->getQueryParams(),
+ <<<'EOT'
+ array {
+ id: positive-int,
+ }
+ EOT,
+ );
+
+ $file = new File($parameters['id']);
+ if (!$file->fileID) {
+ throw new IllegalLinkException();
+ }
+
+ FileProcessor::getInstance()->generateThumbnails($file);
+
+ return new EmptyResponse();
+ }
+}
use wcf\http\Helper;
use wcf\system\exception\IllegalLinkException;
use wcf\system\io\File as IoFile;
+use wcf\system\request\LinkHandler;
final class FileUploadAction implements RequestHandlerInterface
{
$thumbnailFormats = $processor->getThumbnailFormats();
if ($thumbnailFormats !== []) {
// TODO: Endpoint to generate thumbnails.
- $endpointThumbnails = '';
+ $endpointThumbnails = LinkHandler::getInstance()->getControllerLink(
+ FileGenerateThumbnailsAction::class,
+ ['id' => $file->fileID],
+ );
}
}
* @property-read int $uploadTime timestamp at which the attachment has been uploaded
* @property-read int $showOrder position of the attachment in relation to the other attachment to the same message
* @property-read int|null $fileID
+ * @property-read int|null $thumbnailID
+ * @property-read int|null $tinyThumbnailID
*/
class Attachment extends DatabaseObject implements ILinkableObject, IRouteController, IThumbnailFile
{
$folderB = \substr($this->fileHash, 2, 2);
return \sprintf(
- \WCF_DIR . '_data/public/fileUpload/%s/%s/',
+ \WCF_DIR . '_data/private/fileUpload/%s/%s/',
$folderA,
$folderB,
);
* @author Alexander Ebert
* @copyright 2001-2023 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
*
* @method FileTemporary create()
* @method FileTemporaryEditor[] getObjects()
* @author Alexander Ebert
* @copyright 2001-2023 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
*
* @method FileTemporary current()
* @method FileTemporary[] getObjects()
--- /dev/null
+<?php
+
+namespace wcf\data\file\thumbnail;
+
+use wcf\data\DatabaseObject;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ *
+ * @property-read int $thumbnailID
+ * @property-read int $fileID
+ * @property-read string $identifier
+ * @property-read string $fileHash
+ * @property-read string $fileExtension
+ */
+class FileThumbnail extends DatabaseObject
+{
+ public function getPath(): string
+ {
+ $folderA = \substr($this->fileHash, 0, 2);
+ $folderB = \substr($this->fileHash, 2, 2);
+
+ return \sprintf(
+ \WCF_DIR . '_data/public/thumbnail/%s/%s/',
+ $folderA,
+ $folderB,
+ );
+ }
+
+ public function getSourceFilename(): string
+ {
+ return \sprintf(
+ '%d-%s.%s',
+ $this->thumbnailID,
+ $this->fileHash,
+ $this->fileExtension,
+ );
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\data\file\thumbnail;
+
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ *
+ * @method FileThumbnail create()
+ * @method FileThumbnailEditor[] getObjects()
+ * @method FileThumbnailEditor getSingleObject()
+ */
+class FileThumbnailAction extends AbstractDatabaseObjectAction
+{
+ protected $className = FileThumbnailEditor::class;
+}
--- /dev/null
+<?php
+
+namespace wcf\data\file\thumbnail;
+
+use wcf\data\DatabaseObjectEditor;
+use wcf\data\file\File;
+use wcf\system\file\processor\ThumbnailFormat;
+use wcf\util\FileUtil;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ *
+ * @method static FileThumbnail create(array $parameters = [])
+ * @method FileThumbnail getDecoratedObject()
+ * @mixin FileThumbnail
+ */
+class FileThumbnailEditor extends DatabaseObjectEditor
+{
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = FileThumbnail::class;
+
+ public static function createFromTemporaryFile(
+ File $file,
+ ThumbnailFormat $format,
+ string $filename
+ ): FileThumbnail {
+ $mimeType = FileUtil::getMimeType($filename);
+ $fileExtension = match ($mimeType) {
+ 'image/gif' => 'gif',
+ 'image/jpg', 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/webp' => 'webp',
+ };
+
+ $action = new FileThumbnailAction([], 'create', [
+ 'data' => [
+ 'fileID' => $file->fileID,
+ 'identifier' => $format->identifier,
+ 'fileHash' => hash_file('sha256', $filename),
+ 'fileExtension' => $fileExtension,
+ ],
+ ]);
+ $fileThumbnail = $action->executeAction()['returnValues'];
+ \assert($fileThumbnail instanceof FileThumbnail);
+
+ $filePath = $fileThumbnail->getPath();
+ if (!\is_dir($filePath)) {
+ \mkdir($filePath, recursive: true);
+ }
+
+ \rename(
+ $filename,
+ $filePath . $fileThumbnail->getSourceFilename()
+ );
+
+ return $fileThumbnail;
+ }
+}
--- /dev/null
+<?php
+
+namespace wcf\data\file\thumbnail;
+
+use wcf\data\DatabaseObjectList;
+
+/**
+ * @author Alexander Ebert
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.1
+ *
+ * @method FileThumbnail current()
+ * @method FileThumbnail[] getObjects()
+ * @method FileThumbnail|null getSingleObject()
+ * @method FileThumbnail|null search($objectID)
+ * @property FileThumbnail[] $objects
+ */
+class FileThumbnailList extends DatabaseObjectList
+{
+ public $className = FileThumbnail::class;
+}
use wcf\data\attachment\Attachment;
use wcf\data\attachment\AttachmentEditor;
use wcf\data\file\File;
+use wcf\data\file\thumbnail\FileThumbnail;
use wcf\system\attachment\AttachmentHandler;
+use wcf\system\exception\NotImplementedException;
/**
* @author Alexander Ebert
];
}
- #[\Override]
public function toHtmlElement(string $objectType, int $objectID, string $tmpHash, int $parentObjectID): string
{
return FileProcessor::getInstance()->getHtmlElement(
public function getThumbnailFormats(): array
{
return [
+ new ThumbnailFormat(
+ '',
+ \ATTACHMENT_THUMBNAIL_HEIGHT,
+ \ATTACHMENT_THUMBNAIL_WIDTH,
+ !!\ATTACHMENT_RETAIN_DIMENSIONS,
+ ),
new ThumbnailFormat(
'tiny',
144,
144,
false,
),
- new ThumbnailFormat(
- 'default',
- \ATTACHMENT_THUMBNAIL_HEIGHT,
- \ATTACHMENT_THUMBNAIL_WIDTH,
- !!\ATTACHMENT_RETAIN_DIMENSIONS,
- ),
];
}
+
+ #[\Override]
+ public function adoptThumbnail(FileThumbnail $thumbnail): void
+ {
+ $attachment = Attachment::findByFileID($thumbnail->fileID);
+ if ($attachment === null) {
+ // TODO: How to handle this case?
+ return;
+ }
+
+ $columnName = match ($thumbnail->identifier) {
+ '' => 'thumbnailID',
+ 'tiny'=>'tinyThumbnailID',
+ 'default'=>throw new \RuntimeException('TODO'), // TODO
+ };
+
+ $attachmentEditor = new AttachmentEditor($attachment);
+ $attachmentEditor->update([
+ $columnName => $thumbnail->thumbnailID,
+ ]);
+ }
}
namespace wcf\system\file\processor;
use wcf\action\FileUploadPreflightAction;
+use wcf\data\file\File;
+use wcf\data\file\thumbnail\FileThumbnail;
+use wcf\data\file\thumbnail\FileThumbnailEditor;
+use wcf\data\file\thumbnail\FileThumbnailList;
use wcf\system\event\EventHandler;
use wcf\system\file\processor\event\FileProcessorCollecting;
+use wcf\system\image\adapter\ImageAdapter;
+use wcf\system\image\ImageHandler;
use wcf\system\request\LinkHandler;
use wcf\system\SingletonFactory;
+use wcf\util\FileUtil;
use wcf\util\JSON;
use wcf\util\StringUtil;
StringUtil::encodeHTML($allowedFileExtensions),
);
}
+
+ public function generateThumbnails(File $file): void
+ {
+ $processor = $file->getProcessor();
+ if ($processor === null) {
+ return;
+ }
+
+ $formats = $processor->getThumbnailFormats();
+ if ($formats === []) {
+ return;
+ }
+
+ $thumbnailList = new FileThumbnailList();
+ $thumbnailList->getConditionBuilder()->add("fileID = ?", [$file->fileID]);
+ $thumbnailList->readObjects();
+
+ $existingThumbnails = [];
+ foreach ($thumbnailList as $thumbnail) {
+ \assert($thumbnail instanceof FileThumbnail);
+ $existingThumbnails[$thumbnail->identifier] = $thumbnail;
+ }
+
+ $imageAdapter = null;
+ foreach ($formats as $format) {
+ if (isset($existingThumbnails[$format->identifier])) {
+ continue;
+ }
+
+ if ($imageAdapter === null) {
+ $imageAdapter = ImageHandler::getInstance()->getAdapter();
+ $imageAdapter->loadFile($file->getPath() . $file->getSourceFilename());
+ }
+
+ assert($imageAdapter instanceof ImageAdapter);
+ $image = $imageAdapter->createThumbnail($format->width, $format->height, $format->retainDimensions);
+
+ $filename = FileUtil::getTemporaryFilename();
+ $imageAdapter->writeImage($image, $filename);
+
+ $fileThumbnail = FileThumbnailEditor::createFromTemporaryFile($file, $format, $filename);
+ $processor->adoptThumbnail($fileThumbnail);
+ }
+ }
}
namespace wcf\system\file\processor;
use wcf\data\file\File;
+use wcf\data\file\thumbnail\FileThumbnail;
/**
* @author Alexander Ebert
public function adopt(File $file, array $context): void;
+ public function adoptThumbnail(FileThumbnail $thumbnail): void;
+
public function canDownload(File $file): bool;
public function getAllowedFileExtensions(array $context): array;
showOrder SMALLINT(5) NOT NULL DEFAULT 0,
fileID INT,
+ thumbnailID INT,
+ tinyThumbnailID INT,
KEY (objectTypeID, objectID),
KEY (objectTypeID, tmpHash),
chunks VARBINARY(255) NOT NULL
);
+DROP TABLE IF EXISTS wcf1_file_thumbnail;
+CREATE TABLE wcf1_file_thumbnail (
+ thumbnailID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ fileID INT NOT NULL,
+ identifier VARCHAR(50) NOT NULL,
+ fileHash CHAR(64) NOT NULL,
+ fileExtension VARCHAR(10) NOT NULL
+);
+
/* As the flood control table can be a high traffic table and as it is periodically emptied,
there is no foreign key on the `objectTypeID` to speed up insertions. */
DROP TABLE IF EXISTS wcf1_flood_control;
ALTER TABLE wcf1_attachment ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
ALTER TABLE wcf1_attachment ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
ALTER TABLE wcf1_attachment ADD FOREIGN KEY (fileID) REFERENCES wcf1_file (fileID) ON DELETE SET NULL;
+ALTER TABLE wcf1_attachment ADD FOREIGN KEY (thumbnailID) REFERENCES wcf1_file_thumbnail (thumbnailID) ON DELETE SET NULL;
+ALTER TABLE wcf1_attachment ADD FOREIGN KEY (tinyThumbnailID) REFERENCES wcf1_file_thumbnail (thumbnailID) ON DELETE SET NULL;
ALTER TABLE wcf1_bbcode ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
ALTER TABLE wcf1_event_listener ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_file_thumbnail ADD FOREIGN KEY (fileID) REFERENCES wcf1_file (fileID) ON DELETE CASCADE;
+
ALTER TABLE wcf1_language_item ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE;
ALTER TABLE wcf1_language_item ADD FOREIGN KEY (languageCategoryID) REFERENCES wcf1_language_category (languageCategoryID) ON DELETE CASCADE;
ALTER TABLE wcf1_language_item ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;