Images can be uploaded with alt text
This means associating images to notes via a pivot table, we also finish off the migration of medai from S3.
This commit is contained in:
parent
76db869af5
commit
9e788e0fe8
9 changed files with 200 additions and 19 deletions
92
app/Console/Commands/MigrateMedia.php
Normal file
92
app/Console/Commands/MigrateMedia.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Media;
|
||||
use App\Models\Note;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class MigrateMedia extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:migrate-media';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Migrate media';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
// First check new `media_note` table exists
|
||||
if (! DB::getSchemaBuilder()->hasTable('media_note')) {
|
||||
$this->error('The table "media_note" does not exist.');
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Load all media already saved in `media_endpoint` table
|
||||
$this->line('Updating existing local media');
|
||||
$mediaEndpointMedia = Media::all();
|
||||
// Save relationship in new `media_note` table based on `media_endpoint.note_id`
|
||||
$this->withProgressBar($mediaEndpointMedia, function (Media $mediaEndpointMediaItem) {
|
||||
$note = Note::find($mediaEndpointMediaItem->note_id);
|
||||
if ($note) {
|
||||
$note->media()->attach($mediaEndpointMediaItem->id);
|
||||
}
|
||||
});
|
||||
|
||||
// Load all media records from `media` table
|
||||
$this->line('');
|
||||
$this->line('Migrating old media from S3');
|
||||
$oldMedia = DB::table('media')->get();
|
||||
foreach ($oldMedia as $oldMediaItem) {
|
||||
// We only want to process the S3 media
|
||||
if ($oldMediaItem->disk !== 's3') {
|
||||
$this->warn('Original media item never stored in S3');
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check media exists in S3
|
||||
if (! Storage::disk('s3')->exists($oldMediaItem->id.DIRECTORY_SEPARATOR.$oldMediaItem->file_name)) {
|
||||
$this->warn('Original media item not found in S3');
|
||||
|
||||
continue;
|
||||
}
|
||||
// We want to just copy the file, check it does not already exist locally
|
||||
if (Storage::disk('public')->exists('media'.DIRECTORY_SEPARATOR.$oldMediaItem->file_name)) {
|
||||
$this->warn('File already exists locally with filename of original media item');
|
||||
|
||||
continue;
|
||||
}
|
||||
// Copy the file
|
||||
Storage::disk('public')->writeStream('media'.DIRECTORY_SEPARATOR.$oldMediaItem->file_name, Storage::disk('s3')->readStream($oldMediaItem->id.DIRECTORY_SEPARATOR.$oldMediaItem->file_name));
|
||||
|
||||
// Save relationship based on `media.model_id`
|
||||
// I have already checked they are all notes
|
||||
$note = $oldMediaItem->model_id;
|
||||
$newMediaItem = Media::create([
|
||||
'path' => 'media'.DIRECTORY_SEPARATOR.$oldMediaItem->file_name,
|
||||
'type' => 'image',
|
||||
]);
|
||||
$note->media()->attach($newMediaItem->id);
|
||||
|
||||
$this->info('Media item migrated from S3');
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->line('Migration finished');
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ namespace App\Models;
|
|||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Media extends Model
|
||||
|
|
@ -20,9 +20,11 @@ class Media extends Model
|
|||
/** @var array<int, string> */
|
||||
protected $fillable = ['token', 'path', 'type', 'image_widths'];
|
||||
|
||||
public function note(): BelongsTo
|
||||
public function notes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(Note::class);
|
||||
return $this->belongsToMany(Note::class)
|
||||
->withPivot('alt_text', 'order')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
protected function url(): Attribute
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
|
@ -92,9 +91,12 @@ class Note extends Model
|
|||
return $this->belongsTo(Place::class);
|
||||
}
|
||||
|
||||
public function media(): HasMany
|
||||
public function media(): BelongsToMany
|
||||
{
|
||||
return $this->hasMany(Media::class);
|
||||
return $this->BelongsToMany(Media::class)
|
||||
->withPivot('alt_text', 'order')
|
||||
->withTimestamps()
|
||||
->orderBy('order');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -51,8 +51,11 @@ class NoteService
|
|||
|
||||
// $note->instagram_url = $this->getInstagramUrl($request);
|
||||
|
||||
foreach ($this->getMedia($data) as $media) {
|
||||
$note->media()->save($media);
|
||||
foreach ($this->getMedia($data) as $index => $media) {
|
||||
$note->media()->attach($media['value']->id, [
|
||||
'alt_text' => $media['alt_text'],
|
||||
'order' => $index,
|
||||
]);
|
||||
}
|
||||
|
||||
$note->save();
|
||||
|
|
@ -195,16 +198,37 @@ class NoteService
|
|||
|
||||
if (isset($photos)) {
|
||||
foreach ((array) $photos as $photo) {
|
||||
// $photo can be a string of the URL opf the photo
|
||||
// or it can be an object with a `value` and `alt`
|
||||
$photoUrl = null;
|
||||
$photoAlt = null;
|
||||
if (is_string($photo)) {
|
||||
$photoUrl = $photo;
|
||||
} elseif (is_array($photo)) {
|
||||
$photoUrl = $photo['value'];
|
||||
$photoAlt = $photo['alt'];
|
||||
}
|
||||
|
||||
if (empty($photoUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check the media was uploaded to my endpoint, and use path
|
||||
if (Str::startsWith($photo, config('filesystems.disks.public.url'))) {
|
||||
$path = substr($photo, strlen(config('filesystems.disks.public.url')));
|
||||
$media[] = Media::where('path', ltrim($path, '/'))->firstOrFail();
|
||||
if (Str::startsWith($photoUrl, config('filesystems.disks.public.url'))) {
|
||||
$path = substr($photoUrl, strlen(config('filesystems.disks.public.url')));
|
||||
$media[] = [
|
||||
'value' => Media::where('path', ltrim($path, '/'))->firstOrFail(),
|
||||
'alt' => $photoAlt,
|
||||
];
|
||||
} else {
|
||||
$newMedia = Media::firstOrNew(['path' => $photo]);
|
||||
$newMedia = Media::firstOrNew(['path' => $photoUrl]);
|
||||
// currently assuming this is a photo from Swarm or OwnYourGram
|
||||
$newMedia->type = 'image';
|
||||
$newMedia->save();
|
||||
$media[] = $newMedia;
|
||||
$media[] = [
|
||||
'value' => $newMedia,
|
||||
'alt' => $photoAlt,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('media_note', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('note_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('media_id')->constrained('media_endpoint')->onDelete('cascade');
|
||||
$table->text('alt_text')->nullable();
|
||||
$table->integer('order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['note_id', 'media_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('media_note');
|
||||
}
|
||||
};
|
||||
|
|
@ -84,7 +84,9 @@ class NotesTableSeeder extends Seeder
|
|||
// copy aaron’s profile pic in place
|
||||
$spl = new SplFileInfo(public_path() . '/assets/profile-images/aaronparecki.com');
|
||||
if ($spl->isDir() === false) {
|
||||
mkdir(public_path() . '/assets/profile-images/aaronparecki.com', 0755);
|
||||
if (! mkdir($concurrentDirectory = public_path() . '/assets/profile-images/aaronparecki.com', 0755) && ! is_dir($concurrentDirectory)) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
|
||||
}
|
||||
copy(base_path() . '/tests/aaron.png', public_path() . '/assets/profile-images/aaronparecki.com/image');
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +173,9 @@ class NotesTableSeeder extends Seeder
|
|||
$noteWithOnlyImage->setCreatedAt($now);
|
||||
$noteWithOnlyImage->setUpdatedAt($now);
|
||||
$noteWithOnlyImage->save();
|
||||
$noteWithOnlyImage->media()->save($media);
|
||||
$noteWithOnlyImage->media()->attach($media->id, [
|
||||
'alt_text' => 'Test alt text',
|
||||
]);
|
||||
DB::table('notes')
|
||||
->where('id', $noteWithOnlyImage->id)
|
||||
->update(['updated_at' => $now->toDateTimeString()]);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
@foreach($note->media as $media)
|
||||
@if($media->type === 'image')
|
||||
<a class="naked-link" href="{{ $media->url }}">
|
||||
<img class="u-photo" src="{{ $media->url }}" alt="" @if($media->image_widths !== null) srcset="{{ $media->url }} {{ $media->image_widths }}w, {{ $media->mediumurl }} 1000w, {{ $media->smallurl }} 500w" sizes="80vh"@endif>
|
||||
<img class="u-photo" alt="{{ $media->pivot->alt_text }}" @if($media->image_widths !== null) srcset="{{ $media->url }} {{ $media->image_widths }}w, {{ $media->mediumurl }} 1000w, {{ $media->smallurl }} 500w" sizes="min(80ch,80vw)" @else src="{{ $media->url }}" @endif>
|
||||
</a>
|
||||
@endif
|
||||
@if($media->type === 'audio')
|
||||
|
|
|
|||
|
|
@ -15,11 +15,18 @@ class MediaTest extends TestCase
|
|||
use RefreshDatabase;
|
||||
|
||||
#[Test]
|
||||
public function get_the_note_that_media_instance_belongs_to(): void
|
||||
public function media_can_belong_to_multiple_notes(): void
|
||||
{
|
||||
$media = Media::factory()->for(Note::factory())->create();
|
||||
$media = Media::factory()->create();
|
||||
$note1 = Note::factory()->create();
|
||||
$note2 = Note::factory()->create();
|
||||
|
||||
$this->assertInstanceOf(Note::class, $media->note);
|
||||
$note1->media()->attach($media->id, ['alt_text' => 'Alt text for note 1']);
|
||||
$note2->media()->attach($media->id, ['alt_text' => 'Alt text for note 2']);
|
||||
|
||||
$this->assertCount(2, $media->notes);
|
||||
$this->assertTrue($media->notes->contains($note1));
|
||||
$this->assertTrue($media->notes->contains($note2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
|||
|
|
@ -434,4 +434,21 @@ class NotesTest extends TestCase
|
|||
|
||||
$this->assertSame($expected, $note->note);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function note_can_have_media_with_alt_text(): void
|
||||
{
|
||||
$note = Note::factory()->create();
|
||||
$media = Media::factory()->create();
|
||||
|
||||
$note->media()->attach($media->id, [
|
||||
'alt_text' => 'Test alt text',
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
$note->refresh();
|
||||
|
||||
$this->assertCount(1, $note->media);
|
||||
$this->assertEquals('Test alt text', $note->media->first()->pivot->alt_text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue