背景
最近,手头一个php的项目需要迭代文件的上传、下载等功能,打算使用minio作为文件服务。
网上搜索minio的相关资料,关于php的示例相对较少、零散。遂写下这篇文章,作为自己对minio使用的记录,主要包含:
1.minio server安装、web界面交互使用的介绍;
2.aws-sdk-php的安装;
3.php对minio的交互使用,诸如:上传、下载、创建桶。
希望这篇文章对你有一丁点帮助。
实践
1. minio是什么?
官方定义,MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
个人理解,当你不打算买oss服务,但是想要一个文件服务来支撑你的业务需求,可以考虑使用minio来存储、管理你的文件,而不是把文件放在自己搭建的linux服务器上的特定目录下,自己去写一套api去维护文件管理服务。
2. 如何安装minio
既然minio作为文件服务器,提供了一个web交互界面作为文件管理的可视化服务。我们先安装一个minio来感受一下它的直观功能。
现在是2021年,docker已经流行了很多年了,这里我直接用docker来安装minio,安装步骤:
首先,去dockerhub官方镜像仓库(传送门:https://hub.docker.com/)查找minio的镜像介绍页面,
镜像的介绍主页会包含镜像的版本信息、拉取镜像的命令、镜像的启动命令、启动可选配置项、以及该镜像的其他使用说明!
然后,我们启动命令是:
docker run -d --name minio-server -p 9000:9000 -v /home/wzp/minio-data:/data minio/minio server /data
The MinIO deployment starts using default root credentials minioadmin:minioadmin
启动后,查看容器状态,如下所示:
说明:
注意-v挂载的目录是用于持久化minio文件存储服务的宿主机绝对路径,在/home/wzp/minio-data下面存的就是你上传到minio服务器的文件。
最后,咱们访问一下我们启动的minio容器为我们提供的web交互服务,打开你的浏览器,
输入:http://10.251.9.189:9000 (注意:这里的ip和端口号,取决于你容器的宿主机的ip和容器启动后和宿主机的端口号映射关系,-p 9000:9000 是冒号前面的端口号,才代表你的宿主机的端口号)
点击登陆,展示如下图:
3.编程语言如何使用minio
到这一步,我们已经安装好了minio的server服务器端。
那我们现在如何通过编程语言来和minio server通信呢?语言作为minio client,minio server已经为我们定义好了一套标准的api,我们只要在客户端调用api就能和minio server进行交互,完成诸如:上传、下载、删除文件、创建桶等操作。
通过访问minio的官网,我们得知官方已经提供了很多现有的sdk(针对多语言的sdk)来简化client端的交互体验。
传送门:https://docs.min.io/docs/java-client-quickstart-guide.html
不要慌,php有单独的客户端sdk,但是叫aws-sdk-php,介绍如下:
https://docs.min.io/docs/how-to-use-aws-sdk-for-php-with-minio-server.html
里面介绍了minio的安装和aws-sdk-php的composer安装方式!
安装成功!
如果你不是用的阿里镜像源,可能会遇到这种报错:
原因是,随着aliyun对国外资源的镜像站支持的日益完善,诸如:
https://pkg.phpcomposer.com/
https://php.cnpkg.org/
https://learnku.com/articles/30758
已经慢慢不支持镜像同步功能了。
直接替换为aliyun的镜像地址,方法如下:
//1.全局替换,所有项目的镜像站地址都替换,下次composer download直接去阿里云拿数据
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
//2.刷新 composer.lock 文件(不执行这一步,composer.lock里面已存在的包的下载依赖地址还是老镜像地址;运行 composer install 后会出现 404 并从源仓库开始下载,导致下载速度非常慢。这种情况即使是你全局配置了加速镜像也会出现)
composer update nothing
//3.再次执行composer require aws-sdk-php
体会到什么了吗?
只有商用的东西,才是最稳定的,因为背后会有资金源源不断地支撑。
以后关于国外资源,诸如:docker镜像、composer镜像、各种linux服务器的镜像、apt源、yum源第一时间都考虑使用aliyun的服务,毕竟阿里云的服务器生意天生就需要对这些国外资源进行支撑,方便购买服务器的人使用!
4.php的代码部分的展示
下面废话不多说了,上一段代码,包含基于minio的api实现文件的上传、删除、下载。
<?php
namespace app\index\controller;
use app\index\model\MinioClient;
use think\Db;
use think\Exception;
use app\index\traits\CommonMethod;
use think\exception\ValidateException;
use think\Log;
use Aws\S3\Exception\S3Exception;
/**
* 功能概述:
* 样本管理中心,负责样本上传、样本删除、样本投放、样本投放历史列表、样本下载等功能
* Class RuleOperation
* @package app\index\controller
*/
class SampleOperation extends CommonController
{
//调用接口传递过来的json字符串
protected $reqContent;
//json数组
protected $reqJsonArr;
//upload file max size
protected $uploadFileMaxSize=256*1024*1024;
//upload file allowed ext
protected $uploadFileAllowedExt=['apk'];
//upload file allowed MIME type
protected $uploadFileAllowedMIME=['application/java-archive'];
//upload file save temp dir()
protected $uploadFileSavePath=ROOT_PATH .'uploads'.DS."sample_file".DS;
//在调用该控制器的其他自定义方法前,会先触发_initialize;连带触发
public function _initialize()
{
parent::_initialize();
//当前控制器内公用
$this->reqContent = file_get_contents('php://input');
$this->reqJsonArr = json_decode($this->reqContent, true);
}
/**
* 样本文件上传(仅支持单文件上传,前端要做多文件上传,需要多次调用该接口)
*/
public function upload(){
//0.暂存目录创建
$path=$this->uploadFileSavePath;
if(!is_dir($path)){
$done=mkdir($path);
if(!$done){
Log::write("文件目录生成失败:$path",'info');
throw new ValidateException("文件目录生成失败");
}
}
//1.接收文件
$file = request()->file('upload_file');
//2.校验文件大小(<=256MB)、扩展名:.apk 、MIME类型: application/vnd.android
$oldName=$_FILES['upload_file']['name'];
$info = $file
->validate(['size'=>$this->uploadFileMaxSize,'ext'=>$this->uploadFileAllowedExt,'type'=>$this->uploadFileAllowedMIME])
->rule(function () use ($oldName){ //自定义生成形如:20210317-155656-12-原文件名的文件名
$preTimeName=produceDateWithMicrosecond();
return $preTimeName.'-'.$oldName;
})
->move($this->uploadFileSavePath);
if(!$info){
// 上传失败获取错误信息
throw new ValidateException($file->getError());
}
$infoArr=$info->getInfo();
$saveFileName=$info->getFilename();
$ext=$info->getExtension();
//3.上传文件到minio服务器
try {
//实例化minio的客户端
$minioClient=MinioClient::getInstance();
$minioConfig=config('minio');
//判断桶是不是存在;不存在,就创建一个
if(!$minioClient->doesBucketExist($minioConfig['bucket'])){
$minioClient->createBucket(['Bucket' =>$minioConfig['bucket']]);
}
//上传
$fullFileName=$path.$saveFileName;
$minioClient->putObject([
'Bucket' => $minioConfig['bucket'],
'Key' => $minioConfig['prefix'].$saveFileName, //bucket作为桶的名字,是顶层的文件目录;剩余下级目录的表示,通过Key来实现
'Body' => fopen($fullFileName, 'r'),
'ACL' => $minioConfig['acl'],
]);
} catch (S3Exception $e) {
throw new ValidateException($e->getAwsErrorMessage());
}
//5.计算样本文件的md5值返回给前端
$sampleApkMd5=md5_file($fullFileName);
//6.记录上传日志记录
try{
$insertData=[
'sample_hash'=>$sampleApkMd5,
'user_id'=>$this->getUserId(),
'old_name'=>$oldName,
'save_name'=>$saveFileName,
'ext'=>$ext,
'size'=>$infoArr['size'], //单位是字节Byte
'minio_bucket_name'=>$minioConfig['bucket'],
'minio_prefix'=>$minioConfig['prefix'],
'create_time'=>date('Y-m-d H:i:s')
];
Db::table('hl_upload_sample_file_log')->insert($insertData);
}catch(Exception $e){
Log::write($e->getMessage(),'info');
throw new ValidateException('系统异常,请稍后再试');
}
$frontRes=["sample_hash"=>$sampleApkMd5,"size"=>$infoArr['size']];
return response(buildSuccessResponseJson($frontRes),200,[],'json');
}
//下载样本接口(从minio下载)
public function download(){
if(empty($this->reqJsonArr["sample_hash"])){
throw new ValidateException('sample_hash不能为空');
}
$sampleHash=$this->reqJsonArr["sample_hash"];
//根据样本hash获取minio_path作为后续的key参数
$key=Db::table('hl_sample_put_record')->where('sample_hash',$sampleHash)->value('minio_path');
if(empty($key)){
Log::write($sampleHash.'对应的hl_sample_put_record记录的minio_path异常,为空!','sql');
throw new ValidateException('数据异常,请反馈给管理员');
}
try {
$minioClient=MinioClient::getInstance();
$minioConfig=config('minio');
// Get the object.
$result = $minioClient->getObject([
'Bucket' => $minioConfig['bucket'],
'Key' => $key
]);
// Display the object in the browser.
header("Content-Type: {$result['ContentType']}");
echo $result['Body'];
} catch (S3Exception $e) {
Log::write($e->getMessage(),'notice');
throw new ValidateException('数据异常,请反馈给管理员');
}
}
//删除已上传,却未投放的样本(只删minio的文件,至于PHP特定目录的暂存文件,通过command命令行脚本实现定时删除一批旧数据)
public function delSampleApk(){
if(empty($this->reqJsonArr["sample_hash"])){
throw new ValidateException('sample_hash不能为空');
}
$sampleHash=$this->reqJsonArr["sample_hash"];
try{
//检查样本是否存在于已投放列表
$isExist=Db::table('hl_sample_put_record')->where('sample_hash',$sampleHash)->count();
if($isExist){
throw new ValidateException('该样本已投放,不能删除!');
}
//查询上传日志表
$uploadFileInfo=Db::table('hl_upload_sample_file_log')
->field('minio_bucket_name,minio_prefix,save_name')
->where('sample_hash',$sampleHash)
->order('id','desc')
->find();
if(empty($uploadFileInfo)){
Log::write($sampleHash.'对应的hl_upload_sample_file_log记录的minio信息异常,为空!','sql');
throw new ValidateException('数据异常,请反馈给管理员');
}
}catch(Exception $e){
Log::write($e->getMessage(),'sql');
throw new ValidateException('系统异常,请稍后再试');
}
//删除minio的文件
try {
$minioClient=MinioClient::getInstance();
$minioClient->deleteObject([
'Bucket' => $uploadFileInfo['minio_bucket_name'],
'Key' => $uploadFileInfo['minio_prefix'].$uploadFileInfo['save_name'],
]);
return response(buildSuccessResponseJson([]),200,[],'json');
} catch (S3Exception $e) {
Log::write($e->getMessage(),'notice');
throw new ValidateException('系统异常,请稍后再试');
}
}
<?PHP
namespace app\index\model;
use Aws\S3\S3Client;
class MinioClient
{
protected $options = [
'endpoint' => 'http://127.0.0.1:9000',
'version' => 'latest',
'region' => 'cn-north-1', //China (Beijing)
'use_path_style_endpoint' => true,
'credentials' => [
'key' => 'minioadmin',
'secret' => 'minioadmin',
],
'bucket' => 'wxqb',
'prefix' => 'sample_apk/', //自定义的键名,bucket作为桶的名字,是顶层的文件目录;剩余下级目录的表示,通过Key来实现;会存在桶名下的prefix目录下
'acl' => 'public-read',
];
protected static $instance;
protected $handler;
/**
* 构造函数
* @param array $options 缓存参数
* @access public
*/
public function __construct($options = [])
{
if(empty($options)) {
$options = config('minio'); //构造没穿参数,就从配置读取
if (!empty($options)) {
$this->options = array_merge($this->options, $options);
}
}
$this->handler = new S3Client([
'version' => $this->options['version'],
'region' => $this->options['region'],
'endpoint' => $this->options['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $this->options['credentials']['key'],
'secret' => $this->options['credentials']['secret'],
],
]);
}
/**
* 单例模式 获取实例
* @return MinioClient
*/
public static function getInstance()
{
if(empty(self::$instance)) {
self::$instance = new MinioClient();
}
return self::$instance;
}
/**
* call me
* @param $name
* @param $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
return call_user_func_array(
array($this->handler, $name),
$arguments);
}
}
<?php
return array(
//minio server
'minio'=>[
'endpoint' => 'http://10.251.9.189:9000',
'version' => 'latest',
'region' => 'cn-north-1',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => 'minioadmin', //用户名
'secret' => 'minioadmin', //密码
],
'bucket' => 'wxqb', //桶名,其实就是一级目录的名称(一般就用项目名)
'prefix' => 'sample_apk/', //自定义的键名,bucket作为桶的名字,是顶层的文件目录;剩余下级目录的表示,通过Key来实现;会存在桶名下的prefix目录下
'acl' => 'public-read',
],
);
如果有接口参数不详的还可以参考,aws_sdk_php的英文接口文档:
https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.S3ClientInterface.html#_doesBucketExist
https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#deleteobjects
https://docs.aws.amazon.com/code-samples/latest/catalog/php-s3-s3-downloading-object.php.html