前言
首先要承认一个错误,那就是我有点“哗众取宠”了,这个文章的题目,更确切来说应该是:通过Serveerless Framework部署在腾讯云上的项目,应该如何实现文件上传功能?
说到文件上传,可能对传统开发来说貌似并没有太多的东西需要分享,但是在Serverless架构下,就要说一下了,其实无论是腾讯云的SCF,还是AWS的Lambda,在通过API网关触发函数的时候,都是有一个数据包大小限制的。以腾讯云为例,这个限制是6M。
这个6M的限制应该怎么理解呢?可认为是如果我们要在客户端(可能是Web,可能是小程序或者其他)上传通过API网关一个文件到SCF,那么这个整体的数据包不能超过6M,而云函数对接受二进制的内容,貌似先天缺少魂魄,所以通常情况下,推荐的是Base64之后再进行上传。众所周知,Base64之后的数据包大小可能会发生变化,所以实际上,通过这种方法,我们上传到云函数的数据包可能只有4M左右。
4M是一个什么概念呢?
上图所示,我拿出了我的手机,拍了一张图片,图片大小是6.21M,也就是说如果我想把这张图片上传到SCF来进行一些处理是“不可能”事件,或者说,我就做了一个相册的功能,我就直接把图片上传到SCF,SCF再存储到对象存储中,这个操作是因为数据包大小而被限制住。
所以此时此刻,就衍生出了第二种解决方法(不要说我时序图画的丑,因为这个不算时序图,看得懂就好了,哈哈哈哈哈)
在这个方法中,客户端发起三个请求,分别是获取临时上传地址、将文件上传到COS、获取处理结果(当然,如果不需要获取处理结果什么的,例如就是用户单纯的上传个文件到自己的账号下,那这种情况就不需要第三次请求了)。
相对于之前说的直接上传SCF,SCF再进行文件的持久化而言,这种方法可能不是很容易想到,而且实现起来稍微复杂一点。
针对几个不同场景的推荐:
- 场景1: 用户上传头像功能
针对这样的场景,其实直接选用方案1,就可以了。因为一般情况下,头像都不是很大的,完全可以在客户端对图像进行一次压缩和裁剪,完成之后,直接带着用户的一些参数,例如用户的token等,上传到SCF,在SCF中对图片转存到COS以及将图像和用户信息进行关联,并将某些结果返回给客户端。整个流程只需要一个函数,方便快捷。
- 场景2: 用户上传图片到相册系统中
针对这样的场景,其实方案2是更好的,因为很多时候上传图片到相册,都是会希望保留原图,而不希望被压缩,那么原图大小很可能超过6M,方案1也并不是十分合理,而且APIGW+SCF的组合,本身就不是非常适合进行文件的传输等,这个时候优先上传COS是比较合理的方案。
用户可以带着图像要上传的相册以及图片名称,用户的token发起获取临时密钥到函数1中,函数1将用户、相册、图片以及状态(例如待上传、待处理、已处理等)等信息关联,并且存储,然后将临时地址返回给客户端,客户端将图片上传到COS中,通过COS触发器触发函数2,函数2对图像进行压缩(一般情况下,相册列表都会显示压缩图片,点到相册详情才会有完整的无损图片),并且和之前信息进行关联,修改数据状态。在用户上传图片完成之后,如果有需要,客户端就可以发起第三次请求获取图像存储/处理结果,函数3会查询数据库状态,在某个时间阈值内,如果数据状态是完成,则表示数据已经上传并且完成了部分处理,否则会返回对应的异常信息。
Show CCCCCCCCCode
接下来分享上面两种方法的实现过程:
函数1,实现第一种方案,文件通过Base64,传递到SCF,由SCF转存到COS:
def uploadToScf(event, context):
print('event', event)
print('context', context)
body = json.loads(event['body'])
# 可以通过客户端传来的token进行鉴权,只有鉴权通过才可以获得临时上传地址
# 这一部分可以按需修改,例如用户的token可以在redis获取,可以通过某些加密方法获取等
# 也可以是传来一个username和一个token,然后去数据库中找这个username对应的token是否
# 与之匹配等,这样会尽可能的提升安全性
if "key" not in body or "token" not in body or body['token'] != 'mytoken' or "key" not in body:
return {"url": None}
pictureBase64 = body["picture"].split("base64,")[1]
with open('/tmp/%s' % body['key'], 'wb') as f:
f.write(base64.b64decode(pictureBase64))
region = os.environ.get("region")
secret_id = os.environ.get("TENCENTCLOUD_SECRETID")
secret_key = os.environ.get("TENCENTCLOUD_SECRETKEY")
token = os.environ.get("TENCENTCLOUD_SESSIONTOKEN")
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token)
client = CosS3Client(config)
response = client.upload_file(
Bucket=os.environ.get("bucket_name"),
LocalFilePath='/tmp/%s' % body['key'],
Key=body['key'],
)
return {
"uploaded": 1,
"url": 'https://%s.cos.%s.myqcloud.com' % (
os.environ.get("bucket_name"), os.environ.get("region")) + body['key']
}
函数1,实现第二种方案,进行临时签名URL的获取:
def getPresignedUrl(event, context):
print('event', event)
print('context', context)
body = json.loads(event['body'])
# 可以通过客户端传来的token进行鉴权,只有鉴权通过才可以获得临时上传地址
# 这一部分可以按需修改,例如用户的token可以在redis获取,可以通过某些加密方法获取等
# 也可以是传来一个username和一个token,然后去数据库中找这个username对应的token是否
# 与之匹配等,这样会尽可能的提升安全性
if "key" not in body or "token" not in body or body['token'] != 'mytoken' or "key" not in body:
return {"url": None}
# 初始化COS对象
region = os.environ.get("region")
secret_id = os.environ.get("TENCENTCLOUD_SECRETID")
secret_key = os.environ.get("TENCENTCLOUD_SECRETKEY")
token = os.environ.get("TENCENTCLOUD_SESSIONTOKEN")
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token)
client = CosS3Client(config)
response = client.get_presigned_url(
Method='PUT',
Bucket=os.environ.get('bucket_name'),
Key=body['key'],
Expired=30,
)
return {"url": response.split("?sign=")[0],
"sign": urllib.parse.unquote(response.split("?sign=")[1]),
"token": os.environ.get("TENCENTCLOUD_SESSIONTOKEN")}
HTML页面基本实现:
HTML部分:
<div style="width: 70%">
<div style="text-align: center">
<h3>Web端上传文件</h3>
</div>
<hr>
<div>
<p>
方案1:通过上传到SCF,进行处理再转存到COS,这种方法比较直观,但是问题是SCF从APIGW处只能接收到小于6M的数据,而且对二进制文件处理并不好。
</p>
<input type="file" name="file" id="fileScf"/>
<input type="button" onclick="UpladFileSCF()" value="上传"/>
</div>
<hr>
<div>
<p>
方案2:
直接上传到COS,流程是先从SCF获得临时地址,进行数据存储(例如将文件信息存到redis等),然后再从客户端进行上传COS,上传结束可通过COS触发器触发函数,从存储系统(例如已经存储到redis)读取到更对信息,在对图像进行处理。
</p>
<input type="file" name="file" id="fileCos"/>
<input type="button" onclick="UpladFileCOS()" value="上传"/>
</div>
</div>
方案1上传部分JS:
function UpladFileSCF() {
var oFReader = new FileReader();
oFReader.readAsDataURL(document.getElementById("fileScf").files[0]);
oFReader.onload = function (oFREvent) {
const key = Math.random().toString(36).substr(2);
var xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"))
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
if (JSON.parse(xmlhttp.responseText)['uploaded'] == 1) {
alert("上传成功")
}
}
}
var url = " https://service-f1zk07f3-1256773370.bj.apigw.tencentcs.com/release/upload/cos"
xmlhttp.open("POST", url, true);
xmlhttp.setRequestHeader("Content-type", "application/json");
var postData = {
picture: oFREvent.target.result,
token: 'mytoken',
key: key,
}
xmlhttp.send(JSON.stringify(postData));
}
}
方案2上传部分JS:
function doUpload(key, bodyUrl, bodySign, bodyToken) {
var fileObj = document.getElementById("fileCos").files[0];
xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"));
xmlhttp.open("PUT", bodyUrl, true);
xmlhttp.onload = function () {
console.log(xmlhttp.responseText)
if (!xmlhttp.responseText) {
alert("上传成功")
}
};
xmlhttp.setRequestHeader("Authorization", bodySign);
xmlhttp.setRequestHeader("x-cos-security-token", bodyToken);
xmlhttp.send(fileObj);
}
function UpladFileCOS() {
const key = Math.random().toString(36).substr(2);
var xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"))
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var body = JSON.parse(xmlhttp.responseText)
if (body['url']) {
doUpload(key, body['url'], body['sign'], body['token'])
}
}
}
var getUploadUrl = 'https://service-f1zk07f3-1256773370.bj.apigw.tencentcs.com/release/upload/presigned'
xmlhttp.open("POST", getUploadUrl, true);
xmlhttp.setRequestHeader("Content-type", "application/json");
xmlhttp.send(JSON.stringify({
token: 'mytoken',
key: key,
}));
}
这里面可以看到获取用户密钥信息的方法是os.environ.get("TENCENTCLOUD_SECRETID"),想要通过这种方法获取密钥信息,需要给予函数相关的角色和对角色进行相关的权限,以Serverless Framework为例,可以使用tencent-cam-role,例如创建一个全局组件:
Conf:
component: "serverless-global"
inputs:
region: ap-beijing
runtime: Python3.6
role: SCF_UploadToCOSRole
bucket_name: scf-upload-1256773370
然后创建一个增加Role的组件:
UploadToCOSRole:
component: "@gosls/tencent-cam-role"
inputs:
roleName: ${Conf.role}
service:
- scf.qcloud.com
policy:
policyName:
- QcloudCOSFullAccess
接下来就是函数的创建,函数创建时需要绑定刚才的这个role:
getUploadPresignedUrl:
component: "@gosls/tencent-scf"
inputs:
name: Upload_getUploadPresignedUrl
role: ${Conf.role}
codeUri: ./fileUploadToCos
handler: index.getPresignedUrl
runtime: ${Conf.runtime}
region: ${Conf.region}
description: 获取cos临时上传地址
memorySize: 64
timeout: 3
environment:
variables:
region: ${Conf.region}
bucket_name: ${Conf.bucket_name}
同时将这个函数绑定APIGW:
UploadService:
component: "@gosls/tencent-apigateway"
inputs:
region: ${Conf.region}
protocols:
- http
- https
serviceName: UploadAPI
environment: release
endpoints:
- path: /upload/cos
description: 通过SCF上传cos
method: POST
enableCORS: TRUE
function:
functionName: Upload_uploadToSCFToCOS
- path: /upload/presigned
description: 获取临时地址
method: POST
enableCORS: TRUE
function:
functionName: Upload_getUploadPresignedUrl
另外,本例子还需要一个COS存储桶来作为测试使用,由于Web服务可能存在跨域问题,所以需要对COS进行跨域设置:
SCFUploadBucket:
component: '@gosls/tencent-cos'
inputs:
bucket: ${Conf.bucket_name}
region: ${Conf.region}
cors:
- id: abc
maxAgeSeconds: '10'
allowedMethods:
- POST
- PUT
allowedOrigins:
- '*'
allowedHeaders:
- '*'
完成之后,可以快速部署:
(venv) DFOUNDERLIU-MB0:test dfounderliu$ sls --debug
DEBUG ─ Resolving the template's static variables.
DEBUG ─ Collecting components from the template.
DEBUG ─ Downloading any NPM components found in the template.
... ...
apis:
-
path: /upload/cos
method: POST
apiId: api-0lkhke0c
-
path: /upload/presigned
method: POST
apiId: api-b7j5ikoc
15s › uploadToSCFToCOS › done
最后,为了方便大家测试使用:
https://github.com/anycodes/S...
我把它放到了Git上(可以通过点击原文获取):
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。