mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat(plus): add SFTP support
This commit is contained in:
parent
448cbed731
commit
aa787edb2e
41 changed files with 552 additions and 160 deletions
|
@ -9,14 +9,15 @@ enum SongStorageType: string
|
|||
case S3 = 's3';
|
||||
case S3_LAMBDA = 's3-lambda';
|
||||
case DROPBOX = 'dropbox';
|
||||
case SFTP = 'sftp';
|
||||
case LOCAL = '';
|
||||
|
||||
public function supported(): bool
|
||||
{
|
||||
if ($this === self::LOCAL || $this === self::S3_LAMBDA) {
|
||||
if (License::isPlus()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return License::isPlus();
|
||||
return $this === self::LOCAL || $this === self::S3_LAMBDA;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ class RegisterPlayController extends Controller
|
|||
InteractionService $interactionService,
|
||||
?Authenticatable $user
|
||||
) {
|
||||
/** @var Song $song */
|
||||
$song = Song::query()->findOrFail($request->song);
|
||||
$this->authorize('access', $song);
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@ namespace App\Models;
|
|||
use App\Builders\SongBuilder;
|
||||
use App\Enums\SongStorageType;
|
||||
use App\Models\Concerns\SupportsDeleteWhereValueNotIn;
|
||||
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
|
||||
use App\Values\SongStorageMetadata\DropboxMetadata;
|
||||
use App\Values\SongStorageMetadata\LegacyS3Metadata;
|
||||
use App\Values\SongStorageMetadata\LocalMetadata;
|
||||
use App\Values\SongStorageMetadata\S3CompatibleMetadata;
|
||||
use App\Values\SongStorageMetadata\S3LambdaMetadata;
|
||||
use App\Values\SongStorageMetadata\SftpMetadata;
|
||||
use App\Values\SongStorageMetadata\SongStorageMetadata;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
@ -170,13 +171,17 @@ class Song extends Model
|
|||
get: function (): SongStorageMetadata {
|
||||
try {
|
||||
switch ($this->storage) {
|
||||
case SongStorageType::SFTP:
|
||||
preg_match('/^sftp:\\/\\/(.*)/', $this->path, $matches);
|
||||
return SftpMetadata::make($matches[1]);
|
||||
|
||||
case SongStorageType::S3:
|
||||
preg_match('/^s3:\\/\\/(.*)\\/(.*)/', $this->path, $matches);
|
||||
return S3CompatibleMetadata::make($matches[1], $matches[2]);
|
||||
|
||||
case SongStorageType::S3_LAMBDA:
|
||||
preg_match('/^s3:\\/\\/(.*)\\/(.*)/', $this->path, $matches);
|
||||
return LegacyS3Metadata::make($matches[1], $matches[2]);
|
||||
return S3LambdaMetadata::make($matches[1], $matches[2]);
|
||||
|
||||
case SongStorageType::DROPBOX:
|
||||
preg_match('/^dropbox:\\/\\/(.*)/', $this->path, $matches);
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Providers;
|
|||
use App\Services\SongStorages\DropboxStorage;
|
||||
use App\Services\SongStorages\LocalStorage;
|
||||
use App\Services\SongStorages\S3CompatibleStorage;
|
||||
use App\Services\SongStorages\SftpStorage;
|
||||
use App\Services\SongStorages\SongStorage;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
@ -16,6 +17,7 @@ class SongStorageServiceProvider extends ServiceProvider
|
|||
$concrete = match (config('koel.storage_driver')) {
|
||||
's3' => S3CompatibleStorage::class,
|
||||
'dropbox' => DropboxStorage::class,
|
||||
'sftp' => SftpStorage::class,
|
||||
default => LocalStorage::class,
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Models\Song;
|
|||
use App\Models\SongZipArchive;
|
||||
use App\Services\SongStorages\DropboxStorage;
|
||||
use App\Services\SongStorages\S3CompatibleStorage;
|
||||
use App\Services\SongStorages\SftpStorage;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
|
@ -34,6 +35,10 @@ class DownloadService
|
|||
return File::exists($song->path) ? $song->path : null;
|
||||
}
|
||||
|
||||
if ($song->storage === SongStorageType::SFTP) {
|
||||
return app(SftpStorage::class)->copyToLocal($song);
|
||||
}
|
||||
|
||||
switch ($song->storage) {
|
||||
case SongStorageType::DROPBOX:
|
||||
$cloudStorage = app(DropboxStorage::class);
|
||||
|
|
|
@ -2,45 +2,22 @@
|
|||
|
||||
namespace App\Services\SongStorages;
|
||||
|
||||
use App\Exceptions\SongUploadFailedException;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\FileScanner;
|
||||
use App\Values\ScanConfiguration;
|
||||
use App\Values\ScanResult;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use App\Services\SongStorages\Concerns\ScansUploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
abstract class CloudStorage extends SongStorage
|
||||
{
|
||||
use ScansUploadedFile;
|
||||
|
||||
public function __construct(protected FileScanner $scanner)
|
||||
{
|
||||
}
|
||||
|
||||
protected function scanUploadedFile(UploadedFile $file, User $uploader): ScanResult
|
||||
{
|
||||
self::assertSupported();
|
||||
|
||||
// Can't scan the uploaded file directly, as it apparently causes some misbehavior during idv3 tag reading.
|
||||
// Instead, we copy the file to the tmp directory and scan it from there.
|
||||
$tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'koel_tmp';
|
||||
File::ensureDirectoryExists($tmpDir);
|
||||
|
||||
$tmpFile = $file->move($tmpDir, $file->getClientOriginalName());
|
||||
|
||||
$result = $this->scanner->setFile($tmpFile)
|
||||
->scan(ScanConfiguration::make(
|
||||
owner: $uploader,
|
||||
makePublic: $uploader->preferences->makeUploadsPublic
|
||||
));
|
||||
|
||||
throw_if($result->isError(), new SongUploadFailedException($result->error));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function copyToLocal(Song $song): string
|
||||
{
|
||||
self::assertSupported();
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SongStorages\Concerns;
|
||||
|
||||
use App\Models\Song;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as IlluminateFilesystem;
|
||||
use League\Flysystem\Filesystem;
|
||||
|
||||
trait DeletesUsingFilesystem
|
||||
{
|
||||
private function deleteUsingFileSystem(Filesystem | IlluminateFilesystem $disk, Song $song, bool $backup): void
|
||||
{
|
||||
$path = $song->storage_metadata->getPath();
|
||||
|
||||
if ($backup) {
|
||||
$disk->move($path, "backup/$path");
|
||||
}
|
||||
|
||||
$disk->delete($path);
|
||||
}
|
||||
}
|
34
app/Services/SongStorages/Concerns/ScansUploadedFile.php
Normal file
34
app/Services/SongStorages/Concerns/ScansUploadedFile.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SongStorages\Concerns;
|
||||
|
||||
use App\Exceptions\SongUploadFailedException;
|
||||
use App\Models\User;
|
||||
use App\Services\FileScanner;
|
||||
use App\Values\ScanConfiguration;
|
||||
use App\Values\ScanResult;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
trait ScansUploadedFile
|
||||
{
|
||||
protected function scanUploadedFile(FileScanner $scanner, UploadedFile $file, User $uploader): ScanResult
|
||||
{
|
||||
// Can't scan the uploaded file directly, as it apparently causes some misbehavior during idv3 tag reading.
|
||||
// Instead, we copy the file to the tmp directory and scan it from there.
|
||||
$tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'koel_tmp';
|
||||
File::ensureDirectoryExists($tmpDir);
|
||||
|
||||
$tmpFile = $file->move($tmpDir, $file->getClientOriginalName());
|
||||
|
||||
$result = $scanner->setFile($tmpFile)
|
||||
->scan(ScanConfiguration::make(
|
||||
owner: $uploader,
|
||||
makePublic: $uploader->preferences->makeUploadsPublic
|
||||
));
|
||||
|
||||
throw_if($result->isError(), new SongUploadFailedException($result->error));
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use App\Filesystems\DropboxFilesystem;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\FileScanner;
|
||||
use App\Services\SongStorages\Concerns\DeletesUsingFilesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
@ -15,6 +16,8 @@ use Illuminate\Support\Facades\Http;
|
|||
|
||||
final class DropboxStorage extends CloudStorage
|
||||
{
|
||||
use DeletesUsingFilesystem;
|
||||
|
||||
public function __construct(
|
||||
protected FileScanner $scanner,
|
||||
private readonly DropboxFilesystem $filesystem,
|
||||
|
@ -30,7 +33,7 @@ final class DropboxStorage extends CloudStorage
|
|||
self::assertSupported();
|
||||
|
||||
return DB::transaction(function () use ($file, $uploader): Song {
|
||||
$result = $this->scanUploadedFile($file, $uploader);
|
||||
$result = $this->scanUploadedFile($this->scanner, $file, $uploader);
|
||||
$song = $this->scanner->getSong();
|
||||
$key = $this->generateStorageKey($file->getClientOriginalName(), $uploader);
|
||||
|
||||
|
@ -78,22 +81,10 @@ final class DropboxStorage extends CloudStorage
|
|||
return $this->filesystem->temporaryUrl($song->storage_metadata->getPath());
|
||||
}
|
||||
|
||||
public function supported(): bool
|
||||
{
|
||||
return SongStorageType::DROPBOX->supported();
|
||||
}
|
||||
|
||||
public function delete(Song $song, bool $backup = false): void
|
||||
{
|
||||
self::assertSupported();
|
||||
|
||||
$path = $song->storage_metadata->getPath();
|
||||
|
||||
if ($backup) {
|
||||
$this->filesystem->move($path, "backup/$path");
|
||||
}
|
||||
|
||||
$this->filesystem->delete($path);
|
||||
$this->deleteUsingFileSystem($this->filesystem, $song, $backup);
|
||||
}
|
||||
|
||||
public function testSetup(): void
|
||||
|
@ -101,4 +92,9 @@ final class DropboxStorage extends CloudStorage
|
|||
$this->filesystem->write('test.txt', 'Koel test file.');
|
||||
$this->filesystem->delete('test.txt');
|
||||
}
|
||||
|
||||
protected function getStorageType(): SongStorageType
|
||||
{
|
||||
return SongStorageType::DROPBOX;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use function Functional\memoize;
|
|||
|
||||
final class LocalStorage extends SongStorage
|
||||
{
|
||||
public function __construct(private FileScanner $scanner)
|
||||
public function __construct(private readonly FileScanner $scanner)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -90,11 +90,6 @@ final class LocalStorage extends SongStorage
|
|||
return substr(sha1(uniqid()), 0, 6);
|
||||
}
|
||||
|
||||
public function supported(): bool
|
||||
{
|
||||
return SongStorageType::LOCAL->supported();
|
||||
}
|
||||
|
||||
public function delete(Song $song, bool $backup = false): void
|
||||
{
|
||||
$path = $song->storage_metadata->getPath();
|
||||
|
@ -105,4 +100,9 @@ final class LocalStorage extends SongStorage
|
|||
|
||||
throw_unless(File::delete($path), new Exception("Failed to delete song file: $path"));
|
||||
}
|
||||
|
||||
protected function getStorageType(): SongStorageType
|
||||
{
|
||||
return SongStorageType::LOCAL;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Enums\SongStorageType;
|
|||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\FileScanner;
|
||||
use App\Services\SongStorages\Concerns\DeletesUsingFilesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
@ -13,6 +14,8 @@ use Illuminate\Support\Facades\Storage;
|
|||
|
||||
class S3CompatibleStorage extends CloudStorage
|
||||
{
|
||||
use DeletesUsingFilesystem;
|
||||
|
||||
public function __construct(protected FileScanner $scanner, private readonly string $bucket)
|
||||
{
|
||||
parent::__construct($scanner);
|
||||
|
@ -23,7 +26,7 @@ class S3CompatibleStorage extends CloudStorage
|
|||
self::assertSupported();
|
||||
|
||||
return DB::transaction(function () use ($file, $uploader): Song {
|
||||
$result = $this->scanUploadedFile($file, $uploader);
|
||||
$result = $this->scanUploadedFile($this->scanner, $file, $uploader);
|
||||
$song = $this->scanner->getSong();
|
||||
$key = $this->generateStorageKey($file->getClientOriginalName(), $uploader);
|
||||
|
||||
|
@ -50,15 +53,7 @@ class S3CompatibleStorage extends CloudStorage
|
|||
public function delete(Song $song, bool $backup = false): void
|
||||
{
|
||||
self::assertSupported();
|
||||
|
||||
$disk = Storage::disk('s3');
|
||||
$path = $song->storage_metadata->getPath();
|
||||
|
||||
if ($backup) {
|
||||
$disk->move($path, "backup/$path");
|
||||
}
|
||||
|
||||
$disk->delete($song->storage_metadata->getPath());
|
||||
$this->deleteUsingFileSystem(Storage::disk('s3'), $song, $backup);
|
||||
}
|
||||
|
||||
public function testSetup(): void
|
||||
|
@ -67,8 +62,8 @@ class S3CompatibleStorage extends CloudStorage
|
|||
Storage::disk('s3')->delete('test.txt');
|
||||
}
|
||||
|
||||
public function supported(): bool
|
||||
protected function getStorageType(): SongStorageType
|
||||
{
|
||||
return SongStorageType::S3->supported();
|
||||
return SongStorageType::S3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,13 +84,13 @@ final class S3LambdaStorage extends S3CompatibleStorage
|
|||
$song->delete();
|
||||
}
|
||||
|
||||
public function supported(): bool
|
||||
{
|
||||
return SongStorageType::S3_LAMBDA->supported();
|
||||
}
|
||||
|
||||
public function delete(Song $song, bool $backup = false): void
|
||||
{
|
||||
throw new MethodNotImplementedException('Lambda storage does not support deleting from filesystem.');
|
||||
}
|
||||
|
||||
protected function getStorageType(): SongStorageType
|
||||
{
|
||||
return SongStorageType::S3_LAMBDA;
|
||||
}
|
||||
}
|
||||
|
|
86
app/Services/SongStorages/SftpStorage.php
Normal file
86
app/Services/SongStorages/SftpStorage.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\SongStorages;
|
||||
|
||||
use App\Enums\SongStorageType;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Services\FileScanner;
|
||||
use App\Services\SongStorages\Concerns\DeletesUsingFilesystem;
|
||||
use App\Services\SongStorages\Concerns\ScansUploadedFile;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
final class SftpStorage extends SongStorage
|
||||
{
|
||||
use DeletesUsingFilesystem;
|
||||
use ScansUploadedFile;
|
||||
|
||||
public function __construct(protected FileScanner $scanner)
|
||||
{
|
||||
}
|
||||
|
||||
public function storeUploadedFile(UploadedFile $file, User $uploader): Song
|
||||
{
|
||||
self::assertSupported();
|
||||
|
||||
return DB::transaction(function () use ($file, $uploader): Song {
|
||||
$result = $this->scanUploadedFile($this->scanner, $file, $uploader);
|
||||
$song = $this->scanner->getSong();
|
||||
|
||||
$path = $this->generateRemotePath($file->getClientOriginalName(), $uploader);
|
||||
|
||||
Storage::disk('sftp')->put($path, File::get($result->path));
|
||||
|
||||
$song->update([
|
||||
'path' => "sftp://$path",
|
||||
'storage' => SongStorageType::SFTP,
|
||||
]);
|
||||
|
||||
File::delete($result->path);
|
||||
|
||||
return $song;
|
||||
});
|
||||
}
|
||||
|
||||
public function delete(Song $song, bool $backup = false): void
|
||||
{
|
||||
self::assertSupported();
|
||||
$this->deleteUsingFileSystem(Storage::disk('sftp'), $song, $backup);
|
||||
}
|
||||
|
||||
public function getSongContent(Song $song): string
|
||||
{
|
||||
self::assertSupported();
|
||||
|
||||
return Storage::disk('sftp')->get($song->storage_metadata->getPath());
|
||||
}
|
||||
|
||||
public function copyToLocal(Song $song): string
|
||||
{
|
||||
self::assertSupported();
|
||||
|
||||
$tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'koel_tmp';
|
||||
File::ensureDirectoryExists($tmpDir);
|
||||
|
||||
$localPath = $tmpDir . DIRECTORY_SEPARATOR . basename($song->storage_metadata->getPath());
|
||||
|
||||
File::put($localPath, $this->getSongContent($song));
|
||||
|
||||
return $localPath;
|
||||
}
|
||||
|
||||
private function generateRemotePath(string $filename, User $uploader): string
|
||||
{
|
||||
return sprintf('%s__%s__%s', $uploader->id, Str::lower(Ulid::generate()), $filename);
|
||||
}
|
||||
|
||||
protected function getStorageType(): SongStorageType
|
||||
{
|
||||
return SongStorageType::SFTP;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Services\SongStorages;
|
||||
|
||||
use App\Enums\SongStorageType;
|
||||
use App\Exceptions\KoelPlusRequiredException;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
|
@ -9,16 +10,16 @@ use Illuminate\Http\UploadedFile;
|
|||
|
||||
abstract class SongStorage
|
||||
{
|
||||
abstract protected function getStorageType(): SongStorageType;
|
||||
|
||||
abstract public function storeUploadedFile(UploadedFile $file, User $uploader): Song;
|
||||
|
||||
abstract public function delete(Song $song, bool $backup = false): void;
|
||||
|
||||
abstract protected function supported(): bool;
|
||||
|
||||
protected function assertSupported(): void
|
||||
{
|
||||
throw_unless(
|
||||
$this->supported(),
|
||||
$this->getStorageType()->supported(),
|
||||
new KoelPlusRequiredException('The storage driver is only supported in Koel Plus.')
|
||||
);
|
||||
}
|
||||
|
|
45
app/Services/Streamer/Adapters/Concerns/StreamsLocalPath.php
Normal file
45
app/Services/Streamer/Adapters/Concerns/StreamsLocalPath.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Streamer\Adapters\Concerns;
|
||||
|
||||
use DaveRandom\Resume\FileResource;
|
||||
use DaveRandom\Resume\InvalidRangeHeaderException;
|
||||
use DaveRandom\Resume\NonExistentFileException;
|
||||
use DaveRandom\Resume\RangeSet;
|
||||
use DaveRandom\Resume\ResourceServlet;
|
||||
use DaveRandom\Resume\SendFileFailureException;
|
||||
use DaveRandom\Resume\UnreadableFileException;
|
||||
use DaveRandom\Resume\UnsatisfiableRangeException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
use function DaveRandom\Resume\get_request_header;
|
||||
|
||||
trait StreamsLocalPath
|
||||
{
|
||||
private function streamLocalPath(string $path): string
|
||||
{
|
||||
try {
|
||||
$rangeHeader = get_request_header('Range');
|
||||
|
||||
// On Safari, "Range" header value can be "bytes=0-1" which breaks streaming.
|
||||
$rangeHeader = $rangeHeader === 'bytes=0-1' ? 'bytes=0-' : $rangeHeader;
|
||||
|
||||
$rangeSet = RangeSet::createFromHeader($rangeHeader);
|
||||
$resource = new FileResource($path, mime_content_type($path));
|
||||
(new ResourceServlet($resource))->sendResource($rangeSet);
|
||||
} catch (InvalidRangeHeaderException) {
|
||||
abort(Response::HTTP_BAD_REQUEST);
|
||||
} catch (UnsatisfiableRangeException) {
|
||||
abort(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
} catch (NonExistentFileException) {
|
||||
abort(Response::HTTP_NOT_FOUND);
|
||||
} catch (UnreadableFileException) {
|
||||
abort(Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
} catch (SendFileFailureException $e) {
|
||||
abort_unless(headers_sent(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
echo "An error occurred while attempting to send the requested resource: {$e->getMessage()}";
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
|
@ -3,45 +3,14 @@
|
|||
namespace App\Services\Streamer\Adapters;
|
||||
|
||||
use App\Models\Song;
|
||||
use DaveRandom\Resume\FileResource;
|
||||
use DaveRandom\Resume\InvalidRangeHeaderException;
|
||||
use DaveRandom\Resume\NonExistentFileException;
|
||||
use DaveRandom\Resume\RangeSet;
|
||||
use DaveRandom\Resume\ResourceServlet;
|
||||
use DaveRandom\Resume\SendFileFailureException;
|
||||
use DaveRandom\Resume\UnreadableFileException;
|
||||
use DaveRandom\Resume\UnsatisfiableRangeException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
use function DaveRandom\Resume\get_request_header;
|
||||
use App\Services\Streamer\Adapters\Concerns\StreamsLocalPath;
|
||||
|
||||
class PhpStreamerAdapter extends LocalStreamerAdapter
|
||||
{
|
||||
use StreamsLocalPath;
|
||||
|
||||
public function stream(Song $song, array $config = []): void
|
||||
{
|
||||
try {
|
||||
$path = $song->storage_metadata->getPath();
|
||||
$rangeHeader = get_request_header('Range');
|
||||
|
||||
// On Safari, "Range" header value can be "bytes=0-1" which breaks streaming.
|
||||
$rangeHeader = $rangeHeader === 'bytes=0-1' ? 'bytes=0-' : $rangeHeader;
|
||||
|
||||
$rangeSet = RangeSet::createFromHeader($rangeHeader);
|
||||
$resource = new FileResource($path, mime_content_type($path));
|
||||
(new ResourceServlet($resource))->sendResource($rangeSet);
|
||||
} catch (InvalidRangeHeaderException) {
|
||||
abort(Response::HTTP_BAD_REQUEST);
|
||||
} catch (UnsatisfiableRangeException) {
|
||||
abort(Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
} catch (NonExistentFileException) {
|
||||
abort(Response::HTTP_NOT_FOUND);
|
||||
} catch (UnreadableFileException) {
|
||||
abort(Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
} catch (SendFileFailureException $e) {
|
||||
abort_unless(headers_sent(), Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
echo "An error occurred while attempting to send the requested resource: {$e->getMessage()}";
|
||||
}
|
||||
|
||||
exit;
|
||||
$this->streamLocalPath($song->storage_metadata->getPath());
|
||||
}
|
||||
}
|
||||
|
|
21
app/Services/Streamer/Adapters/SftpStreamerAdapter.php
Normal file
21
app/Services/Streamer/Adapters/SftpStreamerAdapter.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Streamer\Adapters;
|
||||
|
||||
use App\Models\Song;
|
||||
use App\Services\SongStorages\SftpStorage;
|
||||
use App\Services\Streamer\Adapters\Concerns\StreamsLocalPath;
|
||||
|
||||
class SftpStreamerAdapter implements StreamerAdapter
|
||||
{
|
||||
use StreamsLocalPath;
|
||||
|
||||
public function __construct(private readonly SftpStorage $storage)
|
||||
{
|
||||
}
|
||||
|
||||
public function stream(Song $song, array $config = []): void
|
||||
{
|
||||
$this->streamLocalPath($this->storage->copyToLocal($song));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use App\Models\Song;
|
|||
use App\Services\Streamer\Adapters\DropboxStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\LocalStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\S3CompatibleStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\SftpStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\StreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\TranscodingStreamerAdapter;
|
||||
use Illuminate\Support\Arr;
|
||||
|
@ -16,17 +17,15 @@ use Illuminate\Support\Str;
|
|||
|
||||
class Streamer
|
||||
{
|
||||
private StreamerAdapter $adapter;
|
||||
|
||||
public function __construct(
|
||||
private readonly Song $song,
|
||||
?StreamerAdapter $adapter = null,
|
||||
private ?StreamerAdapter $adapter = null,
|
||||
private readonly array $config = []
|
||||
) {
|
||||
// Turn off error reporting to make sure our stream isn't interfered.
|
||||
@error_reporting(0);
|
||||
|
||||
$this->adapter = $adapter ?? $this->resolveAdapter();
|
||||
$this->adapter ??= $this->resolveAdapter();
|
||||
}
|
||||
|
||||
private function resolveAdapter(): StreamerAdapter
|
||||
|
@ -39,6 +38,7 @@ class Streamer
|
|||
|
||||
return match ($this->song->storage) {
|
||||
SongStorageType::LOCAL => app(LocalStreamerAdapter::class),
|
||||
SongStorageType::SFTP => app(SftpStreamerAdapter::class),
|
||||
SongStorageType::S3, SongStorageType::S3_LAMBDA => app(S3CompatibleStreamerAdapter::class),
|
||||
SongStorageType::DROPBOX => app(DropboxStreamerAdapter::class),
|
||||
};
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\SongStorageMetadata\Contracts;
|
||||
|
||||
interface SongStorageMetadata
|
||||
{
|
||||
public function getPath(): string;
|
||||
}
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
|
||||
|
||||
class DropboxMetadata implements SongStorageMetadata
|
||||
final class DropboxMetadata extends SongStorageMetadata
|
||||
{
|
||||
private function __construct(public string $path)
|
||||
{
|
||||
|
@ -12,7 +10,7 @@ class DropboxMetadata implements SongStorageMetadata
|
|||
|
||||
public static function make(string $key): self
|
||||
{
|
||||
return new static($key);
|
||||
return new self($key);
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
final class LegacyS3Metadata extends S3CompatibleMetadata
|
||||
{
|
||||
}
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
|
||||
|
||||
final class LocalMetadata implements SongStorageMetadata
|
||||
final class LocalMetadata extends SongStorageMetadata
|
||||
{
|
||||
private function __construct(public string $path)
|
||||
{
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
use App\Values\SongStorageMetadata\Contracts\SongStorageMetadata;
|
||||
|
||||
class S3CompatibleMetadata implements SongStorageMetadata
|
||||
class S3CompatibleMetadata extends SongStorageMetadata
|
||||
{
|
||||
private function __construct(public string $bucket, public string $key)
|
||||
{
|
||||
|
|
7
app/Values/SongStorageMetadata/S3LambdaMetadata.php
Normal file
7
app/Values/SongStorageMetadata/S3LambdaMetadata.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
final class S3LambdaMetadata extends S3CompatibleMetadata
|
||||
{
|
||||
}
|
20
app/Values/SongStorageMetadata/SftpMetadata.php
Normal file
20
app/Values/SongStorageMetadata/SftpMetadata.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
final class SftpMetadata extends SongStorageMetadata
|
||||
{
|
||||
private function __construct(public string $path)
|
||||
{
|
||||
}
|
||||
|
||||
public static function make(string $key): self
|
||||
{
|
||||
return new self($key);
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
}
|
8
app/Values/SongStorageMetadata/SongStorageMetadata.php
Normal file
8
app/Values/SongStorageMetadata/SongStorageMetadata.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\SongStorageMetadata;
|
||||
|
||||
abstract class SongStorageMetadata
|
||||
{
|
||||
abstract public function getPath(): string;
|
||||
}
|
|
@ -40,7 +40,8 @@
|
|||
"saloonphp/laravel-plugin": "^3.0",
|
||||
"laravel/socialite": "^5.12",
|
||||
"laravel/ui": "^4.5",
|
||||
"nunomaduro/collision": "^7.10"
|
||||
"nunomaduro/collision": "^7.10",
|
||||
"league/flysystem-sftp-v3": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "~1.0",
|
||||
|
|
61
composer.lock
generated
61
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ca3c6672b523e87b03823ba9476fbd5a",
|
||||
"content-hash": "964433231b2e294b86a53fae4e5297c0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "algolia/algoliasearch-client-php",
|
||||
|
@ -3357,6 +3357,65 @@
|
|||
],
|
||||
"time": "2024-03-15T19:58:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-sftp-v3",
|
||||
"version": "3.26.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-sftp-v3.git",
|
||||
"reference": "bb186407f8b6e71df2caa35436c3426466d84048"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/bb186407f8b6e71df2caa35436c3426466d84048",
|
||||
"reference": "bb186407f8b6e71df2caa35436c3426466d84048",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/flysystem": "^3.0.14",
|
||||
"league/mime-type-detection": "^1.0.0",
|
||||
"php": "^8.0.2",
|
||||
"phpseclib/phpseclib": "^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\PhpseclibV3\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frankdejonge.nl"
|
||||
}
|
||||
],
|
||||
"description": "SFTP filesystem adapter for Flysystem.",
|
||||
"keywords": [
|
||||
"Flysystem",
|
||||
"file",
|
||||
"files",
|
||||
"filesystem",
|
||||
"sftp"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.26.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://ecologi.com/frankdejonge",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/frankdejonge",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-24T12:11:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/mime-type-detection",
|
||||
"version": "1.15.0",
|
||||
|
|
|
@ -44,6 +44,7 @@ return [
|
|||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app'),
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
'ftp' => [
|
||||
|
@ -58,6 +59,7 @@ return [
|
|||
// 'passive' => true,
|
||||
// 'ssl' => true,
|
||||
// 'timeout' => 30,
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
|
@ -78,6 +80,19 @@ return [
|
|||
'refresh_token' => env('DROPBOX_REFRESH_TOKEN'),
|
||||
],
|
||||
|
||||
'sftp' => [
|
||||
'driver' => 'sftp',
|
||||
'host' => env('SFTP_HOST'),
|
||||
'root' => rtrim(env('SFTP_ROOT'), '/\\'),
|
||||
|
||||
'username' => env('SFTP_USERNAME'),
|
||||
'password' => env('SFTP_PASSWORD'),
|
||||
|
||||
'privateKey' => env('SFTP_PRIVATE_KEY'),
|
||||
'passphrase' => env('SFTP_PASSPHRASE'),
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
'rackspace' => [
|
||||
'driver' => 'rackspace',
|
||||
'username' => 'your-username',
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
img, video, figure {
|
||||
border-radius: .375rem; // md
|
||||
border-radius: .375rem;
|
||||
display: block;
|
||||
outline: 1px solid rgba(99, 102, 241, .25);
|
||||
}
|
|
@ -7,7 +7,7 @@ import MobileAppScreenshots from '../components/MobileAppScreenshots.vue'
|
|||
import PlusBadge from '../components/PlusBadge.vue'
|
||||
import CaptionedImage from '../components/CaptionedImage.vue'
|
||||
import Layout from '../layout/Layout.vue'
|
||||
import './custom.scss'
|
||||
import './custom.pcss'
|
||||
|
||||
export default {
|
||||
Layout,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Cloud Storage Support
|
||||
|
||||
In addition to storing your music on the same server as Koel’s installation via the `local` storage driver,
|
||||
Koel Plus offers several different file storage options, including Amazon S3, S3-compatible services, Dropbox, and likely more in the future.
|
||||
In addition to storing your music on the same server as Koel’s installation via the `local` storage drivers,
|
||||
Koel Plus offers several different file storage options, including SFTP, Amazon S3, S3-compatible services, Dropbox, and likely more in the future.
|
||||
This page will guide you through the process of setting up these storage options.
|
||||
|
||||
:::warning Warning
|
||||
|
@ -13,6 +13,32 @@ The screenshots and instructions on this page may not be 100% up-to-date as 3rd-
|
|||
The general idea, however, should remain the same.
|
||||
:::
|
||||
|
||||
## SFTP
|
||||
|
||||
To use SFTP as your storage driver, you need to have an SFTP server set up and running. Many cloud hosting providers offer SFTP access to their storage services.
|
||||
To enable SFTP storage support in Koel, you need to provide the following configuration in your `.env` file:
|
||||
|
||||
```
|
||||
STORAGE_DRIVER=sftp
|
||||
|
||||
SFTP_HOST=
|
||||
SFTP_PORT=
|
||||
|
||||
# The absolute path of the directory to store the media files on the SFTP server.
|
||||
# Make sure the directory exists and is writable by the SFTP user.
|
||||
SFTP_ROOT=
|
||||
|
||||
# You can use either a username/password pair…
|
||||
SFTP_USERNAME=
|
||||
SFTP_PASSWORD=
|
||||
|
||||
# …or private key authentication:
|
||||
SFTP_PRIVATE_KEY=
|
||||
SFTP_PASSPHRASE=
|
||||
```
|
||||
|
||||
After reloading, Koel will start using SFTP as its storage driver. You can now upload your music files to your SFTP server directly from Koel’s web interface.
|
||||
|
||||
## Amazon S3 and Compatible Services
|
||||
|
||||
Since Amazon S3 and S3-compatible services share the same API, you can use the same configuration (`AWS_*`) for both.
|
||||
|
|
|
@ -7,7 +7,7 @@ Koel Plus is the premium version of Koel. It offers additional features and enha
|
|||
- **Multi-library Support**: In Koel Plus, each user has their own library, which they populate by uploading their own music.
|
||||
- **Music Sharing**: By default, uploaded music is private to the user who uploaded it. However, users can choose to share their music with others by marking songs as public.
|
||||
- **[Collaboration](./collaboration)**: Users can invite others to collaborate on their playlists, allowing them to add, remove, and reorder songs.
|
||||
- **[Cloud Storage Support](./cloud-storage-support)**: In addition to local storage, Koel Plus supports cloud storage services like Amazon S3 (or any S3-compatible service) and Dropbox.
|
||||
- **[Cloud Storage Support](./cloud-storage-support)**: In addition to local storage, Koel Plus supports remote and cloud storage drivers like SFTP, Amazon S3 (or any S3-compatible service), and Dropbox.
|
||||
- **[Single Sign-On (SSO) Support](./sso)**: Users can log into Koel Plus using their existing credentials from another service like Google.
|
||||
|
||||
Other features are being planned or actively developed, for example:
|
||||
|
|
18
tests/Integration/Enums/SongStorageTypeTest.php
Normal file
18
tests/Integration/Enums/SongStorageTypeTest.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\Enums;
|
||||
|
||||
use App\Enums\SongStorageType;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SongStorageTypeTest extends TestCase
|
||||
{
|
||||
public function testSupported(): void
|
||||
{
|
||||
self::assertTrue(SongStorageType::LOCAL->supported());
|
||||
self::assertTrue(SongStorageType::S3_LAMBDA->supported());
|
||||
self::assertFalse(SongStorageType::SFTP->supported());
|
||||
self::assertFalse(SongStorageType::DROPBOX->supported());
|
||||
self::assertFalse(SongStorageType::S3->supported());
|
||||
}
|
||||
}
|
16
tests/Integration/KoelPlus/Enums/SongStorageTypeTest.php
Normal file
16
tests/Integration/KoelPlus/Enums/SongStorageTypeTest.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\KoelPlus\Enums;
|
||||
|
||||
use App\Enums\SongStorageType;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
class SongStorageTypeTest extends PlusTestCase
|
||||
{
|
||||
public function testSupported(): void
|
||||
{
|
||||
self::assertTrue(collect(SongStorageType::cases())->every(
|
||||
static fn (SongStorageType $type) => $type->supported()
|
||||
));
|
||||
}
|
||||
}
|
|
@ -49,13 +49,6 @@ class DropboxStorageTest extends PlusTestCase
|
|||
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function testSupported(): void
|
||||
{
|
||||
$this->client->allows('setAccessToken');
|
||||
|
||||
self::assertTrue(app(DropboxStorage::class)->supported());
|
||||
}
|
||||
|
||||
public function testStoreUploadedFile(): void
|
||||
{
|
||||
$this->client->shouldReceive('setAccessToken')->with('free-bird')->once();
|
||||
|
|
|
@ -24,11 +24,6 @@ class S3CompatibleStorageTest extends PlusTestCase
|
|||
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function testSupported(): void
|
||||
{
|
||||
self::assertTrue($this->service->supported());
|
||||
}
|
||||
|
||||
public function testStoreUploadedFile(): void
|
||||
{
|
||||
self::assertEquals(0, Song::query()->where('storage', 's3')->count());
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\KoelPlus\Services\SongStorages;
|
||||
|
||||
use App\Models\Song;
|
||||
use App\Services\SongStorages\SftpStorage;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
use function Tests\test_path;
|
||||
|
||||
class SftpStorageTest extends PlusTestCase
|
||||
{
|
||||
private SftpStorage $service;
|
||||
private UploadedFile $file;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->service = app(SftpStorage::class);
|
||||
$this->file = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
|
||||
}
|
||||
|
||||
public function testStoreUploadedFile(): void
|
||||
{
|
||||
self::assertEquals(0, Song::query()->where('storage', 'sftp')->count());
|
||||
|
||||
Storage::fake('sftp');
|
||||
$song = $this->service->storeUploadedFile($this->file, create_user());
|
||||
|
||||
Storage::disk('sftp')->assertExists($song->storage_metadata->getPath());
|
||||
self::assertEquals(1, Song::query()->where('storage', 'sftp')->count());
|
||||
}
|
||||
|
||||
public function testStoringWithVisibilityPreference(): void
|
||||
{
|
||||
$user = create_user();
|
||||
|
||||
$user->preferences->makeUploadsPublic = true;
|
||||
$user->save();
|
||||
|
||||
self::assertTrue($this->service->storeUploadedFile($this->file, $user)->is_public);
|
||||
|
||||
$user->preferences->makeUploadsPublic = false;
|
||||
$user->save();
|
||||
|
||||
$privateFile = UploadedFile::fromFile(test_path('songs/full.mp3'), 'song.mp3'); //@phpstan-ignore-line
|
||||
self::assertFalse($this->service->storeUploadedFile($privateFile, $user)->is_public);
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
{
|
||||
Storage::fake('sftp');
|
||||
|
||||
$song = $this->service->storeUploadedFile($this->file, create_user());
|
||||
Storage::disk('sftp')->assertExists($song->storage_metadata->getPath());
|
||||
|
||||
$this->service->delete($song);
|
||||
Storage::disk('sftp')->assertMissing($song->storage_metadata->getPath());
|
||||
}
|
||||
|
||||
public function testGetSongContent(): void
|
||||
{
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
||||
Storage::fake('sftp');
|
||||
Storage::shouldReceive('disk->get')->with($song->storage_metadata->getPath())->andReturn('binary-content');
|
||||
|
||||
self::assertEquals('binary-content', $this->service->getSongContent($song));
|
||||
}
|
||||
|
||||
public function testCopyToLocal(): void
|
||||
{
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
||||
Storage::fake('sftp');
|
||||
Storage::shouldReceive('disk->get')->with($song->storage_metadata->getPath())->andReturn('binary-content');
|
||||
|
||||
$localPath = $this->service->copyToLocal($song);
|
||||
|
||||
self::assertStringEqualsFile($localPath, 'binary-content');
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@ use App\Models\Song;
|
|||
use App\Services\Streamer\Adapters\DropboxStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\LocalStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\S3CompatibleStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\SftpStreamerAdapter;
|
||||
use App\Services\Streamer\Streamer;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
|
@ -20,7 +22,7 @@ class StreamerTest extends PlusTestCase
|
|||
collect(SongStorageType::cases())
|
||||
->each(static function (SongStorageType $type): void {
|
||||
/** @var Song $song */
|
||||
$song = Song::factory()->make(['storage' => $type]);
|
||||
$song = Song::factory()->create(['storage' => $type]);
|
||||
$streamer = new Streamer($song);
|
||||
|
||||
switch ($type) {
|
||||
|
@ -36,6 +38,13 @@ class StreamerTest extends PlusTestCase
|
|||
case SongStorageType::LOCAL:
|
||||
self::assertInstanceOf(LocalStreamerAdapter::class, $streamer->getAdapter());
|
||||
break;
|
||||
|
||||
case SongStorageType::SFTP:
|
||||
self::assertInstanceOf(SftpStreamerAdapter::class, $streamer->getAdapter());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Storage type not covered by tests: ' . $type->value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use App\Services\Streamer\Adapters\TranscodingStreamerAdapter;
|
|||
use App\Services\Streamer\Adapters\XAccelRedirectStreamerAdapter;
|
||||
use App\Services\Streamer\Adapters\XSendFileStreamerAdapter;
|
||||
use App\Services\Streamer\Streamer;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -40,6 +41,9 @@ class StreamerTest extends TestCase
|
|||
case SongStorageType::LOCAL:
|
||||
self::assertInstanceOf(LocalStreamerAdapter::class, (new Streamer($song))->getAdapter());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Storage type uncovered by tests.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -123,9 +123,4 @@ class S3LambdaStorageTest extends TestCase
|
|||
|
||||
self::assertModelMissing($song);
|
||||
}
|
||||
|
||||
public function testSupported(): void
|
||||
{
|
||||
self::assertTrue($this->storage->supported());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue