assetUrlService()->buildUrl($request, $this->storagePath($value)); } public function normalizeUrl(string $value, Request $request): string { return $this->assetUrlService()->normalizeUrl($value, $request); } public function storagePath(string $value): string { return ltrim($this->assetUrlService()->storagePath($value), '/'); } public function putUploadedFile(mixed $file, string $relativePath): void { $relativePath = $this->storagePath($relativePath); if ($this->configService()->isOss()) { $this->configService()->assertReady(); $realPath = $file->getRealPath(); if (!is_string($realPath) || $realPath === '' || !is_file($realPath)) { throw new \RuntimeException('上传文件无效'); } $this->ossClient()->uploadFile( $this->configService()->bucket(), $this->configService()->objectKey($relativePath), $realPath ); return; } if ($this->configService()->isQiniu()) { $this->configService()->assertReady(); $realPath = $file->getRealPath(); if (!is_string($realPath) || $realPath === '' || !is_file($realPath)) { throw new \RuntimeException('上传文件无效'); } $key = $this->configService()->objectKey($relativePath); $this->qiniuUploadFile($realPath, $key); return; } $target = public_path() . '/' . $relativePath; if (!is_dir(dirname($target))) { mkdir(dirname($target), 0775, true); } $file->move($target); } public function putContents(string $relativePath, string $content): void { $relativePath = $this->storagePath($relativePath); if ($this->configService()->isOss()) { $this->configService()->assertReady(); $tmpFile = tempnam(sys_get_temp_dir(), 'anxinyan_oss_'); if ($tmpFile === false) { throw new \RuntimeException('无法创建临时文件'); } file_put_contents($tmpFile, $content); try { $this->ossClient()->uploadFile( $this->configService()->bucket(), $this->configService()->objectKey($relativePath), $tmpFile ); } finally { if (file_exists($tmpFile)) { @unlink($tmpFile); } } return; } if ($this->configService()->isQiniu()) { $this->configService()->assertReady(); $tmpFile = tempnam(sys_get_temp_dir(), 'anxinyan_qiniu_'); if ($tmpFile === false) { throw new \RuntimeException('无法创建临时文件'); } file_put_contents($tmpFile, $content); try { $key = $this->configService()->objectKey($relativePath); $this->qiniuUploadFile($tmpFile, $key); } finally { if (file_exists($tmpFile)) { @unlink($tmpFile); } } return; } $target = public_path() . '/' . $relativePath; if (!is_dir(dirname($target))) { mkdir(dirname($target), 0775, true); } file_put_contents($target, $content); } public function exists(string $value): bool { $relativePath = $this->storagePath($value); if ($relativePath === '') { return false; } if ($this->configService()->isOss()) { $this->configService()->assertReady(); return $this->ossClient()->doesObjectExist( $this->configService()->bucket(), $this->configService()->objectKey($relativePath) ); } if ($this->configService()->isQiniu()) { $this->configService()->assertReady(); [$ret, $err] = $this->qiniuBucketManager()->stat( $this->configService()->qiniuBucket(), $this->configService()->objectKey($relativePath) ); if ($err !== null && $this->shouldRetryQiniuWithoutHttps($err)) { [$ret, $err] = $this->qiniuBucketManager(false)->stat( $this->configService()->qiniuBucket(), $this->configService()->objectKey($relativePath) ); } if ($err === null) { return true; } if ((int)$err->code() === 612) { return false; } throw new \RuntimeException('七牛云文件检查失败:' . $err->message()); } return is_file(public_path() . '/' . $relativePath); } public function delete(string $value): void { $relativePath = $this->storagePath($value); if ($relativePath === '') { return; } if ($this->configService()->isOss()) { $this->configService()->assertReady(); $this->ossClient()->deleteObject( $this->configService()->bucket(), $this->configService()->objectKey($relativePath) ); return; } if ($this->configService()->isQiniu()) { $this->configService()->assertReady(); [$ret, $err] = $this->qiniuBucketManager()->delete( $this->configService()->qiniuBucket(), $this->configService()->objectKey($relativePath) ); if ($err !== null && $this->shouldRetryQiniuWithoutHttps($err)) { [$ret, $err] = $this->qiniuBucketManager(false)->delete( $this->configService()->qiniuBucket(), $this->configService()->objectKey($relativePath) ); } if ($err !== null && (int)$err->code() !== 612) { throw new \RuntimeException('七牛云删除失败:' . $err->message()); } return; } $fullPath = public_path() . '/' . $relativePath; if (file_exists($fullPath) && is_file($fullPath)) { @unlink($fullPath); } } private function ossClient(): OssClient { if ($this->ossClient instanceof OssClient) { return $this->ossClient; } $this->configService()->assertReady(); $this->ensureCaBundleConfigured(); return $this->ossClient = new OssClient( $this->configService()->accessKeyId(), $this->configService()->accessKeySecret(), $this->configService()->endpoint() ); } private function qiniuAuth(): QiniuAuth { if ($this->qiniuAuth instanceof QiniuAuth) { return $this->qiniuAuth; } $this->configService()->assertReady(); $this->ensureCaBundleConfigured(); return $this->qiniuAuth = new QiniuAuth( $this->configService()->qiniuAccessKey(), $this->configService()->qiniuSecretKey() ); } private function qiniuConfig(bool $useHttps = true): QiniuConfig { $config = new QiniuConfig(); $config->useHTTPS = $useHttps; $config->useCdnDomains = false; return $config; } private function qiniuUploadManager(bool $useHttps = true): QiniuUploadManager { return new QiniuUploadManager($this->qiniuConfig($useHttps)); } private function qiniuBucketManager(bool $useHttps = true): QiniuBucketManager { return new QiniuBucketManager( $this->qiniuAuth(), $this->qiniuConfig($useHttps) ); } private function qiniuUploadFile(string $filePath, string $key): void { $token = $this->qiniuAuth()->uploadToken($this->configService()->qiniuBucket(), $key); try { [$ret, $err] = $this->qiniuUploadManager()->putFile($token, $key, $filePath); } catch (\Throwable $e) { $err = $e; } if ($err !== null && $this->shouldRetryQiniuWithoutHttps($err)) { try { [$ret, $err] = $this->qiniuUploadManager(false)->putFile($token, $key, $filePath); } catch (\Throwable $e) { $err = $e; } } if ($err !== null) { throw new \RuntimeException('七牛云上传失败:' . $this->qiniuErrorMessage($err)); } } private function shouldRetryQiniuWithoutHttps(mixed $err): bool { return stripos($this->qiniuErrorMessage($err), 'SSL certificate problem') !== false; } private function qiniuErrorMessage(mixed $err): string { if ($err instanceof \Throwable) { return $err->getMessage(); } if (is_object($err) && method_exists($err, 'message')) { return (string)$err->message(); } if (is_string($err) && $err !== '') { return $err; } return '未知错误'; } private function assetUrlService(): PublicAssetUrlService { return new PublicAssetUrlService(); } private function configService(): FileStorageConfigService { return new FileStorageConfigService(); } private function ensureCaBundleConfigured(): void { if (self::$caBundleInitialized) { return; } $currentCurlCa = (string)ini_get('curl.cainfo'); $currentOpensslCa = (string)ini_get('openssl.cafile'); if (($currentCurlCa !== '' && is_file($currentCurlCa)) || ($currentOpensslCa !== '' && is_file($currentOpensslCa))) { self::$caBundleInitialized = true; return; } $candidates = [ '/etc/ssl/cert.pem', '/private/etc/ssl/cert.pem', '/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/opt/homebrew/etc/openssl@3/cert.pem', '/usr/local/etc/openssl@3/cert.pem', ]; foreach ($candidates as $path) { if (!is_file($path)) { continue; } @ini_set('curl.cainfo', $path); @ini_set('openssl.cafile', $path); self::$caBundleInitialized = true; return; } } }