对象存储是什么?这里直接引用一下:

对象存储(Cloud Object Storage,COS)是腾讯云提供的一种存储海量文件的分布式存储服务,具有高扩展性、低成本、可靠安全等优点。通过控制台、API、SDK 和工具等多样化方式,用户可简单、快速地接入 COS,进行多格式文件的上传、下载和管理,实现海量数据存储和管理。

说白了就是有一堆附加功能的高速网盘。因为用量不大,而且喜欢搞点花的,所以我用得还挺多,图片资源、文件分享、备份存档之类的常规应用场景都能用上。

其实阿里云、腾讯云、华为云都有类似的服务,也不知道和AWS S3谁先谁后,不过至少定位都差不多,理论上功能也是差不多的。不过使用体验价格还是略微有些差别。我之前一直用的是阿里云(主要是懒得探索),后面才发现AWS的不错,腾讯云的也不错,就迁移过去了。

为什么用腾讯云

首先,论价格的话似乎是要比另外两家稍贵一些的,但是差别不大。优势主要在实用性方面。

由于众所周知的原因,国内的网站部署多少有些麻烦,而国外的AWS自然是没有这个问题的,所以直接在S3上部署一个静态网站加上域名解析也很容易;但是阿里云就比较拧巴,而且连图片这类静态资源都不愿意正常响应。在一次偶然的尝试里,我发现腾讯云居然基本不设限制,而且HTTP响应头也可以自定义(后来发现其实阿里云的也可以),除却链接太长的问题以外,完全可以直接部署一个能用的静态网站。

另外一个比较吸引的地方是配套设施很全,包括经典的CLI工具(虽然有点笨蛋)、各种编程语言的SDK,还有功能姑且算是比较齐全的Web客户端、桌面客户端和手机端。在前面两个配套设施到处都有的情况下,一套能用且好用的客户端对我这种弱弱的个人用户来说还是很方便的。在需要和别人合作的时候,不需要强迫别人接受Geek的工作方式,又可以兼顾自己的工作效率,也是加分点。

自造轮子的动机

一般情况下,客户端已经非常够用。分几个简单的Bucket,可以手动建立子账号然后分配权限,各得其所,一人发一个链接、一个ID、一个Key就完事了。但是如果是不一般的情况呢?比如我设想的应用场景:

如果我想用这玩意收作业呢?

为什么要折腾这个?传统的模式当然是用邮件收发,也附带交流功能,但是这要求助教发挥工匠精神一个一个确认下载,分类。虽然应该也可以一键导出,但还是省不了分类这一步。而且,邮件服务提供商也经常有莫名其妙的拒收,非常逆天。

备选方案是学校网盘。比如,BNU的师大云盘有比较完善的权限管理、版本控制功能,带宽也不错,可惜有一点小问题:读权限和写权限不太分离,交作业的时候可能可以看到别人的进度,而且授权也需要工匠精神。而且,其他网盘说不定没有颗粒度这么细的权限管理。

浏览文档目录之后,我发现COS可以用SDK生成临时授权的Key和ID,除了有效期限以外,有最完整的权限管理,包括但不限于文件前缀、IP地址、列举文件、读、写、完全控制。

这个颗粒度对于完美实现收作业来说是很有必要的:

  • 有效期保证学生只能在指定时间内提交;
  • 文件前缀强迫学生必须按照规定格式命名;
  • 不授权文件列举,保证学生不会被别人卷到;
  • 根据需要设置能不能多次提交、检查。

其实子账号也可以实现类似的功能,但是显得有些大炮打蚊子,而且会在看起来就很重要的访问控制里留下一堆答辩。而临时授权不留痕迹,到期自动失效,很清爽。

但是问题来了:所有官方客户端都不支持临时授权的Key和ID,因为会附带一个额外的SessionToken字段。考虑到Javascript SDK可以干这个,应该不麻烦,所以我就干脆试了一下动手做Web UI实现这个功能。

生成临时授权文件

在上面设想的情景里,助教应该是有学生邮箱的,而且给学生发一个纯文本的JSON应该也不大可能会被拒收。这里用Python实现,加上了Fire的命令行模式。大部分是从官方demo里面抄的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import fire
import json
import os

from sts.sts import Sts # 记得装一下

def get_credential_demo():
config = {
# 请求URL,域名部分必须和domain保持一致
# 使用外网域名时:https://sts.tencentcloudapi.com/
# 使用内网域名时:https://sts.internal.tencentcloudapi.com/
'url': 'https://sts.tencentcloudapi.com/',
# 域名,非必须,默认为 sts.tencentcloudapi.com
# 内网域名:sts.internal.tencentcloudapi.com
'domain': 'sts.tencentcloudapi.com',
# 临时密钥有效时长,单位是秒
'duration_seconds': 60*60*12,
'secret_id': os.environ['COS_SECRET_ID'],
# 固定密钥
'secret_key': os.environ['COS_SECRET_KEY'],
# 设置网络代理
# 'proxy': {
# 'http': 'xx',
# 'https': 'xx'
# },
# 换成你的 bucket
'bucket': 'hoshino-test-1304089692',
# 换成 bucket 所在地区
'region': 'ap-guangzhou',
# 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
# 例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
'allow_prefix': ['temp/*'],
'allow_actions': [
# # 列举文件
# 'name/cos:GetBucket',
# 简单上传
'name/cos:PutObject',
'name/cos:PostObject',
# 分片上传
'name/cos:InitiateMultipartUpload',
'name/cos:ListMultipartUploads',
'name/cos:ListParts',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload'
],

"condition": {
"bool_equal": {
"cos:secure-transport": "true" # 这里设置成是个人都行
}
}
}

try:
sts = Sts(config)
response = sts.get_credential()
output = {
'StartTime': response['startTime'],
'ExpiredTime': response['expiredTime'],
'SecurityToken': response['credentials']['sessionToken'],
'TmpSecretId': response['credentials']['tmpSecretId'],
'TmpSecretKey': response['credentials']['tmpSecretKey']
}
print(json.dumps(output))
with open('credential.json', 'w', encoding='utf-8') as f:
json.dump(output, f, indent=4)

except Exception as e:
print(e)

if __name__ == '__main__':
fire.Fire(get_credential_demo)

这样就会存出来一个credential.json,发给用户就行。

调用JavaScript SDK

在我们需要的极简界面里,只需要三个控件:

  • 选择授权文件的按钮;
  • 选择待上传文件的按钮;
  • 上传文件的按钮。

前两个比较烂大街,随便弄一下就行了,还得是第三个。参考官方文档,我们首先要初始化,把这部分脚本放在靠前的<script>标签里。虽然getAuthorization是到调用的时候才会执行,但还是得提前定义一下,所以我把读取授权信息的逻辑也写在这个位置了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Bucket = 'hoshino-test-1304089692';
var Region = 'ap-guangzhou';
var cos = new COS({
getAuthorization: function (options, callback) {
var fileInput = document.getElementById('credentials'); // 选择凭证的Input
var file = fileInput.files[0];
if (!file) {
alert('没有选择密钥文件!');
return;
}
var reader = new FileReader();
reader.readAsText(file);
reader.onload = function() {
var key = JSON.parse(this.result);
callback(key);
}
}
});

上传也很好办,而且高级API会异步返回进度,所以我干脆搞了一个提示状态的文本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
document.getElementById('button-submit').addEventListener('click', function() {
var file = document.getElementById('uploadee').files[0]; // 选择待上传文件的Input
if (!file) {
alert('没有选择文件!');
return;
}
cos.uploadFile({
Bucket: 'hoshino-test-1304089692',
Region: 'ap-guangzhou',
Key: 'temp/' + file.name,
Body: file,
onProgress: function (progressData) {
document.getElementById('status-text').innerText = '上传中... '
+ parseInt(progressData.percent * 100) + '%';
},
}, function(err, data) {
console.log(err || data);
document.getElementById('status-text').innerHTML = err?
'上传失败' : '成功,校验信息如下。<br>MD5 = ' + eval(data.ETag) + ',<br>请求ID = ' + data.RequestId;
});
});

然后再随便加点CSS就完工了。

效果图

至此就完成了我们的目标。

效果演示:链接

打包源码: cos-interface.7z