写点什么

【最不佳实践】文件上传并不简单

用户头像
刘宇
关注
发布于: 21 小时前
【最不佳实践】文件上传并不简单

Serverless 架构带来的除了一种新的架构,一种新的编程范式,还有就是一种思路上的转变,尤其是开发过程上的一些思路变化。有人说要把 Serverless 架构看成是一种天然的分布式架构,需要用分布式架构的思路去开发 Serverless 应用,诚然,这种说法是正确的,但是在一些情况下,Serverless 还是有着一些特有的“特性”,所以在 Serverless 架构下,还是要有一些开发的“观念转变”。

传统文件上传

传统 Web 框架中,我们上传文件是非常简单和便捷的,例如 Python 的 Flask 框架:


f = request.files['file']f.save('my_file_path')
复制代码

Serverless 架构下文件上传

但是在 Serverless 架构下,却不能直接上传文件,因为:


  • 一般情况下,一些云平台的 API 网关触发器会讲二进制文件转换成字符串;不便直接获取和存储;

  • 一般情况下,API 网关与 FaaS 平台之间传递的数据包有大小限制,很多平台被限制在 6M;

  • FaaS 平台大都是无状态的,即使存储到当前实例中,也会随着实例释放而导致文件丢失;


所以,传统框架中常用的上传方案是不太适合在 Serverless 架构中直接使用的,在 Serverless 架构上传文件的方法通常有两种:


  • 一种是 BASE64 后上传,持久化到对象存储或者是 NAS 中,这种做法可能会触及到 API 网关与 FaaS 平台之间传递的数据包有大小限制,所以一般使用这种上传方法的通常是上传头像头像等小文件的业务场景;

  • 第二种上传方法是通过对象存储等平台来上传,因为客户端直接通过密钥等信息来将文件直传到对象存储是有一定风险的,所以通常情况是客户端发起上传请求,函数计算根据请求内容进行预签名操作,并将预签名地址返回给客户端,客户端再使用指定的方法进行上传,上传完成之后,可以通过对象存储触发器等来对上传结果进行更新等:



以阿里云函数计算为例,针对上述两种常见的上传方法通过 Python Web 框架 Bottle 来实现:


在函数计算中,先初始化对象存储相关的对象等:


AccessKey = {    "id": '',    "secret": ''}OSSConf = {    'endPoint': 'oss-cn-hangzhou.aliyuncs.com',    'bucketName': 'bucketName',    'objectSignUrlTimeOut': 60}
# 获取获取/上传文件到OSS的临时地址auth = oss2.Auth(AccessKey['id'], AccessKey['secret'])bucket = oss2.Bucket(auth, OSSConf['endPoint'], OSSConf['bucketName'])# 对象存储操作getUrl = lambda object, method: bucket.sign_url(method, object, OSSConf['objectSignUrlTimeOut'])getSignUrl = lambda object: getUrl(object, "GET")putSignUrl = lambda object: getUrl(object, "PUT")
# 获取随机字符串randomStr = lambda len: "".join(random.sample('abcdefghijklqrstuvwxyz123456789ABCDEFGZSA' * 100, len))
复制代码

简单上传:适合小文件

第一种上传方法,通过 Base64 上传之后,持久化到对象存储,适合小文件上传,流程比较简单:


# 文件上传# URI: /file/upload# Method: POST@bottle.route('/file/upload', "POST")def postFileUpload():    try:        pictureBase64 = bottle.request.GET.get('picture', '').split("base64,")[1]        object = randomStr(100)        with open('/tmp/%s' % object, 'wb') as f:            f.write(base64.b64decode(pictureBase64))        bucket.put_object_from_file(object, '/tmp/%s' % object)        return response({            "status": 'ok',        })    except Exception as e:        print("Error: ", e)        return response(ERROR['SystemError'], 'SystemError')
复制代码

科学上传:更安全稳定上传方法

第二种上传方法,获取预签名的对象存储地址,再在客户端发起上传请求,直传到对象存储,相对来说会更科学更通用,但是流程相对复杂:


# 获取文件上传地址# URI: /file/upload/url# Method: GET@bottle.route('/file/upload/url', "GET")def getFileUploadUrl():    try:        object = randomStr(100)        return response({            "upload": putSignUrl(object),            "download": 'https://download.xshu.cn/%s' % (object)        })    except Exception as e:        print("Error: ", e)        return response(ERROR['SystemError'], 'SystemError')
复制代码

客户端实现/预览

HTML 部分:


<div style="width: 70%">    <div style="text-align: center">        <h3>Web端上传文件</h3>    </div>    <hr>    <div>        <p>            方案1:通过上传到函数计算,进行处理再转存到对象存储,这种方法比较直观,但是问题是FaaS平台与API网关处有数据包大小上限,而且对二进制文件处理并不好。        </p>        <input type="file" name="file" id="fileFc"/>        <input type="button" onclick="UpladFileFC()" value="上传"/>    </div>    <hr>    <div>        <p>            方案2:            直接上传到对象存储,流程是先从函数计算获得临时地址,进行数据存储(例如将文件信息存到redis等),然后再从客户端进行上传到对象存储,上传结束可通过对象存储触发器触发函数,从存储系统(例如已经存储到redis)读取到更对信息,在对图像进行处理。        </p>        <input type="file" name="file" id="fileOss"/>        <input type="button" onclick="UpladFileOSS()" value="上传"/>    </div></div>
复制代码


通过 Base64 上传的客户端 JavaScript 实现:


function UpladFileFC() {    const oFReader = new FileReader();    oFReader.readAsDataURL(document.getElementById("fileFc").files[0]);    oFReader.onload = function (oFREvent) {        const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"))        xmlhttp.onreadystatechange = function () {            if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {                alert(xmlhttp.responseText)            }        }        const url = "https://domain.com/file/upload"        xmlhttp.open("POST", url, true);        xmlhttp.setRequestHeader("Content-type", "application/json");        xmlhttp.send(JSON.stringify({            picture: oFREvent.target.result        }));    }}
复制代码


客户端通过预签名地址,直传到对象存储的客户端 JavaScript 实现:


function doUpload(bodyUrl) {    const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP"));    xmlhttp.open("PUT", bodyUrl, true);    xmlhttp.onload = function () {        alert(xmlhttp.responseText)    };    xmlhttp.send(document.getElementById("fileOss").files[0]);}
function UpladFileOSS() { const xmlhttp = window.XMLHttpRequest ? (new XMLHttpRequest()) : (new ActiveXObject("Microsoft.XMLHTTP")) xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { const body = JSON.parse(xmlhttp.responseText) if (body['url']) { doUpload(body['url']) } } } const getUploadUrl = 'https://domain.com/file/upload/url' xmlhttp.open("POST", getUploadUrl, true); xmlhttp.setRequestHeader("Content-type", "application/json"); xmlhttp.send();}
复制代码


整体效果:



发布于: 21 小时前阅读数: 5
用户头像

刘宇

关注

阿里云Serverless云布道师 2020.01.04 加入

阿里云Serverless产品经理,国防科大在读博士,《Serverless架构》、《Serverless实践》、《人人都能学会的Serverless架构》等书籍作者,Serverless Devs发起人,Anycodes在线编程负责人。

评论

发布
暂无评论
【最不佳实践】文件上传并不简单