Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion app/Filament/App/Resources/GedcomResource/Pages/CreateGedcom.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,42 @@ protected function afterCreate(): void
return;
}

$fullPath = Storage::disk('private')->path($path);
$disk = Storage::disk('private');

$path = (string) $path;

// If the file landed in livewire-tmp (Livewire's temporary upload directory),
// move it to the permanent gedcom-form-imports directory so storage is organised
// correctly and the file survives queue processing.
if (str_starts_with($path, 'livewire-tmp/') && $disk->exists($path)) {
$newPath = 'gedcom-form-imports/' . basename($path);
$disk->move($path, $newPath);
$record->update(['filename' => $newPath]);
$path = $newPath;

Log::info('CreateGedcom: moved upload from livewire-tmp to gedcom-form-imports', [
'gedcom_id' => $record->getKey(),
'new_path' => $newPath,
]);
}

// Verify the file actually exists before dispatching the job
if (! $disk->exists($path)) {
Log::error('CreateGedcom::afterCreate: file does not exist on private disk, aborting dispatch', [
'gedcom_id' => $record->getKey(),
'path' => $path,
]);

Notification::make()
->title('Import failed')
->body('The uploaded file could not be found. Please try uploading again.')
->danger()
->send();

return;
}

$fullPath = $disk->path($path);
$extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));

// Pre-create the ImportJob so the user can track it immediately
Expand Down
22 changes: 22 additions & 0 deletions app/Jobs/ImportGedcom.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,26 @@ public function handle(): int

return 0;
}

/**
* Handle a job failure that occurs outside the try/catch in handle()
* (e.g. serialisation errors, queue-worker crashes, max-attempts exceeded).
* Ensures the ImportJob record is always set to 'failed' with an error message.
*/
public function failed(?Throwable $exception): void
{
if ($this->slug) {
ImportJob::where('slug', $this->slug)->update([
'status' => 'failed',
'error_message' => $exception?->getMessage() ?? 'Job failed unexpectedly.',
]);
}

Log::error('ImportGedcom job failed', [
'file_path' => $this->filePath,
'user_id' => $this->user->getKey(),
'slug' => $this->slug,
'error' => $exception?->getMessage(),
]);
}
}
22 changes: 22 additions & 0 deletions app/Jobs/ImportGrampsXml.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,28 @@ public function handle(): int
return 0;
}

/**
* Handle a job failure that occurs outside the try/catch in handle()
* (e.g. serialisation errors, queue-worker crashes, max-attempts exceeded).
* Ensures the ImportJob record is always set to 'failed' with an error message.
*/
public function failed(?Throwable $exception): void
{
if ($this->slug) {
ImportJob::where('slug', $this->slug)->update([
'status' => 'failed',
'error_message' => $exception?->getMessage() ?? 'Job failed unexpectedly.',
]);
}

Log::error('ImportGrampsXml job failed', [
'file_path' => $this->filePath,
'user_id' => $this->user->getKey(),
'slug' => $this->slug,
'error' => $exception?->getMessage(),
]);
}

/**
* Convert GrampsXML data to GEDCOM format
* This is a basic conversion that maps GrampsXML structure to GEDCOM
Expand Down
64 changes: 59 additions & 5 deletions tests/Feature/Filament/Resources/GedcomResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public function test_export_gedcom_does_not_dispatch_without_authenticated_user(
public function test_after_create_dispatches_import_gedcom_for_ged_file(): void
{
Auth::login($this->user);
Storage::fake('private');
$disk = Storage::fake('private');
$disk->put('gedcom-form-imports/test.ged', '0 HEAD');

$gedcom = Gedcom::create(['filename' => 'gedcom-form-imports/test.ged']);

Expand All @@ -101,7 +102,8 @@ public function test_after_create_dispatches_import_gedcom_for_ged_file(): void
public function test_after_create_dispatches_gedcom_job_when_filename_is_array(): void
{
Auth::login($this->user);
Storage::fake('private');
$disk = Storage::fake('private');
$disk->put('gedcom-form-imports/test.ged', '0 HEAD');

// Filament FileUpload may persist as an array even for single-file uploads;
// CreateGedcom::afterCreate must extract the first element safely.
Expand All @@ -122,7 +124,8 @@ public function test_after_create_dispatches_gedcom_job_when_filename_is_array()
public function test_after_create_dispatches_import_gramps_xml_for_gramps_file(): void
{
Auth::login($this->user);
Storage::fake('private');
$disk = Storage::fake('private');
$disk->put('gedcom-form-imports/test.gramps', '<database/>');

$gedcom = Gedcom::create(['filename' => 'gedcom-form-imports/test.gramps']);

Expand Down Expand Up @@ -153,10 +156,59 @@ public function test_after_create_does_not_dispatch_when_filename_is_empty(): vo
Queue::assertNotPushed(ImportGrampsXml::class);
}

public function test_after_create_pre_creates_import_job_before_dispatch(): void
public function test_after_create_does_not_dispatch_when_file_not_found_on_disk(): void
{
Auth::login($this->user);
Storage::fake('private');
// File is NOT placed in the fake disk; afterCreate should abort gracefully.

$gedcom = Gedcom::create(['filename' => 'gedcom-form-imports/missing.ged']);

$page = new CreateGedcom();
$page->record = $gedcom;

$method = new \ReflectionMethod($page, 'afterCreate');
$method->invoke($page);

Queue::assertNotPushed(ImportGedcom::class);
Queue::assertNotPushed(ImportGrampsXml::class);
}

public function test_after_create_moves_file_from_livewire_tmp_and_dispatches_job(): void
{
Auth::login($this->user);
$disk = Storage::fake('private');

// Simulate a file that Livewire stored in its temporary directory instead of
// the final gedcom-form-imports directory (e.g. when Filament's file-move step
// did not run before afterCreate was called).
$tmpPath = 'livewire-tmp/abcdef-test.ged';
$disk->put($tmpPath, '0 HEAD');

$gedcom = Gedcom::create(['filename' => $tmpPath]);

$page = new CreateGedcom();
$page->record = $gedcom;

$method = new \ReflectionMethod($page, 'afterCreate');
$method->invoke($page);

// The file should have been moved out of livewire-tmp
$disk->assertMissing($tmpPath);
$disk->assertExists('gedcom-form-imports/abcdef-test.ged');

// The Gedcom record should point to the new location
$this->assertEquals('gedcom-form-imports/abcdef-test.ged', $gedcom->fresh()->filename);

// The import job should still be dispatched
Queue::assertPushed(ImportGedcom::class);
}

public function test_after_create_pre_creates_import_job_before_dispatch(): void
{
Auth::login($this->user);
$disk = Storage::fake('private');
$disk->put('gedcom-form-imports/test.ged', '0 HEAD');

$gedcom = Gedcom::create(['filename' => 'gedcom-form-imports/test.ged']);

Expand All @@ -177,7 +229,8 @@ public function test_after_create_pre_creates_import_job_before_dispatch(): void
public function test_after_create_dispatches_gedcom_job_with_slug(): void
{
Auth::login($this->user);
Storage::fake('private');
$disk = Storage::fake('private');
$disk->put('gedcom-form-imports/test.ged', '0 HEAD');

$gedcom = Gedcom::create(['filename' => 'gedcom-form-imports/test.ged']);

Expand Down Expand Up @@ -222,3 +275,4 @@ public function test_file_upload_component_accepted_file_types_includes_ged_exte
$this->assertContains('text/plain', $acceptedTypes, 'FileUpload should accept text/plain MIME type');
}
}

Loading