mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 21:40:50 +00:00
694 lines
28 KiB
PHP
694 lines
28 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||
// +----------------------------------------------------------------------
|
||
// | Author: CRMEB Team <admin@crmeb.com>
|
||
// +----------------------------------------------------------------------
|
||
|
||
namespace app\services\driver\upload;
|
||
|
||
|
||
use app\dao\driver\upload\UploadStorageDao;
|
||
use app\services\driver\upload\UploadDriverServices;
|
||
use crmeb\basic\BaseService;
|
||
use crmeb\exceptions\AdminException;
|
||
use crmeb\services\CacheService;
|
||
use crmeb\services\driver\DriverRegistry;
|
||
use crmeb\services\upload\Upload;
|
||
use crmeb\utils\FormBuilder;
|
||
|
||
/**
|
||
* 存储空间管理服务
|
||
*
|
||
* 注意:此服务不直接依赖扩展,配置字段从数据库读取
|
||
* 扩展通过 UploadDriverServices 注册配置到数据库
|
||
*
|
||
* Class UploadStorageServices
|
||
* @package app\services\driver\upload
|
||
*/
|
||
class UploadStorageServices extends BaseService
|
||
{
|
||
/**
|
||
* @var UploadStorageDao
|
||
*/
|
||
protected $dao;
|
||
|
||
/**
|
||
* @var UploadDriverServices
|
||
*/
|
||
protected $driverServices;
|
||
|
||
/**
|
||
* UploadStorageServices constructor.
|
||
* @param UploadStorageDao $dao
|
||
* @param UploadDriverServices $driverServices
|
||
*/
|
||
public function __construct(UploadStorageDao $dao, UploadDriverServices $driverServices)
|
||
{
|
||
$this->dao = $dao;
|
||
$this->driverServices = $driverServices;
|
||
}
|
||
|
||
/**
|
||
* 将动态类型编号或驱动 key 解析为已注册的上传驱动
|
||
* @param int|string $typeOrKey
|
||
* @return string
|
||
*/
|
||
protected function resolveDriverKey($typeOrKey): string
|
||
{
|
||
if (is_int($typeOrKey) || (is_string($typeOrKey) && ctype_digit($typeOrKey))) {
|
||
$driverKey = $this->driverServices->getTypeDriverKeyMap()[(int)$typeOrKey] ?? '';
|
||
} else {
|
||
$driverKey = is_string($typeOrKey) ? $typeOrKey : '';
|
||
}
|
||
if ($driverKey === '' || !preg_match('/^[a-z][a-z0-9_]*$/', $driverKey) || !$this->driverServices->getByDriverKey($driverKey)) {
|
||
throw new AdminException('上传驱动不存在');
|
||
}
|
||
return $driverKey;
|
||
}
|
||
|
||
/**
|
||
* 校验访问地址,允许路径前缀但不允许凭据、查询或目录穿越
|
||
* @param mixed $value
|
||
* @param int $maxLength
|
||
* @param bool $optional
|
||
* @return string
|
||
*/
|
||
protected function validateStorageUrl($value, int $maxLength = 255, bool $optional = true): string
|
||
{
|
||
if (!is_string($value)) throw new AdminException('空间域名格式错误');
|
||
$value = trim($value);
|
||
if ($value === '' && $optional) return '';
|
||
$url = parse_url($value);
|
||
if (strlen($value) > $maxLength || !filter_var($value, FILTER_VALIDATE_URL) || !$url
|
||
|| !in_array(strtolower($url['scheme'] ?? ''), ['http', 'https'], true) || empty($url['host'])
|
||
|| isset($url['user']) || isset($url['pass']) || isset($url['query']) || isset($url['fragment'])) {
|
||
throw new AdminException('空间域名格式错误,请输入 HTTP 或 HTTPS 地址');
|
||
}
|
||
foreach (explode('/', rawurldecode($url['path'] ?? '')) as $part) {
|
||
if ($part === '..' || preg_match('/[\x00-\x1f\x7f\\\\]/', $part)) throw new AdminException('空间域名路径不合法');
|
||
}
|
||
return rtrim($value, '/');
|
||
}
|
||
|
||
/**
|
||
* 校验 CNAME 目标,空字符串可清除可选配置
|
||
* @param mixed $value
|
||
* @return string
|
||
*/
|
||
protected function validateStorageCname($value): string
|
||
{
|
||
if (!is_string($value)) throw new AdminException('CNAME 格式错误');
|
||
$value = trim($value);
|
||
if ($value !== '' && (strlen($value) > 253 || !filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME))) {
|
||
throw new AdminException('CNAME 格式错误');
|
||
}
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* 从声明字段构造配置并检查必填项
|
||
* @param string $driverKey
|
||
* @param array $data
|
||
* @return array
|
||
*/
|
||
protected function prepareConfigFields(string $driverKey, array $data): array
|
||
{
|
||
$fields = $this->driverServices->getConfigFields($driverKey);
|
||
foreach ($fields as &$field) {
|
||
$name = $field['field'];
|
||
if (array_key_exists($name, $data)) {
|
||
if (!is_scalar($data[$name]) && $data[$name] !== null) throw new AdminException('驱动配置格式错误');
|
||
$field['value'] = $data[$name] === null ? '' : $data[$name];
|
||
}
|
||
if (!empty($field['required']) && trim((string)($field['value'] ?? '')) === '') {
|
||
throw new AdminException('请填写必填驱动配置');
|
||
}
|
||
}
|
||
unset($field);
|
||
return $fields;
|
||
}
|
||
|
||
/**
|
||
* 使用核心上传管理器创建指定空间客户端,避免 app 直接实例化驱动
|
||
* @param string $driverKey
|
||
* @param array|\ArrayAccess $storage
|
||
* @return Upload
|
||
*/
|
||
protected function storageUpload(string $driverKey, $storage = []): Upload
|
||
{
|
||
$config = [];
|
||
foreach ($this->prepareConfigFields($driverKey, []) as $field) $config[$field['field']] = $field['value'] ?? '';
|
||
foreach ($config as $name => $value) {
|
||
if (str_starts_with($name, $driverKey . '_')) {
|
||
$shortName = substr($name, strlen($driverKey) + 1);
|
||
if (!array_key_exists($shortName, $config)) $config[$shortName] = $value;
|
||
}
|
||
}
|
||
$config['storageName'] = $storage['name'] ?? '';
|
||
$config['storageRegion'] = $storage['region'] ?? ($config['storageRegion'] ?? '');
|
||
$config['uploadUrl'] = $storage['domain'] ?? '';
|
||
$config['cdn'] = $storage['cdn'] ?? '';
|
||
return new Upload($driverKey, $config);
|
||
}
|
||
|
||
/**
|
||
* 根据 driver_key 获取驱动配置
|
||
*
|
||
* 配置字段的值保存在 eb_driver 表的 config_fields JSON 字段中
|
||
* (与 saveConfigByDriverKey 写入逻辑对应)
|
||
*
|
||
* @param string $driverKey
|
||
* @return array
|
||
*/
|
||
public function getStorageConfigByDriverKey(string $driverKey)
|
||
{
|
||
$config = [];
|
||
$configFields = $this->driverServices->getConfigFields($driverKey);
|
||
|
||
if (empty($configFields)) {
|
||
return $config;
|
||
}
|
||
|
||
foreach ($configFields as $field) {
|
||
$fieldName = $field['field'];
|
||
$config[$fieldName] = $field['value'] ?? '';
|
||
}
|
||
|
||
return $config;
|
||
}
|
||
|
||
/**
|
||
* 获取存储空间配置
|
||
* @param string $driverKey
|
||
* @return array
|
||
*/
|
||
public function getConfigByDriverKey(string $driverKey)
|
||
{
|
||
$res = ['name' => '', 'region' => '', 'domain' => '', 'cdn' => ''];
|
||
$config = $this->dao->get(['driver_key' => $driverKey, 'status' => 1, 'is_delete' => 0], ['name', 'region', 'domain', 'cdn']);
|
||
if ($config) return ['name' => $config->name, 'region' => $config->region, 'domain' => $config->domain, 'cdn' => $config->cdn];
|
||
return $res;
|
||
}
|
||
|
||
/**
|
||
* 根据 type 获取配置表单
|
||
* @param int $type
|
||
* @return array
|
||
* @throws \FormBuilder\Exception\FormBuilderException
|
||
*/
|
||
public function getFormStorageConfig($type)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($type);
|
||
return $this->getFormStorageConfigByDriverKey($driverKey);
|
||
}
|
||
|
||
/**
|
||
* 根据 type 获取添加存储空间表单
|
||
* @param int $type
|
||
* @return array
|
||
* @throws \FormBuilder\Exception\FormBuilderException
|
||
*/
|
||
public function getFormStorage($type)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($type);
|
||
return $this->getFormStorageByDriverKey($driverKey);
|
||
}
|
||
|
||
/**
|
||
* 获取配置表单
|
||
* @param string $driverKey
|
||
* @return array
|
||
* @throws \FormBuilder\Exception\FormBuilderException
|
||
*/
|
||
public function getFormStorageConfigByDriverKey(string $driverKey)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($driverKey);
|
||
$configFields = $this->driverServices->getPublicConfigFields($driverKey);
|
||
|
||
$rule = [
|
||
FormBuilder::hidden('driver_key', $driverKey),
|
||
];
|
||
|
||
foreach ($configFields as $field) {
|
||
$fieldName = $field['field'];
|
||
$fieldLabel = $field['label'] ?? $fieldName;
|
||
$fieldType = $field['type'] ?? 'text';
|
||
$required = $field['required'] ?? false;
|
||
$value = $field['value'] ?? '';
|
||
|
||
switch ($fieldType) {
|
||
case 'password':
|
||
$input = FormBuilder::input($fieldName, $fieldLabel, $value)->type('password');
|
||
break;
|
||
case 'select':
|
||
$options = $field['options'] ?? [];
|
||
$input = FormBuilder::select($fieldName, $fieldLabel, $value)->options($options);
|
||
break;
|
||
default:
|
||
$input = FormBuilder::input($fieldName, $fieldLabel, $value);
|
||
break;
|
||
}
|
||
if ($required) $input->required();
|
||
$rule[] = $input;
|
||
}
|
||
|
||
return create_form('配置信息', $rule, '/system/config/storage/config');
|
||
}
|
||
|
||
/**
|
||
* 根据 type 保存配置
|
||
* @param int|string $type
|
||
* @param array $data
|
||
* @return bool
|
||
*/
|
||
public function saveConfig($type, array $data)
|
||
{
|
||
return $this->saveConfigByDriverKey($this->resolveDriverKey($type), $data);
|
||
}
|
||
|
||
/**
|
||
* 保存配置
|
||
*
|
||
* 将配置字段的值写入 eb_driver 表的 config_fields JSON 字段中,
|
||
* 与 getStorageConfigByDriverKey 的读取逻辑对应
|
||
*
|
||
* @param string $driverKey
|
||
* @param array $data
|
||
* @return bool
|
||
*/
|
||
public function saveConfigByDriverKey(string $driverKey, array $data)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($driverKey);
|
||
if ($this->driverServices->saveDriverConfig($driverKey, $data) === false) throw new \Exception(getLang('保存上传驱动配置失败'));
|
||
CacheService::delete('storage_list');
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置空间状态,云端 CORS 成功后才切换同驱动空间
|
||
* @param int $id
|
||
* @param int $status
|
||
* @return bool
|
||
*/
|
||
public function setStorageStatus(int $id, int $status = 1): bool
|
||
{
|
||
if ($id <= 0 || !in_array($status, [0, 1], true)) throw new AdminException('参数错误');
|
||
$info = $this->dao->get(['id' => $id, 'is_delete' => 0]);
|
||
if (!$info) throw new AdminException('云存储不存在');
|
||
$driverKey = $this->resolveDriverKey($info['driver_key']);
|
||
if ($status === 0) {
|
||
if ((int)$info['status'] === 1 && in_array($driverKey, array_column($this->driverServices->getEnabledDrivers(), 'driver_key'), true)) {
|
||
throw new AdminException('请先切换本地存储,再停用默认云存储空间');
|
||
}
|
||
} else {
|
||
$this->validateStorageUrl($info['domain'], 100, false);
|
||
$upload = $this->storageUpload($driverKey, $info);
|
||
if ($upload->setBucketCors($info['name'], $info['region']) === false) throw new \Exception(getLang('设置云空间跨域规则失败'));
|
||
}
|
||
$this->transaction(function () use ($info, $driverKey, $status) {
|
||
if ($status === 1 && $this->dao->disableByDriverKey($driverKey) === false) throw new \Exception(getLang('切换云空间失败'));
|
||
$info->status = $status;
|
||
$info->update_time = time();
|
||
if ($info->save() === false) throw new \Exception(getLang('保存云空间状态失败'));
|
||
});
|
||
CacheService::delete('storage_list');
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 切换默认上传驱动,云存储必须先选定可用空间
|
||
* @param int|string $typeOrKey
|
||
* @return bool
|
||
*/
|
||
public function switchDriver($typeOrKey): bool
|
||
{
|
||
$driverKey = $this->resolveDriverKey($typeOrKey);
|
||
if ($driverKey !== 'local' && !$this->dao->count(['driver_key' => $driverKey, 'status' => 1, 'is_delete' => 0])) {
|
||
throw new AdminException('未有正在使用的存储空间');
|
||
}
|
||
if ($this->driverServices->updateStatus($driverKey, 1) === false) throw new \Exception(getLang('切换上传驱动失败'));
|
||
CacheService::delete('storage_list');
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取列表
|
||
* @param array $where
|
||
* @return array
|
||
*/
|
||
public function getList(array $where)
|
||
{
|
||
[$page, $limit, $defaultLimit] = $this->getPageValue();
|
||
$page = max(1, $page);
|
||
$limit = $limit > 0 ? $limit : max(1, $defaultLimit);
|
||
|
||
// 如果传入的是 type,转换为 driver_key
|
||
if (isset($where['type'])) {
|
||
$key = $this->resolveDriverKey($where['type']);
|
||
if (!empty($where['driver_key']) && $where['driver_key'] !== $key) throw new AdminException('上传驱动参数不一致');
|
||
$where['driver_key'] = $key;
|
||
unset($where['type']);
|
||
}
|
||
|
||
$driverKey = $where['driver_key'] ?? '';
|
||
if ($driverKey !== '') $where['driver_key'] = $this->resolveDriverKey($driverKey);
|
||
|
||
$list = $this->dao->getList($where, ['id', 'type', 'driver_key', 'name', 'region', 'domain', 'cdn', 'cname', 'acl', 'is_ssl', 'status', 'add_time', 'update_time'], $page, $limit, 'add_time');
|
||
foreach ($list as &$item) {
|
||
// 优先使用数据库中实际的 cname 值,为空时从 domain 推导
|
||
if (empty($item['cname'])) {
|
||
$item['cname'] = parse_url($item['domain'], PHP_URL_HOST) ?: '';
|
||
}
|
||
$item['_region'] = $this->getRegionName($item['region'], $driverKey ?: $item['driver_key']);
|
||
$item['_add_time'] = date('Y-m-d H:i:s', (int)$item['add_time']);
|
||
$item['_update_time'] = date('Y-m-d H:i:s', (int)$item['update_time']);
|
||
}
|
||
$count = $this->dao->count($where);
|
||
return compact('list', 'count');
|
||
}
|
||
|
||
/**
|
||
* 从驱动注册中心读取 region_map 并转换区域代码为中文名
|
||
* @param string $region
|
||
* @param string $driverKey
|
||
* @return string
|
||
*/
|
||
protected function getRegionName(string $region, string $driverKey): string
|
||
{
|
||
if (!$region) return '';
|
||
|
||
// 统一走驱动注册中心(plugin.json 的 region_map 经全量透传)
|
||
$info = DriverRegistry::get('upload', $driverKey);
|
||
$map = $info['region_map'] ?? [];
|
||
return $map[$region] ?? $region;
|
||
}
|
||
|
||
/**
|
||
* 获取添加存储空间表单
|
||
* @param string $driverKey
|
||
* @return array
|
||
* @throws \FormBuilder\Exception\FormBuilderException
|
||
*/
|
||
public function getFormStorageByDriverKey(string $driverKey)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($driverKey);
|
||
$bucketFields = $this->driverServices->getBucketFields($driverKey);
|
||
|
||
$ruleConfig = [];
|
||
|
||
// 根据 config_fields 动态生成配置字段
|
||
$configFields = $this->driverServices->getPublicConfigFields($driverKey);
|
||
foreach ($configFields as $field) {
|
||
$fieldName = $field['field'];
|
||
$fieldLabel = $field['label'] ?? $fieldName;
|
||
$required = $field['required'] ?? false;
|
||
$value = $field['value'] ?? '';
|
||
|
||
if ($value === '') {
|
||
$input = FormBuilder::input($fieldName, $fieldLabel, $value);
|
||
if (($field['type'] ?? '') === 'password') $input->type('password');
|
||
if ($required) $input->required();
|
||
$ruleConfig[] = $input;
|
||
}
|
||
}
|
||
|
||
// 根据 bucket_fields 动态生成存储空间字段
|
||
$rule = [];
|
||
foreach ($bucketFields as $field) {
|
||
$fieldName = $field['field'];
|
||
$fieldLabel = $field['label'] ?? $fieldName;
|
||
$fieldType = $field['type'] ?? 'text';
|
||
$required = $field['required'] ?? false;
|
||
$default = $field['value'] ?? '';
|
||
|
||
switch ($fieldType) {
|
||
case 'select':
|
||
$options = $field['options'] ?? [];
|
||
$input = FormBuilder::select($fieldName, $fieldLabel, $default)->options($options);
|
||
break;
|
||
case 'radio':
|
||
$options = $field['options'] ?? [];
|
||
$input = FormBuilder::radio($fieldName, $fieldLabel, $default)->options($options);
|
||
break;
|
||
default:
|
||
$input = FormBuilder::input($fieldName, $fieldLabel, $default);
|
||
break;
|
||
}
|
||
if ($required) $input->required();
|
||
$rule[] = $input;
|
||
}
|
||
|
||
$rule = array_merge($ruleConfig, $rule);
|
||
return create_form('添加云空间', $rule, '/system/config/storage/' . $driverKey);
|
||
}
|
||
|
||
/**
|
||
* 删除空间
|
||
* @param int $id
|
||
* @return bool
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function deleteStorage(int $id)
|
||
{
|
||
if ($id <= 0) throw new AdminException('参数错误');
|
||
$storageInfo = $this->dao->get(['is_delete' => 0, 'id' => $id]);
|
||
if (!$storageInfo) {
|
||
throw new AdminException('删除的云存储不存在');
|
||
}
|
||
if ($storageInfo->status) {
|
||
throw new AdminException('云存储正在使用中,需要启动其他空间才能删除');
|
||
}
|
||
|
||
$storageInfo->is_delete = 1;
|
||
$storageInfo->update_time = time();
|
||
if ($storageInfo->save() === false) throw new \Exception(getLang('删除云存储配置失败'));
|
||
CacheService::delete('storage_list');
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 同步云存储空间列表
|
||
*
|
||
* 根据 type 或 driver_key 获取已保存的配置,
|
||
* 实例化对应的上传驱动,调用 listbuckets() 拉取远程存储空间列表
|
||
*
|
||
* @param string|int $type 驱动类型(数字)或 driver_key(字符串)
|
||
* @return array
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function synchronization($type)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($type);
|
||
if ($driverKey === 'local') return [];
|
||
$storage = $this->dao->get(['driver_key' => $driverKey, 'status' => 1, 'is_delete' => 0]);
|
||
$driver = $this->storageUpload($driverKey, $storage ?: []);
|
||
$buckets = $driver->listbuckets($storage['region'] ?? '');
|
||
if ($buckets === false) throw new \Exception(getLang('同步云空间失败'));
|
||
if (!is_array($buckets)) throw new \Exception(getLang('云空间列表格式错误'));
|
||
$existingNames = $this->dao->getColumn(['driver_key' => $driverKey, 'is_delete' => 0], 'name');
|
||
$type = $this->driverServices->getTypeByDriverKey($driverKey);
|
||
$driverInfo = $this->driverServices->getByDriverKey($driverKey);
|
||
$domainTemplate = $driverInfo['domain_template'] ?? '';
|
||
$appid = '';
|
||
if (str_contains($domainTemplate, '{appid}')) {
|
||
$config = $this->getStorageConfigByDriverKey($driverKey);
|
||
$appid = (string)($config['appid'] ?? $config[$driverKey . '_appid'] ?? '');
|
||
}
|
||
$records = [];
|
||
foreach ($buckets as $bucket) {
|
||
if (is_array($bucket) || is_object($bucket)) {
|
||
$bucketArr = (array)$bucket;
|
||
$name = $bucketArr['name'] ?? $bucketArr['Name'] ?? $bucketArr['id'] ?? '';
|
||
$region = $bucketArr['region'] ?? $bucketArr['Region'] ?? $bucketArr['location'] ?? $bucketArr['Location'] ?? '';
|
||
} elseif (is_string($bucket)) {
|
||
$name = $bucket;
|
||
$region = '';
|
||
} else {
|
||
continue;
|
||
}
|
||
|
||
if (!is_string($name) || !is_string($region) || $name === '' || in_array($name, $existingNames, true)) continue;
|
||
if (!preg_match('/^[a-z0-9][a-z0-9_.-]{0,99}$/i', $name)) throw new AdminException('云空间名称格式错误');
|
||
if (strlen($region) > 100 || ($region !== '' && !preg_match('/^[a-z0-9-]+$/i', $region))) throw new AdminException('云空间区域格式错误');
|
||
$domain = $this->formatStorageDomain($domainTemplate, $name, $region, $appid);
|
||
$records[] = [
|
||
'name' => $name,
|
||
'region' => $region,
|
||
'domain' => $domain,
|
||
'type' => $type,
|
||
'driver_key' => $driverKey,
|
||
'is_delete' => 0,
|
||
'status' => 0,
|
||
'is_ssl' => str_starts_with($domain, 'https://') ? 1 : 0,
|
||
'add_time' => time(),
|
||
'update_time' => time(),
|
||
];
|
||
$existingNames[] = $name;
|
||
}
|
||
if ($records) {
|
||
// 远程请求已完成,事务中只执行批量元数据写入。
|
||
$this->transaction(function () use ($records) {
|
||
if ($this->dao->saveAll($records) === false) throw new \Exception(getLang('保存同步云空间失败'));
|
||
});
|
||
CacheService::delete('storage_list');
|
||
}
|
||
return $buckets;
|
||
}
|
||
|
||
/**
|
||
* 根据 type 保存云存储
|
||
* @param int|string $type
|
||
* @param array $data
|
||
* @return mixed
|
||
*/
|
||
public function saveStorage($type, array $data)
|
||
{
|
||
return $this->saveStorageByDriverKey($this->resolveDriverKey($type), $data);
|
||
}
|
||
|
||
/**
|
||
* 保存云存储
|
||
* @param string $driverKey
|
||
* @param array $data
|
||
* @return mixed
|
||
*/
|
||
public function saveStorageByDriverKey(string $driverKey, array $data)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($driverKey);
|
||
$metadata = ['name' => '', 'region' => '', 'acl' => 'public-read', 'domain' => '', 'cdn' => '', 'cname' => ''];
|
||
foreach ($this->driverServices->getBucketFields($driverKey) as $field) {
|
||
$name = $field['field'];
|
||
if (array_key_exists($name, $metadata) && isset($field['value'])) $metadata[$name] = $field['value'];
|
||
}
|
||
$metadata = array_replace($metadata, array_intersect_key($data, $metadata));
|
||
if (!is_string($metadata['name']) || !preg_match('/^[a-z0-9][a-z0-9_.-]{0,99}$/i', $metadata['name'])) throw new AdminException('云空间名称格式错误');
|
||
if (!is_string($metadata['region']) || strlen($metadata['region']) > 100 || ($metadata['region'] !== '' && !preg_match('/^[a-z0-9-]+$/i', $metadata['region']))) throw new AdminException('云空间区域格式错误');
|
||
if (!in_array($metadata['acl'], ['private', 'public-read', 'public-read-write'], true)) throw new AdminException('云空间权限格式错误');
|
||
foreach ($this->driverServices->getBucketFields($driverKey) as $field) {
|
||
$value = $metadata[$field['field']] ?? ($data[$field['field']] ?? '');
|
||
if (!empty($field['required']) && (!is_scalar($value) || trim((string)$value) === '')) throw new AdminException('请填写必填云空间信息');
|
||
$options = array_column($field['options'] ?? [], 'value');
|
||
if ($options && $value !== '' && !in_array($value, $options, true)) throw new AdminException('云空间选项不合法');
|
||
}
|
||
if ($this->dao->count(['name' => $metadata['name'], 'driver_key' => $driverKey, 'is_delete' => 0])) throw new AdminException('云空间名称不能重复');
|
||
if ($metadata['domain'] === '') $metadata['domain'] = $this->getDomain($driverKey, $metadata['name'], $metadata['region']);
|
||
$metadata['domain'] = $this->validateStorageUrl($metadata['domain'], 100);
|
||
$metadata['cdn'] = $this->validateStorageUrl($metadata['cdn']);
|
||
$metadata['cname'] = $this->validateStorageCname($metadata['cname']);
|
||
$metadata += ['driver_key' => $driverKey, 'type' => $this->driverServices->getTypeByDriverKey($driverKey), 'status' => 0, 'is_delete' => 0,
|
||
'is_ssl' => str_starts_with($metadata['domain'], 'https://') ? 1 : 0, 'add_time' => time(), 'update_time' => time()];
|
||
$configValues = array_intersect_key($data, array_column($this->driverServices->getConfigFields($driverKey), null, 'field'));
|
||
// 此入口只登记已有云空间;驱动配置和空间记录必须共同提交,不创建真实存储桶。
|
||
$result = $this->transaction(function () use ($driverKey, $configValues, $metadata) {
|
||
$this->saveConfigByDriverKey($driverKey, $configValues);
|
||
$result = $this->dao->save($metadata);
|
||
if (!$result) throw new \Exception(getLang('保存云空间失败'));
|
||
return $result;
|
||
});
|
||
CacheService::delete('storage_list');
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 获取云存储配置
|
||
* @param int $type
|
||
* @return array|string[]
|
||
*/
|
||
public function getConfig($type)
|
||
{
|
||
$driverKey = $this->resolveDriverKey($type);
|
||
return $this->getConfigByDriverKey($driverKey);
|
||
}
|
||
|
||
/**
|
||
* 获取修改域名表单
|
||
* @param int $id
|
||
* @return array
|
||
* @throws \FormBuilder\Exception\FormBuilderException
|
||
*/
|
||
public function getUpdateDomainForm(int $id)
|
||
{
|
||
$storage = $this->dao->get(['id' => $id, 'is_delete' => 0], ['domain', 'cdn', 'cname']);
|
||
if (!$storage) throw new AdminException('云存储不存在');
|
||
$rule = [
|
||
FormBuilder::input('domain', '空间域名', $storage['domain']),
|
||
FormBuilder::input('cdn', 'cdn域名', $storage['cdn']),
|
||
FormBuilder::input('cname', 'CNAME记录值', $storage['cname'])->placeholder('请先在云存储平台绑定域名后,将提供的CNAME目标地址粘贴至此'),
|
||
];
|
||
return create_form('修改空间域名', $rule, '/system/config/storage/domain/' . $id);
|
||
}
|
||
|
||
/**
|
||
* 修改域名并绑定
|
||
* @param int $id
|
||
* @param string $domain
|
||
* @param array $data
|
||
* @return bool
|
||
* @throws \think\db\exception\DataNotFoundException
|
||
* @throws \think\db\exception\DbException
|
||
* @throws \think\db\exception\ModelNotFoundException
|
||
*/
|
||
public function updateDomain(int $id, string $domain, array $data = [])
|
||
{
|
||
$info = $this->dao->get(['id' => $id, 'is_delete' => 0]);
|
||
if (!$info) {
|
||
throw new AdminException('数据不存在');
|
||
}
|
||
|
||
$domain = $this->validateStorageUrl($domain, 100, false);
|
||
$changes = ['domain' => $domain, 'is_ssl' => str_starts_with($domain, 'https://') ? 1 : 0, 'update_time' => time()];
|
||
if (array_key_exists('cdn', $data)) $changes['cdn'] = $this->validateStorageUrl($data['cdn']);
|
||
if (array_key_exists('cname', $data)) $changes['cname'] = $this->validateStorageCname($data['cname']);
|
||
if ($info->save($changes) === false) throw new \Exception(getLang('保存云空间域名失败'));
|
||
CacheService::delete('storage_list');
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 获取域名
|
||
* 从数据库读取域名模板,不硬编码
|
||
* @param string $driverKey
|
||
* @param string $name
|
||
* @param string $region
|
||
* @param string $appid
|
||
* @return string
|
||
*/
|
||
public function getDomain(string $driverKey, string $name, string $region, string $appid = '')
|
||
{
|
||
$driver = $this->driverServices->getByDriverKey($driverKey);
|
||
$domainTemplate = $driver['domain_template'] ?? '';
|
||
if ($appid === '' && str_contains($domainTemplate, '{appid}')) {
|
||
$config = $this->getStorageConfigByDriverKey($driverKey);
|
||
$appid = (string)($config['appid'] ?? $config[$driverKey . '_appid'] ?? '');
|
||
}
|
||
return $this->formatStorageDomain($domainTemplate, $name, $region, $appid);
|
||
}
|
||
|
||
/**
|
||
* 纯模板转换,批量同步无需逐个空间查询驱动配置
|
||
* @param string $template
|
||
* @param string $name
|
||
* @param string $region
|
||
* @param string $appid
|
||
* @return string
|
||
*/
|
||
protected function formatStorageDomain(string $template, string $name, string $region, string $appid): string
|
||
{
|
||
if ($template === '') return '';
|
||
$values = ['{name}' => $name, '{region}' => $region, '{appid}' => $appid];
|
||
foreach ($values as $placeholder => $value) if ($value === '' && str_contains($template, $placeholder)) return '';
|
||
return $this->validateStorageUrl(strtr($template, $values), 100, false);
|
||
}
|
||
}
|