diff --git a/app/Http/Controllers/Admin/FileController.php b/app/Http/Controllers/Admin/FileController.php index e3952ab..c6e1067 100644 --- a/app/Http/Controllers/Admin/FileController.php +++ b/app/Http/Controllers/Admin/FileController.php @@ -34,6 +34,9 @@ class FileController extends Controller if (! $uploaded || ! $uploaded->isValid()) { return response()->json(['message' => 'فایل بارگذاری نشد یا خالی است'], 422); } + if ($uploaded->getSize() < 1) { + return response()->json(['message' => 'فایل خالی است'], 422); + } $path = $this->createDir(); $data = $request->all(); @@ -53,6 +56,7 @@ class FileController extends Controller $data['user_id'] = auth()->id(); $data['sts'] = 1; $file_create = File::create($data); + $file_create->setAttribute('full_src', File::publicUrl($data['src'])); return response()->json($file_create->fresh()); } diff --git a/app/Models/File.php b/app/Models/File.php index 407741d..6e0fa61 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -2,7 +2,6 @@ namespace App\Models; -use App\Models\Setting; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -52,7 +51,45 @@ class File extends Model $trimmed = trim($path); if (preg_match('/^https?:\/\//i', $trimmed)) { - return $trimmed; + return self::rewriteWrongPublicUrl($trimmed) ?? $trimmed; + } + + $relative = self::normalizeRelativeStoragePath($trimmed); + $base = self::resolvePublicBaseUrl(); + if ($base === '' || $relative === '') { + return null; + } + + return $base.'/'.$relative; + } + + /** + * مسیر نسبی داخل public/storage — حتی اگر قبلاً URL کامل اشتباه در DB ذخیره شده باشد. + */ + public static function normalizeRelativeStoragePath(string $path): string + { + $path = trim($path); + + if (preg_match('#/storage/(.+)$#i', $path, $matches)) { + return 'storage/'.ltrim($matches[1], '/'); + } + + $relative = ltrim($path, '/'); + + if (! str_starts_with($relative, 'storage/')) { + $relative = 'storage/'.$relative; + } + + return $relative; + } + + /** + * اصلاح URLهای قدیمی: https://site.ir/storage/... → https://site.ir/server/storage/... + */ + public static function rewriteWrongPublicUrl(string $url): ?string + { + if (! preg_match('#^(https?://[^/]+)/storage/(.+)$#i', $url, $matches)) { + return null; } $base = self::resolvePublicBaseUrl(); @@ -60,36 +97,45 @@ class File extends Model return null; } - $relative = ltrim($trimmed, '/'); - if (! str_starts_with($relative, 'storage/')) { - $relative = 'storage/'.$relative; + if (str_contains($base, '/server') && ! str_contains($matches[1].'/storage/', '/server/storage/')) { + return $base.'/storage/'.$matches[2]; } - return $base.'/'.$relative; + return null; } /** - * پایهٔ URL فایل‌های storage — cache-safe و مستقل از env() خام. + * پایهٔ URL فایل‌های storage. */ public static function resolvePublicBaseUrl(): string { + $fromSettings = self::baseUrlFromSettings(); + if ($fromSettings !== '') { + return $fromSettings; + } + $domain = config('app.domain'); if (is_string($domain) && $domain !== '' && ! str_contains($domain, 'localhost')) { - return rtrim($domain, '/'); + return self::ensureServerPublicBase(rtrim($domain, '/')); } $appUrl = config('app.url'); - if (is_string($appUrl) && $appUrl !== '' && ! str_contains($appUrl, 'localhost') && str_contains($appUrl, '/server')) { - return rtrim($appUrl, '/'); + if (is_string($appUrl) && $appUrl !== '' && ! str_contains($appUrl, 'localhost')) { + return self::ensureServerPublicBase(rtrim($appUrl, '/')); } + return ''; + } + + private static function baseUrlFromSettings(): string + { try { $rows = Setting::query() ->whereIn('key', ['bace_url', 'server_url', 'admin_url', 'app_url']) ->where('is_active', 1) ->pluck('value', 'key'); - foreach (['bace_url', 'server_url'] as $key) { + foreach (['server_url', 'bace_url'] as $key) { $value = trim((string) ($rows[$key] ?? '')); if ($value !== '' && preg_match('/^https?:\/\//i', $value)) { return rtrim($value, '/'); @@ -106,13 +152,17 @@ class File extends Model return rtrim($siteUrl, '/').'/server'; } } catch (\Throwable) { - // DB unavailable during deploy/migrate - } - - if (is_string($appUrl) && $appUrl !== '' && ! str_contains($appUrl, 'localhost')) { - return rtrim($appUrl, '/'); } return ''; } + + private static function ensureServerPublicBase(string $base): string + { + if (preg_match('#/(server|api)(/|$)#i', $base)) { + return rtrim($base, '/'); + } + + return rtrim($base, '/').'/server'; + } } diff --git a/tests/Unit/FilePublicUrlTest.php b/tests/Unit/FilePublicUrlTest.php index 4d595e8..c43aa36 100644 --- a/tests/Unit/FilePublicUrlTest.php +++ b/tests/Unit/FilePublicUrlTest.php @@ -3,14 +3,24 @@ namespace Tests\Unit; use App\Models\File; +use App\Models\Setting; use Illuminate\Support\Facades\Config; use Tests\TestCase; class FilePublicUrlTest extends TestCase { - public function test_public_url_uses_config_domain_with_storage_path(): void + private function seedGaroosiAdminUrl(): void { - Config::set('app.domain', 'https://garoosigholaktala.ir/server'); + Setting::query()->updateOrCreate( + ['key' => 'admin_url'], + ['value' => 'https://garoosigholaktala.ir/admin/', 'is_active' => 1, 'title_fa' => 'admin'] + ); + } + + public function test_public_url_uses_server_storage_path_from_site_domain(): void + { + $this->seedGaroosiAdminUrl(); + Config::set('app.domain', 'https://garoosigholaktala.ir'); $url = File::publicUrl('storage/1405/02/27/sample.jpg'); @@ -20,18 +30,25 @@ class FilePublicUrlTest extends TestCase ); } - public function test_public_url_derives_server_base_from_admin_url_setting(): void + public function test_public_url_rewrites_legacy_wrong_absolute_url(): void + { + $this->seedGaroosiAdminUrl(); + Config::set('app.domain', 'https://garoosigholaktala.ir'); + + $url = File::publicUrl('https://garoosigholaktala.ir/storage/1405/02/27/sample.jpg'); + + $this->assertSame( + 'https://garoosigholaktala.ir/server/storage/1405/02/27/sample.jpg', + $url + ); + } + + public function test_resolve_base_from_admin_url_setting(): void { Config::set('app.domain', null); Config::set('app.url', 'http://localhost'); + $this->seedGaroosiAdminUrl(); - \App\Models\Setting::query()->updateOrCreate( - ['key' => 'admin_url'], - ['value' => 'https://garoosigholaktala.ir/admin/', 'is_active' => 1, 'title_fa' => 'admin'] - ); - - $base = File::resolvePublicBaseUrl(); - - $this->assertSame('https://garoosigholaktala.ir/server', $base); + $this->assertSame('https://garoosigholaktala.ir/server', File::resolvePublicBaseUrl()); } }