Implement creator studio and upload updates
This commit is contained in:
@@ -12,8 +12,9 @@ use App\Models\Artwork;
|
||||
use App\Repositories\Uploads\ArtworkFileRepository;
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class UploadPipelineService
|
||||
{
|
||||
@@ -21,6 +22,7 @@ final class UploadPipelineService
|
||||
private readonly UploadStorageService $storage,
|
||||
private readonly UploadSessionRepository $sessions,
|
||||
private readonly UploadValidationService $validator,
|
||||
private readonly UploadArchiveValidationService $archiveValidator,
|
||||
private readonly UploadHashService $hasher,
|
||||
private readonly UploadScanService $scanner,
|
||||
private readonly UploadAuditService $audit,
|
||||
@@ -85,6 +87,27 @@ final class UploadPipelineService
|
||||
return new UploadValidatedFile($validation, $hash);
|
||||
}
|
||||
|
||||
public function validateAndHashArchive(string $sessionId): UploadValidatedFile
|
||||
{
|
||||
$session = $this->sessions->getOrFail($sessionId);
|
||||
$validation = $this->archiveValidator->validate($session->tempPath);
|
||||
|
||||
if (! $validation->ok) {
|
||||
$this->quarantine($session, $validation->reason);
|
||||
return new UploadValidatedFile($validation, null);
|
||||
}
|
||||
|
||||
$hash = $this->hasher->hashFile($session->tempPath);
|
||||
$this->sessions->updateStatus($sessionId, UploadSessionStatus::VALIDATED);
|
||||
$this->sessions->updateProgress($sessionId, 30);
|
||||
$this->audit->log($session->userId, 'upload_archive_validated', $session->ip, [
|
||||
'session_id' => $sessionId,
|
||||
'hash' => $hash,
|
||||
]);
|
||||
|
||||
return new UploadValidatedFile($validation, $hash);
|
||||
}
|
||||
|
||||
public function scan(string $sessionId): UploadScanResult
|
||||
{
|
||||
$session = $this->sessions->getOrFail($sessionId);
|
||||
@@ -104,72 +127,154 @@ final class UploadPipelineService
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function processAndPublish(string $sessionId, string $hash, int $artworkId, ?string $originalFileName = null): array
|
||||
public function processAndPublish(
|
||||
string $sessionId,
|
||||
string $hash,
|
||||
int $artworkId,
|
||||
?string $originalFileName = null,
|
||||
?string $archiveSessionId = null,
|
||||
?string $archiveHash = null,
|
||||
?string $archiveOriginalFileName = null,
|
||||
array $additionalScreenshotSessions = []
|
||||
): array
|
||||
{
|
||||
$session = $this->sessions->getOrFail($sessionId);
|
||||
|
||||
$originalPath = $this->derivatives->storeOriginal($session->tempPath, $hash, $originalFileName);
|
||||
$origFilename = basename($originalPath);
|
||||
$originalRelative = $this->storage->sectionRelativePath('original', $hash, $origFilename);
|
||||
$origMime = File::exists($originalPath) ? File::mimeType($originalPath) : 'application/octet-stream';
|
||||
$this->artworkFiles->upsert($artworkId, 'orig', $originalRelative, $origMime, (int) filesize($originalPath));
|
||||
|
||||
$publicAbsolute = $this->derivatives->generatePublicDerivatives($session->tempPath, $hash);
|
||||
$publicRelative = [];
|
||||
|
||||
foreach ($publicAbsolute as $variant => $absolutePath) {
|
||||
$filename = $hash . '.webp';
|
||||
$relativePath = $this->storage->sectionRelativePath($variant, $hash, $filename);
|
||||
$this->artworkFiles->upsert($artworkId, $variant, $relativePath, 'image/webp', (int) filesize($absolutePath));
|
||||
$publicRelative[$variant] = $relativePath;
|
||||
$archiveSession = null;
|
||||
if (is_string($archiveSessionId) && trim($archiveSessionId) !== '') {
|
||||
$archiveSession = $this->sessions->getOrFail($archiveSessionId);
|
||||
}
|
||||
|
||||
$dimensions = @getimagesize($session->tempPath);
|
||||
$width = is_array($dimensions) && isset($dimensions[0]) ? (int) $dimensions[0] : 1;
|
||||
$height = is_array($dimensions) && isset($dimensions[1]) ? (int) $dimensions[1] : 1;
|
||||
$resolvedAdditionalScreenshots = collect($additionalScreenshotSessions)
|
||||
->filter(fn ($payload) => is_array($payload) && is_string($payload['session_id'] ?? null) && is_string($payload['hash'] ?? null))
|
||||
->values()
|
||||
->map(function (array $payload): array {
|
||||
return [
|
||||
'session' => $this->sessions->getOrFail((string) $payload['session_id']),
|
||||
'hash' => trim((string) $payload['hash']),
|
||||
'file_name' => is_string($payload['file_name'] ?? null) ? $payload['file_name'] : null,
|
||||
];
|
||||
});
|
||||
|
||||
$origExt = strtolower(pathinfo($originalPath, PATHINFO_EXTENSION) ?: '');
|
||||
$downloadFileName = $origFilename;
|
||||
$localCleanup = [];
|
||||
$objectCleanup = [];
|
||||
|
||||
if (is_string($originalFileName) && trim($originalFileName) !== '') {
|
||||
$candidate = basename(str_replace('\\', '/', $originalFileName));
|
||||
$candidate = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $candidate) ?? '';
|
||||
$candidate = trim((string) $candidate);
|
||||
try {
|
||||
$imageOriginal = $this->derivatives->storeOriginal($session->tempPath, $hash, $originalFileName);
|
||||
$localCleanup[] = $imageOriginal['local_path'];
|
||||
$objectCleanup[] = $imageOriginal['object_path'];
|
||||
|
||||
if ($candidate !== '') {
|
||||
$candidateExt = strtolower((string) pathinfo($candidate, PATHINFO_EXTENSION));
|
||||
if ($candidateExt === '' && $origExt !== '') {
|
||||
$candidate .= '.' . $origExt;
|
||||
$archiveOriginal = null;
|
||||
if ($archiveSession && is_string($archiveHash) && trim($archiveHash) !== '') {
|
||||
$archiveOriginal = $this->derivatives->storeOriginal($archiveSession->tempPath, trim($archiveHash), $archiveOriginalFileName);
|
||||
$localCleanup[] = $archiveOriginal['local_path'];
|
||||
$objectCleanup[] = $archiveOriginal['object_path'];
|
||||
}
|
||||
|
||||
$additionalScreenshotOriginals = [];
|
||||
foreach ($resolvedAdditionalScreenshots as $index => $payload) {
|
||||
$storedScreenshot = $this->derivatives->storeOriginal(
|
||||
$payload['session']->tempPath,
|
||||
$payload['hash'],
|
||||
$payload['file_name']
|
||||
);
|
||||
$storedScreenshot['variant'] = $this->screenshotVariantName($index + 1);
|
||||
$additionalScreenshotOriginals[] = $storedScreenshot;
|
||||
$localCleanup[] = $storedScreenshot['local_path'];
|
||||
$objectCleanup[] = $storedScreenshot['object_path'];
|
||||
}
|
||||
|
||||
$publicAssets = $this->derivatives->generatePublicDerivatives($session->tempPath, $hash);
|
||||
foreach ($publicAssets as $asset) {
|
||||
$objectCleanup[] = $asset['path'];
|
||||
}
|
||||
|
||||
$dimensions = @getimagesize($session->tempPath);
|
||||
$width = is_array($dimensions) && isset($dimensions[0]) ? (int) $dimensions[0] : 1;
|
||||
$height = is_array($dimensions) && isset($dimensions[1]) ? (int) $dimensions[1] : 1;
|
||||
|
||||
$primaryOriginal = $archiveOriginal ?? $imageOriginal;
|
||||
$downloadFileName = $this->resolveDownloadFileName($primaryOriginal['filename'], $primaryOriginal['ext'], $archiveOriginal ? $archiveOriginalFileName : $originalFileName);
|
||||
|
||||
DB::transaction(function () use ($artworkId, $imageOriginal, $archiveOriginal, $additionalScreenshotOriginals, $publicAssets, $primaryOriginal, $downloadFileName, $hash, $width, $height) {
|
||||
$this->artworkFiles->upsert($artworkId, 'orig', $primaryOriginal['object_path'], $primaryOriginal['mime'], $primaryOriginal['size']);
|
||||
$this->artworkFiles->upsert($artworkId, 'orig_image', $imageOriginal['object_path'], $imageOriginal['mime'], $imageOriginal['size']);
|
||||
|
||||
if ($archiveOriginal) {
|
||||
$this->artworkFiles->upsert($artworkId, 'orig_archive', $archiveOriginal['object_path'], $archiveOriginal['mime'], $archiveOriginal['size']);
|
||||
} else {
|
||||
$this->artworkFiles->deleteVariant($artworkId, 'orig_archive');
|
||||
}
|
||||
|
||||
$downloadFileName = $candidate;
|
||||
$this->artworkFiles->deleteScreenshotVariants($artworkId);
|
||||
foreach ($additionalScreenshotOriginals as $screenshot) {
|
||||
$this->artworkFiles->upsert($artworkId, $screenshot['variant'], $screenshot['object_path'], $screenshot['mime'], $screenshot['size']);
|
||||
}
|
||||
|
||||
foreach ($publicAssets as $variant => $asset) {
|
||||
$this->artworkFiles->upsert($artworkId, (string) $variant, $asset['path'], $asset['mime'], $asset['size']);
|
||||
}
|
||||
|
||||
Artwork::query()->whereKey($artworkId)->update([
|
||||
'file_name' => $downloadFileName,
|
||||
'file_path' => $primaryOriginal['object_path'],
|
||||
'file_size' => $primaryOriginal['size'],
|
||||
'mime_type' => $primaryOriginal['mime'],
|
||||
'hash' => $hash,
|
||||
'file_ext' => $primaryOriginal['ext'],
|
||||
'thumb_ext' => 'webp',
|
||||
'width' => max(1, $width),
|
||||
'height' => max(1, $height),
|
||||
]);
|
||||
});
|
||||
|
||||
$this->storage->deleteLocalFile($session->tempPath);
|
||||
if ($archiveSession) {
|
||||
$this->storage->deleteLocalFile($archiveSession->tempPath);
|
||||
}
|
||||
foreach ($resolvedAdditionalScreenshots as $payload) {
|
||||
$this->storage->deleteLocalFile($payload['session']->tempPath);
|
||||
}
|
||||
|
||||
$this->sessions->updateStatus($sessionId, UploadSessionStatus::PROCESSED);
|
||||
$this->sessions->updateProgress($sessionId, 100);
|
||||
if ($archiveSession) {
|
||||
$this->sessions->updateStatus($archiveSession->id, UploadSessionStatus::PROCESSED);
|
||||
$this->sessions->updateProgress($archiveSession->id, 100);
|
||||
}
|
||||
foreach ($resolvedAdditionalScreenshots as $payload) {
|
||||
$this->sessions->updateStatus($payload['session']->id, UploadSessionStatus::PROCESSED);
|
||||
$this->sessions->updateProgress($payload['session']->id, 100);
|
||||
}
|
||||
$this->audit->log($session->userId, 'upload_processed', $session->ip, [
|
||||
'session_id' => $sessionId,
|
||||
'hash' => $hash,
|
||||
'artwork_id' => $artworkId,
|
||||
'archive_session_id' => $archiveSession?->id,
|
||||
'additional_screenshot_session_ids' => $resolvedAdditionalScreenshots->map(fn (array $payload): string => $payload['session']->id)->values()->all(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'orig' => $primaryOriginal['object_path'],
|
||||
'orig_image' => $imageOriginal['object_path'],
|
||||
'orig_archive' => $archiveOriginal['object_path'] ?? null,
|
||||
'screenshots' => array_map(static fn (array $screenshot): array => [
|
||||
'variant' => $screenshot['variant'],
|
||||
'path' => $screenshot['object_path'],
|
||||
], $additionalScreenshotOriginals),
|
||||
'public' => array_map(static fn (array $asset): string => $asset['path'], $publicAssets),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
foreach ($localCleanup as $path) {
|
||||
$this->storage->deleteLocalFile($path);
|
||||
}
|
||||
|
||||
foreach ($objectCleanup as $path) {
|
||||
$this->storage->deleteObject($path);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Artwork::query()->whereKey($artworkId)->update([
|
||||
'file_name' => $downloadFileName,
|
||||
'file_path' => '',
|
||||
'file_size' => (int) filesize($originalPath),
|
||||
'mime_type' => $origMime,
|
||||
'hash' => $hash,
|
||||
'file_ext' => $origExt,
|
||||
'thumb_ext' => 'webp',
|
||||
'width' => max(1, $width),
|
||||
'height' => max(1, $height),
|
||||
]);
|
||||
|
||||
$this->sessions->updateStatus($sessionId, UploadSessionStatus::PROCESSED);
|
||||
$this->sessions->updateProgress($sessionId, 100);
|
||||
$this->audit->log($session->userId, 'upload_processed', $session->ip, [
|
||||
'session_id' => $sessionId,
|
||||
'hash' => $hash,
|
||||
'artwork_id' => $artworkId,
|
||||
]);
|
||||
|
||||
return [
|
||||
'orig' => $originalRelative,
|
||||
'public' => $publicRelative,
|
||||
];
|
||||
}
|
||||
|
||||
public function originalHashExists(string $hash): bool
|
||||
@@ -193,4 +298,31 @@ final class UploadPipelineService
|
||||
'reason' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDownloadFileName(string $storedFilename, string $ext, ?string $preferredFileName): string
|
||||
{
|
||||
$downloadFileName = $storedFilename;
|
||||
|
||||
if (is_string($preferredFileName) && trim($preferredFileName) !== '') {
|
||||
$candidate = basename(str_replace('\\', '/', $preferredFileName));
|
||||
$candidate = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $candidate) ?? '';
|
||||
$candidate = trim((string) $candidate);
|
||||
|
||||
if ($candidate !== '') {
|
||||
$candidateExt = strtolower((string) pathinfo($candidate, PATHINFO_EXTENSION));
|
||||
if ($candidateExt === '' && $ext !== '') {
|
||||
$candidate .= '.' . $ext;
|
||||
}
|
||||
|
||||
$downloadFileName = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $downloadFileName;
|
||||
}
|
||||
|
||||
private function screenshotVariantName(int $position): string
|
||||
{
|
||||
return sprintf('shot%02d', max(1, min(99, $position)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user