Skip to content

Commit

Permalink
Squash
Browse files Browse the repository at this point in the history
  • Loading branch information
ildyria committed Oct 26, 2024
1 parent b156ec4 commit 30350d1
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 4 deletions.
11 changes: 11 additions & 0 deletions app/Exceptions/Internal/ZipExtractionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace App\Exceptions\Internal;

class ZipExtractionException extends LycheeDomainException
{
public function __construct(string $path, string $to)
{
parent::__construct(sprintf('Could not extract %s to %s', $path, $to));
}
}
9 changes: 9 additions & 0 deletions app/Http/Controllers/Gallery/PhotoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use App\Image\Files\NativeLocalFile;
use App\Image\Files\ProcessableJobFile;
use App\Image\Files\UploadedFile;
use App\Jobs\ExtractZip;
use App\Jobs\ProcessImageJob;
use App\Models\Configs;
use App\Models\Photo;
Expand Down Expand Up @@ -81,6 +82,14 @@ private function process(
$processableFile->close();
// End of work-around

if (Configs::getValueAsBool('extract_zip_on_upload') &&
\Str::endsWith($processableFile->getPath(), '.zip')) {
ExtractZip::dispatch($processableFile, $album?->id, $file_last_modified_time);
$meta->stage = FileStatus::DONE;

return $meta;
}

if (Configs::getValueAsBool('use_job_queues')) {
ProcessImageJob::dispatch($processableFile, $album, $file_last_modified_time);
$meta->stage = FileStatus::READY;
Expand Down
28 changes: 28 additions & 0 deletions app/Image/Files/ExtractedJobFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace App\Image\Files;

/**
* Class ExtractedJobFile.
*
* Represents a local file which has been extracted from an Archive.
* It does not hold content.
*/
readonly class ExtractedJobFile
{
public function __construct(
public string $path,
public string $baseName,
) {
}

public function getPath(): string
{
return $this->path;
}

public function getOriginalBasename(): string
{
return $this->baseName;
}
}
2 changes: 1 addition & 1 deletion app/Image/Files/ProcessableJobFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use function Safe\mkdir;

/**
* Class TemporaryJobFile.
* Class ProcessableJobFile.
*
* Represents a local file with an automatically chosen, unique name intended
* to be used temporarily before being processed in a Job.
Expand Down
79 changes: 79 additions & 0 deletions app/Jobs/CleanUpExtraction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace App\Jobs;

use App\Enum\JobStatus;
use App\Models\JobHistory;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use function Safe\rmdir;

class CleanUpExtraction implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

protected JobHistory $history;

public string $folderPath;
public int $userId;

/**
* Create a new job instance.
*/
public function __construct(
string $folderPath,
) {
$this->folderPath = $folderPath;
$this->userId = Auth::user()->id;

// Set up our new history record.
$this->history = new JobHistory();
$this->history->owner_id = $this->userId;
$this->history->job = Str::limit('Removing ' . basename($this->folderPath), 200);
$this->history->status = JobStatus::READY;

$this->history->save();
}

/**
* Execute the job.
*/
public function handle(): void
{
// $this->history->status = JobStatus::STARTED;
// $this->history->save();

rmdir($this->folderPath);

$this->history->status = JobStatus::SUCCESS;
$this->history->save();
}

/**
* Catch failures.
*
* @param \Throwable $th
*
* @return void
*/
public function failed(\Throwable $th): void
{
$this->history->status = JobStatus::FAILURE;
$this->history->save();

if ($th->getCode() === 999) {
$this->release();
} else {
Log::error(__LINE__ . ':' . __FILE__ . ' ' . $th->getMessage(), $th->getTrace());
}
}
}
184 changes: 184 additions & 0 deletions app/Jobs/ExtractZip.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

namespace App\Jobs;

use App\Actions\Album\Create;
use App\Contracts\Models\AbstractAlbum;
use App\Enum\JobStatus;
use App\Enum\SmartAlbumType;
use App\Exceptions\Internal\ZipExtractionException;
use App\Image\Files\ExtractedJobFile;
use App\Image\Files\ProcessableJobFile;
use App\Models\Album;
use App\Models\JobHistory;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use function Safe\date;
use function Safe\unlink;

class ExtractZip implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

protected JobHistory $history;

public string $filePath;
public string $originalBaseName;
public ?string $albumID;
public int $userId;
public ?int $fileLastModifiedTime;

/**
* Create a new job instance.
*/
public function __construct(
ProcessableJobFile $file,
string|AbstractAlbum|null $albumID,
?int $fileLastModifiedTime,
) {
$this->filePath = $file->getPath();
$this->originalBaseName = $file->getOriginalBasename();
$this->albumID = is_string($albumID) ? $albumID : $albumID?->id;
$this->userId = Auth::user()->id;
$this->fileLastModifiedTime = $fileLastModifiedTime;

// Set up our new history record.
$this->history = new JobHistory();
$this->history->owner_id = $this->userId;
$this->history->job = Str::limit('Extracting: ' . $this->originalBaseName, 200);
$this->history->status = JobStatus::READY;

$this->history->save();
}

/**
* Execute the job.
*/
public function handle(): void
{
$this->history->status = JobStatus::STARTED;
$this->history->save();

$extractedFolderName = $this->getExtractFolderName();

$pathExtracted = Storage::disk('extract-jobs')->path(date('Ymd') . $extractedFolderName);
$zip = new \ZipArchive();
if ($zip->open($this->filePath) === true) {
$zip->extractTo($pathExtracted);
$zip->close();

// clean up the zip file
unlink($this->filePath);

$this->history->status = JobStatus::SUCCESS;
$this->history->save();
} else {
throw new ZipExtractionException($this->filePath, $pathExtracted);
}

$newAlbum = $this->createAlbum($extractedFolderName, $this->albumID);
$jobs = [];
foreach (new \DirectoryIterator($pathExtracted) as $fileInfo) {
if ($fileInfo->isDot() || $fileInfo->isDir()) {
continue;
}

$extractedFile = new ExtractedJobFile($fileInfo->getRealPath(), $fileInfo->getFilename());
$jobs[] = new ProcessImageJob($extractedFile, $newAlbum, $fileInfo->getMTime());
}

$jobs[] = new CleanUpExtraction($pathExtracted);
foreach ($jobs as $job) {
dispatch($job);
}
}

/**
* Catch failures.
*
* @param \Throwable $th
*
* @return void
*/
public function failed(\Throwable $th): void
{
$this->history->status = JobStatus::FAILURE;
$this->history->save();

if ($th->getCode() === 999) {
$this->release();
} else {
Log::error(__LINE__ . ':' . __FILE__ . ' ' . $th->getMessage(), $th->getTrace());
}
}

/**
* Given a name and parent we create it.
*
* @param string $newAlbumName
* @param string|null $parentID
*
* @return Album new album
*/
private function createAlbum(string $newAlbumName, ?string $parentID): Album
{
if (SmartAlbumType::tryFrom($parentID) !== null) {
$parentID = null;
}

/** @var Album $parentAlbum */
$parentAlbum = $parentID !== null ? Album::query()->findOrFail($parentID) : null; // in case no ID provided -> import to root folder
$createAlbum = new Create($this->userId);

return $createAlbum->create($this->prepareAlbumName($newAlbumName), $parentAlbum);
}

/**
* Todo Later: add renamer module.
*
* @param string $albumNameCandidate
*
* @return string
*/
private function prepareAlbumName(string $albumNameCandidate): string
{
return trim(str_replace('_', ' ', $albumNameCandidate));
}

/**
* Returns a folder name where:
* - spaces are replaced by `_`
* - if folder already exists (with date prefix) then we pad with _(xx) where xx is the next available number.
*
* @return string
*/
private function getExtractFolderName(): string
{
$baseNameWithoutExtension = substr($this->originalBaseName, 0, -4);

// Save that one (is default if no existing folder found).
$orignalName = str_replace(' ', '_', $baseNameWithoutExtension);

// Iterate on that one.
$candidateName = $orignalName;

// count
$i = 0;
while (Storage::disk('extract-jobs')->exists(date('Ymd') . $candidateName)) {
$candidateName = $orignalName . '_(' . $i . ')';
$i++;
}

return $candidateName;
}
}
3 changes: 2 additions & 1 deletion app/Jobs/ProcessImageJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\DTO\ImportMode;
use App\Enum\JobStatus;
use App\Factories\AlbumFactory;
use App\Image\Files\ExtractedJobFile;
use App\Image\Files\ProcessableJobFile;
use App\Image\Files\TemporaryJobFile;
use App\Models\Configs;
Expand Down Expand Up @@ -44,7 +45,7 @@ class ProcessImageJob implements ShouldQueue
* Create a new job instance.
*/
public function __construct(
ProcessableJobFile $file,
ProcessableJobFile|ExtractedJobFile $file,
string|AbstractAlbum|null $album,
?int $fileLastModifiedTime,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
use App\Models\Extensions\BaseConfigMigration;

return new class() extends BaseConfigMigration {
public const OAUTH = 'OAuth & SSO';

public function getConfigs(): array
{
return [
Expand Down
23 changes: 23 additions & 0 deletions database/migrations/2024_10_17_064538_extract_zip_on_upload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

use App\Models\Extensions\BaseConfigMigration;

return new class() extends BaseConfigMigration {
public const PROCESSING = 'Image Processing';

public function getConfigs(): array
{
return [
[
'key' => 'extract_zip_on_upload',
'value' => '0',
'cat' => self::PROCESSING,
'type_range' => self::BOOL,
'description' => 'Extract uploaded zip file and import content.',
'details' => 'Zip file will stay on your server unless it is properly extracted without faults (after which it is removed).',
'is_secret' => false,
'level' => 1, // Only for SE.
],
];
}
};

0 comments on commit 30350d1

Please sign in to comment.