Skip to content

Commit ca23652

Browse files
committed
squash
1 parent a293683 commit ca23652

File tree

9 files changed

+502
-2
lines changed

9 files changed

+502
-2
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ CLOCKWORK_ENABLE=false
2929
# enable s3 bucket (required in addition to needing AWS_ACCESS_KEY_ID)
3030
# S3_ENABLED=true
3131

32+
#FEATURE_EXTRACT_ZIP_FILES_ON_UPLOAD=true
33+
3234
# If you spread old links of to your albums in your Lychee instance starting with
3335
# https://lychee.text/#albumID/PhotoId
3436
# Set this value to true to enable redirection.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace App\Exceptions\Internal;
4+
5+
class ZipExtractionException extends LycheeDomainException
6+
{
7+
public function __construct(string $path, string $to)
8+
{
9+
parent::__construct(sprintf('Could not extract %s to %s', $path, $to));
10+
}
11+
}

app/Image/Files/ExtractedJobFile.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Image\Files;
4+
5+
/**
6+
* Class ExtractedJobFile.
7+
*
8+
* Represents a local file which has been extracted from an Archive.
9+
* It does not hold content.
10+
*/
11+
readonly class ExtractedJobFile
12+
{
13+
public function __construct(
14+
public string $path,
15+
public string $baseName
16+
) {
17+
}
18+
19+
public function getPath(): string
20+
{
21+
return $this->path;
22+
}
23+
24+
public function getOriginalBasename(): string
25+
{
26+
return $this->baseName;
27+
}
28+
}

app/Image/Files/ProcessableJobFile.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use function Safe\mkdir;
88

99
/**
10-
* Class TemporaryJobFile.
10+
* Class ProcessableJobFile.
1111
*
1212
* Represents a local file with an automatically chosen, unique name intended
1313
* to be used temporarily before being processed in a Job.

app/Jobs/CleanUpExtraction.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Enum\JobStatus;
6+
use App\Models\JobHistory;
7+
use Illuminate\Bus\Queueable;
8+
use Illuminate\Contracts\Queue\ShouldQueue;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Queue\InteractsWithQueue;
11+
use Illuminate\Queue\SerializesModels;
12+
use Illuminate\Support\Facades\Auth;
13+
use Illuminate\Support\Facades\Log;
14+
use Illuminate\Support\Str;
15+
use function Safe\rmdir;
16+
17+
class CleanUpExtraction implements ShouldQueue
18+
{
19+
use Dispatchable;
20+
use InteractsWithQueue;
21+
use Queueable;
22+
use SerializesModels;
23+
24+
protected JobHistory $history;
25+
26+
public string $folderPath;
27+
public int $userId;
28+
29+
/**
30+
* Create a new job instance.
31+
*/
32+
public function __construct(
33+
string $folderPath,
34+
) {
35+
$this->folderPath = $folderPath;
36+
$this->userId = Auth::user()->id;
37+
38+
// Set up our new history record.
39+
$this->history = new JobHistory();
40+
$this->history->owner_id = $this->userId;
41+
$this->history->job = Str::limit('Removing ' . basename($this->folderPath), 200);
42+
$this->history->status = JobStatus::READY;
43+
44+
$this->history->save();
45+
}
46+
47+
/**
48+
* Execute the job.
49+
*/
50+
public function handle(): void
51+
{
52+
// $this->history->status = JobStatus::STARTED;
53+
// $this->history->save();
54+
55+
rmdir($this->folderPath);
56+
57+
$this->history->status = JobStatus::SUCCESS;
58+
$this->history->save();
59+
}
60+
61+
/**
62+
* Catch failures.
63+
*
64+
* @param \Throwable $th
65+
*
66+
* @return void
67+
*/
68+
public function failed(\Throwable $th): void
69+
{
70+
$this->history->status = JobStatus::FAILURE;
71+
$this->history->save();
72+
73+
if ($th->getCode() === 999) {
74+
$this->release();
75+
} else {
76+
Log::error(__LINE__ . ':' . __FILE__ . ' ' . $th->getMessage(), $th->getTrace());
77+
}
78+
}
79+
}

app/Jobs/ExtractZip.php

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Actions\Album\Create;
6+
use App\Contracts\Models\AbstractAlbum;
7+
use App\Enum\JobStatus;
8+
use App\Enum\SmartAlbumType;
9+
use App\Exceptions\Internal\ZipExtractionException;
10+
use App\Image\Files\ExtractedJobFile;
11+
use App\Image\Files\ProcessableJobFile;
12+
use App\Models\Album;
13+
use App\Models\JobHistory;
14+
use Illuminate\Bus\Queueable;
15+
use Illuminate\Contracts\Queue\ShouldQueue;
16+
use Illuminate\Foundation\Bus\Dispatchable;
17+
use Illuminate\Queue\InteractsWithQueue;
18+
use Illuminate\Queue\SerializesModels;
19+
use Illuminate\Support\Facades\Auth;
20+
use Illuminate\Support\Facades\Log;
21+
use Illuminate\Support\Facades\Storage;
22+
use Illuminate\Support\Str;
23+
use function Safe\date;
24+
use function Safe\unlink;
25+
26+
class ExtractZip implements ShouldQueue
27+
{
28+
use Dispatchable;
29+
use InteractsWithQueue;
30+
use Queueable;
31+
use SerializesModels;
32+
33+
protected JobHistory $history;
34+
35+
public string $filePath;
36+
public string $originalBaseName;
37+
public ?string $albumID;
38+
public int $userId;
39+
public ?int $fileLastModifiedTime;
40+
41+
/**
42+
* Create a new job instance.
43+
*/
44+
public function __construct(
45+
ProcessableJobFile $file,
46+
string|AbstractAlbum|null $albumID,
47+
?int $fileLastModifiedTime,
48+
) {
49+
$this->filePath = $file->getPath();
50+
$this->originalBaseName = $file->getOriginalBasename();
51+
$this->albumID = is_string($albumID) ? $albumID : $albumID?->id;
52+
$this->userId = Auth::user()->id;
53+
$this->fileLastModifiedTime = $fileLastModifiedTime;
54+
55+
// Set up our new history record.
56+
$this->history = new JobHistory();
57+
$this->history->owner_id = $this->userId;
58+
$this->history->job = Str::limit('Extracting: ' . $this->originalBaseName, 200);
59+
$this->history->status = JobStatus::READY;
60+
61+
$this->history->save();
62+
}
63+
64+
/**
65+
* Execute the job.
66+
*/
67+
public function handle(): void
68+
{
69+
$this->history->status = JobStatus::STARTED;
70+
$this->history->save();
71+
72+
$extractedFolderName = $this->getExtractFolderName();
73+
74+
$pathExtracted = Storage::disk('extract-jobs')->path(date('Ymd') . $extractedFolderName);
75+
$zip = new \ZipArchive();
76+
if ($zip->open($this->filePath) === true) {
77+
$zip->extractTo($pathExtracted);
78+
$zip->close();
79+
80+
// clean up the zip file
81+
unlink($this->filePath);
82+
83+
$this->history->status = JobStatus::SUCCESS;
84+
$this->history->save();
85+
} else {
86+
throw new ZipExtractionException($this->filePath, $pathExtracted);
87+
}
88+
89+
$newAlbum = $this->createAlbum($extractedFolderName, $this->albumID);
90+
$jobs = [];
91+
foreach (new \DirectoryIterator($pathExtracted) as $fileInfo) {
92+
if ($fileInfo->isDot() || $fileInfo->isDir()) {
93+
continue;
94+
}
95+
96+
$extractedFile = new ExtractedJobFile($fileInfo->getRealPath(), $fileInfo->getFilename());
97+
$jobs[] = new ProcessImageJob($extractedFile, $newAlbum, $fileInfo->getMTime());
98+
}
99+
100+
$jobs[] = new CleanUpExtraction($pathExtracted);
101+
foreach ($jobs as $job) {
102+
dispatch($job);
103+
}
104+
}
105+
106+
/**
107+
* Catch failures.
108+
*
109+
* @param \Throwable $th
110+
*
111+
* @return void
112+
*/
113+
public function failed(\Throwable $th): void
114+
{
115+
$this->history->status = JobStatus::FAILURE;
116+
$this->history->save();
117+
118+
if ($th->getCode() === 999) {
119+
$this->release();
120+
} else {
121+
Log::error(__LINE__ . ':' . __FILE__ . ' ' . $th->getMessage(), $th->getTrace());
122+
}
123+
}
124+
125+
/**
126+
* Given a name and parent we create it.
127+
*
128+
* @param string $newAlbumName
129+
* @param string|null $parentID
130+
*
131+
* @return Album new album
132+
*/
133+
private function createAlbum(string $newAlbumName, ?string $parentID): Album
134+
{
135+
if (SmartAlbumType::tryFrom($parentID) !== null) {
136+
$parentID = null;
137+
}
138+
139+
/** @var Album $parentAlbum */
140+
$parentAlbum = $parentID !== null ? Album::query()->findOrFail($parentID) : null; // in case no ID provided -> import to root folder
141+
$createAlbum = new Create($this->userId);
142+
143+
return $createAlbum->create($this->prepareAlbumName($newAlbumName), $parentAlbum);
144+
}
145+
146+
/**
147+
* Todo Later: add renamer module.
148+
*
149+
* @param string $albumNameCandidate
150+
*
151+
* @return string
152+
*/
153+
private function prepareAlbumName(string $albumNameCandidate): string
154+
{
155+
return trim(str_replace('_', ' ', $albumNameCandidate));
156+
}
157+
158+
/**
159+
* Returns a folder name where:
160+
* - spaces are replaced by `_`
161+
* - if folder already exists (with date prefix) then we pad with _(xx) where xx is the next available number.
162+
*
163+
* @return string
164+
*/
165+
private function getExtractFolderName(): string
166+
{
167+
$baseNameWithoutExtension = substr($this->originalBaseName, 0, -4);
168+
169+
// Save that one (is default if no existing folder found).
170+
$orignalName = str_replace(' ', '_', $baseNameWithoutExtension);
171+
172+
// Iterate on that one.
173+
$candidateName = $orignalName;
174+
175+
// count
176+
$i = 0;
177+
while (Storage::disk('extract-jobs')->exists(date('Ymd') . $candidateName)) {
178+
$candidateName = $orignalName . '_(' . $i . ')';
179+
$i++;
180+
}
181+
182+
return $candidateName;
183+
}
184+
}

app/Jobs/ProcessImageJob.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\DTO\ImportMode;
88
use App\Enum\JobStatus;
99
use App\Factories\AlbumFactory;
10+
use App\Image\Files\ExtractedJobFile;
1011
use App\Image\Files\ProcessableJobFile;
1112
use App\Image\Files\TemporaryJobFile;
1213
use App\Models\Configs;
@@ -44,7 +45,7 @@ class ProcessImageJob implements ShouldQueue
4445
* Create a new job instance.
4546
*/
4647
public function __construct(
47-
ProcessableJobFile $file,
48+
ProcessableJobFile|ExtractedJobFile $file,
4849
string|AbstractAlbum|null $album,
4950
?int $fileLastModifiedTime,
5051
) {

0 commit comments

Comments
 (0)