Files
miniapi/app/service/OssUploadService.php
gaofeng 81ade70944 提交
2026-06-17 09:23:18 +08:00

204 lines
6.8 KiB
PHP

<?php
namespace app\service;
use Throwable;
class OssUploadService
{
public function uploadBase64(string $fileName, string $base64File, array $extraFields = []): array
{
$fileContent = base64_decode($base64File, true);
if ($fileContent === false || $fileContent === '') {
return $this->createErrorResult('文件解析错误');
}
return $this->uploadBinaryContent($fileName, $fileContent, $extraFields);
}
public function uploadBinary(string $fileName, string $fileContent, array $extraFields = []): array
{
if ($fileContent === '') {
return $this->createErrorResult('Binary payload is empty');
}
return $this->uploadBinaryContent($fileName, $fileContent, $extraFields);
}
private function uploadBinaryContent(string $fileName, string $fileContent, array $extraFields = []): array
{
$uploadConfig = config('filesystem.upload_conf') ?: [];
$maxSize = (int)($uploadConfig['fileSize'] ?? 10 * 1024 * 1024);
if (strlen($fileContent) > $maxSize) {
return $this->createErrorResult('File size exceeds limit');
}
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedExts = $uploadConfig['fileExt'] ?? [];
if ($ext === '' || (!empty($allowedExts) && !in_array($ext, $allowedExts, true))) {
return $this->createErrorResult('File extension not allowed');
}
$bucket = (string)config('oss.bucket');
$folder = (string)($extraFields['Folder'] ?? ($extraFields['Bucket'] ?? ''));
$key = $extraFields['Key'] ?? basename($fileName);
$objectKey = $this->buildObjectKey($folder, (string)$key);
if ($bucket === '' || $objectKey === '') {
return $this->createErrorResult('Bucket or key missing');
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = (string)$finfo->buffer($fileContent);
try {
$service = new OssService(config('oss'));
$result = $service->uploadFromBinary($fileContent, $objectKey, $bucket, [
'contentType' => $this->resolveContentType($mimeType, $ext),
]);
$result['url'] = $service->buildObjectUrl($objectKey, $bucket);
$result['folder'] = $folder;
return [
'code' => 1,
'msg' => 'upload ok',
'data' => $result,
];
} catch (Throwable $e) {
$fallback = $this->saveToLocalFallback($fileContent, $bucket, $objectKey);
return $this->createErrorResult(
'OSS upload failed: ' . $e->getMessage(),
[
'bucket' => $bucket,
'key' => $objectKey,
'folder' => $folder,
'fallback_saved' => $fallback['saved'],
'fallback_path' => $fallback['path'],
'fallback_error' => $fallback['error'],
]
);
}
}
private function buildObjectKey(string $folder, string $key): string
{
$folder = trim(str_replace('\\', '/', $folder), '/');
$key = trim(str_replace('\\', '/', $key), '/');
if ($folder === '') {
return $key;
}
if ($key === '') {
return $folder;
}
if (str_starts_with($key . '/', $folder . '/')) {
return $key;
}
return $folder . '/' . $key;
}
private function saveToLocalFallback(string $fileContent, string $bucket, string $key): array
{
$baseDir = $this->resolveFallbackBaseDir();
$safeBucket = $this->sanitizePathSegment($bucket);
$safeKey = $this->sanitizeObjectKey($key);
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $safeBucket . DIRECTORY_SEPARATOR . $safeKey;
$directory = dirname($fullPath);
try {
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new \RuntimeException('Unable to create fallback directory');
}
if (file_put_contents($fullPath, $fileContent) === false) {
throw new \RuntimeException('Unable to write fallback file');
}
return [
'saved' => true,
'path' => $fullPath,
'error' => null,
];
} catch (Throwable $e) {
return [
'saved' => false,
'path' => $fullPath,
'error' => $e->getMessage(),
];
}
}
private function resolveFallbackBaseDir(): string
{
$uploadConfig = config('filesystem.upload_conf') ?: [];
$configured = (string)($uploadConfig['fallbackDir'] ?? '');
if ($configured !== '') {
return rtrim($configured, "\\/");
}
$localRoot = (string)(config('filesystem.disks.local.root') ?? '');
if ($localRoot !== '') {
return rtrim($localRoot, "\\/") . DIRECTORY_SEPARATOR . 'oss-fallback';
}
return app()->getRuntimePath() . 'storage' . DIRECTORY_SEPARATOR . 'oss-fallback';
}
private function sanitizeObjectKey(string $key): string
{
$normalized = str_replace('\\', '/', $key);
$segments = array_filter(explode('/', $normalized), static function (string $segment): bool {
return $segment !== '' && $segment !== '.' && $segment !== '..';
});
if ($segments === []) {
return 'unnamed-file';
}
$safeSegments = array_map([$this, 'sanitizePathSegment'], $segments);
return implode(DIRECTORY_SEPARATOR, $safeSegments);
}
private function sanitizePathSegment(string $segment): string
{
$cleaned = preg_replace('/[^A-Za-z0-9._-]/', '_', $segment) ?? '';
return $cleaned !== '' ? $cleaned : 'unknown';
}
private function resolveContentType(string $mimeType, string $ext): string
{
if ($mimeType !== '' && $mimeType !== 'application/octet-stream') {
return $mimeType;
}
return match ($ext) {
'jpg', 'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'bmp' => 'image/bmp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
default => 'application/octet-stream',
};
}
private function createErrorResult(string $message, array $data = []): array
{
$result = [
'code' => 0,
'msg' => $message,
];
if ($data !== []) {
$result['data'] = $data;
}
return $result;
}
}