|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Concerns\EnsuresPublicDiskWritable;
|
|
|
|
|
use App\Http\Controllers\Concerns\StoresPublicUploadWithoutFileinfo;
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
|
use Throwable;
|
|
|
|
|
|
|
|
|
|
class UploadController extends Controller
|
|
|
|
|
{
|
|
|
|
|
use EnsuresPublicDiskWritable;
|
|
|
|
|
use StoresPublicUploadWithoutFileinfo;
|
|
|
|
|
|
|
|
|
|
public function store(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
if ($early = $this->ensurePublicDiskReady()) {
|
|
|
|
|
return $early;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$stored = $this->storeUploadedFileAsUniqueName($uploaded, 'uploads', 1024);
|
|
|
|
|
} catch (ValidationException $e) {
|
|
|
|
|
throw $e;
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
Log::error('upload_putfile_failed', [
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
'public_path' => storage_path('app/public'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => '文件保存失败',
|
|
|
|
|
'detail' => config('app.debug') ? $e->getMessage() : null,
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'path' => $stored['path'],
|
|
|
|
|
'url' => url('/storage/'.$stored['path']),
|
|
|
|
|
'mime' => self::jsonSafeString($stored['mime']),
|
|
|
|
|
'size' => $uploaded->getSize(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 避免 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';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除 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' => '已删除']);
|
|
|
|
|
}
|
|
|
|
|
}
|