You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
3.9 KiB

1 week ago
<?php
namespace App\Http\Controllers\Api;
1 week ago
use App\Http\Controllers\Concerns\EnsuresPublicDiskWritable;
1 week ago
use App\Http\Controllers\Concerns\StoresPublicUploadWithoutFileinfo;
1 week ago
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
1 week ago
use Illuminate\Support\Facades\Log;
15 hours ago
use Illuminate\Support\Facades\Storage;
1 week ago
use Throwable;
1 week ago
class UploadController extends Controller
{
1 week ago
use EnsuresPublicDiskWritable;
1 week ago
use StoresPublicUploadWithoutFileinfo;
1 week ago
1 week ago
public function store(Request $request): JsonResponse
{
1 week ago
if ($early = $this->ensurePublicDiskReady()) {
return $early;
}
1 week ago
if (!$request->hasFile('file')) {
return response()->json([
'message' => '未收到文件,请使用 multipart 表单字段名 file。',
], 422);
}
$uploaded = $request->file('file');
if (!$uploaded->isValid()) {
return response()->json([
'message' => '上传未通过校验:'.$uploaded->getErrorMessage(),
], 422);
}
1 week ago
$data = $request->validate([
1 week ago
'file' => ['required', 'file'],
1 week ago
]);
1 week ago
try {
1 week ago
$path = $this->storeUploadedFileAsUniqueName($data['file'], 'uploads');
1 week ago
} catch (Throwable $e) {
report($e);
1 week ago
Log::error('upload_putfile_failed', [
'message' => $e->getMessage(),
'public_path' => storage_path('app/public'),
]);
1 week ago
return response()->json([
'message' => '文件保存失败',
1 week ago
'detail' => config('app.debug') ? $e->getMessage() : null,
1 week ago
], 500);
}
if ($path === false) {
1 week ago
Log::error('upload_putfile_returned_false', [
'public_path' => storage_path('app/public'),
]);
1 week ago
return response()->json([
1 week ago
'message' => '文件保存失败(写入返回失败)',
'detail' => config('app.debug') ? 'Storage::putFile 返回 false多为磁盘 public 配置或权限问题' : null,
1 week ago
], 500);
}
1 week ago
$mime = self::jsonSafeString($this->mimeForUploadResponse($data['file']));
1 week ago
1 week ago
return response()->json([
'path' => $path,
1 week ago
'url' => url('/storage/'.str_replace('\\', '/', $path)),
1 week ago
'mime' => $mime,
1 week ago
'size' => $data['file']->getSize(),
]);
}
1 week ago
/** 避免 mime 等字段含非法 UTF-8 导致 json_encode 抛错成 500 */
private static function jsonSafeString(string $value): string
{
if ($value === '') {
return '';
}
if (function_exists('mb_scrub')) {
return mb_scrub($value, 'UTF-8');
}
$clean = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
return $clean !== false ? $clean : 'application/octet-stream';
}
15 hours ago
/**
* 删除 public 磁盘下已上传文件(仅 uploads/ 根目录内扁平文件)。仅超级管理员。
*/
public function remove(Request $request): JsonResponse
{
abort_unless($request->user()?->isSuperAdmin(), 403, '仅超级管理员可删除上传文件');
$data = $request->validate([
'path' => ['required', 'string', 'max:500'],
]);
$path = str_replace('\\', '/', trim($data['path']));
if (str_contains($path, '..') || ! str_starts_with($path, 'uploads/')) {
return response()->json(['message' => '无效路径'], 422);
}
if (! preg_match('#^uploads/[a-zA-Z0-9][a-zA-Z0-9._-]*$#', $path)) {
return response()->json(['message' => '无效路径'], 422);
}
if (! Storage::disk('public')->exists($path)) {
return response()->json(['message' => '文件不存在'], 404);
}
Storage::disk('public')->delete($path);
return response()->json(['message' => '已删除']);
}
1 week ago
}