在odoo中自定义字段,实现上传s3服务器

项目中文件存储在亚马逊服务器s3上,需要文件上传到s3服务器。传统的思路是先将文件上传到odoo服务器,然后odoo服务器调用s3接口上传文件。这样做的缺点在于:

  1. 一旦文件超过odoo默认的25M,文件将无法上传,即便修改了这个参数限制,odoo的上传也十分缓慢并且经常出现长时间等待
  2. 用户将文件上传到odoo服务器已经花费掉一部分时间,odoo服务器再上传到s3又将花费一部分时间,效率低下。

所以,我们希望实现的方式是用户通过浏览器直接向s3服务器上传文件,并返回存储的url,存到odoo里。这里又有两个问题需要解决:

  1. s3服务器的认证如何实现,我们不可能将s3的accessid和accesskey写到前端代码中,没有安全性。
  2. 即便文件上传了,如何将s3的url存储到odoo中。

我们先来看第一个问题,s3的文件上传。

browse-base方式上传

在亚马逊的官方文档中,写有解决我们这个问题的方案,简单说就是前端先访问odoo服务器,获取访问s3服务器的token然后前端将token带入到form-post的参数中,向s3服务器发起请求,s3校验通过,将form-post中的文件写入服务器并返回存储的url。这种方式完整的名称叫amazon s3 browser-based uploads.

具体内容参见官方文档

下面我们来实现这种browse-base的上传

后端token的生成

关于token,s3有一套自己的生成逻辑,用户可以按照逻辑自己实现。同时官方提供了sdk,简单起见,我们这里用了官方库boto3

1
2
3
4
5
6
7
8
9
10
11
def get_pre_post(self):
BUCKET = "yourbucketaccount"
REGION = "yourregion"
s3 = boto3.client('s3', region_name=REGION, aws_access_key_id=youraccessskeyid,
aws_secret_access_key=youaccesskey)
key = self.env.context['key']
return s3.generate_presigned_post(Bucket=BUCKET,Key=key, Fields={
'success_action_status': '201',
'acl':'public-read',
}, Conditions=[{"success_action_status": "201"},{"acl": "public-read"}])

key是上传的文件名,fields是生成的form参数,acl是文件的访问控制,这里设置为public-read,否则你上传的文件将无法被用户访问。还有一点需要注意,就是form的参数是被严格限制的,在s3的post policy中没有声明的参数不允许添加,否则上传无法成功。

前端

后端其实相对简单,因为boto3库已经为我们做了大部分工作。现在我们需要做的就是在前端将文件上传并写入返回的url。
由于odoo没有现成的widget可以使用,所以,我们这里需要自己定义一个widget来实现。

自定义字段

这里我们采用自定义字段的方式,声明一个字段,然后绑定我们的widget:

1
2
3
4
5
6
7
8
9
10
class S3Field(fields.Field):
type = 's3field'
column_type = ('varchar', 'varchar')
def __init__(self, string='', **kwargs):
super(S3Field, self).__init__(string=string, **kwargs)
def convert_to_column(self, value, record):
return value or u''

自定义widget

widget的组成可以是一个label加一个按扭,label用来显示url,按钮用来上传。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<templates id="template" xml:space="preserve">
<t t-name="S3Field">
<div>
<span class="s3_form_field"></span>
</div>
<div>
<form id="s3form" class="s3_form_field_upload" enctype="multipart/form-data">
<div>
<input type="file" class="o_form_input s3_file"/>
<button type="button" class="btn btn-sm btn-primary o_select_file_button" title="Select">上传媒体文件</button>
</div>
</form>
</div>
</t>
</templates>

最后一步,获取后端生成的form-data并上传到s3:

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
this.$('.o_select_file_button').click(function () {
var $input = $('.s3_file');
var files = $input.prop('files');
if (!files.length) {
alert("请选择要上传的文件");
return;
}
var context = self.build_context().eval();
context['key'] = files[0].name
new Model(self.view.model).call('get_pre_post', [[self.view.datarecord.id], context]).then(function (res) {
var data = new FormData();
/*browse base方式,上传格式有严格的POST Policy限制,不能随意添加字段*/
data.append('key', files[0].name);
data.append('acl', "public-read");
data.append('success_action_status', '201');
data.append('policy', res['fields']['policy']);
data.append('x-amz-algorithm', res['fields']['x-amz-algorithm']);
data.append('x-amz-credential', res['fields']['x-amz-credential']);
data.append('x-amz-date', res['fields']['x-amz-date']);
data.append('x-amz-signature', res['fields']['x-amz-signature']);
data.append('file', files[0]);
/*上传到S3服务器*/
$.ajax({
type: 'POST',
url: res['url'],
contentType: false,
processData: false,
data: data,
success: function (resx) {
var rurl = $(resx).find('Location').text();
self.set_value(rurl);
alert("上传成功!")
}
});
});
});

这样我们就实现了browse-base的方式上传。

注:Ajax跨域会需要在Bucket中设置权限,否则不会返回响应值。