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:
Jonny Barnes 2026-01-04 10:07:25 +00:00
commit 9e788e0fe8
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8
9 changed files with 200 additions and 19 deletions

View 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');
}
}

View file

@ -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

View file

@ -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');
}
/**

View file

@ -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,
];
}
}
}