first
This commit is contained in:
363
server-api/app/support/FileStorageService.php
Normal file
363
server-api/app/support/FileStorageService.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace app\support;
|
||||
|
||||
use OSS\OssClient;
|
||||
use Qiniu\Auth as QiniuAuth;
|
||||
use Qiniu\Config as QiniuConfig;
|
||||
use Qiniu\Storage\BucketManager as QiniuBucketManager;
|
||||
use Qiniu\Storage\UploadManager as QiniuUploadManager;
|
||||
use support\Request;
|
||||
use function dirname;
|
||||
use function file_exists;
|
||||
use function file_put_contents;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function ltrim;
|
||||
use function mkdir;
|
||||
use function sys_get_temp_dir;
|
||||
use function tempnam;
|
||||
use function unlink;
|
||||
|
||||
class FileStorageService
|
||||
{
|
||||
private static bool $caBundleInitialized = false;
|
||||
|
||||
private ?OssClient $ossClient = null;
|
||||
private ?QiniuAuth $qiniuAuth = null;
|
||||
|
||||
public function publicUrl(Request $request, string $value): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user