-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Stream downloads instead of redirecting (#145)
* feat: download service serves responses directly now * refactor: make DownloadService stream directly * tweak: set up some real caching (default 10 days) * config: use app.aspirecloud.download.base * fix: move /download routes from web.php to api.php
- Loading branch information
1 parent
179cdc1
commit 025e93e
Showing
14 changed files
with
245 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Contracts\Downloads; | ||
|
||
use App\Enums\AssetType; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
interface Downloader | ||
{ | ||
public function download(AssetType $type, string $slug, string $file, ?string $revision = null): Response; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php | ||
|
||
namespace App\Services\Downloads; | ||
|
||
use App\Contracts\Downloads\Downloader; | ||
use App\Enums\AssetType; | ||
use App\Events\AssetCacheHit; | ||
use App\Events\AssetCacheMissed; | ||
use App\Models\WpOrg\Asset; | ||
use Illuminate\Http\RedirectResponse; | ||
use Illuminate\Support\Facades\Log; | ||
use Illuminate\Support\Facades\Storage; | ||
|
||
class RedirectingDownloadService implements Downloader | ||
{ | ||
public const int TEMPORARY_URL_EXPIRE_MINS = 5; | ||
|
||
/** | ||
* Get the file download response. If the asset exists locally, return a redirect url to it. | ||
* Otherwise, redirect to WordPress.org and queue a job to download it for future requests. | ||
*/ | ||
public function download(AssetType $type, string $slug, string $file, ?string $revision = null): RedirectResponse | ||
{ | ||
Log::debug("DOWNLOAD", compact("type", "slug", "file", "revision")); | ||
|
||
if ($revision === 'head') { | ||
// head is there to have something in the url, but it behaves the same as not passing it | ||
$revision = null; | ||
} | ||
// Check if we have it locally | ||
$asset = Asset::query() | ||
->where('asset_type', $type->value) | ||
->where('slug', $slug) | ||
->where('local_path', 'LIKE', "%{$file}") | ||
->when($revision, fn($q) => $q->where('revision', $revision)) | ||
->orderBy('revision', 'desc') | ||
->first(); | ||
|
||
if ($asset && Storage::exists($asset->local_path)) { | ||
// TODO: handle case where asset exists but local path does not (DownloadAssetJob always creates a new Asset) | ||
event(new AssetCacheHit($asset)); | ||
Log::debug("Serving existing asset", ["asset" => $asset]); | ||
return $this->response( | ||
Storage::temporaryUrl($asset->local_path, now()->addMinutes(self::TEMPORARY_URL_EXPIRE_MINS)), | ||
); | ||
} | ||
|
||
$upstreamUrl = $type->buildUpstreamUrl($slug, $file, $revision); | ||
|
||
event( | ||
new AssetCacheMissed(type: $type, slug: $slug, file: $file, upstreamUrl: $upstreamUrl, revision: $revision), | ||
); | ||
return $this->response($upstreamUrl); | ||
} | ||
|
||
private function response(string $url): RedirectResponse | ||
{ | ||
return redirect()->away($url); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,4 +27,3 @@ | |
}); | ||
|
||
require __DIR__ . '/inc/admin-web.php'; | ||
require __DIR__ . '/inc/download.php'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<?php | ||
|
||
use App\Enums\AssetType; | ||
use App\Jobs\DownloadAssetJob; | ||
use Illuminate\Support\Facades\Http; | ||
use Illuminate\Support\Facades\Queue; | ||
use Illuminate\Support\Facades\Storage; | ||
|
||
beforeEach(function () { | ||
Storage::fake('local'); | ||
Queue::fake(); | ||
Http::fake(); | ||
}); | ||
|
||
|
||
|
||
describe('Download Routes', function () { | ||
$getJob = function (): DownloadAssetJob { | ||
$jobs = Queue::pushed(DownloadAssetJob::class); | ||
expect($jobs)->toHaveCount(1); | ||
return $jobs->first(); | ||
}; | ||
|
||
it('handles WordPress core download requests', function () use ($getJob) { | ||
$response = $this->get('/download/wordpress-6.4.2.zip'); | ||
|
||
expect($response->status())->toBe(302); | ||
/** @noinspection PhpUndefinedMethodInspection */ | ||
expect($response->getTargetUrl())->toBe('https://wordpress.org/wordpress-6.4.2.zip'); | ||
|
||
$job = $getJob(); | ||
expect($job->type) | ||
->toBe(AssetType::CORE) | ||
->and($job->file)->toBe('wordpress-6.4.2.zip') | ||
->and($job->slug)->toBe('wordpress') | ||
->and($job->upstreamUrl)->toBe('https://wordpress.org/wordpress-6.4.2.zip') | ||
->and($job->revision)->toBeNull(); | ||
}); | ||
|
||
it('handles plugin download requests', function () use ($getJob) { | ||
$response = $this->get('/download/plugin/test-plugin.1.0.0.zip'); | ||
|
||
expect($response->status())->toBe(302); | ||
/** @noinspection PhpUndefinedMethodInspection */ | ||
expect($response->getTargetUrl())->toBe('https://downloads.wordpress.org/plugin/test-plugin.1.0.0.zip'); | ||
|
||
$job = $getJob(); | ||
expect($job->type) | ||
->toBe(AssetType::PLUGIN) | ||
->and($job->file)->toBe('test-plugin.1.0.0.zip') | ||
->and($job->slug)->toBe('test-plugin') | ||
->and($job->upstreamUrl)->toBe('https://downloads.wordpress.org/plugin/test-plugin.1.0.0.zip') | ||
->and($job->revision)->toBeNull(); | ||
}); | ||
|
||
it('handles theme download requests', function () use ($getJob) { | ||
$response = $this->get('/download/theme/test-theme.1.0.0.zip'); | ||
|
||
expect($response->status())->toBe(302); | ||
/** @noinspection PhpUndefinedMethodInspection */ | ||
expect($response->getTargetUrl())->toBe('https://downloads.wordpress.org/theme/test-theme.1.0.0.zip'); | ||
|
||
$job = $getJob(); | ||
expect($job->type) | ||
->toBe(AssetType::THEME) | ||
->and($job->file)->toBe('test-theme.1.0.0.zip') | ||
->and($job->slug)->toBe('test-theme') | ||
->and($job->upstreamUrl)->toBe('https://downloads.wordpress.org/theme/test-theme.1.0.0.zip') | ||
->and($job->revision)->toBeNull(); | ||
}); | ||
|
||
it('handles plugin asset download requests', function () use ($getJob) { | ||
$response = $this->get('/download/assets/plugin/test-plugin/head/screenshot-1.png'); | ||
expect($response->status())->toBe(302); | ||
/** @noinspection PhpUndefinedMethodInspection */ | ||
expect($response->getTargetUrl())->toBe('https://ps.w.org/test-plugin/assets/screenshot-1.png'); | ||
|
||
$job = $getJob(); | ||
expect($job->type) | ||
->toBe(AssetType::PLUGIN_SCREENSHOT) | ||
->and($job->file)->toBe('screenshot-1.png') | ||
->and($job->slug)->toBe('test-plugin') | ||
->and($job->upstreamUrl)->toBe('https://ps.w.org/test-plugin/assets/screenshot-1.png') | ||
->and($job->revision)->toBeNull(); | ||
}); | ||
|
||
it('handles asset download requests with revision', function () use ($getJob) { | ||
$response = $this->get('/download/assets/plugin/test-plugin/3164133/banner-1544x500.png'); | ||
|
||
expect($response->status())->toBe(302); | ||
/** @noinspection PhpUndefinedMethodInspection */ | ||
expect($response->getTargetUrl())->toBe('https://ps.w.org/test-plugin/assets/banner-1544x500.png?rev=3164133'); | ||
|
||
$job = $getJob(); | ||
expect($job->type) | ||
->toBe(AssetType::PLUGIN_BANNER) | ||
->and($job->file)->toBe('banner-1544x500.png') | ||
->and($job->slug)->toBe('test-plugin') | ||
->and($job->upstreamUrl)->toBe('https://ps.w.org/test-plugin/assets/banner-1544x500.png?rev=3164133') | ||
->and($job->revision)->toBe('3164133'); | ||
}); | ||
})->todo("turn these into tests of AssetType::buildUpstreamUrl"); |
Oops, something went wrong.