Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jobs processing #1785

Merged
merged 8 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ DB_LIST_FOREIGN_KEYS=true
CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_LIFETIME=120
# `sync` if jobs needs to be executed live (default) or `database` if they can be defered.
QUEUE_CONNECTION=sync

SECURITY_HEADER_HSTS_ENABLE=false
SESSION_SECURE_COOKIE=false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/.env.mariadb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ DB_LIST_FOREIGN_KEYS=true

CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_DRIVER=sync
QUEUE_CONNECTION=sync
2 changes: 1 addition & 1 deletion .github/workflows/.env.postgresql
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ DB_PASSWORD=postgres

CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_DRIVER=sync
QUEUE_CONNECTION=sync
2 changes: 1 addition & 1 deletion .github/workflows/.env.sqlite
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ DB_LIST_FOREIGN_KEYS=true

CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_DRIVER=sync
QUEUE_CONNECTION=sync
42 changes: 19 additions & 23 deletions app/Http/Controllers/PhotoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
namespace App\Http\Controllers;

use App\Actions\Photo\Archive;
use App\Actions\Photo\Create;
use App\Actions\Photo\Delete;
use App\Actions\Photo\Duplicate;
use App\Actions\Photo\Strategies\ImportMode;
use App\Actions\User\Notify;
use App\Contracts\Exceptions\InternalLycheeException;
use App\Contracts\Exceptions\LycheeException;
use App\Exceptions\MediaFileOperationException;
use App\Exceptions\ModelDBException;
use App\Exceptions\UnauthenticatedException;
use App\Factories\AlbumFactory;
use App\Http\Requests\Photo\AddPhotoRequest;
use App\Http\Requests\Photo\ArchivePhotosRequest;
use App\Http\Requests\Photo\ClearSymLinkRequest;
Expand All @@ -28,8 +26,9 @@
use App\Http\Requests\Photo\SetPhotosTitleRequest;
use App\Http\Requests\Photo\SetPhotoUploadDateRequest;
use App\Http\Resources\Models\PhotoResource;
use App\Image\Files\TemporaryLocalFile;
use App\Image\Files\ProcessableJobFile;
use App\Image\Files\UploadedFile;
use App\Jobs\ProcessImageJob;
use App\ModelFunctions\SymLinkFunctions;
use App\Models\Configs;
use App\Models\Photo;
Expand All @@ -38,20 +37,18 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class PhotoController extends Controller
{
private SymLinkFunctions $symLinkFunctions;

/**
* @param SymLinkFunctions $symLinkFunctions
* @param AlbumFactory $albumFactory
*/
public function __construct(
SymLinkFunctions $symLinkFunctions
private SymLinkFunctions $symLinkFunctions,
private AlbumFactory $albumFactory
) {
$this->symLinkFunctions = $symLinkFunctions;
}

/**
Expand Down Expand Up @@ -93,16 +90,13 @@ public function getRandom(): PhotoResource
*
* @param AddPhotoRequest $request
*
* @return PhotoResource
* @return PhotoResource|JsonResponse
*
* @throws LycheeException
* @throws ModelNotFoundException
*/
public function add(AddPhotoRequest $request): PhotoResource
public function add(AddPhotoRequest $request): PhotoResource|JsonResponse
{
/** @var int $currentUserId */
$currentUserId = Auth::id() ?? throw new UnauthenticatedException();

// This code is a nasty work-around which should not exist.
// PHP stores a temporary copy of the uploaded file without a file
// extension.
Expand All @@ -121,23 +115,25 @@ public function add(AddPhotoRequest $request): PhotoResource
// Hence, we must make a deep copy.
// TODO: Remove this code again, if all other TODOs regarding MIME and file handling are properly refactored and we have stopped using absolute file paths as the least common denominator to pass around files.
$uploadedFile = new UploadedFile($request->uploadedFile());
$copiedFile = new TemporaryLocalFile(
$processableFile = new ProcessableJobFile(
$uploadedFile->getOriginalExtension(),
$uploadedFile->getOriginalBasename()
);
$copiedFile->write($uploadedFile->read());
$processableFile->write($uploadedFile->read());

$uploadedFile->close();
$uploadedFile->delete();
$processableFile->close();
// End of work-around

// As the file has been uploaded, the (temporary) source file shall be
// deleted
$create = new Create(
new ImportMode(deleteImported: true, skipDuplicates: Configs::getValueAsBool('skip_duplicates')),
$currentUserId
);
if (Configs::getValueAsBool('use_job_queues')) {
ProcessImageJob::dispatch($processableFile, $request->album());

return new JsonResponse(null, 201);
}

$photo = $create->add($copiedFile, $request->album());
$job = new ProcessImageJob($processableFile, $request->album());
$photo = $job->handle($this->albumFactory);
$isNew = $photo->created_at->toIso8601String() === $photo->updated_at->toIso8601String();

return PhotoResource::make($photo)->setStatus($isNew ? 201 : 200);
Expand Down
54 changes: 54 additions & 0 deletions app/Image/Files/LoadTemporaryFileTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Image\Files;

use App\Exceptions\MediaFileOperationException;
use function Safe\fopen;

trait LoadTemporaryFileTrait
{
/**
* This returns the base path to use to store files.
*
* @return string
*/
abstract protected function getFileBasePath(): string;

/**
* Prepare a temporary file to be loaded.
* Name is randomly generated and will be placed in getFileBasePath() directory.
*
* @param string $fileExtension
*
* @return string
*
* @throws MediaFileOperationException
*/
protected function load(string $fileExtension): string
{
// We must not use the usual PHP method `tempnam`, because that
// method does not handle file extensions well, but our temporary
// files need a proper (and correct) extension for the MIME extractor
// to work.
$lastException = null;
$retryCounter = 5;
do {
try {
$retryCounter--;
$tempFilePath = $this->getFileBasePath() .
DIRECTORY_SEPARATOR .
strtr(base64_encode(random_bytes(12)), '+/', '-_') .
$fileExtension;
$this->stream = fopen($tempFilePath, 'x+b');
} catch (\ErrorException|\Exception $e) {
$tempFilePath = null;
$lastException = $e;
}
} while ($tempFilePath === null && $retryCounter > 0);
if ($tempFilePath === null) {
throw new MediaFileOperationException('unable to create temporary file', $lastException);
}

return $tempFilePath;
}
}
58 changes: 58 additions & 0 deletions app/Image/Files/ProcessableJobFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace App\Image\Files;

use App\Exceptions\MediaFileOperationException;
use function Safe\mkdir;

/**
* Class TemporaryJobFile.
*
* Represents a local file with an automatically chosen, unique name intended
* to be used temporarily before being processed in a Job.
*/
class ProcessableJobFile extends NativeLocalFile
{
use LoadTemporaryFileTrait;

protected string $fakeBaseName;

/**
* Creates a new temporary file with a random file name.
* Do note that we MUST use storage_path() instead of sys_get_temp_dir() as
* tmp is not shared across processes, meaning that the queues will not be able to see the files.
*
* @param string $fileExtension the file extension of the new temporary file incl. a preceding dot
* @param string $fakeBaseName the fake base name of the file; e.g. the original name prior to up-/download
*
* @throws MediaFileOperationException
*/
public function __construct(string $fileExtension, string $fakeBaseName = '')
{
$tempFilePath = $this->load($fileExtension);
parent::__construct($tempFilePath);
$this->fakeBaseName = $fakeBaseName;
}

/**
* {@inheritDoc}
*/
protected function getFileBasePath(): string
{
$tempDirPath = storage_path() . DIRECTORY_SEPARATOR . 'image-jobs';

if (!file_exists($tempDirPath)) {
mkdir($tempDirPath);
}

return $tempDirPath;
}

/**
* {@inheritDoc}
*/
public function getOriginalBasename(): string
{
return $this->fakeBaseName;
}
}
66 changes: 66 additions & 0 deletions app/Image/Files/TemporaryJobFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace App\Image\Files;

use App\Exceptions\MediaFileOperationException;
use function Safe\fopen;

/**
* Class TemporaryJobFile.
*
* Represents a local file with an automatically chosen, unique name intended
* to be used temporarily.
*/
class TemporaryJobFile extends NativeLocalFile
{
protected string $fakeBaseName;

/**
* Once we are done with the process, we can delete the image.
*
* @throws MediaFileOperationException
*/
public function __destruct()
{
$this->delete();
parent::__destruct();
}

/**
* Load a temporary file with a previously generated file name.
*
* @param string $filePath the path of a Processable Job file
* @param string $fakeBaseName the fake base name of the file; e.g. the original name prior to up-/download
*
* @throws MediaFileOperationException
*/
public function __construct(string $filePath, string $fakeBaseName = '')
{
$lastException = null;
$retryCounter = 5;
do {
try {
$tempFilePath = $filePath;
$retryCounter--;
// We open wih c+b because the file already exists (from ProcessableJobFile)
$this->stream = fopen($tempFilePath, 'c+b');
} catch (\ErrorException|\Exception $e) {
$tempFilePath = null;
$lastException = $e;
}
} while ($tempFilePath === null && $retryCounter > 0);
if ($tempFilePath === null) {
throw new MediaFileOperationException('unable to create temporary file', $lastException);
}
parent::__construct($tempFilePath);
$this->fakeBaseName = $fakeBaseName;
}

/**
* {@inheritDoc}
*/
public function getOriginalBasename(): string
{
return $this->fakeBaseName;
}
}
35 changes: 11 additions & 24 deletions app/Image/Files/TemporaryLocalFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace App\Image\Files;

use App\Exceptions\MediaFileOperationException;
use function Safe\fopen;

/**
* Class TemporaryLocalFile.
Expand All @@ -13,6 +12,8 @@
*/
class TemporaryLocalFile extends NativeLocalFile
{
use LoadTemporaryFileTrait;

protected string $fakeBaseName;

/**
Expand All @@ -34,33 +35,19 @@ public function __destruct()
*/
public function __construct(string $fileExtension, string $fakeBaseName = '')
{
// We must not use the usual PHP method `tempnam`, because that
// method does not handle file extensions well, but our temporary
// files need a proper (and correct) extension for the MIME extractor
// to work.
$lastException = null;
$retryCounter = 5;
do {
try {
$tempFilePath = sys_get_temp_dir() .
DIRECTORY_SEPARATOR .
'lychee-' .
strtr(base64_encode(random_bytes(12)), '+/', '-_') .
$fileExtension;
$retryCounter--;
$this->stream = fopen($tempFilePath, 'x+b');
} catch (\ErrorException|\Exception $e) {
$tempFilePath = null;
$lastException = $e;
}
} while ($tempFilePath === null && $retryCounter > 0);
if ($tempFilePath === null) {
throw new MediaFileOperationException('unable to create temporary file', $lastException);
}
$tempFilePath = $this->load($fileExtension);
parent::__construct($tempFilePath);
$this->fakeBaseName = $fakeBaseName;
}

/**
* {@inheritDoc}
*/
protected function getFileBasePath(): string
{
return sys_get_temp_dir();
}

/**
* {@inheritDoc}
*/
Expand Down
Loading