传统开发中,文件上传是比较自由的:上传什么文件、怎么上传、存储到哪里等问题往往都是由开发者决定的,但是在 Serverless 架构下,上传文件就没有这么自由了。无论是成本原因,还是某些服务限制,我们都需要寻求一些比较 " 优 " 的解决方案。

Serverless 架构与文件上传

由于 Serverless 架构中函数计算的部分是没有办法做文件持久化,函数执行的容器用完过后就会被回收,所以如果想要存储文件,就需要借助对象存储等相关服务。

将文件上传到对象存储服务的方法很多,本文主要介绍两种:

  • 函数计算 -> 对象存储
  • 对象存储

一般情况下,如果某个有文件上传功能,我们会用multipart/form-data,或者将文件进行base64编码之后再上传。但在 Serverless 架构下,这种思路需要做出一些改变。

函数计算 -> 对象存储是上传文件比较常见,也是比较容易的方式。文件直接通过 API 网关,传送到云函数中,并重做一些处理(例如压缩图像、视频转码、数据入库等),然后再由云函数将结果存储到对象存储中,做文件资源的持久化。

这个思路看起来很顺畅,但是实际操作起来也会遇到很多问题:

首先,通过这种方式将文件传给函数时,函数计算通过 API 网关得到的数据结构往往是 JSON 格式,或者是字符串。这样的设计使得函数计算对二进制的支持非常不友好,我们只能将文件转换为base64编码后再进行传输,通过 API 网关之后,函数接收到数据,再将base64编码的文件解码,经过相关处理后持久化对象存储。

其次,无论是 AWS 的 Lambda,还是腾讯云的 SCF,通过 API 网关触发函数时都会有数据包大小限制的。以腾讯云为例,数据包的限制是 6M。也就是说,无论发送多大的数据,从 API 网关到函数计算都会有一个数据包的最大限制,上传文件过大,就无法进行资源的传输。所以,上传到云函数的文件必须在 6M 以下,而函数计算对二进制文件不友好,经过base64编码的数据包通常会大些,这样上传到云函数的数据包必须在 4M 左右。

4M 的图片是什么概念呢?如上图所示,是我用手机随机拍了一张图片,大小是 6.21M,这时我是无法将这张图片上传到 SCF 进行处理的。

除了对文件大小有限制之外,上述方法对成本也有一定影响,API 网关并不是一个适合传输文件的方法。我们可以单从流量费用来对比一下对象存储和 API 网关的区别:

  • API 网关的收费:
  • 对象存储的收费:

单从流量维度来看,API 网关的费用比 COS 高了许多,主要原因可能是因为 API 网关更侧重于控制流,在数据存储传输方面,对象存储更适合。

那么,有什么方法可以直接将文件等资源上传到对象存储呢?这条资源数据又如何入库呢(例如用户上传图片到相册功能,若使用传统方法,系统接收到图片之后,会将数据入库,但若是将图片直接上传到对象存储,我们如何得知这个图片是谁给我们的)?另外,将文件上传到对象存储需要写入权限,那么是将权限开发?还是使用密钥?如果是一个 Web 服务,这个密钥信息又应该存储在哪里?如何存储?

于是,就衍生出了第二种解决方法:

在对象存储方法中,客户端会发起三个请求,分别是获取临时上传地址、将文件上传到 COS、获取处理结果。相比于之前的方法,这个方法会复杂一些,但是能够很好的支持二进制上传、文件资源的大小以及成本控制。

针对不同场景的的不同适用方案:

  • 场景 1: 用户上传头像功能

针对这样的场景,直接选用方案 1。

一般情况下,头像都不会很大,完全可以在客户端对图像进行一次压缩和裁剪之后,直接带着用户的参数,例如 token 等,上传到函数计算,在函数计算中将图片转存到对象存储,将图像和用户信息进行关联,并将某些结果返回给客户端。整个流程只需要一个函数,方便快捷。

  • 场景 2: 用户上传图片到相册系统中

针对这样的场景,方案 2 会更好。

如果用户是上传图片到相册,那么基本都是希望保留原图,不希望被压缩,而原图大小很可能会超过 6M,这时方案 1 就不是特别合理了。使用对象存储方法,用户可以带着图像要上传的相册以及图片名称,用户的 token 发起获取临时密钥到函数 1 中,函数 1 将用户、相册、图片以及状态(例如待上传、待处理、已处理等)等信息关联、存储,并将临时地址返回给客户端,客户端将图片上传到对象存储中,通过对象存储触发器触发函数 2,函数 2 对图像进行压缩(一般情况下,相册列表都会显示压缩图片,点到相册详情才会有完整的无损图片),并且和之前信息进行关联,修改数据状态。在用户上传图片完成之后,如果有需要,客户端就可以发起第三次请求获取图像存储 / 处理结果,函数 3 会查询数据库状态,在某个时间阈值内,如果数据状态是完成,则表示数据已经上传并且完成了部分处理,否则会返回对应的异常信息。

代码实例

接下来分享上面两种方法的实现过程:

函数 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 部分:

复制代码

Web 端上传文件

方案 1:通过上传到 SCF,进行处理再转存到 COS,这种方法比较直观,但是问题是 SCF 从 APIGW 处只能接收到小于 6M 的数据,而且对二进制文件处理并不好。

方案 2: 直接上传到 COS,流程是先从 SCF 获得临时地址,进行数据存储(例如将文件信息存到 redis 等),然后再从客户端进行上传 COS,上传结束可通过 COS 触发器触发函数,从存储系统(例如已经存储到 redis)读取到更对信息,在对图像进行处理。

方案 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

至此,我们完成了项目部署,可以进行测试与适用。

总结

Serverless 可以看作是一个新的技术、新的架构。我们在接触新鲜事物的时候,或多或少都要有一个适应期,如何在 Serverless 架构下上传文件,就是需要适应的部分。我们之前习惯了直接将文件上传到服务器的,但在接触 Serverless 架构之后,由于网关 -> 函数对二进制支持和数据包大小问题,出于安全考虑,前端不方便直接放密钥信息等问题,之前简单的事情可能会变得复杂。

作者介绍:

刘宇,腾讯 Serverless 团队后台研发工程师。毕业于浙江大学,硕士研究生学历,曾在滴滴出行、腾讯科技做产品经理,本科开始有自主创业经历,是 Anycodes 在线编程的负责人(该软件累计下载量超 100 万次)。目前投身于 Serverless 架构研发,著书《Serverless 架构:从原理、设计到项目实战》,参与开发和维护多个 Serverless 组件,是活跃的 Serverless Framework 的贡献者,也曾多次公开演讲和分享 Serverless 相关技术与经验,致力于 Serverless 的落地与项目上云。

datalist获取id传到下一个页面_如何在 Serverless 架构下优雅上传文件?相关推荐

  1. python 点击tree目录、执行下一个操作_如何使用python解决下一个iter(xml.etree.ElementTree)?...

    我假设您正在使用xml.etree.ElementTree,因为它是标准库的一部分.考虑以下片段:appelation = re.compile('Mr') points = root.iter('p ...

  2. asp如何将图片文件上传到mysql数据库中_怎样才能利用ASP把图片上传到数据库

    欢迎来到小编的文章进行学习阅读,想必大家又有很多问题吧,在这里会有你想要收获的答案,请大家慢慢学习吧! ASP(Active Server Pages)是Microsoft很早就推出的一种WEB应用程 ...

  3. 华为云备份会上传私密相册吗_云相册有哪些作用?上传照片会不会占用手机内存?...

    现在苹果.华为.小米.OPPO等手机都支持"云相册"功能,而且一般大容量的云相册都是收费的,根据云相册的容量一个月需要几元或者几十元不等.付费越多,云相册的空间就越大. 很多网友以 ...

  4. ih5怎么切换下一个页面_区块链是下一个风口?那PPT该怎么做?

    把区块链作为核心技术自主创新重要突破口.10月25日傍晚,这条新闻出来后,全网又一次炸开了锅.知道区块链的人很多,但能把区块链讲清楚的却很少.早两年很多打着区块链幌子做项目.发行空气币非法集资,坑了很 ...

  5. 打开另外一个页面_如何在PDF页面中插入图片?

    如何给PDF添加图片?有些时候为了丰富PDF的文档内容,需要添加一些图片,相比Word或PPT文档可以直接插入图片,而PDF的操作很多人可能并不熟悉,下面一起来看看如何在PDF文档中插入图片. 关于P ...

  6. 用js把数据从一个页面传到另一个页面

    打开支付宝首页搜索: 4046160(可保存下来,每天可领一次) 领取方式,一定要点击 立即领取 按钮 这个才是金额比较大的余额宝红包. 红包平均为 3-5块 ,每天可领一次,有效期三天,必须上一次领 ...

  7. 商品列表选择尺寸和颜色高亮,并且把选择的数据传递到下一个页面

    微信小程序开发交流qq群   173683895    承接微信小程序开发.扫码加微信. 需求:商品列表选择属性,给中的属性显示高亮,并且把选择的数据记录下来传递到下一个页面. 项目下载地址:点击去下 ...

  8. uniapp微信小程序:点击按钮先判断用户是否授权位置信息、用户位置信息授权、进入下一个页面

    文章目录 前言 一.创建点击事件的方法 二.判断用户是否授权位置 三.弹出位置授权框 四.坐标到坐标所在位置的文字描述的转换(逆地址解析) 五.拒接授权后再次点击按钮跳转到允许访问位置设置 总结 前言 ...

  9. 如何把html传到另一个页面中,如何把一个页面的值传到另一个页面

    在实际项目中有时候需要把上一个页面输入的值保留到下一个页面使用,现在根据个人总结如下: 1.最常用的方法url?传递方式 A: 下一页面 B: %> 2.通过submit A: function ...

  10. 一个能防止改名木马漏洞的无组件上传类

    现在流行的asp上传组件除了无惧的化境之外,最多的可能就是ewebEditor 和Fckeditor的上传是,但是经过测试都很难防止改名为gif和asp文件上传,在FckEditor中改名后的asp木 ...

最新文章

  1. JS 添加网页桌面快捷方式的代码
  2. 图片(img标签)的onerror事件,你有用过嘛?
  3. [luogu5004]专心OI - 跳房子【矩阵加速+动态规划】
  4. next_permutation函数
  5. Algorithm:C++语言实现之求最大连续子数组(暴力法、分治法、分析法、动态规划法)
  6. iphone怎样关闭副屏_iPhone手机关掉这3个设置,不仅省电,而且手机还不会卡
  7. 视频人脸检测——Dlib版(六)
  8. 2016物联网大趋势搞不懂?别担心,CES为你指点迷津
  9. Google Go 初识
  10. python基础教程pdf-python基础教程第三版.pdf
  11. 蓝桥杯2016年第七届C/C++省赛A组第一题-网友年龄
  12. 把ipa包上传到AppStore
  13. visual studio 2019 marketplace下载安装vsix插件
  14. DOS攻击工具——pentmenu
  15. MD5加密算法(python)
  16. 我是如何一步步获取房东的WiFi后台管理密码的【社工思路】
  17. EasyGBS国标视频云服务平台可以获取录像却无法播放是什么原因?
  18. 计算机英语带字幕,计算机专业英语听力字幕.doc
  19. 微商大佬爆料:工商部门严查微商和社交电商机构
  20. C语言程序设计现代方法(第二版)十二章编程题答案

热门文章

  1. MySQL复制篇之---半同步复制
  2. 【高效程序员系列】1、好马配好鞍——舒适的工作环境
  3. [2018.11.03 T4] 7w523
  4. 函数的参数对象$event的使用和利用他找到事件对象
  5. vue学习笔记-9-tab选项卡小案例
  6. c语言键盘函数空格,C语言中关于scanf函数的用法
  7. mysql中文乱码过滤_记一次mysql中文字符乱码的问题排查
  8. 不能使用泛型的形参创建对象_泛型就这么简单
  9. vue element ui_vue+element-ui实现表格里嵌套表格
  10. springcloud config不重启修改配置_SpringCloud实战-Bus消息总线(动态修改配置)