<?php
namespace App\Services;
use App\Exceptions\SongUploadFailedException;
use App\Models\Song;
use App\Models\User;
use App\Services\Scanners\FileScanner;
use App\Services\SongStorages\Contracts\MustDeleteTemporaryLocalFileAfterUpload;
use App\Services\SongStorages\SongStorage;
use App\Values\Scanning\ScanConfiguration;
use App\Values\UploadReference;
use Illuminate\Support\Facades\File;
use Throwable;
class UploadService
{
public function __construct(
private readonly SongService $songService,
private readonly SongStorage $storage,
private readonly FileScanner $scanner,
) {
}
public function handleUpload(string $filePath, User $uploader): Song
{
$uploadReference = $this->storage->storeUploadedFile($filePath, $uploader);
$config = ScanConfiguration::make(
owner: $uploader,
makePublic: $uploader->preferences->makeUploadsPublic,
extractFolderStructure: $this->storage->getStorageType()->supportsFolderStructureExtraction(),
);
try {
$song = $this->songService->createOrUpdateSongFromScan(
$this->scanner->scan($uploadReference->localPath),
$config,
);
} catch (Throwable $error) {
$this->handleUploadFailure($uploadReference, $error);
}
if ($this->storage instanceof MustDeleteTemporaryLocalFileAfterUpload) {
File::delete($uploadReference->localPath);
}
// Since we scanned a local file, the song's path was initially set to the local path.
// We need to update it to the actual storage (e.g. S3) and location (e.g., the S3 key) if applicable.
if ($song->path !== $uploadReference->location || $song->storage !== $this->storage->getStorageType()) {
$song->update([
'path' => $uploadReference->location,
'storage' => $this->storage->getStorageType(),
]);
}
return $song;
}
private function handleUploadFailure(UploadReference $reference, Throwable|string $error): never
{
$this->storage->undoUpload($reference);
throw SongUploadFailedException::make($error);
}
}