From 8aa3144745f07e069b8c9a6d59bf7f4d5dfc8956 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 12 Sep 2026 15:34:09 +0930 Subject: [PATCH] Fix story video upload probe path (#7203) apiV1Add passed an absolute path to FFMpeg::open(), which laravel-ffmpeg resolves relative to the local disk root, producing a doubled path and an ffprobe 'Unable to probe / No such file or directory' failure. Open the file via the disk instead: FFMpeg::fromDisk('local')->open($path) for local storage, and an on-demand local disk rooted at the temp dir for cloud storage. Adds feature tests that pin the disk-relative path resolution. --- .../Controllers/StoryComposeController.php | 13 +-- .../Feature/Api/StoryVideoUploadPathTest.php | 84 +++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/Api/StoryVideoUploadPathTest.php diff --git a/app/Http/Controllers/StoryComposeController.php b/app/Http/Controllers/StoryComposeController.php index 9c72a504e..24ed2deee 100644 --- a/app/Http/Controllers/StoryComposeController.php +++ b/app/Http/Controllers/StoryComposeController.php @@ -91,16 +91,19 @@ class StoryComposeController extends Controller if ($story->type === 'video') { if ($localFs) { - $videoPath = storage_path('app/'.$path); + $media = FFMpeg::fromDisk('local')->open($path); } else { - $tempPath = sys_get_temp_dir().'/'.Str::random(40).'.mp4'; + $tempName = Str::random(40).'.mp4'; + $tempPath = sys_get_temp_dir().'/'.$tempName; file_put_contents($tempPath, $disk->get($path)); - $videoPath = $tempPath; + $media = FFMpeg::fromDisk(Storage::build([ + 'driver' => 'local', + 'root' => sys_get_temp_dir(), + ]))->open($tempName); } try { - $video = FFMpeg::open($videoPath); - $duration = $video->getDurationInSeconds(); + $duration = $media->getDurationInSeconds(); $res['media_duration'] = $duration; if ($duration > 500) { diff --git a/tests/Feature/Api/StoryVideoUploadPathTest.php b/tests/Feature/Api/StoryVideoUploadPathTest.php new file mode 100644 index 000000000..54e375782 --- /dev/null +++ b/tests/Feature/Api/StoryVideoUploadPathTest.php @@ -0,0 +1,84 @@ + true]); + config(['filesystems.default' => 'local']); +}); + +it('opens the uploaded story video via the local disk with a disk-relative path', function () { + $user = User::factory()->create(); + $user->refresh(); + $this->actingAs($user); + + $captured = []; + + // Swap the FFMpeg facade for a spy so we never shell out to ffprobe. + // fromDisk()/open() return the spy itself (fluent chain), and we record + // the disk + path the controller asks for. + $spy = Mockery::mock(); + $spy->shouldReceive('fromDisk')->andReturnUsing(function ($disk) use (&$captured, $spy) { + $captured['disk'] = $disk; + + return $spy; + }); + $spy->shouldReceive('open')->andReturnUsing(function ($path) use (&$captured, $spy) { + $captured['path'] = $path; + + return $spy; + }); + $spy->shouldReceive('getDurationInSeconds')->andReturn(10); + \ProtoneMedia\LaravelFFMpeg\Support\FFMpeg::swap($spy); + + $response = $this->postJson('/api/web/stories/v1/add', [ + 'file' => UploadedFile::fake()->create('story.mp4', 500, 'video/mp4'), + ]); + + $response->assertOk(); + expect($response->json('media_type'))->toBe('video'); + expect($response->json('media_duration'))->toBe(10); + + // Opened from the local disk... + expect($captured['disk'])->toBe('local'); + + // ...with a disk-relative path (the regression passed an absolute path). + $path = $captured['path']; + expect($path)->not->toStartWith('/'); + expect($path)->not->toContain(storage_path()); + expect($path)->toStartWith('public/_esm.t3/'); + + // And that disk-relative path resolves to the file that was actually + // stored, so ffprobe would find it. + Storage::disk('local')->assertExists($path); + + $story = Story::whereProfileId($user->profile_id)->firstOrFail(); + expect($story->type)->toBe('video'); + expect($story->path)->toBe($path); +}); + +it('rejects a story video longer than the allowed duration', function () { + $user = User::factory()->create(); + $user->refresh(); + $this->actingAs($user); + + $spy = Mockery::mock(); + $spy->shouldReceive('fromDisk')->andReturnSelf(); + $spy->shouldReceive('open')->andReturnSelf(); + $spy->shouldReceive('getDurationInSeconds')->andReturn(501); + \ProtoneMedia\LaravelFFMpeg\Support\FFMpeg::swap($spy); + + $response = $this->postJson('/api/web/stories/v1/add', [ + 'file' => UploadedFile::fake()->create('story.mp4', 500, 'video/mp4'), + ]); + + $response->assertStatus(422); + expect(Story::whereProfileId($user->profile_id)->count())->toBe(0); +});