前情提示

需要一定的php基础

介绍

在wp插件库里有几款又拍云的插件,而且功能很全,但都不是我想要的。要么有广告、要么很久没维护有bug,所以我根据官方sdk自己写了一个类似的功能。实现原理还是很简单的,一起来看看吧。

这是我写的一个前台设置,一共4个重要参数:

  • 服务名称
  • 加速域名
  • 操作员用户名
  • 操作员授权密码

加速域名主要用在访问存储桶文件,其他三个用来上传文件。我还加了一个查看当前云存储使用量,还是挺不错的。

导包

wp是用php编写的,所以我使用官方提供的php-sdk工具包

GitHub地址:https://github.com/upyun/php-sdk

可以用composer命令来安装:composer require upyun/sdk

官方提供的sdk功能还是很丰富的,如下:

又拍云php-sdk功能列表

write、has、delete、usage,这四个功能是我使用到的,他们的作用已经写的很详细了。

使用方法

在wp的functions.php中引入库,添加如下代码:

require_once get_template_directory().'/vendor/autoload.php';
use Upyun\Upyun;
use Upyun\Config;

$config = new Config('yourServiceName', 'yourOperatorName', 'yourOperatorPwd');
$upyun = new Upyun($config);
  • yourServiceName:存储桶名称(服务名称)
  • yourOperatorName:操作员用户名
  • yourOperatorPwd:操作员授权密码

上传文件

在上传文件时,有个钩子叫 wp_generate_attachment_metadata ,可以用它来获取图片信息然后上传到又拍云。钩子传递两个参数 $metadata$attachment_id ,分别是元数据信息和附件id。我们可以用一个函数来调用对象,然后在函数里使用upyun对象。

获取文件类型

wp对图片有特殊的处理,如裁剪不同尺寸等,我们需要手动适配一下。先拿到附件id和元数据,然后进行判断筛选,利用循环将其上传,可以通过以下代码获取附件路径和类型,然后对其判断:

$file_path = get_attached_file($attachment_id);
$filetype = wp_check_filetype($file_path);

if (isset($filetype['type']) && strpos($filetype['type'], 'image') !== false)

然后获取主图与裁剪后的图片:

// 获取图片主图和所有裁剪版本
$files = [];
if (isset($metadata['file'])) {
  $files['full'] = $metadata['file'];
}
$sizes = ['large', 'medium', 'thumbnail'];
foreach ($sizes as $size) {
  if (isset($metadata['sizes'][$size]['file'])) {
    $files[$size] = $metadata['sizes'][$size]['file'];
  }
}

然后再循环获取他们的实际路径和需要上传到云存储的路径,上传代码:

$upload_file = fopen($file, 'r');
$upyun -> write($upload_path, $upload_file, true);
  • $upload_path:存储在本地的路径
  • $upload_file:上传云存储的路径

删除文件

在删除附件时,wp提供了一个action:delete_attachment ,提供附件id参数,可以通过附件id获取元数据信息:

$metadata = wp_get_attachment_metadata($attachment_id);

上面判断方法可以不变,然后依然是循环判断:

if ($upyun -> has($upload_path)) {
  $upyun -> delete($upload_path, true);
}
  • $upyun -> has($upload_path):判断云存储里是否有这个文件
  • $upyun -> delete($upload_path, true):删除该文件

上传基本逻辑已经理清,接下来就是替换路径,可以使用钩子:upload_dir

$upload['baseurl'] = rtrim($domain, '/');
  • $upload['baseurl']:wp默认路径
  • $domain:就是最开始使用的加速域名,将wp默认的域名路径替换成加速域名即可。

代码片段

当然以上只是简单解析上传和删除流程,下面是一些实例代码:

以下代码经过博主修改后提供,仅供参考

上传与删除

require_once get_template_directory().'/vendor/autoload.php';
use Upyun\Upyun;
use Upyun\Config;

// 禁用图片尺寸
add_filter('intermediate_image_sizes_advanced', 'only_image_sizes', 10);
function only_image_sizes($sizes) {
  if (get_option('oyiso_ban_thumbnail')) {
    $allowed_sizes = ['full'];
  } else {
    $allowed_sizes = ['thumbnail', 'medium', 'large', 'full'];
  }
  
  foreach ($sizes as $size => $details) {
    if (!in_array($size, $allowed_sizes)) {
      unset($sizes[$size]);
    }
  }
  return $sizes;
}

// 截取域名路径
function replacePath() {
  $upyun_domain = 'https://upyun.oyiso.cn';
  $upyun_domain_path = parse_url($upyun_domain, PHP_URL_PATH);
  $upyun_domain_path_lastchar = substr($upyun_domain_path, -1);
  if ($upyun_domain_path_lastchar === '/') {
    $domain = $upyun_domain;
    $domain_path = $upyun_domain_path;
  } else {
    $domain = $upyun_domain.'/';
    $domain_path = $upyun_domain_path.'/';
  }
  return array('domain' => $domain, 'path' => $domain_path);
}

// 替换所有文件url
add_filter('upload_dir', 'oyiso_upload_url', 20);
function oyiso_upload_url($upload) {
  $upload['baseurl'] = rtrim(replacePath()['domain'], '/');
  return $upload;
}

// 上传文件
add_filter('wp_generate_attachment_metadata', 'oyiso_upload_attachment', 10, 2);
function oyiso_upload_attachment($metadata, $attachment_id) {
  operationFile($attachment_id, $metadata, 'upload');
  return $metadata;
}

// 删除文件
add_action('delete_attachment', 'oyiso_delete_attachment');
function oyiso_delete_attachment($attachment_id) {
  $metadata = wp_get_attachment_metadata($attachment_id);
  operationFile($attachment_id, $metadata, 'delete');
}

// 文件操作
function operationFile($attachment_id, $metadata, $action) {
  $upload_dir = wp_upload_dir();
  $config = new Config('yourServiceName', 'yourOperatorName', 'yourOperatorPwd');
  $upyun = new Upyun($config);
  $file_path = get_attached_file($attachment_id);
  $filetype = wp_check_filetype($file_path);
  $replace_path = replacePath()['path'];

  // error_log(print_r($upload_dir, true));
  // error_log(print_r($metadata, true));

  // 如果是图片
  if (isset($filetype['type']) && strpos($filetype['type'], 'image') !== false) {
    // 获取图片主图和所有裁剪版本
    $files = [];
    if (isset($metadata['file'])) {
      $files['full'] = $metadata['file'];
    }
    $sizes = ['large', 'medium', 'thumbnail'];
    foreach ($sizes as $size) {
      if (isset($metadata['sizes'][$size]['file'])) {
        $files[$size] = $metadata['sizes'][$size]['file'];
      }
    }
    // error_log(print_r($files, true));
    foreach($files as $i => $f) {
      if ($i == 'full') {
        $path = '/'.$f;
        $file = $upload_dir['basedir'].'/'.$f;
      } else {
        $path = $upload_dir['subdir'].'/'.$f;
        $file = $upload_dir['path'].'/'.$f;
      }
      $upload_path = rtrim($replace_path, '/').$path;
      // error_log(print_r($upload_path, true));

      // 上传
      if ($action == 'upload') { 
        $upload_file = fopen($file, 'r');
        $upyun -> write($upload_path, $upload_file, true);
        // error_log(print_r($file, true));

        // 删除
      } else if ($action == 'delete') { 
        if ($upyun -> has($upload_path)) {
          $upyun -> delete($upload_path, true);
        }
      }
    }

  // 如果是其他文件
  } else {
    $file_name = basename($file_path);
    $upload_path = rtrim($replace_path, '/').$upload_dir['subdir'].'/'.$file_name;
    // error_log(print_r($upload_path, true));

    // 上传
    if ($action == 'upload') {
      $upload_file = fopen($file_path, 'r');
      $upyun -> write($upload_path, $upload_file, true);

      // 删除
    } else if ($action == 'delete') { 
      if ($upyun -> has($upload_path)) {
        $upyun -> delete($upload_path, true);
      }
    }
  }
}

云存储使用量

我使用的是wp的ajax功能来异步获取云存储使用量:

// 又拍云存储使用量
function upyun_usage() {
  // 检查用户权限
  if (!current_user_can('administrator')) {
    wp_send_json_error('failed');
  } else {
    $upyun_use = '未知';

    $config = new Config('yourServiceName', 'yourOperatorName', 'yourOperatorPwd');
    $upyun = new Upyun($config);
    function formatBytes($bytes, $precision = 2) {
      $units = array('B', 'KB', 'MB', 'GB', 'TB'); 
      $bytes = max($bytes, 0); 
      $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 
      $pow = min($pow, count($units) - 1); 
      $bytes /= pow(1024, $pow);
      return round($bytes, $precision) . ' ' . $units[$pow]; 
    } 
    try {
      $upyun_use_bytes = $upyun -> usage();
      $upyun_use = formatBytes($upyun_use_bytes);
    } catch (Exception $e) {
      $upyun_use = '未知';
    }

    wp_send_json_success($upyun_use);
  }
}
add_action('wp_ajax_upyun_usage', 'upyun_usage');

有趣的部分

然后这里有个特别注意的点,经过博主几天的测试,默认的sdk只能上传小于1mb的文件,大于1mb就会一直处于处理状态(就是一直转圈),这个过程可能会让一些配置较低的服务器直接宕机(死机)。但是又拍云文件管理会发现文件已经上传成功,只是没有正确返回结果。博主也是询问了又拍云客服,表示sdk已经暂停维护,并提供了一些相关文档(毫无作用)。

询问又拍云客服结果

在询问客服的同时也在文档中进行搜索,我发现一段代码,如下:

代码地址:https://techs.upyun.com/popular/PHP%20%E4%B8%8A%E4%BC%A0.html

<?php
    class upyun{
        const END_POINT = "http://v0.api.upyun.com";
        private $bucketname;
        private $username;
        private $password;
        private $ctimeout;
    
        public function __construct($bucketname, $username, $password, $ctimeout){
    
            $this->bucketname = $bucketname;
            $this->username = $username;
            $this->password = $password;
            $this->ctimeout = $ctimeout;
    
        }
    
        public function restupload($localpath, $savepath){
    
            $uri = "/{$this->bucketname}$savepath";
            $date = gmdate('D, d M Y H:i:s \G\M\T');
            $fsize = filesize($localpath);
            $signature = base64_encode(hash_hmac("sha1", "PUT&$uri&$date", md5("{$this->password}"), true));
            $header = array("Content-Length:$fsize", "Authorization:UPYUN {$this->username}:$signature", "Date:$date");
    
            $fh = fopen($localpath,'rb');
            $ch = curl_init(self::END_POINT.$uri);
            curl_setopt($ch, CURLOPT_INFILE, $fh);
            curl_setopt($ch, CURLOPT_INFILESIZE, $fsize);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
            curl_setopt($ch, CURLOPT_TIMEOUT, "{$this->ctimeout}");
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
            curl_exec($ch);
            $rsp_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            echo $rsp_code;
    
        }
    
    }
    
    $upyun = new upyun("服务名", "操作员账号", "操作员密码", 3600);
    $upyun->restupload("本地文件名", "/上传文件路径/文件名");
    
    ?>

当我拿到代码后进行正常的测试,发现无果后将timeout改成10秒后,你猜怎么着,结果返回了 100 ,正常来说不应该是 200 吗?后面会讲到。

好在我继续搜索文档时发现一篇文章:https://docs.upyun.com/guide/php_rest_guide/

这篇文章写的方法居然完美运行,实际代码如下:

<?php
    $bucketName   = 'demobucket';
    $operatorName = 'operator';
    $operatorPwd  = 'operatorpassword';

    //被上传的文件路径
    $filePath = 'assets/bar.txt';
    $fileSize = filesize($filePath);
    //文件上传到服务器的服务端路径
    $serverPath = 'foo.txt';
    $uri = "/$bucketName/$serverPath";


    //生成签名时间。得到的日期格式如:Thu, 11 Jul 2014 05:34:12 GMT
    $date = gmdate('D, d M Y H:i:s \G\M\T');
    $sign = md5("PUT&{$uri}&{$date}&{$fileSize}&".md5($operatorPwd));

    $ch = curl_init('http://v0.api.upyun.com' . $uri);

    $headers = array(
        "Expect:",
        "Date: ".$date, // header 中需要使用生成签名的时间
        "Authorization: UpYun $operatorName:".$sign
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_PUT, true);

    $fh = fopen($filePath, 'rb');
    curl_setopt($ch, CURLOPT_INFILE, $fh);
    curl_setopt($ch, CURLOPT_INFILESIZE, $fileSize);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

    $result = curl_exec($ch);
    if(curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200) {
        //"上传成功"
        echo '上传成功';
    } else {
        $errorMessage = sprintf("UPYUN API ERROR:%s", $result);
        echo $errorMessage;
    }
    curl_close($ch);

没想到可以正常上传并返回结果,虽然...但是我得找到真正的原因。于是请了朋友的帮助,在一个库为 GuzzleHttp 里找到了端倪。

当安装php-sdk后会自动下载 GuzzleHttp 库,而由于又拍云提供的sdk都是好几年前的且一直没有维护,http库中的有个机制:Expect: 100-Continue,他是客户端可以在发送大型请求主体之前,先与服务器进行一次预检,避免在服务器不愿意接收请求主体的情况下浪费网络带宽和服务器资源。如果服务器愿意接受请求主体,它会返回一个 100 Continue 状态码。这表示服务器已经准备好接收请求主体,客户端随后可以继续发送请求主体。

这一下就通透了,这就是上面我提到的为什么会返回100的原因了!当文件大于1mb时就会触发,这可能是客户端没做出对应的反应或者服务器没有继续处理,所以他会一直转圈,在sdk中将其关闭即可。

于是在又拍云php-sdk中,对文件:vendor/upyun/sdk/src/Upyun/Api/Rest.php,第85行做出修改。

修改如下(30、31):

/**
     * @return mixed|\Psr\Http\Message\ResponseInterface
     */
    public function send()
    {
        $client = new Client([
            'timeout' => $this->config->timeout,
        ]);

        $url = $this->endpoint . $this->storagePath;
        $body = null;
        if ($this->file && $this->method === 'PUT') {
            $body = $this->file;
        }

        $request = new Psr7\Request(
            $this->method,
            Util::encodeURI($url),
            $this->headers,
            $body
        );
        $authHeader = Signature::getHeaderSign($this->config,
            $this->method,
            $request->getUri()->getPath()
        );
        foreach ($authHeader as $head => $value) {
            $request = $request->withHeader($head, $value);
        }
        $response = $client->send($request, [
            'debug' => $this->config->debug, // 别忘了半角逗号
            'expect' => false, // 加入这一行
        ]);

        return $response;
    }

总结

制作一个功能很简单(bushi),先理清思路,然后动手😅(手动狗头)。在最后上传的时候出现个抓耳挠腮的问题,部分服务器可能不支持或未正确处理 Expect: 100-Continue 标头。在这种情况下,服务器可能会忽略该标头,导致客户端一直处于等待状态。