Skip to content
Merged
14 changes: 13 additions & 1 deletion app/Actions/Photo/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,18 @@ public function __construct(
?ImportMode $import_mode,
int $intended_owner_id,
UserUploadTrustLevel $upload_trust_level,
?string $title = null,
?string $description = null,
?string $preallocated_id = null,
) {
$this->strategy_parameters = new ImportParam($import_mode, $intended_owner_id, upload_trust_level: $upload_trust_level);
$this->strategy_parameters = new ImportParam(
$import_mode,
$intended_owner_id,
title: $title,
description: $description,
preallocated_id: $preallocated_id,
upload_trust_level: $upload_trust_level,
);
}

/**
Expand Down Expand Up @@ -171,6 +181,7 @@ private function handleStandalone(InitDTO $init_dto): Photo
$pipes = [
Standalone\FixTimeStamps::class,
Standalone\InitNamingStrategy::class,
Standalone\ApplyUserProvidedMetadata::class,
Shared\HydrateMetadata::class,
Shared\SetHighlighted::class,
Shared\SetOwnership::class,
Expand Down Expand Up @@ -269,6 +280,7 @@ private function handlePhotoLivePartner(InitDTO $init_dto): Photo
$stand_alone_pipes = [
Standalone\FixTimeStamps::class,
Standalone\InitNamingStrategy::class,
Standalone\ApplyUserProvidedMetadata::class,
Shared\HydrateMetadata::class,
Shared\SetHighlighted::class,
Shared\SetOwnership::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Actions\Photo\Pipes\Standalone;

use App\Contracts\PhotoCreate\StandalonePipe;
use App\DTO\PhotoCreate\StandaloneDTO;

/**
* Apply user-supplied title and description to the photo before EXIF hydration.
*
* When a caller explicitly provides a title or description at upload time
* (FR-041-01, FR-041-02), those values are written to the photo model here
* so that {@link \App\Actions\Photo\Pipes\Shared\HydrateMetadata} — which only
* overwrites null fields — will then leave them untouched.
*
* This pipe is a no-op when the values are absent (FR-041-05, FR-041-03).
* For duplicate uploads this pipe never runs; the duplicate keeps its existing
* title and description.
*/
class ApplyUserProvidedMetadata implements StandalonePipe
{
public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO
{
if ($state->title !== null) {
$state->photo->title = $state->title;
}

if ($state->description !== null) {
$state->photo->description = $state->description;
}

return $next($state);
}
}
6 changes: 6 additions & 0 deletions app/Actions/Photo/Pipes/Standalone/AutoRenamer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ class AutoRenamer implements StandalonePipe
{
public function handle(StandaloneDTO $state, \Closure $next): StandaloneDTO
{
// Skip if the caller explicitly provided a title at upload time (FR-041-06).
// User-supplied titles take precedence and must not be overwritten by renamer rules.
if ($state->title !== null) {
return $next($state);
}

// Skip if not enabled.
if (!$state->shall_rename_photo_title) {
return $next($state);
Expand Down
6 changes: 6 additions & 0 deletions app/DTO/ImportParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ final class ImportParam
* @param bool $is_highlighted indicates whether the new photo shall be highlighted
* @param Extractor|null $exif_info the extracted EXIF information
* @param bool|null $apply_watermark whether to apply watermark (null = use global setting)
* @param string|null $title user-supplied title override (takes precedence over EXIF-extracted title when non-null)
* @param string|null $description user-supplied description override (takes precedence over EXIF-extracted description when non-null)
* @param string|null $preallocated_id pre-allocated photo ID to be used on insert (see HasRandomIDAndLegacyTimeBasedID::preallocateId)
*
* @return void
*/
Expand All @@ -34,6 +37,9 @@ public function __construct(
public bool $is_highlighted = false,
public Extractor|null $exif_info = null,
public ?bool $apply_watermark = null,
public ?string $title = null,
public ?string $description = null,
public ?string $preallocated_id = null,
?UserUploadTrustLevel $upload_trust_level = null,
) {
$this->upload_trust_level = $upload_trust_level ?? throw new LycheeLogicException('Upload trust level must be provided');
Expand Down
12 changes: 12 additions & 0 deletions app/DTO/PhotoCreate/InitDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ class InitDTO
// that should be preserved as a RAW size variant after conversion to JPEG.
public NativeLocalFile|null $raw_source_file = null;

// User-supplied title override (takes precedence over EXIF-extracted title when non-null).
public ?string $title = null;

// User-supplied description override (takes precedence over EXIF-extracted description when non-null).
public ?string $description = null;

// Pre-allocated photo ID to be used on insert (see HasRandomIDAndLegacyTimeBasedID::preallocateId).
public ?string $preallocated_id = null;

public function __construct(
ImportParam $parameters,
NativeLocalFile $source_file,
Expand All @@ -70,5 +79,8 @@ public function __construct(
$this->apply_watermark = $parameters->apply_watermark;
$this->album = $album;
$this->file_last_modified_time = $file_last_modified_time;
$this->title = $parameters->title;
$this->description = $parameters->description;
$this->preallocated_id = $parameters->preallocated_id;
}
}
15 changes: 15 additions & 0 deletions app/DTO/PhotoCreate/StandaloneDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class StandaloneDTO implements PhotoDTO
// that should be preserved as a RAW size variant after conversion to JPEG.
public NativeLocalFile|null $raw_source_file = null;

// User-supplied title override (takes precedence over EXIF-extracted title when non-null).
public ?string $title = null;

// User-supplied description override (takes precedence over EXIF-extracted description when non-null).
public ?string $description = null;

public function __construct(
// The resulting photo
public Photo $photo,
Expand Down Expand Up @@ -75,6 +81,15 @@ public static function ofInit(InitDTO $init_dto): StandaloneDTO
apply_watermark: $init_dto->apply_watermark,
);
$dto->raw_source_file = $init_dto->raw_source_file;
$dto->title = $init_dto->title;
$dto->description = $init_dto->description;

// Pre-allocate the photo ID so the controller can return it in the upload response
// before the job finishes. The trait's generateKey() will consume and then clear
// this value; a DB-collision retry will therefore generate a fresh random ID.
if ($init_dto->preallocated_id !== null) {
$dto->photo->preallocateId($init_dto->preallocated_id);
}

return $dto;
}
Expand Down
37 changes: 37 additions & 0 deletions app/Factories/IdFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Factories;

use App\Constants\RandomID;
use App\Exceptions\InsufficientEntropyException;

class IdFactory
{
public function createRandomID(): string
{
// URl-compatible variant of base64 encoding
// `+` and `/` are replaced by `-` and `_`, resp.
// The other characters (a-z, A-Z, 0-9) are legal within an URL.
// As the number of bytes is divisible by 3, no trailing `=` occurs.
try {
$id = strtr(base64_encode(random_bytes(3 * RandomID::ID_LENGTH / 4)), '+/', '-_');
// Last character whould not be a - for some version of android.
// this will reduce the entropy and induce a slight bias but we are still
// above the birthday bounds.
if ($id[23] === '-') {
$id[23] = '0';
}
// @codeCoverageIgnoreStart
} catch (\Exception $e) {
throw new InsufficientEntropyException($e);
}

return $id;
}
}
13 changes: 11 additions & 2 deletions app/Http/Controllers/Gallery/PhotoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Enum\SizeVariantType;
use App\Exceptions\ConfigurationException;
use App\Exceptions\ConflictingPropertyException;
use App\Factories\IdFactory;
use App\Http\Requests\Photo\CopyPhotosRequest;
use App\Http\Requests\Photo\DeletePhotosRequest;
use App\Http\Requests\Photo\EditPhotoRequest;
Expand Down Expand Up @@ -81,8 +82,16 @@ public function upload(UploadPhotoRequest $request): UploadMetaResource
return $meta;
}

// Last chunk
// Last chunk — generate expected_id for non-zip uploads and store title/description.
$meta->stage = FileStatus::PROCESSING;
$meta->title = $request->title();
$meta->description = $request->description();

$is_zip = strtolower(pathinfo($meta->file_name, PATHINFO_EXTENSION)) === 'zip';
if (!$is_zip) {
$id_factory = resolve(IdFactory::class);
$meta->expected_id = $id_factory->createRandomID();
}

return $this->process(
$request->verify(),
Expand Down Expand Up @@ -127,7 +136,7 @@ private function process(
return $meta;
}

ProcessImageJob::dispatch($processable_file, $album, $file_last_modified_time, $apply_watermark);
ProcessImageJob::dispatch($processable_file, $album, $file_last_modified_time, $apply_watermark, $meta->expected_id, $meta->title, $meta->description);
$meta->stage = config('queue.default') === 'sync' ? FileStatus::DONE : FileStatus::READY;

return $meta;
Expand Down
19 changes: 19 additions & 0 deletions app/Http/Requests/Photo/UploadPhotoRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
use App\Http\Resources\Editable\UploadMetaResource;
use App\Policies\AlbumPolicy;
use App\Rules\AlbumIDRule;
use App\Rules\DescriptionRule;
use App\Rules\ExtensionRule;
use App\Rules\FileUuidRule;
use App\Rules\TitleRule;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Gate;

Expand All @@ -34,6 +36,8 @@ class UploadPhotoRequest extends BaseApiRequest implements HasAbstractAlbum
protected UploadMetaResource $meta;
protected int $file_size;
protected ?bool $apply_watermark = null;
protected ?string $title = null;
protected ?string $description = null;

/**
* {@inheritDoc}
Expand All @@ -58,6 +62,8 @@ public function rules(): array
'chunk_number' => 'required|integer|min:1',
'total_chunks' => 'required|integer|gte:chunk_number',
'apply_watermark' => 'sometimes|boolean',
RequestAttribute::TITLE_ATTRIBUTE => ['sometimes', 'nullable', new TitleRule()],
RequestAttribute::DESCRIPTION_ATTRIBUTE => ['sometimes', 'nullable', new DescriptionRule()],
];
}

Expand All @@ -83,6 +89,9 @@ protected function processValidatedValues(array $values, array $files): void
if (isset($values['apply_watermark'])) {
$this->apply_watermark = self::toBoolean($values['apply_watermark']);
}
// Store optional user-supplied title and description
$this->title = $values[RequestAttribute::TITLE_ATTRIBUTE] ?? null;
$this->description = $values[RequestAttribute::DESCRIPTION_ATTRIBUTE] ?? null;
}

public function uploaded_file_chunk(): UploadedFile
Expand All @@ -104,4 +113,14 @@ public function apply_watermark(): ?bool
{
return $this->apply_watermark;
}

public function title(): ?string
{
return $this->title;
}

public function description(): ?string
{
return $this->description;
}
}
3 changes: 3 additions & 0 deletions app/Http/Resources/Editable/UploadMetaResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public function __construct(
public FileStatus $stage,
public int $chunk_number,
public int $total_chunks,
public ?string $expected_id = null,
public ?string $title = null,
public ?string $description = null,
) {
}
}
13 changes: 13 additions & 0 deletions app/Jobs/ProcessImageJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class ProcessImageJob implements ShouldQueue
public UserUploadTrustLevel $upload_trust_level;
public ?int $file_last_modified_time;
public ?bool $apply_watermark;
public ?string $expected_id;
public ?string $title;
public ?string $description;

/**
* Create a new job instance.
Expand All @@ -62,6 +65,9 @@ public function __construct(
string|AbstractAlbum|null $abstract_album,
?int $file_last_modified_time,
?bool $apply_watermark = null,
?string $expected_id = null,
?string $title = null,
?string $description = null,
) {
$this->file_path = $file->getPath();
$this->original_base_name = $file->getOriginalBasename();
Expand Down Expand Up @@ -105,6 +111,10 @@ public function __construct(
$this->apply_watermark = $apply_watermark;
}

$this->expected_id = $expected_id;
$this->title = $title;
$this->description = $description;

// Set up our new history record.
$this->history = new JobHistory();
$this->history->owner_id = $this->user_id;
Expand Down Expand Up @@ -139,6 +149,9 @@ public function handle(AlbumFactory $album_factory): Photo
),
intended_owner_id: $this->user_id,
upload_trust_level: $this->upload_trust_level,
title: $this->title,
description: $this->description,
preallocated_id: $this->expected_id,
);

$album = null;
Expand Down
Loading
Loading