chore: remove deprecated S3Service

This commit is contained in:
Phan An 2024-02-05 23:47:13 +01:00
parent b4ea2856f3
commit 0e634a33a8
7 changed files with 89 additions and 176 deletions

View file

@ -1,18 +1,18 @@
<?php
namespace App\Http\Controllers\API\ObjectStorage\S3;
namespace App\Http\Controllers\API;
use App\Exceptions\SongPathNotFoundException;
use App\Http\Controllers\Controller;
use App\Http\Requests\API\ObjectStorage\S3\PutSongRequest;
use App\Http\Requests\API\ObjectStorage\S3\RemoveSongRequest;
use App\Services\S3Service;
use App\Services\SongStorage\S3LambdaStorage;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
class SongController extends Controller
class LambdaSongController extends Controller
{
public function __construct(private S3Service $s3Service)
public function __construct(private S3LambdaStorage $storage)
{
}
@ -20,7 +20,7 @@ class SongController extends Controller
{
$artist = Arr::get($request->tags, 'artist', '');
$song = $this->s3Service->createSongEntry(
$song = $this->storage->createSongEntry(
$request->bucket,
$request->key,
$artist,
@ -39,7 +39,7 @@ class SongController extends Controller
public function remove(RemoveSongRequest $request)
{
try {
$this->s3Service->deleteSongEntry($request->bucket, $request->key);
$this->storage->deleteSongEntry($request->bucket, $request->key);
} catch (SongPathNotFoundException) {
abort(Response::HTTP_NOT_FOUND);
}

View file

@ -40,7 +40,6 @@ use Throwable;
* @property ?bool $liked Whether the song is liked by the current user (dynamically calculated)
* @property ?int $play_count The number of times the song has been played by the current user (dynamically calculated)
* @property Carbon $created_at
* @property array<mixed> $s3_params
* @property int $owner_id
* @property bool $is_public
* @property User $owner
@ -188,19 +187,6 @@ class Song extends Model
);
}
protected function s3Params(): Attribute
{
return Attribute::get(function (): ?array {
if (!preg_match('/^s3:\\/\\/(.*)/', $this->path, $matches)) {
return null;
}
[$bucket, $key] = explode('/', $matches[1], 2);
return compact('bucket', 'key');
});
}
public static function getPathFromS3BucketAndKey(string $bucket, string $key): string
{
return "s3://$bucket/$key";

View file

@ -1,101 +0,0 @@
<?php
namespace App\Services;
use App\Events\LibraryChanged;
use App\Exceptions\SongPathNotFoundException;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
use App\Repositories\SongRepository;
use App\Repositories\UserRepository;
use App\Values\SongStorageTypes;
use Aws\S3\S3ClientInterface;
use Illuminate\Cache\Repository as Cache;
class S3Service implements ObjectStorageInterface
{
public function __construct(
private ?S3ClientInterface $s3Client,
private Cache $cache,
private MediaMetadataService $mediaMetadataService,
private SongRepository $songRepository,
private UserRepository $userRepository,
) {
}
public function getSongPublicUrl(Song $song): string
{
return $this->cache->remember("OSUrl/$song->id", now()->addHour(), function () use ($song): string {
$cmd = $this->s3Client->getCommand('GetObject', [
'Bucket' => $song->s3_params['bucket'],
'Key' => $song->s3_params['key'],
]);
// Here we specify that the request is valid for 1 hour.
// We'll also cache the public URL for future reuse.
$request = $this->s3Client->createPresignedRequest($cmd, '+1 hour');
return (string) $request->getUri();
});
}
public function createSongEntry(
string $bucket,
string $key,
string $artistName,
string $albumName,
string $albumArtistName,
?array $cover,
string $title,
float $duration,
int $track,
string $lyrics
): Song {
$user = $this->userRepository->getDefaultAdminUser();
$path = Song::getPathFromS3BucketAndKey($bucket, $key);
$artist = Artist::getOrCreate($artistName);
$albumArtist = $albumArtistName && $albumArtistName !== $artistName
? Artist::getOrCreate($albumArtistName)
: $artist;
$album = Album::getOrCreate($albumArtist, $albumName);
if ($cover) {
$this->mediaMetadataService->writeAlbumCover(
$album,
base64_decode($cover['data'], true),
$cover['extension']
);
}
/** @var Song $song */
$song = Song::query()->updateOrCreate(['path' => $path], [
'album_id' => $album->id,
'artist_id' => $artist->id,
'title' => $title,
'length' => $duration,
'track' => $track,
'lyrics' => $lyrics,
'mtime' => time(),
'owner_id' => $user->id,
'is_public' => true,
'storage' => SongStorageTypes::S3_LAMBDA,
]);
event(new LibraryChanged());
return $song;
}
public function deleteSongEntry(string $bucket, string $key): void
{
$path = Song::getPathFromS3BucketAndKey($bucket, $key);
$song = $this->songRepository->findOneByPath($path);
throw_unless((bool) $song, SongPathNotFoundException::create($path));
$song->delete();
event(new LibraryChanged());
}
}

View file

@ -2,9 +2,16 @@
namespace App\Services\SongStorage;
use App\Events\LibraryChanged;
use App\Exceptions\MethodNotImplementedException;
use App\Exceptions\SongPathNotFoundException;
use App\Models\Album;
use App\Models\Artist;
use App\Models\Song;
use App\Models\User;
use App\Repositories\SongRepository;
use App\Repositories\UserRepository;
use App\Services\MediaMetadataService;
use App\Values\SongStorageTypes;
use Illuminate\Http\UploadedFile;
@ -14,11 +21,78 @@ use Illuminate\Http\UploadedFile;
*/
final class S3LambdaStorage extends S3CompatibleStorage
{
public function __construct( // @phpcs:ignore
private MediaMetadataService $mediaMetadataService,
private SongRepository $songRepository,
private UserRepository $userRepository
) {
}
public function storeUploadedFile(UploadedFile $file, User $uploader): Song
{
throw new MethodNotImplementedException('Lambda storage does not support uploading.');
}
public function createSongEntry(
string $bucket,
string $key,
string $artistName,
string $albumName,
string $albumArtistName,
?array $cover,
string $title,
float $duration,
int $track,
string $lyrics
): Song {
$user = $this->userRepository->getDefaultAdminUser();
$path = Song::getPathFromS3BucketAndKey($bucket, $key);
$artist = Artist::getOrCreate($artistName);
$albumArtist = $albumArtistName && $albumArtistName !== $artistName
? Artist::getOrCreate($albumArtistName)
: $artist;
$album = Album::getOrCreate($albumArtist, $albumName);
if ($cover) {
$this->mediaMetadataService->writeAlbumCover(
$album,
base64_decode($cover['data'], true),
$cover['extension']
);
}
/** @var Song $song */
$song = Song::query()->updateOrCreate(['path' => $path], [
'album_id' => $album->id,
'artist_id' => $artist->id,
'title' => $title,
'length' => $duration,
'track' => $track,
'lyrics' => $lyrics,
'mtime' => time(),
'owner_id' => $user->id,
'is_public' => true,
'storage' => SongStorageTypes::S3_LAMBDA,
]);
event(new LibraryChanged());
return $song;
}
public function deleteSongEntry(string $bucket, string $key): void
{
$path = Song::getPathFromS3BucketAndKey($bucket, $key);
$song = $this->songRepository->findOneByPath($path);
throw_unless((bool) $song, SongPathNotFoundException::create($path));
$song->delete();
event(new LibraryChanged());
}
public function supported(): bool
{
return SongStorageTypes::supported(SongStorageTypes::S3_LAMBDA);

View file

@ -22,9 +22,9 @@ use App\Http\Controllers\API\FetchRecentlyPlayedSongController;
use App\Http\Controllers\API\FetchSongsForQueueController;
use App\Http\Controllers\API\GenreController;
use App\Http\Controllers\API\GenreSongController;
use App\Http\Controllers\API\LambdaSongController as S3SongController;
use App\Http\Controllers\API\LikeMultipleSongsController;
use App\Http\Controllers\API\MovePlaylistSongsController;
use App\Http\Controllers\API\ObjectStorage\S3\SongController as S3SongController;
use App\Http\Controllers\API\PlaylistCollaboration\AcceptPlaylistCollaborationInviteController;
use App\Http\Controllers\API\PlaylistCollaboration\CreatePlaylistCollaborationTokenController;
use App\Http\Controllers\API\PlaylistCollaboration\PlaylistCollaboratorController;

View file

@ -7,14 +7,6 @@ use Tests\TestCase;
class SongTest extends TestCase
{
public function testGettingS3HostedSongs(): void
{
/** @var Song $song */
$song = Song::factory()->create(['path' => 's3://foo/bar']);
self::assertEquals(['bucket' => 'foo', 'key' => 'bar'], $song->s3_params);
}
public function testLyricsDoNotContainTimestamps(): void
{
/** @var Song $song */

View file

@ -1,17 +1,13 @@
<?php
namespace Tests\Unit\Services;
namespace Tests\Unit\Services\SongStorage;
use App\Events\LibraryChanged;
use App\Models\Song;
use App\Repositories\SongRepository;
use App\Repositories\UserRepository;
use App\Services\MediaMetadataService;
use App\Services\S3Service;
use Aws\CommandInterface;
use Aws\S3\S3ClientInterface;
use GuzzleHttp\Psr7\Request;
use Illuminate\Cache\Repository as Cache;
use App\Services\SongStorage\S3LambdaStorage;
use Mockery;
use Mockery\LegacyMockInterface;
use Mockery\MockInterface;
@ -19,28 +15,21 @@ use Tests\TestCase;
use function Tests\create_admin;
class S3ServiceTest extends TestCase
class S3LambdaStorageTest extends TestCase
{
private S3ClientInterface|LegacyMockInterface|MockInterface $s3Client;
private Cache|LegacyMockInterface|MockInterface $cache;
private SongRepository|LegacyMockInterface|MockInterface $songRepository;
private UserRepository|LegacyMockInterface|MockInterface $userRepository;
private S3Service $s3Service;
private S3LambdaStorage $storage;
public function setUp(): void
{
parent::setUp();
$this->s3Client = Mockery::mock(S3ClientInterface::class);
$this->cache = Mockery::mock(Cache::class);
$metadataService = Mockery::mock(MediaMetadataService::class);
$this->songRepository = Mockery::mock(SongRepository::class);
$this->userRepository = Mockery::mock(UserRepository::class);
$this->s3Service = new S3Service(
$this->s3Client,
$this->cache,
$this->storage = new S3LambdaStorage(
$metadataService,
$this->songRepository,
$this->userRepository
@ -56,7 +45,7 @@ class S3ServiceTest extends TestCase
->once()
->andReturn($user);
$song = $this->s3Service->createSongEntry(
$song = $this->storage->createSongEntry(
bucket: 'foo',
key: 'bar',
artistName: 'Queen',
@ -93,7 +82,7 @@ class S3ServiceTest extends TestCase
'path' => 's3://foo/bar',
]);
$this->s3Service->createSongEntry(
$this->storage->createSongEntry(
bucket: 'foo',
key: 'bar',
artistName: 'Queen',
@ -134,35 +123,8 @@ class S3ServiceTest extends TestCase
->once()
->andReturn($song);
$this->s3Service->deleteSongEntry('foo', 'bar');
$this->storage->deleteSongEntry('foo', 'bar');
self::assertModelMissing($song);
}
public function testGetSongPublicUrl(): void
{
/** @var Song $song */
$song = Song::factory()->create(['path' => 's3://foo/bar']);
$cmd = Mockery::mock(CommandInterface::class);
$this->s3Client->shouldReceive('getCommand')
->with('GetObject', [
'Bucket' => 'foo',
'Key' => 'bar',
])
->andReturn($cmd);
$request = Mockery::mock(Request::class, ['getUri' => 'https://aws.com/foo.mp3']);
$this->s3Client->shouldReceive('createPresignedRequest')
->with($cmd, '+1 hour')
->andReturn($request);
$this->cache->shouldReceive('remember')
->once()
->andReturn('https://aws.com/foo.mp3');
self::assertSame('https://aws.com/foo.mp3', $this->s3Service->getSongPublicUrl($song));
}
}