diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8044fcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/.idea/ +**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ddcbad --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +##### 说明 + +本扩展为yangyifan/upload的分支,仅为解决在开发过这种遇到的问题: +* 原扩展没有release版本,导致开发时composer.json未设置"minimum-stability": "dev"时候,不能正常安装的问题,即便设置了 "minimum-stability": "dev",直接使用composer update时会使其他依赖包也强制更新到了dev-master版本,带来不稳定因素。 + + +##### 安装 + +* composer require "ikodota/upload" 。 +* 添加 UploadServiceProvider 到您项目 config/app.php 中的 providers 部分: Yangyifan\Upload\UploadServiceProvider。 +* 支持七牛,upyun,oss。 +* 完成按照官方 ``` Storage ``` 来扩展。所以不需要修改代码,只需要新增配置文件信息,就可以替换任何一种存储引擎。 + +###### 开始 + +* use Storage + +``` +use Storage; +``` + +* 示例代码 + +``` + + $image = "11/22/33/7125_yangxiansen.jpg"; + $image2 = "111.png"; + $image3 = "2.txt"; + + $drive = \Storage::drive('oss'); //选择oss上传引擎 + + dump($drive->getMetadata($image2)); //判断文件是否存在 + dump($drive->has($image2)); //判断文件是否存在 + dump($drive->listContents('')); //列出文件列表 + dump($drive->getSize($image2)); //获得图片大小 + dump($drive->getMimetype($image2)); //获得图片mime类型 + dump($drive->getTimestamp($image2)); //获得图片上传时间戳 + dump($drive->read($image3)); //获得文件信息 + dump($drive->readStream($image3)); //获得文件信息 + dump($drive->rename($image3, '4.txt/')); //重命名文件 + dump($drive->copy('4.txt/', '/txt/5.txt')); //复制文件 + dump($drive->delete('/txt/5.txt')); //删除文件 + dump ($drive->write("/txt/4.txt", $drive->read("/4.txt")) ); //上传文件 + dump($drive->write("/test2.txt", "111222")); //上传文件 + dump($drive->deleteDir('txt/')); //删除文件夹 + dump($drive->createDir('test3/')); //创建文件夹 + $handle = fopen('/tmp/email.png', 'r'); + dump ($drive->writeStream("/write/test3.png", $handle ) ); //上传文件(文件流方式) + dump ($drive->writeStream("/test6.png", $drive->readStream('/write/test3.png') ) ); //上传文件(文件流方式) + +``` + + +###### 配置信息 + +``` + +'qiniu' => [ + 'driver' => 'qiniu', + 'domain' => '',//你的七牛域名 + 'access_key' => '',//AccessKey + 'secret_key' => '',//SecretKey + 'bucket' => '',//Bucket名字 + 'transport' => 'http',//如果支持https,请填写https,如果不支持请填写http + ], + + 'upyun' => [ + 'driver' => 'upyun', + 'domain' => '',//你的upyun域名 + 'username' => '',//UserName + 'password' => '',//Password + 'bucket' => '',//Bucket名字 + 'timeout' => 130,//超时时间 + 'endpoint' => null,//线路 + 'transport' => 'http',//如果支持https,请填写https,如果不支持请填写http + ], + + 'oss' => [ + 'driver' => 'oss', + 'accessKeyId' => '', + 'accessKeySecret' => '', + 'endpoint' => '', + 'isCName' => false, + 'securityToken' => null, + 'bucket' => '', + 'timeout' => '5184000', + 'connectTimeout' => '10', + 'transport' => 'http',//如果支持https,请填写https,如果不支持请填写http + 'max_keys' => 1000,//max-keys用于限定此次返回object的最大数,如果不设定,默认为100,max-keys取值不能大于1000 + ], + ], + +``` + +###### 其他 + +* 如果需要支持其他的上传引擎,请联系我,如果我有空,我会去扩展,希望这个能帮助大家开发,谢谢,有问题pr我,或者邮件联系我,我的邮箱是:ikodota@gmail.com(原作者邮箱:yangyifanphp@gmail.com)。 +* 下一步计划将完成单元测试。 + +###### 协议 + +MIT \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c55c9a7 --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "ikodota/upload", + "description": "上传 SDK for Laravel", + "keywords": ["upload", "laravel", "sdk", "qiniu", "upyun", "oss"], + "require": { + "qiniu/php-sdk": "v7.0.*", + "aliyuncs/oss-sdk-php" : "*" + }, + "autoload": { + "psr-4": { + "Ikodota\\Upload\\": "src/" + } + }, + + "license": "MIT", + "minimum-stability" : "dev", + "authors": [ + { + "name": "yangyifan", + "email": "yangyifanphp@gmail.com" + }, + { + "name": "ikodota", + "email": "ikodota@gmail.com" + } + ] +} diff --git a/src/Functions/FileFunction.php b/src/Functions/FileFunction.php new file mode 100644 index 0000000..88ec8cf --- /dev/null +++ b/src/Functions/FileFunction.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- + +namespace Ikodota\Upload\Functions; + + +class FileFunction +{ + + /** + * 获得文件mime_type + * + * @param $file + * @return bool|mixed + * @author yangyifan + */ + public static function getFileMimeType($file) + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + if ($finfo) { + $mime_type = finfo_file($finfo, $file); + finfo_close($finfo); + return $mime_type; + } + return false; + } + + /** + * 获得一个临时文件 + * + * @return string + * @author yangyifan + */ + public static function getTmpFile() + { + $tmpfname = tempnam("/tmp", "dir"); + chmod($tmpfname, 0777); + return $tmpfname; + } + + /** + * 删除一个临时文件 + * + * @param $file_name + * @return bool + * @author yangyifan + */ + public static function deleteTmpFile($file_name) + { + return unlink($file_name); + } +} \ No newline at end of file diff --git a/src/Oss/OssAdapter.php b/src/Oss/OssAdapter.php new file mode 100644 index 0000000..1f11c0c --- /dev/null +++ b/src/Oss/OssAdapter.php @@ -0,0 +1,503 @@ + +// +---------------------------------------------------------------------- + + +namespace Ikodota\Upload\Oss; + +use League\Flysystem\Adapter\AbstractAdapter; +use League\Flysystem\Config; +use Yangyifan\Upload\Functions\FileFunction; +use Exception; +use OSS\OssClient; +use OSS\Core\OssException; + +class OssAdapter extends AbstractAdapter +{ + + const FILE_TYPE_FILE = 'file';//类型是文件 + const FILE_TYPE_DIR = 'dir';//类型是文件夹 + + /** + * 配置信息 + * + * @var + */ + protected $config; + + /** + * oss client 上传对象 + * + * @var OssClient + */ + protected $upload; + + /** + * 构造方法 + * + * @param $config 配置信息 + * @author yangyifan + */ + public function __construct($config) + { + $this->config = $config; + $this->bucket = $this->config['bucket']; + //设置路径前缀 + $this->setPathPrefix($this->config['transport'] . '://' . $this->config['bucket'] . '.' . $this->config['endpoint']); + } + + /** + * 获得OSS client上传对象 + * + * @return \OSS\OssClient + * @author yangyifan + */ + protected function getOss() + { + if (!$this->upload) { + $this->upload = new OssClient( + $this->config['accessKeyId'], + $this->config['accessKeySecret'], + $this->config['endpoint'], + $this->config['isCName'], + $this->config['securityToken'] + ); + + //设置请求超时时间 + $this->upload->setTimeout($this->config['timeout']); + + //设置连接超时时间 + $this->upload->setConnectTimeout($this->config['connectTimeout']); + } + + return $this->upload; + } + + /** + * 组合路径 + * + * @param $path + * @return string + * @author yangyifan + */ + protected function mergePath($path) + { + return trim($path, '/') . '/'; + } + + /** + * 判断文件是否存在 + * + * @param string $path + * @return bool + * @author yangyifan + */ + public function has($path) + { + try { + return $this->getOss()->doesObjectExist($this->bucket, $path) != false ? true : false; + }catch (OssException $e){ + + } + return false; + } + + /** + * 读取文件 + * + * @param $file_name + * @author yangyifan + */ + public function read($path) + { + try { + return ['contents' => $this->getOss()->getObject($this->bucket, $path) ]; + }catch (OssException $e){ + + } + return false; + + } + + /** + * 获得文件流 + * + * @param string $path + * @return array + * @author yangyifan + */ + public function readStream($path) + { + try { + //获得一个临时文件 + $tmpfname = FileFunction::getTmpFile(); + + file_put_contents($tmpfname, $this->read($path)['contents'] ); + + $handle = fopen($tmpfname, 'r'); + + //删除临时文件 + FileFunction::deleteTmpFile($tmpfname); + + return ['stream' => $handle]; + }catch (OssException $e){ + + } + return false; + } + + /** + * 写入文件 + * + * @param $file_name + * @param $contents + * @author yangyifan + */ + public function write($path, $contents, Config $config) + { + try { + $this->getOss()->putObject($this->bucket, $path, $contents, $option = []); + + return true; + }catch (OssException $e){ + + } + return false; + } + + /** + * 写入文件流 + * + * @param string $path + * @param resource $resource + * @param array $config + */ + public function writeStream($path, $resource, Config $config) + { + try{ + //获得一个临时文件 + $tmpfname = FileFunction::getTmpFile(); + + file_put_contents($tmpfname, $resource ); + + $this->getOss()->uploadFile($this->bucket, $path, $tmpfname, $option = []); + + //删除临时文件 + FileFunction::deleteTmpFile($tmpfname); + + fclose($resource); + + return true; + } + catch (OssException $e){ + + } + return false; + } + + /** + * 更新文件 + * + * @param string $path + * @param string $contents + * @param array $config + */ + public function update($path, $contents, Config $config) + { + return $this->write($path, $contents, $config); + } + + /** + * 更新文件流 + * + * @param string $path + * @param resource $resource + * @param array $config + */ + public function updateStream($path, $resource, Config $config) + { + return $this->writeStream($path, $resource, $config); + } + + /** + * 列出目录文件 + * + * @param string $directory + * @param bool|false $recursive + * @return array + * @author yangyifan + */ + public function listContents($directory = '', $recursive = false) + { + try{ + $directory = $directory == '' || $directory == '/' ? '' : $this->mergePath($directory); + + $options = [ + 'delimiter' => '/' , + 'prefix' => $directory, + 'max-keys' => $this->config['max_keys'], + 'marker' => '', + ]; + + $result_obj = $this->getOss()->listObjects($this->bucket, $options); + + $file_list = $result_obj->getObjectList();//文件列表 + $dir_list = $result_obj->getPrefixList();//文件夹列表 + $data = []; + + if (is_array($dir_list) && count($dir_list) > 0 ) { + foreach ($dir_list as $key => $dir) { + $data[] = [ + 'path' => $dir->getPrefix(), + 'prefix' => $options['prefix'], + 'marker' => $options['marker'], + 'file_type' => self::FILE_TYPE_DIR + ]; + } + } + + if (is_array($file_list) && count($file_list) > 0 ) { + foreach ($file_list as $key => $file) { + if ($key == 0 ) { + $data[] = [ + 'path' => $file->getKey(), + 'prefix' => $options['prefix'], + 'marker' => $options['marker'], + 'file_type' => self::FILE_TYPE_DIR + ]; + } else { + $data[] = [ + 'path' => $file->getKey(), + 'last_modified' => $file->getLastModified(), + 'e_tag' => $file->getETag(), + 'file_size' => $file->getSize(), + 'prefix' => $options['prefix'], + 'marker' => $options['marker'], + 'file_type' => self::FILE_TYPE_FILE, + ]; + } + } + } + + return $data; + }catch (Exception $e){ + + } + return []; + } + + /** + * 获取资源的元信息,但不返回文件内容 + * + * @param $path + * @return array + * @author yangyifan + */ + public function getMetadata($path) + { + try { + $file_info = $this->getOss()->getObjectMeta($this->bucket, $path); + if ( !empty($file_info) ) { + return $file_info; + } + }catch (OssException $e) { + + } + return false; + } + + /** + * 获得文件大小 + * + * @param string $path + * @return int + * @author yangyifan + */ + public function getSize($path) + { + $file_info = $this->getMetadata($path); + return $file_info != false && $file_info['content-length'] > 0 ? [ 'size' => $file_info['content-length'] ] : false; + } + + /** + * 获得文件Mime类型 + * + * @param string $path + * @return mixed string|null + * @author yangyifan + */ + public function getMimetype($path) + { + $file_info = $this->getMetadata($path); + return $file_info != false && !empty($file_info['content-type']) ? [ 'mimetype' => $file_info['content-type'] ] : false; + } + + /** + * 获得文件最后修改时间 + * + * @param string $path + * @return int 时间戳 + * @author yangyifan + */ + public function getTimestamp($path) + { + $file_info = $this->getMetadata($path); + return $file_info != false && !empty($file_info['last-modified']) ? ['timestamp' => strtotime($file_info['last-modified']) ] : false; + } + + /** + * 获得文件模式 (未实现) + * + * @param string $path + * @author yangyifan + */ + public function getVisibility($path) + { + return self::VISIBILITY_PUBLIC; + } + + /** + * 重命名文件 + * + * @param $oldname + * @param $newname + * @return boolean + * @author yangyifan + */ + public function rename($path, $newpath) + { + try { + /** + * 如果是一个资源,请保持最后不是以“/”结尾! + * + */ + $path = rtrim($this->mergePath($path), '/'); + + $this->getOss()->copyObject($this->bucket, $path, $this->bucket, rtrim($this->mergePath($newpath), '/'), []); + $this->delete($path); + return true; + }catch (OssException $e){ + + } + return false; + } + + /** + * 复制文件 + * + * @param $path + * @param $newpath + * @return boolean + * @author yangyifan + */ + public function copy($path, $newpath) + { + try { + $this->getOss()->copyObject($this->bucket, $path, $this->bucket, $newpath, []); + return true; + }catch (OssException $e){ + + } + return false; + } + + /** + * 删除文件或者文件夹 + * + * @param $path + * @author yangyifan + */ + public function delete($path) + { + try{ + $this->getOss()->deleteObject($this->bucket, $path); + return true; + }catch (OssException $e){ + + } + return false; + } + + /** + * 删除文件夹 + * + * @param string $path + * @return mixed + * @author yangyifan + */ + public function deleteDir($path) + { + try{ + //递归去删除全部文件 + $this->recursiveDelete($path); + + return true; + }catch (OssException $e){ + + } + return false; + } + + /** + * 递归删除全部文件 + * + * @param $path + * @author yangyifan + */ + protected function recursiveDelete($path) + { + $file_list = $this->listContents(rtrim($path, '/')); + //如果当前文件夹文件不为空,则直接去删除文件夹 + if ( is_array($file_list) && count($file_list) > 0 ) { + foreach ($file_list as $file) { + if ($file['path'] == $path) { + continue; + } + if ($file['file_type'] == self::FILE_TYPE_FILE) { + $this->delete($file['path']); + } else { + $this->recursiveDelete($file['path']); + } + } + } + $this->getOss()->deleteObject($this->bucket, $path); + } + + /** + * 创建文件夹 + * + * @param string $dirname + * @param array $config + * @author yangyifan + */ + public function createDir($dirname, Config $config) + { + try{ + $this->getOss()->createObjectDir($this->bucket, $dirname); + return true; + }catch (OssException $e){ + + } + return false; + } + + /** + * 设置文件模式 (未实现) + * + * @param string $path + * @param string $visibility + * @return bool + * @author yangyifan + */ + public function setVisibility($path, $visibility) + { + return true; + } + +} \ No newline at end of file diff --git a/src/Qiniu/QiniuAdapter.php b/src/Qiniu/QiniuAdapter.php new file mode 100644 index 0000000..141aae0 --- /dev/null +++ b/src/Qiniu/QiniuAdapter.php @@ -0,0 +1,412 @@ + +// +---------------------------------------------------------------------- + +namespace Ikodota\Upload\Qiniu; + +use League\Flysystem\Adapter\AbstractAdapter; +use League\Flysystem\Config; +use Qiniu\Storage\ResumeUploader; +use Qiniu\Storage\UploadManager; +use Qiniu\Auth; +use Qiniu\Storage\BucketManager; +use Qiniu\Config AS QiniuConfig; +use Symfony\Component\Finder\SplFileInfo; +use InvalidArgumentException; +use Yangyifan\Upload\Functions\FileFunction; + +class QiniuAdapter extends AbstractAdapter +{ + /** + * Auth + * + * @var Auth + */ + protected $auth; + + /** + * token + * + * @var string + */ + protected $token; + + /** + * bucket + * + * @var + */ + protected $bucket; + + /** + * 七牛空间管理对象 + * + * @var + */ + protected $bucketManager; + + /** + * 上传对象 + * + * @var + */ + protected $uploadManager; + + /** + * 二进制流上传对象 + * + * @var + */ + protected $resumeUploader; + + /** + * 配置信息 + * + * @var array + */ + protected $config; + + /** + * 构造方法 + * + * @param array $config 配置信息 + * @author yangyifan + */ + public function __construct($config) + { + $this->config = $config; + $this->bucket = $this->config['bucket']; + $this->auth = new Auth($this->config['access_key'], $this->config['secret_key']); + $this->token = $this->auth->uploadToken($this->bucket); + + //设置路径前缀 + $this->setPathPrefix($this->config['transport'] . '://' . $this->config['domain']); + } + + /** + * 获得七牛空间管理对象 + * + * @return BucketManager + * @author yangyifan + */ + protected function getBucketManager() + { + if (!$this->bucketManager) { + $this->bucketManager = new BucketManager($this->auth); + } + return $this->bucketManager; + } + + /** + * 获得七牛上传对象 + * + * @return UploadManager + * @author yangyifan + */ + protected function getUploadManager() + { + if (!$this->uploadManager) { + $this->uploadManager = new UploadManager(); + } + return $this->uploadManager; + } + + /** + * 获得二进制流上传对象 + * + * @param $key 上传文件名 + * @param $inputStream 上传二进制流 + * @param $config 配置信息 + * @param $params 自定义变量 + * @return ResumeUploader + * @author yangyifan + */ + protected function getResumeUpload($key, $inputStream, Config $config, $params = null) + { + if (!$this->resumeUploader) { + if (!$config->has('file_path')) { + throw new InvalidArgumentException("请配置 file_path 选项,此选项表示需要上传的文件的路径"); + } + + $file_path = $config->get('file_path'); + + if ( $file_path ) { + $file_info = new \SplFileInfo($file_path); + + if ($file_info->isFile() == true) { + $this->resumeUploader = new ResumeUploader( $this->token, $key, $inputStream, $file_info->getSize(), $params, FileFunction::getFileMimeType($file_path), (new QiniuConfig()) ); + } + } + + throw new InvalidArgumentException("{$file_path} 不是一个文件"); + } + return $this->resumeUploader; + } + + /** + * 判断文件是否存在 + * + * @param string $path + * @return bool + * @author yangyifan + */ + public function has($path) + { + $file_stat = $this->getMetadata($path); + return !empty($file_stat) ? true : false; + } + + /** + * 读取文件 + * + * @param $file_name + * @author yangyifan + */ + public function read($path) + { + return ['contents' => file_get_contents($this->applyPathPrefix($path)) ]; + } + + /** + * 获得文件流 + * + * @param string $path + * @return array + * @author yangyifan + */ + public function readStream($path) + { + return ['stream' => fopen($this->applyPathPrefix($path), 'r')]; + } + + /** + * 写入文件 + * + * @param $file_name + * @param $contents + * @author yangyifan + */ + public function write($path, $contents, Config $config) + { + list(, $error) = $this->getUploadManager()->put($this->token, $path, $contents); + + if ($error) { + return false; + } + return true; + } + + /** + * 写入文件流 + * + * @param string $path + * @param resource $resource + * @param array $config + */ + public function writeStream($path, $resource, Config $config) + { + list(, $error) = $this->getResumeUpload($path, $resource, $config)->upload(); + + return $error ? false : true; + } + + /** + * 更新文件 + * + * @param string $path + * @param string $contents + * @param array $config + */ + public function update($path, $contents, Config $config) + { + return $this->write($path, $contents, $config); + } + + /** + * 更新文件流 + * + * @param string $path + * @param resource $resource + * @param array $config + */ + public function updateStream($path, $resource, Config $config) + { + return $this->writeStream($path, $resource, $config); + } + + /** + * 列出目录文件 + * + * @param string $directory + * @param bool|false $recursive + * @return array + * @author yangyifan + */ + public function listContents($directory = '', $recursive = false) + { + list($file_list, $marker, $error) = $this->getBucketManager()->listFiles($this->bucket, $directory); + + if (!$error) { + foreach ($file_list as &$file) { + $file['path'] = $file['key']; + $file['marker'] = $marker;//用于下次请求的标识符 + } + return $file_list; + } + return false; + } + + /** + * 获取资源的元信息,但不返回文件内容 + * + * @param $path + * @return array + * @author yangyifan + */ + public function getMetadata($path) + { + list($info, $error) = $this->getBucketManager()->stat($this->bucket, $path); + + if ($error) { + return false; + } + return $info; + } + + /** + * 获得文件大小 + * + * @param string $path + * @return int + * @author yangyifan + */ + public function getSize($path) + { + list($fsize, , , ) = array_values($this->getMetadata($path)); + return $fsize > 0 ? [ 'size' => $fsize ] : false; + } + + /** + * 获得文件Mime类型 + * + * @param string $path + * @return mixed string|null + * @author yangyifan + */ + public function getMimetype($path) + { + list(, , $mimeType,) = array_values($this->getMetadata($path)); + return !empty($mimeType) ? ['mimetype' => $mimeType ] : false; + } + + /** + * 获得文件最后修改时间 + * + * @param string $path + * @return int 时间戳 + * @author yangyifan + */ + public function getTimestamp($path) + { + list(, , , $timestamp) = array_values($this->getMetadata($path)); + return !empty($timestamp) ? ['timestamp' => $timestamp ] : false; + } + + /** + * 获得文件模式 (未实现) + * + * @param string $path + * @author yangyifan + */ + public function getVisibility($path) + { + return self::VISIBILITY_PUBLIC; + } + + /** + * 重命名文件 + * + * @param $oldname + * @param $newname + * @return boolean + * @author yangyifan + */ + public function rename($path, $newpath) + { + return $this->getBucketManager()->rename($this->bucket, $path, $newpath) == null ? true : false; + } + + /** + * 复制文件 + * + * @param $path + * @param $newpath + * @return boolean + * @author yangyifan + */ + public function copy($path, $newpath) + { + return $this->getBucketManager()->copy($this->bucket, $path, $this->bucket, $newpath) == null ? true : false; + } + + /** + * 删除文件或者文件夹 + * + * @param $path + * @author yangyifan + */ + public function delete($path) + { + return $this->getBucketManager()->delete($this->bucket, $path); + } + + /** + * 删除文件夹 + * + * @param string $path + * @return mixed + * @author yangyifan + */ + public function deleteDir($path) + { + list($file_list, , $error) = $this->getBucketManager()->listFiles($this->bucket, $path); + + if (!$error) { + foreach ( $file_list as $file) { + $this->delete($file['key']); + } + } + return true; + } + + /** + * 创建文件夹(因为七牛没有文件夹的概念,所以此方法没有实现) + * + * @param string $dirname + * @param array $config + * @author yangyifan + */ + public function createDir($dirname, Config $config) + { + return true; + } + + /** + * 设置文件模式 (未实现) + * + * @param string $path + * @param string $visibility + * @return bool + * @author yangyifan + */ + public function setVisibility($path, $visibility) + { + return true; + } +} \ No newline at end of file diff --git a/src/UploadServiceProvider.php b/src/UploadServiceProvider.php new file mode 100644 index 0000000..e60942b --- /dev/null +++ b/src/UploadServiceProvider.php @@ -0,0 +1,80 @@ +extendQiniuStorage(); + + //实现upyun文件系统 + $this->extendUpyunStorage(); + + //实现oss文件系统 + $this->extendOssStorage(); + } + + /** + * Register any application services. + * + * This service provider is a great spot to register your various container + * bindings with the application. As you can see, we are registering our + * "Registrar" implementation here. You can add your own bindings too! + * + * @return void + */ + public function register() + { + + } + + /** + * 实现七牛文件系统 + * + * @author yangyifan + */ + protected function extendQiniuStorage() + { + \Storage::extend('qiniu', function($app, $config){ + return new Filesystem(new QiniuAdapter($config), $config); + }); + } + + /** + * 实现upyun文件系统 + * + * @author yangyifan + */ + protected function extendUpyunStorage() + { + \Storage::extend('upyun', function($app, $config){ + return new Filesystem(new UpyunAdapter($config), $config); + }); + } + + /** + * 实现oss文件系统 + * + * @author yangyifan + */ + protected function extendOssStorage() + { + \Storage::extend('oss', function($app, $config){ + return new Filesystem(new OssAdapter($config), $config); + }); + } + +} diff --git a/src/Upyun/UpyunAdapter.php b/src/Upyun/UpyunAdapter.php new file mode 100644 index 0000000..42d8716 --- /dev/null +++ b/src/Upyun/UpyunAdapter.php @@ -0,0 +1,405 @@ + +// +---------------------------------------------------------------------- + +namespace Ikodota\Upload\Upyun; + +use League\Flysystem\Adapter\AbstractAdapter; +use League\Flysystem\Config; +use Yangyifan\Upload\Functions\FileFunction; +use Exception; + +class UpyunAdapter extends AbstractAdapter +{ + /** + * 配置信息 + * + * @var + */ + protected $config; + + /** + * upyun上传对象 + * + * @var UpYun + */ + protected $upload; + + /** + * + * 文件类型 + * + */ + const FILE_TYPE_FILE = 'file';//文件类型为文件 + const FILE_TYPE_FOLDER = 'folder';//文件类型是文件夹 + + /** + * 构造方法 + * + * @param $config 配置信息 + * @author yangyifan + */ + public function __construct($config) + { + $this->config = $config; + + //设置路径前缀 + $this->setPathPrefix($this->config['transport'] . '://' . $this->config['domain']); + } + + /** + * 获得Upyun上传对象 + * + * @return UpYun + * @author yangyifan + */ + protected function getUpyun() + { + if (!$this->upload) { + $this->upload = new UpYun( + $this->config['bucket'],//空间名称 + $this->config['username'],//用户名 + $this->config['password'],//密码 + $this->config['endpoint'],//线路 + $this->config['timeout']//超时时间 + ); + } + return $this->upload; + } + + /** + * 重写组合upyun合肥路径 + * + * @param $path + * @return string + * @author yangyifan + */ + protected function mergePath($path) + { + return '/' . trim($path, '/'); + } + + /** + * 判断文件是否存在 + * + * @param string $path + * @return bool + * @author yangyifan + */ + public function has($path) + { + return $this->getMetadata($path) != false ? true : false; + } + + /** + * 读取文件 + * + * @param $file_name + * @author yangyifan + */ + public function read($path) + { + return ['contents' => file_get_contents($this->applyPathPrefix($path)) ]; + } + + /** + * 获得文件流 + * + * @param string $path + * @return array + * @author yangyifan + */ + public function readStream($path) + { + //获得一个临时文件 + $tmpfname = FileFunction::getTmpFile(); + + file_put_contents($tmpfname, file_get_contents($this->applyPathPrefix($path)) ); + + $handle = fopen($tmpfname, 'r'); + + //删除临时文件 + FileFunction::deleteTmpFile($tmpfname); + + return ['stream' => $handle]; + } + + /** + * 写入文件 + * + * @param $file_name + * @param $contents + * @author yangyifan + */ + public function write($path, $contents, Config $config) + { + return $this->getUpyun()->writeFile($this->mergePath($path), $contents, $auto_mkdir = true); + } + + /** + * 写入文件流 + * + * @param string $path + * @param resource $resource + * @param array $config + */ + public function writeStream($path, $resource, Config $config) + { + $status = $this->getUpyun()->writeFile($this->mergePath($path), $resource, true); + fclose($resource); + + return $status; + } + + /** + * 更新文件 + * + * @param string $path + * @param string $contents + * @param array $config + */ + public function update($path, $contents, Config $config) + { + return $this->write($path, $contents, $config); + } + + /** + * 更新文件流 + * + * @param string $path + * @param resource $resource + * @param array $config + */ + public function updateStream($path, $resource, Config $config) + { + return $this->writeStream($path, $resource, $config); + } + + /** + * 列出目录文件 + * + * @param string $directory + * @param bool|false $recursive + * @return array + * @author yangyifan + */ + public function listContents($directory = '', $recursive = false) + { + try{ + //组合目录 + $directory = $this->mergePath($directory); + + $file_list = $this->getUpyun()->getList($directory); + + if (is_array($file_list) && count($file_list) > 0 ) { + foreach ($file_list as &$file) { + $file['path'] = ltrim($directory, '/') . DIRECTORY_SEPARATOR . $file['name']; + } + } + return $file_list; + }catch (Exception $e){ + + } + return []; + } + + /** + * 获取资源的元信息,但不返回文件内容 + * + * @param $path + * @return array + * @author yangyifan + */ + public function getMetadata($path) + { + try { + $file_info = $this->getUpyun()->getFileInfo($path); + if ( !empty($file_info) ) { + return $file_info; + } + }catch (Exception $exception){ + //调试信息 + //echo $exception->getMessage(); + } + return false; + } + + /** + * 获得文件大小 + * + * @param string $path + * @return int + * @author yangyifan + */ + public function getSize($path) + { + $file_info = $this->getMetadata($path); + return $file_info != false && $file_info['x-upyun-file-size'] > 0 ? [ 'size' => $file_info['x-upyun-file-size'] ] : false; + } + + /** + * 获得文件Mime类型 + * + * @param string $path + * @return mixed string|null + * @author yangyifan + */ + public function getMimetype($path) + { + //创建一个临时文件 + $tmp_file = FileFunction::getTmpFile(); + + file_put_contents($tmp_file, $this->readStream($path)['stream']); + + $mime_type = FileFunction::getFileMimeType($tmp_file); + + //删除临时文件 + + FileFunction::deleteTmpFile($tmp_file); + + return ['mimetype' => $mime_type]; + } + + /** + * 获得文件最后修改时间 + * + * @param string $path + * @return int 时间戳 + * @author yangyifan + */ + public function getTimestamp($path) + { + $file_info = $this->getMetadata($path); + return $file_info != false && !empty($file_info['x-upyun-file-date']) ? ['timestamp' => $file_info['x-upyun-file-date'] ] : false; + } + + /** + * 获得文件模式 (未实现) + * + * @param string $path + * @author yangyifan + */ + public function getVisibility($path) + { + return self::VISIBILITY_PUBLIC; + } + + /** + * 重命名文件 + * + * @param $oldname + * @param $newname + * @return boolean + * @author yangyifan + */ + public function rename($path, $newpath) + { + $newpath = $this->mergePath($newpath); + + $this->writeStream($newpath, $this->readStream($path)['stream'], new Config() ); + + $this->delete($path); + + return true; + } + + /** + * 复制文件 + * + * @param $path + * @param $newpath + * @return boolean + * @author yangyifan + */ + public function copy($path, $newpath) + { + $this->writeStream($this->mergePath($newpath), $this->readStream($this->mergePath($path))['stream'], new Config() ); + + return true; + } + + /** + * 删除文件或者文件夹 + * + * @param $path + * @author yangyifan + */ + public function delete($path) + { + return $this->getUpyun()->delete($this->mergePath($path)); + } + + /** + * 删除文件夹 + * + * @param string $path + * @return mixed + * @author yangyifan + */ + public function deleteDir($path) + { + if ( $this->has($path) ) { + //递归删除全部子文件 + $this->recursiveDeleteDir($path); + return true; + } + return false; + } + + /** + * 递归删除全部文件夹 + * + * @param $path + * @author yangyifan + */ + protected function recursiveDeleteDir($path) + { + $path = $this->mergePath($path); + $file_list = $this->listContents($path); + + if ( is_array($file_list) && count($file_list) > 0 ) { + foreach ($file_list as $file) { + //如果是文件,则把文件删除 + if ($file['type'] == self::FILE_TYPE_FILE) { + $this->delete($path . $this->pathSeparator . $file['name']); + } else { + $this->recursiveDeleteDir($path . $this->pathSeparator . $file['name']); + } + } + } + $this->getUpyun()->rmDir($path); + } + + /** + * 创建文件夹 + * + * @param string $dirname + * @param array $config + * @author yangyifan + */ + public function createDir($dirname, Config $config) + { + $this->getUpyun()->makeDir($this->mergePath($dirname)); + return true; + } + + /** + * 设置文件模式 (未实现) + * + * @param string $path + * @param string $visibility + * @return bool + * @author yangyifan + */ + public function setVisibility($path, $visibility) + { + return true; + } + +} \ No newline at end of file diff --git a/src/Upyun/upyun.php b/src/Upyun/upyun.php new file mode 100644 index 0000000..d12c3d8 --- /dev/null +++ b/src/Upyun/upyun.php @@ -0,0 +1,511 @@ +code}]: {$this->message}\n"; + } +}/*}}}*/ + +class UpYunAuthorizationException extends UpYunException {/*{{{*/ + public function __construct($message, $code = 0, Exception $previous = null) { + parent::__construct($message, 401, $previous); + } +}/*}}}*/ + +class UpYunForbiddenException extends UpYunException {/*{{{*/ + public function __construct($message, $code = 0, Exception $previous = null) { + parent::__construct($message, 403, $previous); + } +}/*}}}*/ + +class UpYunNotFoundException extends UpYunException {/*{{{*/ + public function __construct($message, $code = 0, Exception $previous = null) { + parent::__construct($message, 404, $previous); + } +}/*}}}*/ + +class UpYunNotAcceptableException extends UpYunException {/*{{{*/ + public function __construct($message, $code = 0, Exception $previous = null) { + parent::__construct($message, 406, $previous); + } +}/*}}}*/ + +class UpYunServiceUnavailable extends UpYunException {/*{{{*/ + public function __construct($message, $code = 0, Exception $previous = null) { + parent::__construct($message, 503, $previous); + } +}/*}}}*/ + +class UpYun { + const VERSION = '2.0'; + +/*{{{*/ + const ED_AUTO = 'v0.api.upyun.com'; + const ED_TELECOM = 'v1.api.upyun.com'; + const ED_CNC = 'v2.api.upyun.com'; + const ED_CTT = 'v3.api.upyun.com'; + + const CONTENT_TYPE = 'Content-Type'; + const CONTENT_MD5 = 'Content-MD5'; + const CONTENT_SECRET = 'Content-Secret'; + + // 缩略图 + const X_GMKERL_THUMBNAIL = 'x-gmkerl-thumbnail'; + const X_GMKERL_TYPE = 'x-gmkerl-type'; + const X_GMKERL_VALUE = 'x-gmkerl-value'; + const X_GMKERL_QUALITY = 'x­gmkerl-quality'; + const X_GMKERL_UNSHARP = 'x­gmkerl-unsharp'; +/*}}}*/ + + private $_bucketname; + private $_username; + private $_password; + private $_timeout = 30; + + /** + * @deprecated + */ + private $_content_md5 = NULL; + + /** + * @deprecated + */ + private $_file_secret = NULL; + + /** + * @deprecated + */ + private $_file_infos= NULL; + + protected $endpoint; + + /** + * @var string: UPYUN 请求唯一id, 出现错误时, 可以将该id报告给 UPYUN,进行调试 + */ + private $x_request_id; + + /** + * 初始化 UpYun 存储接口 + * @param $bucketname 空间名称 + * @param $username 操作员名称 + * @param $password 密码 + * + * @return object + */ + public function __construct($bucketname, $username, $password, $endpoint = NULL, $timeout = 30) {/*{{{*/ + $this->_bucketname = $bucketname; + $this->_username = $username; + $this->_password = md5($password); + $this->_timeout = $timeout; + + $this->endpoint = is_null($endpoint) ? self::ED_AUTO : $endpoint; + }/*}}}*/ + + /** + * 获取当前SDK版本号 + */ + public function version() { + return self::VERSION; + } + + /** + * 创建目录 + * @param $path 路径 + * @param $auto_mkdir 是否自动创建父级目录,最多10层次 + * + * @return void + */ + public function makeDir($path, $auto_mkdir = false) {/*{{{*/ + $headers = array('Folder' => 'true'); + if ($auto_mkdir) $headers['Mkdir'] = 'true'; + return $this->_do_request('PUT', $path, $headers); + }/*}}}*/ + + /** + * 删除目录和文件 + * @param string $path 路径 + * + * @return boolean + */ + public function delete($path) {/*{{{*/ + return $this->_do_request('DELETE', $path); + }/*}}}*/ + + + /** + * 上传文件 + * @param string $path 存储路径 + * @param mixed $file 需要上传的文件,可以是文件流或者文件内容 + * @param boolean $auto_mkdir 自动创建目录 + * @param array $opts 可选参数 + */ + public function writeFile($path, $file, $auto_mkdir = False, $opts = NULL) {/*{{{*/ + if (is_null($opts)) $opts = array(); + if (!is_null($this->_content_md5) || !is_null($this->_file_secret)) { + //if (!is_null($this->_content_md5)) array_push($opts, self::CONTENT_MD5 . ": {$this->_content_md5}"); + //if (!is_null($this->_file_secret)) array_push($opts, self::CONTENT_SECRET . ": {$this->_file_secret}"); + if (!is_null($this->_content_md5)) $opts[self::CONTENT_MD5] = $this->_content_md5; + if (!is_null($this->_file_secret)) $opts[self::CONTENT_SECRET] = $this->_file_secret; + } + + // 如果设置了缩略版本或者缩略图类型,则添加默认压缩质量和锐化参数 + //if (isset($opts[self::X_GMKERL_THUMBNAIL]) || isset($opts[self::X_GMKERL_TYPE])) { + // if (!isset($opts[self::X_GMKERL_QUALITY])) $opts[self::X_GMKERL_QUALITY] = 95; + // if (!isset($opts[self::X_GMKERL_UNSHARP])) $opts[self::X_GMKERL_UNSHARP] = 'true'; + //} + + if ($auto_mkdir === True) $opts['Mkdir'] = 'true'; + $this->_file_infos = $this->_do_request('PUT', $path, $opts, $file); + + return $this->_file_infos; + }/*}}}*/ + + /** + * 下载文件 + * @param string $path 文件路径 + * @param mixed $file_handle + * + * @return mixed + */ + public function readFile($path, $file_handle = NULL) {/*{{{*/ + return $this->_do_request('GET', $path, NULL, NULL, $file_handle); + }/*}}}*/ + + /** + * 获取目录文件列表 + * + * @param string $path 查询路径 + * + * @return mixed + */ + public function getList($path = '/') {/*{{{*/ + + $rsp = $this->_do_request('GET', $path); + + $list = array(); + if ($rsp) { + $rsp = explode("\n", $rsp); + foreach($rsp as $item) { + @list($name, $type, $size, $time) = explode("\t", trim($item)); + if (!empty($time)) { + $type = $type == 'N' ? 'file' : 'folder'; + } + + $item = array( + 'name' => $name, + 'type' => $type, + 'size' => intval($size), + 'time' => intval($time), + ); + array_push($list, $item); + } + } + + return $list; + }/*}}}*/ + + /** + * @deprecated + * @param string $path 目录路径 + * @return mixed + */ + public function getFolderUsage($path = '/') {/*{{{*/ + $rsp = $this->_do_request('GET', '/?usage'); + return floatval($rsp); + }/*}}}*/ + + /** + * 获取文件、目录信息 + * + * @param string $path 路径 + * + * @return mixed + */ + public function getFileInfo($path) {/*{{{*/ + $path = '/' . $path; + $rsp = $this->_do_request('HEAD', $path); + + return $rsp; + }/*}}}*/ + + /** + * 连接签名方法 + * @param $method 请求方式 {GET, POST, PUT, DELETE} + * return 签名字符串 + */ + private function sign($method, $uri, $date, $length){/*{{{*/ + //$uri = urlencode($uri); + $sign = "{$method}&{$uri}&{$date}&{$length}&{$this->_password}"; + return 'UpYun '.$this->_username.':'.md5($sign); + }/*}}}*/ + + /** + * HTTP REQUEST 封装 + * @param string $method HTTP REQUEST方法,包括PUT、POST、GET、OPTIONS、DELETE + * @param string $path 除Bucketname之外的请求路径,包括get参数 + * @param array $headers 请求需要的特殊HTTP HEADERS + * @param array $body 需要POST发送的数据 + * + * @return mixed + */ + protected function _do_request($method, $path, $headers = NULL, $body= NULL, $file_handle= NULL) {/*{{{*/ + $uri = "/{$this->_bucketname}{$path}"; + $ch = curl_init("http://{$this->endpoint}{$uri}"); + + $_headers = array('Expect:'); + if (!is_null($headers) && is_array($headers)){ + foreach($headers as $k => $v) { + array_push($_headers, "{$k}: {$v}"); + } + } + + $length = 0; + $date = gmdate('D, d M Y H:i:s \G\M\T'); + + if (!is_null($body)) { + if(is_resource($body)){ + fseek($body, 0, SEEK_END); + $length = ftell($body); + fseek($body, 0); + + array_push($_headers, "Content-Length: {$length}"); + curl_setopt($ch, CURLOPT_INFILE, $body); + curl_setopt($ch, CURLOPT_INFILESIZE, $length); + } else { + $length = @strlen($body); + array_push($_headers, "Content-Length: {$length}"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + } else { + array_push($_headers, "Content-Length: {$length}"); + } + + array_push($_headers, "Authorization: {$this->sign($method, $uri, $date, $length)}"); + array_push($_headers, "Date: {$date}"); + + curl_setopt($ch, CURLOPT_HTTPHEADER, $_headers); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->_timeout); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + //curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + + if ($method == 'PUT' || $method == 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + } else { + curl_setopt($ch, CURLOPT_POST, 0); + } + + if ($method == 'GET' && is_resource($file_handle)) { + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FILE, $file_handle); + } + + if ($method == 'HEAD') { + curl_setopt($ch, CURLOPT_NOBODY, true); + } + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + + if ($http_code == 0) throw new UpYunException('Connection Failed', $http_code); + + curl_close($ch); + + $header_string = ''; + $body = ''; + + if ($method == 'GET' && is_resource($file_handle)) { + $header_string = ''; + $body = $response; + } else { + list($header_string, $body) = explode("\r\n\r\n", $response, 2); + } + $this->setXRequestId($header_string); + if ($http_code == 200) { + if ($method == 'GET' && is_null($file_handle)) { + return $body; + } else { + $data = $this->_getHeadersData($header_string); + return count($data) > 0 ? $data : true; + } + } else { + $message = $this->_getErrorMessage($header_string); + if (is_null($message) && $method == 'GET' && is_resource($file_handle)) { + $message = 'File Not Found'; + } + switch($http_code) { + case 401: + throw new UpYunAuthorizationException($message); + break; + case 403: + throw new UpYunForbiddenException($message); + break; + case 404: + throw new UpYunNotFoundException($message); + break; + case 406: + throw new UpYunNotAcceptableException($message); + break; + case 503: + throw new UpYunServiceUnavailable($message); + break; + default: + throw new UpYunException($message, $http_code); + } + } + }/*}}}*/ + + /** + * 处理HTTP HEADERS中返回的自定义数据 + * + * @param string $text header字符串 + * + * @return array + */ + private function _getHeadersData($text) {/*{{{*/ + $headers = explode("\r\n", $text); + $items = array(); + foreach($headers as $header) { + $header = trim($header); + if(stripos($header, 'x-upyun') !== False){ + list($k, $v) = explode(':', $header); + $items[trim($k)] = in_array(substr($k,8,5), array('width','heigh','frame')) ? intval($v) : trim($v); + } + } + return $items; + }/*}}}*/ + + /** + * 获取返回的错误信息 + * + * @param string $header_string + * + * @return mixed + */ + private function _getErrorMessage($header_string) { + list($status, $stash) = explode("\r\n", $header_string, 2); + list($v, $code, $message) = explode(" ", $status, 3); + return $message . " X-Request-Id: " . $this->getXRequestId(); + } + + private function setXRequestId($header_string) { + preg_match('~^X-Request-Id: ([0-9a-zA-Z]{32})~ism', $header_string, $result); + $this->x_request_id = isset($result[1]) ? $result[1] : ''; + } + + public function getXRequestId() { + return $this->x_request_id; + } + + /** + * 删除目录 + * @deprecated + * @param $path 路径 + * + * @return void + */ + public function rmDir($path) {/*{{{*/ + $this->_do_request('DELETE', '/'.$path); + }/*}}}*/ + + /** + * 删除文件 + * + * @deprecated + * @param string $path 要删除的文件路径 + * + * @return boolean + */ + public function deleteFile($path) {/*{{{*/ + $rsp = $this->_do_request('DELETE', '/'.$path); + }/*}}}*/ + + /** + * 获取目录文件列表 + * @deprecated + * + * @param string $path 要获取列表的目录 + * + * @return array + */ + public function readDir($path) {/*{{{*/ + return $this->getList($path); + }/*}}}*/ + + /** + * 获取空间使用情况 + * + * @deprecated 推荐直接使用 getFolderUsage('/')来获取 + * @return mixed + */ + public function getBucketUsage() {/*{{{*/ + return $this->getFolderUsage('/'); + }/*}}}*/ + + /** + * 获取文件信息 + * + * #deprecated + * @param $file 文件路径(包含文件名) + * return array('type'=> file | folder, 'size'=> file size, 'date'=> unix time) 或 null + */ + //public function getFileInfo($file){/*{{{*/ + // $result = $this->head($file); + // if(is_null($r))return null; + // return array('type'=> $this->tmp_infos['x-upyun-file-type'], 'size'=> @intval($this->tmp_infos['x-upyun-file-size']), 'date'=> @intval($this->tmp_infos['x-upyun-file-date'])); + //}/*}}}*/ + + /** + * 切换 API 接口的域名 + * + * @deprecated + * @param $domain {默然 v0.api.upyun.com 自动识别, v1.api.upyun.com 电信, v2.api.upyun.com 联通, v3.api.upyun.com 移动} + * return null; + */ + public function setApiDomain($domain){/*{{{*/ + $this->endpoint = $domain; + }/*}}}*/ + + /** + * 设置待上传文件的 Content-MD5 值(如又拍云服务端收到的文件MD5值与用户设置的不一致,将回报 406 Not Acceptable 错误) + * + * @deprecated + * @param $str (文件 MD5 校验码) + * return null; + */ + public function setContentMD5($str){/*{{{*/ + $this->_content_md5 = $str; + }/*}}}*/ + + /** + * 设置待上传文件的 访问密钥(注意:仅支持图片空!,设置密钥后,无法根据原文件URL直接访问,需带 URL 后面加上 (缩略图间隔标志符+密钥) 进行访问) + * 如缩略图间隔标志符为 ! ,密钥为 bac,上传文件路径为 /folder/test.jpg ,那么该图片的对外访问地址为: http://空间域名/folder/test.jpg!bac + * + * @deprecated + * @param $str (文件 MD5 校验码) + * return null; + */ + public function setFileSecret($str){/*{{{*/ + $this->_file_secret = $str; + }/*}}}*/ + + /** + * @deprecated + * 获取上传文件后的信息(仅图片空间有返回数据) + * @param $key 信息字段名(x-upyun-width、x-upyun-height、x-upyun-frames、x-upyun-file-type) + * return value or NULL + */ + public function getWritedFileInfo($key){/*{{{*/ + if(!isset($this->_file_infos))return NULL; + return $this->_file_infos[$key]; + }/*}}}*/ +}