put($key, $body, $mime); return ['storage' => self::DRIVER_OSS, 'key' => $key, 'size' => strlen($body)]; } $path = self::localPath($key); $dir = dirname($path); if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) { throw new \RuntimeException('无法创建上传目录:' . $dir); } if (@file_put_contents($path, $body) === false) { throw new \RuntimeException('写入文件失败:' . $key); } return ['storage' => self::DRIVER_LOCAL, 'key' => $key, 'size' => strlen($body)]; } /** 读取内容;找不到返回 null */ public static function get(string $storage, string $key, string $fileUrl = ''): ?string { if ($storage === self::DRIVER_OSS) { try { $oss = Settings::ossClient(); return $oss === null ? null : $oss->get($key); } catch (\Throwable $e) { return null; } } $path = self::resolveLocal($key, $fileUrl); if ($path === null || !is_file($path)) { return null; } $data = @file_get_contents($path); return $data === false ? null : $data; } public static function exists(string $storage, string $key, string $fileUrl = ''): bool { if ($storage === self::DRIVER_OSS) { $oss = Settings::ossClient(); if ($oss === null) return false; try { return $oss->head($key) !== null; } catch (\Throwable $e) { return false; } } $path = self::resolveLocal($key, $fileUrl); return $path !== null && is_file($path); } /** 删除;返回是否确实删掉了(不存在也算成功,保持幂等) */ public static function delete(string $storage, string $key, string $fileUrl = ''): bool { if ($storage === self::DRIVER_OSS) { try { $oss = Settings::ossClient(); return $oss === null ? false : $oss->delete($key); } catch (\Throwable $e) { return false; } } $path = self::resolveLocal($key, $fileUrl); if ($path === null) { return true; } return !is_file($path) || @unlink($path); } /** * 本地文件的绝对路径。只接受 uploads/ 下的相对键,挡住 `../` 之类的路径穿越。 * $fileUrl 是历史遗留的 `/uploads/attachments/xxx.jpg` 形式,作为兼容兜底。 */ public static function localPath(string $key): string { $key = ltrim(str_replace('\\', '/', $key), '/'); if (strpos($key, '..') !== false) { throw new \RuntimeException('非法的对象键:' . $key); } return self::uploadsDir() . '/' . $key; } /** * 解析本地文件的绝对路径(找不到/键非法返回 null)。 * public 是给「展示层算体积」这类只读场景用的,仍会挡住路径穿越。 */ public static function resolveLocal(string $key, string $fileUrl = ''): ?string { $key = trim($key) !== '' ? $key : ''; if ($key !== '') { try { return self::localPath($key); } catch (\Throwable $e) { return null; } } // 老数据只有 file_url(/uploads/...) $url = trim($fileUrl); if ($url === '' || strpos($url, '/uploads/') !== 0 || strpos($url, '..') !== false) { return null; } return dirname(__DIR__) . $url; } public static function uploadsDir(): string { return dirname(__DIR__) . '/uploads'; } }