背景
最近在开发一个项目时,遇到了一个打包下载文件的需求。而且它不仅仅是一个简单粗暴的直接打包,还需要按照目录层级打包。具体如下图:

大家开发这个需求的第一反应可能是去找各种各样的开源库、打包工具,然后写各种复杂的遍历、建目录等。然而在云服务大行其道的今天,合理使用云服务能够很轻松地解决我们的问题。比如七牛云服务提供的文件打包功能(mkzip),它的好处有以下几点:
- 功能接入简单,api易理解,提供各种主流开发语言的SDK
- 对于少量文件与大量文件打包,提供不同地打包方式
- 可以根据文件名或者文件别名自动建立目录层级
- 问题反馈迅速。相比开源库,你在官网提交一个工单,可以很快地得到售后工程师的答复。
缺点:
-
只支持异步操作。如果想要获取操作结果,需要调用其它api或者给七牛云提供一个回调通知地址。
-
在私有化环境当中,如果无法连结外网的话,云服务便无法使用。当然一般的云服务商也都提供私有化版本。
接下来让我们具体看看如何使用七牛云打包下载功能。另外本篇文章以实例代码为主,背后理论和原理请看七牛云官方文档:
Demo
本demo实现的目标是将位于七牛云服务上的两个文件按照目录层级打包成可下载的zip文件。两个文件分别是https://file.demo.com/test1.txt
、https://file.demo.com/test2.txt
。另外我们要将test1.txt
放置在一级目录/二级目录
文件夹下,而test2.txt
则放置在根目录下。一些共有的参数分别需要注意的是:
- accessKey和secretKey:七牛云服务的key配置
- zipFileName:打包所生成的zip文件名
- saveBucket:打包后的文件需要存储到哪一个资源空间
- srcBucket:被打包的文件来源的资源空间。需要和saveBucket保持一致
- notifyURL: 处理结果通知接收 URL,七牛将会向设置的URL发起 Content-Type: application/json 的 POST 请求
- force: 1或者true会直接覆盖原来同样的任务操作结果,0或者false则不会覆盖。
- pipeline:多媒体队列,如果没有的话需要在管理后台新建一个

依赖
Nodejs
npm i qiniu
Java
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.2.0</version>
</dependency>
少量文件压缩
在及其少量文件打包的场景下,建议使用这种方式。相较于大量文件打包,这种方式更简单直接,代码量更少,但同时请求的命令字符串长度不能超过2048字节。
Nodejs
'use strict';
const qiniu = require('qiniu');
const accessKey = 'accessKey';
const secretKey = 'secretKey';
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const config = new qiniu.conf.Config();
config.zone = qiniu.zone.Zone_z2;
const operManager = new qiniu.fop.OperationManager(mac, config);
// 处理指令集合
const saveBucket = 'demo-bucket';
const files = [
{
url: 'https://file.demo.com/test1.txt',
alias: '一级目录/二级目录/test1.txt',
},
{
url: 'https://file.demo.com/test2.txt',
},
];
let urlAndAlias = '';
files.forEach(file => {
urlAndAlias += `/url/${qiniu.util.urlsafeBase64Encode(file.url)}`;
if (file.alias) {
urlAndAlias += `/alias/${qiniu.util.urlsafeBase64Encode(file.alias)}`;
}
});
const fops = [`mkzip/2${urlAndAlias}|saveas/${qiniu.util.urlsafeBase64Encode(`${saveBucket }:demo.zip`)}`];
const pipeline = 'pipeline';
const srcBucket = 'demo-bucket';
// srcKey不影响实际操作结果,但是根据七牛云规范,需要传入一个该资源空间实际存在的资源key。所以这里可以使用已经存在的test1.txt
const srcKey = 'test1.txt';
const options = {
notifyURL: 'http://api.example.com/pfop/callback',
force: true,
};
// 持久化数据处理返回的是任务的persistentId,可以根据这个id查询处理状态
operManager.pfop(srcBucket, srcKey, fops, pipeline, options, (err, respBody, respInfo) => {
if (err) {
throw err;
}
if (respInfo.statusCode === 200) {
console.log(respBody.persistentId);
} else {
console.log(respInfo.statusCode);
console.log(respBody);
}
});
Java
List<String> fileUrlList = new ArrayList<>(Arrays.asList("https://file.demo.com/test1.txt", "https://file.demo.com/test2.txt"));
List<String> aliasList = new ArrayList<>(Arrays.asList("一级目录/二级目录/test1.txt", ""));
String urlAndAlias = "";
for(int i = 0 ; i < fileUrlList.size(); i++) {
urlAndAlias += "/url/" + UrlSafeBase64.encodeToString(fileUrlList.get(i));
if (StringUtils.isNotEmpty(aliasList.get(i))) {
urlAndAlias += "/alias/" + UrlSafeBase64.encodeToString(aliasList.get(i));
}
}
//待处理文件所在空间
String srcBucket = "demo-bucket";
String saveBucket = srcBucket;
// srcKey不影响实际操作结果,但是根据七牛云规范,需要传入一个该资源空间实际存在的资源key。所以这里可以使用已经存在的test1.txt
String srcKey = "test1.txt";
String zipFileName = "demo_java.zip";
Auth auth = Auth.create(accessKey, secretKey);
//数据处理指令,支持多个指令
String persistentOpfs = String.format("mkzip/2%s|saveas/%s", urlAndAlias, UrlSafeBase64.encodeToString(saveBucket+ ":"+ zipFileName));
//其它参数
StringMap options = new StringMap();
//数据处理队列名称
options.put("pipeline", "pipeline");
//数据处理完成结果通知地址
options.put("notifyURL", "http://api.example.com/qiniu/pfop/notify");
options.put("force", 1);
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(Zone.zone2());
OperationManager operationManager = new OperationManager(auth, cfg);
try {
String persistentId = operationManager.pfop(srcBucket, srcKey, persistentOpfs, options);
//可以根据该 persistentId 查询任务处理进度
System.out.println(persistentId);
} catch (QiniuException e) {
System.err.println(e.response.toString());
}
大量文件压缩
为了将大量文件压缩,可以将待压缩文件url写入一个索引文件,上传至资源空间,再对该索引文件进行的mkzip操作。这一系列操作可以通过2个请求完成,但也可以只通过一个请求完成。我们这里只介绍第二种最便捷的方法。
Nodejs
'use strict';
const qiniu = require('qiniu');
const fs = require('fs');
const accessKey = 'accessKey';
const secretKey = 'secretKey';
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
// 构造执行打包操作的上传token
const srcBucket = 'demo-bucket';
const zipFileName = 'demo.zip';
const savedZipEntry = qiniu.util.urlsafeBase64Encode(`${srcBucket}:${zipFileName}`);
const persistentOps = `mkzip/4/|saveas/${savedZipEntry}`;
const options = {
scope: srcBucket,
persistentOps,
// 数据处理队列名称,必填
persistentPipeline: 'pipeline',
// 数据处理完成结果通知地址
persistentNotifyUrl: 'http://api.example.com/qiniu/pfop/notify',
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
// 拼装数据写入索引文件
// files不能超过三千个
const files = [
{
url: 'https://file.demo.com/test1.txt',
alias: '一级目录/二级目录/test1.txt',
},
{
url: 'https://file.demo.com/test2.txt',
},
];
let urlAndAlias = '';
files.forEach(file => {
urlAndAlias += `/url/${qiniu.util.urlsafeBase64Encode(file.url)}`;
if (file.alias) {
urlAndAlias += `/alias/${qiniu.util.urlsafeBase64Encode(file.alias)}`;
}
urlAndAlias += '\n';
});
// 为了演示方便,直接在当前目录生成索引文件
const indexFilePath = 'index.txt';
fs.writeFileSync(indexFilePath, urlAndAlias, { encoding: 'utf-8' });
// 上传索引文件,并在云端执行打包操作
const config = new qiniu.conf.Config();
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
const key = indexFilePath;
formUploader.putFile(uploadToken, key, indexFilePath, putExtra, (respErr, respBody, respInfo) => {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode === 200) {
// 根据persistentId可以查询任务执行状态
console.log(respBody.persistentId);
} else {
console.log(respInfo.statusCode);
console.log(respBody);
}
});
Java
String accessKey = "accessKey";
String secretKey = "secretKey";
String zipFileName = "demo_java.zip";
String saveBucket = "demo-bucket";
String srcBucket = saveBucket;
Auth auth = Auth.create(accessKey, secretKey);
StringMap putPolicy = new StringMap();
//数据处理指令
String zipFop = String.format("mkzip/4/|saveas/%s", UrlSafeBase64.encodeToString(saveBucket + ":" + zipFileName));
putPolicy.put("persistentOps", zipFop);
//数据处理队列名称,必填
putPolicy.put("persistentPipeline", "pipeline");
//数据处理完成结果通知地址
putPolicy.put("persistentNotifyUrl", "http://api.example.com/qiniu/pfop/notify");
long expireSeconds = 3600;
String upToken = auth.uploadToken(srcBucket, null, expireSeconds, putPolicy);
//构造一个带指定Zone对象的配置类
Configuration cfg = new Configuration(Zone.zone2());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//默认不指定key的情况下,以文件内容的hash值作为文件名
String srcKey = "index_java.txt";
List<String> fileUrlList = new ArrayList<>(Arrays.asList("https://file.demo.com/test1.txt", "https://file.demo.com/test2.txt"));
List<String> aliasList = new ArrayList<>(Arrays.asList("一级目录/二级目录/test1.txt", ""));
Path path = null;
try {
// 创建临时索引文件
path = Files.createTempFile("indexTempFile", ".txt");
for(int i = 0 ; i < fileUrlList.size(); i++) {
String line = "/url/" + UrlSafeBase64.encodeToString(fileUrlList.get(i));
if (StringUtils.isNotEmpty(aliasList.get(i))) {
line += "/alias/" + UrlSafeBase64.encodeToString(aliasList.get(i));
}
// 按行写入
Files.write(path,
Arrays.asList(line),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
// 应用退出时删除临时文件
path.toFile().deleteOnExit();
} catch (IOException e) {
}
try {
Response response = uploadManager.put(path.toString(), srcKey, upToken);
//解析上传成功的结果
Map putRet = new Gson().fromJson(response.bodyString(), Map.class);
// 根据persistentId可以查询任务执行状态
System.out.println(putRet.get("persistentId"));
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
调试
在开发过程当中,可以通过使用gethttps://api.qiniu.com/status/get/prefop?id={persistentId}
,查看打包任务的执行状况

以上回包字段与七牛云post开发者提供的notifyURL的请求字段一致。
生成打包文件链接
将域名与zipFileName拼接即可。如http://file.demo.com/demo.zip
。


最后
七牛云不仅拥有压缩打包功能,还有音频、视频转码等文件操作功能。在合适的场景下使用云服务可以达到事半功倍的效果。