|
|
<?php
|
|
|
|
|
|
namespace App\Services\Course;
|
|
|
|
|
|
use App\Models\CourseMedia;
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
use Illuminate\Support\Str;
|
|
|
use RuntimeException;
|
|
|
|
|
|
class RemoteCourseAssetImporter
|
|
|
{
|
|
|
/**
|
|
|
* 从远程 URL 下载图片到 public 盘并写入 course_media。
|
|
|
*
|
|
|
* @param string|null $referer 可选 Referer,部分 CDN 防盗链需要
|
|
|
*/
|
|
|
public function importFromUrl(string $url, string $subdir, ?string $referer = null): CourseMedia
|
|
|
{
|
|
|
$url = trim($url);
|
|
|
if ($url === '' || ! preg_match('#^https?://#i', $url)) {
|
|
|
throw new RuntimeException('图片地址无效');
|
|
|
}
|
|
|
|
|
|
if (! in_array($subdir, ['covers', 'promos'], true)) {
|
|
|
throw new RuntimeException('不支持的资源类型');
|
|
|
}
|
|
|
|
|
|
$headers = [
|
|
|
'User-Agent' => 'Mozilla/5.0 (compatible; SlakeSchool/1.0)',
|
|
|
'Accept' => 'image/*,*/*',
|
|
|
];
|
|
|
if ($referer && preg_match('#^https?://#i', $referer)) {
|
|
|
$headers['Referer'] = $referer;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
$response = Http::timeout(30)
|
|
|
->withHeaders($headers)
|
|
|
->get($url);
|
|
|
} catch (\Throwable $e) {
|
|
|
throw new RuntimeException('图片下载失败:'.$e->getMessage());
|
|
|
}
|
|
|
|
|
|
if (! $response->successful()) {
|
|
|
throw new RuntimeException('图片下载失败,HTTP '.$response->status());
|
|
|
}
|
|
|
|
|
|
$body = $response->body();
|
|
|
if ($body === '') {
|
|
|
throw new RuntimeException('图片内容为空');
|
|
|
}
|
|
|
|
|
|
$contentType = $response->header('Content-Type');
|
|
|
if ($contentType && ! str_contains(strtolower($contentType), 'image')) {
|
|
|
throw new RuntimeException('远程资源不是图片');
|
|
|
}
|
|
|
|
|
|
$ext = $this->guessExtension($url, $contentType);
|
|
|
$filename = basename(parse_url($url, PHP_URL_PATH) ?: '') ?: 'import';
|
|
|
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename) ?: 'import';
|
|
|
$filename = Str::limit($filename, 64, '');
|
|
|
if (! preg_match('/\.(png|jpe?g|gif|webp)$/i', $filename)) {
|
|
|
$filename .= '.'.$ext;
|
|
|
}
|
|
|
|
|
|
$path = 'courses/'.$subdir.'/'.date('Ym').'/'.Str::random(16).'_'.$filename;
|
|
|
|
|
|
Storage::disk('public')->put($path, $body);
|
|
|
|
|
|
$publicUrl = CourseMedia::publicAssetUrl($path) ?? '';
|
|
|
|
|
|
return CourseMedia::query()->create([
|
|
|
'disk' => 'public',
|
|
|
'path' => $path,
|
|
|
'url' => $publicUrl,
|
|
|
'category' => $subdir,
|
|
|
'original_name' => $filename,
|
|
|
'mime_type' => $this->guessMimeType($ext, $contentType),
|
|
|
'size_bytes' => strlen($body),
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
protected function guessExtension(string $url, ?string $contentType): string
|
|
|
{
|
|
|
if ($contentType) {
|
|
|
$lower = strtolower($contentType);
|
|
|
if (str_contains($lower, 'png')) {
|
|
|
return 'png';
|
|
|
}
|
|
|
if (str_contains($lower, 'jpeg') || str_contains($lower, 'jpg')) {
|
|
|
return 'jpg';
|
|
|
}
|
|
|
if (str_contains($lower, 'gif')) {
|
|
|
return 'gif';
|
|
|
}
|
|
|
if (str_contains($lower, 'webp')) {
|
|
|
return 'webp';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (preg_match('#\.(png|jpe?g|gif|webp)(\?|$)#i', $url, $m)) {
|
|
|
return strtolower($m[1] === 'jpeg' ? 'jpg' : $m[1]);
|
|
|
}
|
|
|
|
|
|
return 'jpg';
|
|
|
}
|
|
|
|
|
|
protected function guessMimeType(string $ext, ?string $contentType): ?string
|
|
|
{
|
|
|
if ($contentType && str_contains(strtolower($contentType), 'image')) {
|
|
|
return strtok($contentType, ';') ?: null;
|
|
|
}
|
|
|
|
|
|
return match ($ext) {
|
|
|
'png' => 'image/png',
|
|
|
'jpg', 'jpeg' => 'image/jpeg',
|
|
|
'gif' => 'image/gif',
|
|
|
'webp' => 'image/webp',
|
|
|
default => null,
|
|
|
};
|
|
|
}
|
|
|
}
|