201 lines
7.8 KiB
PHP
201 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\User;
|
|
use App\Services\ThumbnailService;
|
|
use App\Services\Uploads\UploadStorageService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Storage::fake('s3');
|
|
|
|
$suffix = Str::lower(Str::random(10));
|
|
$this->localOriginalsRoot = storage_path('framework/testing/square-command-originals-' . $suffix);
|
|
|
|
File::deleteDirectory($this->localOriginalsRoot);
|
|
File::ensureDirectoryExists($this->localOriginalsRoot);
|
|
|
|
config()->set('uploads.local_originals_root', $this->localOriginalsRoot);
|
|
config()->set('uploads.object_storage.disk', 's3');
|
|
config()->set('uploads.object_storage.prefix', 'artworks');
|
|
});
|
|
|
|
afterEach(function () {
|
|
File::deleteDirectory($this->localOriginalsRoot);
|
|
});
|
|
|
|
function generateSqCommandImage(string $root, string $filename = 'source.jpg'): string
|
|
{
|
|
$path = $root . DIRECTORY_SEPARATOR . $filename;
|
|
$image = imagecreatetruecolor(1200, 800);
|
|
$background = imagecolorallocate($image, 18, 22, 29);
|
|
$subject = imagecolorallocate($image, 245, 180, 110);
|
|
imagefilledrectangle($image, 0, 0, 1200, 800, $background);
|
|
imagefilledellipse($image, 280, 340, 360, 380, $subject);
|
|
imagejpeg($image, $path, 90);
|
|
imagedestroy($image);
|
|
|
|
return $path;
|
|
}
|
|
|
|
function generateSqCommandWebp(string $root, string $filename = 'source.webp'): string
|
|
{
|
|
$path = $root . DIRECTORY_SEPARATOR . $filename;
|
|
$image = imagecreatetruecolor(1400, 900);
|
|
$background = imagecolorallocate($image, 16, 21, 28);
|
|
$subject = imagecolorallocate($image, 242, 194, 94);
|
|
imagefilledrectangle($image, 0, 0, 1400, 900, $background);
|
|
imagefilledellipse($image, 360, 420, 420, 360, $subject);
|
|
imagewebp($image, $path, 90);
|
|
imagedestroy($image);
|
|
|
|
return $path;
|
|
}
|
|
|
|
function seedArtworkWithOriginal(string $localOriginalsRoot): Artwork
|
|
{
|
|
$user = User::factory()->create();
|
|
$artwork = Artwork::factory()->for($user)->unpublished()->create();
|
|
$source = generateSqCommandImage($localOriginalsRoot, 'seed.jpg');
|
|
$hash = hash_file('sha256', $source);
|
|
$storage = app(UploadStorageService::class);
|
|
$localTarget = $storage->localOriginalPath($hash, $hash . '.jpg');
|
|
File::ensureDirectoryExists(dirname($localTarget));
|
|
File::copy($source, $localTarget);
|
|
|
|
$artwork->update([
|
|
'hash' => $hash,
|
|
'file_ext' => 'jpg',
|
|
'thumb_ext' => 'webp',
|
|
'width' => 1200,
|
|
'height' => 800,
|
|
]);
|
|
|
|
DB::table('artwork_files')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'variant' => 'orig_image',
|
|
'path' => "artworks/original/" . substr($hash, 0, 2) . "/" . substr($hash, 2, 2) . "/{$hash}.jpg",
|
|
'mime' => 'image/jpeg',
|
|
'size' => (int) filesize($localTarget),
|
|
]);
|
|
|
|
return $artwork->fresh();
|
|
}
|
|
|
|
it('generates missing square thumbnails from the best available source', function () {
|
|
$artwork = seedArtworkWithOriginal($this->localOriginalsRoot);
|
|
|
|
$code = Artisan::call('artworks:generate-missing-sq-thumbs');
|
|
|
|
expect($code)->toBe(0);
|
|
expect(Artisan::output())->toContain('generated=1');
|
|
|
|
$sqPath = "artworks/sq/" . substr($artwork->hash, 0, 2) . "/" . substr($artwork->hash, 2, 2) . "/{$artwork->hash}.webp";
|
|
Storage::disk('s3')->assertExists($sqPath);
|
|
|
|
expect(DB::table('artwork_files')->where('artwork_id', $artwork->id)->where('variant', 'sq')->exists())->toBeTrue();
|
|
|
|
$sqSize = getimagesizefromstring(Storage::disk('s3')->get($sqPath));
|
|
expect($sqSize[0] ?? null)->toBe(512)
|
|
->and($sqSize[1] ?? null)->toBe(512);
|
|
});
|
|
|
|
it('supports dry run without writing sq thumbnails', function () {
|
|
$artwork = seedArtworkWithOriginal($this->localOriginalsRoot);
|
|
|
|
$code = Artisan::call('artworks:generate-missing-sq-thumbs', ['--dry-run' => true]);
|
|
|
|
expect($code)->toBe(0);
|
|
expect(Artisan::output())->toContain('planned=1');
|
|
|
|
$sqPath = "artworks/sq/" . substr($artwork->hash, 0, 2) . "/" . substr($artwork->hash, 2, 2) . "/{$artwork->hash}.webp";
|
|
Storage::disk('s3')->assertMissing($sqPath);
|
|
expect(DB::table('artwork_files')->where('artwork_id', $artwork->id)->where('variant', 'sq')->exists())->toBeFalse();
|
|
});
|
|
|
|
it('forces regeneration when an sq row already exists', function () {
|
|
$artwork = seedArtworkWithOriginal($this->localOriginalsRoot);
|
|
|
|
DB::table('artwork_files')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'variant' => 'sq',
|
|
'path' => "artworks/sq/" . substr($artwork->hash, 0, 2) . "/" . substr($artwork->hash, 2, 2) . "/{$artwork->hash}.webp",
|
|
'mime' => 'image/webp',
|
|
'size' => 0,
|
|
]);
|
|
|
|
$code = Artisan::call('artworks:generate-missing-sq-thumbs', ['--force' => true]);
|
|
|
|
expect($code)->toBe(0);
|
|
expect(Artisan::output())->toContain('generated=1');
|
|
|
|
$sqPath = "artworks/sq/" . substr($artwork->hash, 0, 2) . "/" . substr($artwork->hash, 2, 2) . "/{$artwork->hash}.webp";
|
|
Storage::disk('s3')->assertExists($sqPath);
|
|
});
|
|
|
|
it('purges the canonical sq url after regeneration when Cloudflare is configured', function () {
|
|
Http::fake([
|
|
'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache' => Http::response(['success' => true], 200),
|
|
]);
|
|
|
|
config()->set('cdn.files_url', 'https://cdn.skinbase.org');
|
|
config()->set('cdn.cloudflare.zone_id', 'test-zone');
|
|
config()->set('cdn.cloudflare.api_token', 'test-token');
|
|
|
|
$artwork = seedArtworkWithOriginal($this->localOriginalsRoot);
|
|
|
|
$code = Artisan::call('artworks:generate-missing-sq-thumbs', ['--id' => $artwork->id]);
|
|
|
|
expect($code)->toBe(0);
|
|
|
|
Http::assertSent(function ($request) use ($artwork): bool {
|
|
return $request->url() === 'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache'
|
|
&& $request['files'] === [
|
|
'https://cdn.skinbase.org/artworks/sq/' . substr($artwork->hash, 0, 2) . '/' . substr($artwork->hash, 2, 2) . '/' . $artwork->hash . '.webp',
|
|
];
|
|
});
|
|
});
|
|
|
|
it('falls back to canonical CDN derivatives for legacy artworks without artwork_files rows', function () {
|
|
$cdnRoot = $this->localOriginalsRoot . DIRECTORY_SEPARATOR . 'fake-cdn';
|
|
File::ensureDirectoryExists($cdnRoot);
|
|
config()->set('cdn.files_url', 'file:///' . str_replace(DIRECTORY_SEPARATOR, '/', $cdnRoot));
|
|
|
|
$user = User::factory()->create();
|
|
$hash = '6183c98975512ee6bff4657043067953a33769c7';
|
|
$artwork = Artwork::factory()->for($user)->unpublished()->create([
|
|
'hash' => $hash,
|
|
'file_ext' => 'jpg',
|
|
'thumb_ext' => 'webp',
|
|
'file_path' => 'legacy/uploads/IMG_20210727_090534.jpg',
|
|
'width' => 4624,
|
|
'height' => 2080,
|
|
]);
|
|
|
|
$xlUrl = ThumbnailService::fromHash($hash, 'webp', 'xl');
|
|
$webpSource = generateSqCommandWebp($this->localOriginalsRoot, 'legacy-xl.webp');
|
|
$xlPath = $cdnRoot . DIRECTORY_SEPARATOR . 'artworks' . DIRECTORY_SEPARATOR . 'xl' . DIRECTORY_SEPARATOR . substr($hash, 0, 2) . DIRECTORY_SEPARATOR . substr($hash, 2, 2) . DIRECTORY_SEPARATOR . $hash . '.webp';
|
|
File::ensureDirectoryExists(dirname($xlPath));
|
|
File::copy($webpSource, $xlPath);
|
|
|
|
expect($xlUrl)->toContain('/artworks/xl/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . $hash . '.webp');
|
|
|
|
$code = Artisan::call('artworks:generate-missing-sq-thumbs', ['--id' => $artwork->id]);
|
|
|
|
expect($code)->toBe(0);
|
|
expect(Artisan::output())->toContain('generated=1');
|
|
|
|
$sqPath = "artworks/sq/" . substr($hash, 0, 2) . "/" . substr($hash, 2, 2) . "/{$hash}.webp";
|
|
Storage::disk('s3')->assertExists($sqPath);
|
|
expect(DB::table('artwork_files')->where('artwork_id', $artwork->id)->where('variant', 'sq')->exists())->toBeTrue();
|
|
}); |