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; } }