373 lines
12 KiB
PHP
373 lines
12 KiB
PHP
<?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
|
|
{
|
|
$this->putContentsWithMimeType($relativePath, $content);
|
|
}
|
|
|
|
public function putContentsWithMimeType(string $relativePath, string $content, string $mimeType = ''): 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 {
|
|
$options = $mimeType !== '' ? [
|
|
OssClient::OSS_CONTENT_TYPE => $mimeType,
|
|
] : null;
|
|
$this->ossClient()->uploadFile(
|
|
$this->configService()->bucket(),
|
|
$this->configService()->objectKey($relativePath),
|
|
$tmpFile,
|
|
$options
|
|
);
|
|
} 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, $mimeType !== '' ? $mimeType : 'application/octet-stream');
|
|
} 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, string $mimeType = 'application/octet-stream'): void
|
|
{
|
|
$token = $this->qiniuAuth()->uploadToken($this->configService()->qiniuBucket(), $key);
|
|
try {
|
|
[$ret, $err] = $this->qiniuUploadManager()->putFile($token, $key, $filePath, null, $mimeType);
|
|
} catch (\Throwable $e) {
|
|
$err = $e;
|
|
}
|
|
|
|
if ($err !== null && $this->shouldRetryQiniuWithoutHttps($err)) {
|
|
try {
|
|
[$ret, $err] = $this->qiniuUploadManager(false)->putFile($token, $key, $filePath, null, $mimeType);
|
|
} 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;
|
|
}
|
|
}
|
|
}
|