本文主要涉及两个概念:
- 阿里云OSS:对象存储(Object Storage Service,简称OSS),是阿里云对外提供的海量、安全和高可靠的云存储服务。
- bootstrap-fileinput:An enhanced HTML 5 file input for Bootstrap 3.x with file preview for various files, offers multiple selection, and more.
客服称OSS不限制上下行流量,而且不占用ECS带宽,所以将静态文件存放在OSS是个不错的选择。除了放置站点引用的资源文件外,主要用于用户文件的上传。阿里云提供了Web端直传方式,文件不经过站点服务器,不影响站点的性能和带宽。真正撸代码时,遇到了不少坑,而官方文档秉承阿里一贯的程序员风格,天马行空毫不连贯,博主东拼西凑才理顺各细节,关键点记录如下,供需要的朋友参考。
阿里官方没有提供.NET的demo,找到Post Object,该文档描述了提交的各参数。进行Post操作要求对bucket有写权限,如果bucket为public-read-write,可以不上传签名信息,否则要求对该操作进行签名验证。与Put操作不同,Post操作使用AccessKeySecret对policy进行签名计算出签名字符串作为Signature表单域的值,OSS会验证该值从而判断签名的合法性(不需要遵循用户签名验证(Authentication)的规定,另外在header或url中加上签名)。policy又是啥呢?Post请求的policy表单域用于验证请求的合法性。 policy为一段经过UTF-8和base64编码的JSON文本,声明了Post请求必须满足的条件(文件大小、一些http头等,还有该条件的过期时间)。虽然对于public-read-write的bucket上传时,post表单域为可选项(其它情况,因为需要签名验证,所以是必选项),也强烈建议使用该域来限制Post请求。
so,第一步,构造Post条件,对于不同bucket,可以有不同的条件,可以写在web.config中,如果经常变动,也可以写在数据库中方便管理。
然后,给前端提供一个获取policy的接口,代码就不贴了,使用官方提供的SDK,很简单。这里假设接口名称为GetPolicyForOSSUpload,返回结果是:
Data = new ModelForOSSUpload { AccessKeyId = aliyunconfig.AccessKeyId, PolicyBase64 = encodedPolicy, Signature = signature, Expiration = expiration }
注意,expiration已经被用于构造PolicyBase64了,这里又单独给个字段返回,是给前端用的,在有效时间内,不用再调该接口,能减轻服务器压力一点是一点。
前端js是这样的:
1 _getUploadPreSetting: function (bucketKey, policyName) { 2 var presetting = {}, error = {}, self = this; 3 var inner_getUploadPreSetting = function () { 4 var now = new Date(); 5 if (!presetting.Expiration || presetting.Expiration < now) { 6 presetting = {}; 7 error = {}; 8 $.ajax({ 9 url: self._ossOpts.uploadPreSettingUrl, 10 type: 'GET', 11 async: false, 12 data: { bucketKey: bucketKey, policyName: policyName }, 13 success: function (result) { 14 if (result.IsSucceed) { 15 presetting = result.Data; 16 //应付/Date(1460817323032)/这种奇葩格式 17 presetting.Expiration = eval('new ' + (presetting.Expiration.replace(/\//g, ''))); 18 } 19 else { 20 error = result; 21 } 22 }, 23 error: function (xhr, msg) { 24 error = { IsSucceed: false, Message: msg }; 25 } 26 }); 27 } 28 return presetting || error; 29 }; 30 return inner_getUploadPreSetting; 31 }
此处用了闭包,这是因为不同的policy,我们以bucketKey和policyName进行区分(看贴出来的web.config片段),如果一个页面需要多个policy,每个policy都要对应一个变量,数量不定的情况下,我们也不能硬编码,同时为了让它们互不影响,就采用了闭包的方式。调用如下:
var getUploadPreSetting = this._getUploadPreSetting(this._ossOpts.bucketKey, this._ossOpts.policyName); var presetting = getUploadPreSetting();
policy的部分到此为止,下面我们来看下bootstrap-fileinput这个组件,并对它进行一点点改造,以便于符合OSS的上传规则。
除了文件数据,提交时还要附加OSS要求的一些表单域,比如刚才讲的policy、Signature,还可以给每个文件加上cache-Control、callback(上传成功后,OSS回调我们的接口地址)等。我们在初始化bootstrap-fileinput(下称fileinput)时可以传入一个uploadExtraData配置项(函数或对象),在上传每个文件时,fileinput会调用uploadExtraData,将获取到的数据附加到文件数据后一起上传。然而这就存在一个问题了,OSS文档中说“文件或文本内容,必须是表单中的最后一个域。”,否则会返回“The bucket POST must contain the specified 'key'. If it is specified, please check the order of the fields”的错误信息。
题外话:关于表单域顺序的说明,以前文档中并没有,还是博主在一次工单提交和差点投诉后才加上去的,当时很奇怪难道没人遇到过,还是说这样用OSS的人不多?另外在和客服扯皮的同时,博主也上网搜了一把,发现AWS的用户竟然也遇到类似问题,请看 InvalidArgumentBucket POST must contain a field named 'key'. If it is specified, please check the order of the fields.keyB,连返回的错误消息都大同小异,博主大胆猜测,阿里云和AWS的数据存储用的是同一套组件,还是开源的,博主知识浅陋,有知道的朋友说一声。
于是只能去看fileinput的源码了,幸好它是开源的,我们只需要改动一下_uploadSingle函数的末尾:
self._uploadExtra(previewId, i); //新加 formdata.append(self.uploadFileAttr, files[i], self.filenames[i]); //文件数据 formdata.append('file_id', i); self._ajaxSubmit(fnBefore, fnSuccess, fnComplete, fnError, previewId, i, true); //也要改造下_ajaxSubmit,传入true表示额外数据不再附加
_ajaxSubmit也改造了下,圆圈内为新加:
fileinput改造完毕,so easy!那么当上传成功后,我们一般都是要对应用数据做一些处理,比如更新数据库中的url地址什么的,我们可以获取响应信息,然后再调用相应的后台接口,OSS也提供了直接回调的方式,后者能传递更多关于文件的信息,并且在构造callback时可自定义参数,因此更灵活,方便后台回调接口的数据处理,具体可看Callback。
下例,前端js:
callback:{ "callbackUrl":GlobalConfig.siteurl+'Merchandise/SetMerchandisePicUrl', "callbackBody":"merchandiseid="+vm.merchandise.BasicInfo.ID+"&picname=${object}" }
后端回调接口:
1 [AllowAnonymous] 2 public async Task<ActionResult> SetMerchandisePicUrl(int merchandiseid, string picname) 3 { 4 var aliyunconfig = MvcApplication.AliyunConfig; 5 var bucket = aliyunconfig.OSS.Buckets["merchandise_pictures"]; 6 var picUri = $"http://{ bucket.Value}.{aliyunconfig.OSS.ImgEndpoint}/{picname}"; 7 await MerchandiseContext.AppendPicUri(merchandiseid, picUri); 8 return this.Json(new 9 { 10 initialPreview = new string[] { picUri }, 11 initialPreviewConfig = new List<object> { 12 new { 13 caption = picname, 14 //width ="160px", //并没有什么用 15 url =Url.Action("RemoveMerchandisePicUrl"), 16 extra = new {picUri= picUri,merchandiseid = merchandiseid} 17 } 18 } 19 }); 20 }
接口允许匿名,便于OSS调用,为了防止恶意调用,最好进行签名校验。fileinput根据返回结果判断是否上传成功(没有error属性表示上传成功),若成功且结果里有initialPreview[和initialPreviewConfig]等属性,则将这些属性应用到对应文件显示。特别注意initialPreviewConfig中的url和extra属性,表示删除该文件时调用的接口和传递的参数。
另外,若我们不指定回掉地址,那么OSS默认会返回空文档(在上传成功的情况下),若需要让其返回官方文档中提到的PostResponse,需要指定success_action_status为201。
其它参考资料:
理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型