diff --git a/.example.env b/.example.env index 898b7ea9..aba84550 100644 --- a/.example.env +++ b/.example.env @@ -47,6 +47,13 @@ REFRESH_TOKEN_LIFETIME = 1209600; # 刷新令牌有效期 RESOURCE_IMAGES_DOMAIN = http://local.orico.com; # 图片资源服务器地址 RESOURCE_VIDEOS_DOMAIN = http://local.orico.com; # 视频资源服务器地址 +# 七牛云存储配置 +[QINUI] +BUCKET = orico +BASE_URL = http://local.orico.com +ACCESS_KEY = 1234567890 +SECRET_KEY = 1234567890 + # 前台视图模板规则配置 [INDEX_VIEW_TPL] # 视图目录 diff --git a/app/admin/controller/v1/Upload.php b/app/admin/controller/v1/Upload.php index 231337f5..297ba051 100644 --- a/app/admin/controller/v1/Upload.php +++ b/app/admin/controller/v1/Upload.php @@ -9,12 +9,32 @@ use app\admin\model\v1\SysAttachmentUploadRecordModel; use Intervention\Image\ImageManager; use Intervention\Image\Typography\FontFactory; use think\facade\Filesystem; +use filesystem\Qiniu; /** * 文件上传控制器 */ class Upload { + public function test() + { + set_time_limit(-1); + + // $file = request()->file('image'); + // if (is_null($file)) { + // return error('请确定上传对象或字段是否正确'); + // } + + // $name_rule = fn() => true ? $this->filenameGenerator($file) : null; + + // $filename = Filesystem::disk('video_qiniu')->putFile('unknown', $file, $name_rule()); + + $qiniu_uploader = new Qiniu(); + $filename = $qiniu_uploader->uploadFile('image'); + + dump($filename); + } + // 上传图片 public function image() { diff --git a/app/admin/route/v1.php b/app/admin/route/v1.php index e2aa35f2..f4fc4a78 100644 --- a/app/admin/route/v1.php +++ b/app/admin/route/v1.php @@ -57,6 +57,7 @@ Route::group('v1', function () { Route::group('images', function () { // 图片上传 Route::post('/:module/upload', 'Upload/image'); + Route::post('/upload/test', 'Upload/test'); }); // 视频管理 diff --git a/composer.json b/composer.json index f1bc0ce0..7642adee 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "php": ">=8.0.0", "topthink/framework": "^8.0", "topthink/think-orm": "v3.0.34", - "topthink/think-filesystem": "^2.0", + "topthink/think-filesystem": "^3.0", "topthink/think-multi-app": "^1.1", "topthink/think-migration": "^3.1", "topthink/think-view": "^2.0", @@ -35,7 +35,8 @@ "phpoffice/phpspreadsheet": "^3.8", "friendsofsymfony/oauth2-php": "^1.3", "mobiledetect/mobiledetectlib": "4.8.09", - "qiniu/php-sdk": "^7.14" + "qiniu/php-sdk": "^7.14", + "overtrue/flysystem-qiniu": "^3.2" }, "require-dev": { "symfony/var-dumper": ">=4.2", diff --git a/config/filesystem.php b/config/filesystem.php index 67e5f44a..630758f3 100644 --- a/config/filesystem.php +++ b/config/filesystem.php @@ -39,6 +39,20 @@ return [ // 可见性 'visibility' => 'public', ], + 'video_qiniu' => [ + // 磁盘类型 + 'type' => \filesystem\driver\Qiniu::class, + // bucker 名称 + 'bucket' => 'orico-official-website', + // 访问密钥 + 'access_key' => 'dOsTum4a5qvhPTBbZRPX0pIOU7PZWRX7htKjztms', + // 密钥 + 'secret_key' => 'KFxsGbnErkALFfeGdMa8QWTdodJbamMX0iznLe-q', + // 外部URL + 'base_url' => '//szw73dlk3.hn-bkt.clouddn.com', + // 路径 + 'path' => '/storage/videos', + ] // 更多的磁盘配置信息 ], ]; diff --git a/extend/filesystem/Qiniu.php b/extend/filesystem/Qiniu.php new file mode 100644 index 00000000..7e85e5e4 --- /dev/null +++ b/extend/filesystem/Qiniu.php @@ -0,0 +1,165 @@ + 1024 * 1024 * 50, // 默认最大上传5M + 'fileExt' => 'jpeg,jpg,png,mp4', // 默认上传文件后缀 + 'fileMime' => 'image/jpeg,image/png,image/gif,video/mp4' // 默认上传文件mime + ]; + + private $dir = true; + private $path_prefix = ''; + private $file_name_prefix = 'orico'; + private $keep_original_name = false; + + private $base_url = '//szw73dlk3.hn-bkt.clouddn.com'; + + public function __construct($conf = []) + { + if (!empty($conf['base_url'])) { + $this->base_url = $conf['base_url']; + } + if (!empty($conf['bucket'])) { + $this->bucket = $conf['bucket']; + } + if (!empty($conf['access_key'])) { + $this->access_key = $conf['access_key']; + } + if (!empty($conf['secret_key'])) { + $this->secret_key = $conf['secret_key']; + } + if (!empty($conf['path_prefix'])) { + $this->path_prefix = trim($conf['path_prefix'], '/'); + } + if (!empty($conf['file_name_prefix'])) { + $this->file_name_prefix = $conf['file_name_prefix']; + } + if (!empty($conf['keep_original_name'])) { + $this->keep_original_name = $conf['keep_original_name']; + } + } + + /** + * 生成随机字符串 + */ + private function random($length, $type = "string", $convert = "0") + { + $conf = [ + 'number' => '0123456789', + 'string' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'all' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=' + ]; + $string = $conf[$type]; + if (!$string) { + $string = $conf['string']; + } + $strlen = strlen($string) - 1; + $char = ''; + for ($i = 0; $i < $length; $i++) { + $char .= $string[mt_rand(0, $strlen)]; + } + if ($convert > 0) { + $res = strtoupper($char); + } elseif ($convert == 0) { + $res = $char; + } elseif ($convert < 0) { + $res = strtolower($char); + } + return $res; + } + + /** + * 组装文件名 + */ + private function buildFileName() + { + return $this->file_name_prefix . time() . substr(time(), -5) . substr(microtime(), 2, 3) . $this->random(8); + } + + /** + * 上传验证规则 + */ + public function validate($rule) + { + $this->rule = $rule; + } + + /** + * 上传文件到七牛云 + */ + public function uploadFile($name) + { + // 构建鉴权对象 + $auth = new Auth($this->access_key, $this->secret_key); + + // 生成上传 Token + $token = $auth->uploadToken($this->bucket); + + // 初始化 UploadManager 对象并进行文件的上传。 + $upload_mgr = new UploadManager(); + + $file = request()->file($name); + + $aspect_ratio = []; + if (!empty($this->rule['aspectRatio'])) { + $aspect_ratio = $this->rule['aspectRatio']; + unset($this->rule['aspectRatio']); + } + $validate = validate([$name => $this->rule]); + if (!$validate->check([$name => $file])) { + throw new \Exception($validate->getError()); + } + + $file_name = $file->getOriginalName(); // 文件原名 + if (!$this->keep_original_name) { + $file_name = $this->buildFileName() . '.' . $file->extension(); + if (!$this->dir && !empty($this->path_prefix)) { + $file_name = $this->path_prefix . '/' . $file_name; + } + } + if ($this->dir) { + $file_name = date('Ymd') . '/' . $file_name; + if (!empty($this->path_prefix)) { + $file_name = $this->path_prefix . '/' . $file_name; + } + } + $file_path = $file->getPathname(); // 临时路径 + if (!empty($aspect_ratio)) { // 验证图片宽高 + list($width, $height, $type, $attr) = getimagesize($file); + if ($width != $aspect_ratio['width'] || $height != $aspect_ratio['height']) { + throw new \Exception('图片宽高不符合'); + } + } + $file_type = $file->getOriginalMime(); + list($ret, $err) = $upload_mgr->putFile($token, $file_name, $file_path, null, $file_type, false); + + if ($err !== null) { + throw new \Exception($err); + } else { + return ['hash' => $ret['hash'], 'filename' => $ret['key'], 'remote_url' => $this->base_url . $ret['key']]; + } + } + + /** + * 上传文件到七牛云 + */ + public function deleteFile($name) + { + // 构建鉴权对象 + $auth = new Auth($this->access_key, $this->secret_key); + // 初始化 BucketManager 对象并进行文件的删除。 + $bucket_mgr = new BucketManager($auth); + $ret = $bucket_mgr->delete($this->bucket, $name); + return $ret; + } +} diff --git a/extend/filesystem/adapter/QiniuAdapter.php b/extend/filesystem/adapter/QiniuAdapter.php new file mode 100644 index 00000000..193536cb --- /dev/null +++ b/extend/filesystem/adapter/QiniuAdapter.php @@ -0,0 +1,286 @@ +authMgr ?? new Auth($this->access_key, $this->secret_key); + } + + private function getUploadMgr(): UploadManager + { + return $this->uploadMgr ?? new UploadManager(); + } + + private function getBucketMgr(): BucketManager + { + return $this->bucketMgr ?? new BucketManager($this->authMgr); + } + + private function applyPathPrefix(string $path): string + { + return $this->path . $path; + } + + private static function parseUrl($url): array + { + $result = []; + + // Build arrays of values we need to decode before parsing + $entities = [ + '%21', + '%2A', + '%27', + '%28', + '%29', + '%3B', + '%3A', + '%40', + '%26', + '%3D', + '%24', + '%2C', + '%2F', + '%3F', + '%23', + '%5B', + '%5D', + '%5C' + ]; + $replacements = ['!', '*', "'", '(', ')', ';', ':', '@', '&', '=', '$', ',', '/', '?', '#', '[', ']', '/']; + + // Create encoded URL with special URL characters decoded so it can be parsed + // All other characters will be encoded + $encodedURL = str_replace($entities, $replacements, urlencode($url)); + + // Parse the encoded URL + $encodedParts = parse_url($encodedURL); + + // Now, decode each value of the resulting array + if ($encodedParts) { + foreach ($encodedParts as $key => $value) { + $result[$key] = urldecode(str_replace($replacements, $entities, $value)); + } + } + + return $result; + } + + private function normalizeHost($domain): string + { + if (0 !== stripos($domain, 'https://') && 0 !== stripos($domain, 'http://')) { + $domain = "http://{$domain}"; + } + + return rtrim($domain, '/') . '/'; + } + + private function getUrl(string $path): string + { + $segments = $this->parseUrl($path); + $query = empty($segments['query']) ? '' : '?' . $segments['query']; + + return $this->normalizeHost($this->base_url) . ltrim(implode('/', array_map('rawurlencode', explode('/', $segments['path']))), '/') . $query; + } + + private function privateDownloadUrl(string $path): string + { + return $this->getAuthMgr()->privateDownloadUrl($this->bucket, $this->applyPathPrefix($path)); + } + + private function getMetadata($path): FileAttributes|array + { + $result = $this->getBucketMgr()->stat($this->bucket, $path); + $result[0]['key'] = $path; + + return $this->normalizeFileInfo($result[0]); + } + + private function normalizeFileInfo(array $stats): FileAttributes + { + return new FileAttributes( + $stats['key'], + $stats['fsize'] ?? null, + null, + isset($stats['putTime']) ? floor($stats['putTime'] / 10000000) : null, + $stats['mimeType'] ?? null + ); + } + + public function fileExists(string $path): bool + { + [, $error] = $this->getBucketMgr()->stat($this->bucket, $this->applyPathPrefix($path)); + return is_null($error); + } + + public function directoryExists(string $path): bool + { + // 待实现 + return $this->fileExists($path); + } + + public function write(string $path, string $contents, Config $config): void + { + $mime = $config->get('mime', 'application/octet-stream'); + + /** + * @var Error|null $error + */ + [, $error] = $this->getUploadMgr()->put( + $this->getAuthMgr()->uploadToken($this->bucket), + $this->applyPathPrefix($path), + $contents, + null, + $mime, + $path + ); + + if ($error) { + throw UnableToWriteFile::atLocation($path, $error->message()); + } + } + + public function writeStream(string $path, $contents, Config $config): void + { + $data = ''; + + while (!feof($contents)) { + $data .= fread($contents, 1024); + } + + $this->write($path, $data, $config); + } + + public function read(string $path): string + { + try { + $result = file_get_contents($this->privateDownloadUrl($path)); + } catch (\Exception $th) { + throw UnableToReadFile::fromLocation($path); + } + + if (false === $result) { + throw UnableToReadFile::fromLocation($path); + } + + return $result; + } + + public function readStream(string $path) + { + if (ini_get('allow_url_fopen')) { + if ($result = fopen($this->privateDownloadUrl($path), 'r')) { + return $result; + } + } + + throw UnableToReadFile::fromLocation($path); + } + + public function delete(string $path): void + { + [, $error] = $this->getBucketMgr()->delete($this->bucket, $this->applyPathPrefix($path)); + if (!is_null($error)) { + throw UnableToDeleteFile::atLocation($path); + } + } + + public function deleteDirectory(string $path): void + { + $this->delete($path); + } + + public function createDirectory(string $path, Config $config): void + { + // 待实现 + } + + public function setVisibility(string $path, string $visibility): void + { + throw UnableToSetVisibility::atLocation($path); + } + + public function visibility(string $path): FileAttributes + { + throw UnableToRetrieveMetadata::visibility($path); + } + + public function mimeType(string $path): FileAttributes + { + $meta = $this->getMetadata($path); + + if ($meta->mimeType() === null) { + throw UnableToRetrieveMetadata::mimeType($path); + } + + return $meta; + } + + public function lastModified(string $path): FileAttributes + { + $meta = $this->getMetadata($path); + + if ($meta->lastModified() === null) { + throw UnableToRetrieveMetadata::lastModified($path); + } + return $meta; + } + + public function fileSize(string $path): FileAttributes + { + $meta = $this->getMetadata($path); + + if ($meta->fileSize() === null) { + throw UnableToRetrieveMetadata::fileSize($path); + } + return $meta; + } + + public function listContents(string $path, bool $deep): iterable + { + $result = $this->getBucketMgr()->listFiles($this->bucket, $path); + + foreach ($result[0]['items'] ?? [] as $files) { + yield $this->normalizeFileInfo($files); + } + } + + public function move(string $source, string $destination, Config $config): void + { + [, $error] = $this->getBucketMgr()->rename($this->bucket, $source, $destination); + if (!is_null($error)) { + throw UnableToMoveFile::fromLocationTo($source, $destination); + } + } + + public function copy(string $source, string $destination, Config $config): void + { + [, $error] = $this->getBucketMgr()->copy($this->bucket, $source, $this->bucket, $destination); + if (!is_null($error)) { + throw UnableToCopyFile::fromLocationTo($source, $destination); + } + } +} \ No newline at end of file diff --git a/extend/filesystem/driver/Qiniu.php b/extend/filesystem/driver/Qiniu.php new file mode 100644 index 00000000..bdc5fe1d --- /dev/null +++ b/extend/filesystem/driver/Qiniu.php @@ -0,0 +1,19 @@ +config['access_key'], + $this->config['secret_key'], + $this->config['bucket'], + $this->config['base_url'], + ); + } + +} diff --git a/extend/uploader/Qiniu.php b/extend/uploader/Qiniu.php deleted file mode 100644 index 54551cb3..00000000 --- a/extend/uploader/Qiniu.php +++ /dev/null @@ -1,157 +0,0 @@ - 1024 * 1024 * 5, // 默认最大上传5M - 'fileExt' => 'jpeg,jpg,png', // 默认上传文件后缀 - 'fileMime' => 'image/jpeg,image/png,image/gif' // 默认上传文件mime - ]; - - private $dir = true; - private $originalName = false; - private $pathPrefix = ''; - private $fileNamePrefix = 'orico'; - - static public $domain = 'http://opdfile.f2b211.com/'; - - public function __construct($conf = []) - { - if (!empty($conf['bucket'])) { - $this->bucket = $conf['bucket']; - } - if (!empty($conf['accessKey'])) { - $this->accessKey = $conf['accessKey']; - } - if (!empty($conf['secretKey'])) { - $this->secretKey = $conf['secretKey']; - } - if (!empty($conf['pathPrefix'])) { - $this->pathPrefix = trim($conf['pathPrefix'], '/'); - } - } - - /** - * 生成随机字符串 - */ - private function random($length, $type = "string", $convert = "0") - { - $conf = [ - 'number' => '0123456789', - 'string' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', - 'all' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=' - ]; - $string = $conf[$type]; - if (!$string) { - $string = $conf['string']; - } - $strlen = strlen($string) - 1; - $char = ''; - for ($i = 0; $i < $length; $i++) { - $char .= $string[mt_rand(0, $strlen)]; - } - if ($convert > 0) { - $res = strtoupper($char); - } elseif ($convert == 0) { - $res = $char; - } elseif ($convert < 0) { - $res = strtolower($char); - } - return $res; - } - - /** - * 组装文件名 - */ - private function buildFileName() - { - return $this->fileNamePrefix . time() . substr(time(), -5) . substr(microtime(), 2, 3) . $this->random(8); - } - - /** - * 上传验证规则 - */ - public function validate($rule) - { - $this->rule = $rule; - } - - /** - * 上传文件到七牛云 - */ - public function uploadFile($name) - { - // 构建鉴权对象 - $auth = new Auth($this->accessKey, $this->secretKey); - - // 生成上传 Token - $token = $auth->uploadToken($this->bucket); - - // 初始化 UploadManager 对象并进行文件的上传。 - $uploadMgr = new UploadManager(); - - $file = request()->file($name); - - $aspectRatio = []; - if (!empty($this->rule['aspectRatio'])) { - $aspectRatio = $this->rule['aspectRatio']; - unset($this->rule['aspectRatio']); - } - $validate = validate([$name => $this->rule]); - if (!$validate->check([$name => $file])) { - throw new \Exception($validate->getError()); - } - - $fileName = $file->getOriginalName(); // 文件原名 - if (!$this->originalName) { - $fileName = $this->buildFileName() . '.' . $file->extension(); - if (!$this->dir && !empty($this->pathPrefix)) { - $fileName = $this->pathPrefix . '/' . $fileName; - } - } - if ($this->dir) { - $fileName = date('Y') . '/' . date('m') . '/' . date('d') . '/' . $fileName; - if (!empty($this->pathPrefix)) { - $fileName = $this->pathPrefix . '/' . $fileName; - } - } - $filePath = $file->getPathname(); // 临时路径 - if (!empty($aspectRatio)) { // 验证图片宽高 - list($width, $height, $type, $attr) = getimagesize($file); - if ($width != $aspectRatio['width'] || $height != $aspectRatio['height']) { - throw new \Exception('图片宽高不符合'); - } - } - $fileType = $file->getOriginalMime(); - list($ret, $err) = $uploadMgr->putFile($token, $fileName, $filePath, null, $fileType, false); - - if ($err !== null) { - throw new \Exception($err); - } else { - return ['hash' => $ret['hash'], 'filename' => $ret['key'], 'remote_url' => self::$domain . $ret['key']]; - } - } - - - /** - * 上传文件到七牛云 - */ - public function deleteFile($name) - { - // 构建鉴权对象 - $auth = new Auth($this->accessKey, $this->secretKey); - // 初始化 BucketManager 对象并进行文件的删除。 - $bucketManager = new BucketManager($auth); - $ret = $bucketManager->delete($this->bucket, $name); - return $ret; - } -}