0001-cmis-add-a-jsonschema-for-uploadfile-endpoint-39193.patch
passerelle/apps/cmis/models.py | ||
---|---|---|
27 | 27 |
from cmislib.exceptions import UpdateConflictException |
28 | 28 |
from django.db import models |
29 | 29 |
from django.utils.translation import ugettext_lazy as _ |
30 |
from django.utils.six import StringIO, text_type
|
|
30 |
from django.utils.six import StringIO |
|
31 | 31 |
from django.utils.six.moves.urllib import error as urllib2 |
32 | 32 | |
33 | 33 |
from passerelle.base.models import BaseResource |
34 |
from passerelle.compat import json_loads |
|
35 | 34 |
from passerelle.utils.api import endpoint |
36 | 35 |
from passerelle.utils.jsonresponse import APIError |
37 | 36 | |
38 | 37 | |
39 | 38 |
SPECIAL_CHARS = '!#$%&+-^_`~;[]{}+=~' |
40 |
FILE_PATH_PATTERN = '^(/|(/[\w%s]+)+)$' % re.escape(SPECIAL_CHARS) |
|
41 |
FILE_NAME_PATTERN = '[\w%s\.]+' % re.escape(SPECIAL_CHARS) |
|
42 |
RE_FILE_PATH = re.compile(FILE_PATH_PATTERN) |
|
43 |
RE_FILE_NAME = re.compile(FILE_NAME_PATTERN) |
|
39 |
FILE_PATH_PATTERN = r'^(/|(/[\w%s]+)+)$' % re.escape(SPECIAL_CHARS) |
|
40 |
FILE_NAME_PATTERN = r'[\w%s\.]+$' % re.escape(SPECIAL_CHARS) |
|
41 | ||
42 | ||
43 |
UPLOAD_SCHEMA = { |
|
44 |
'type': 'object', |
|
45 |
'properties': { |
|
46 |
'file': { |
|
47 |
'type': 'object', |
|
48 |
'properties': { |
|
49 |
'filename': { |
|
50 |
'type': 'string', |
|
51 |
'pattern': FILE_NAME_PATTERN, |
|
52 |
}, |
|
53 |
'content': {'type': 'string'}, |
|
54 |
'content_type': {'type': 'string'}, |
|
55 |
}, |
|
56 |
'required': ['content'] |
|
57 |
}, |
|
58 |
'filename': { |
|
59 |
'type': 'string', |
|
60 |
'pattern': FILE_NAME_PATTERN, |
|
61 |
}, |
|
62 |
'path': { |
|
63 |
'type': 'string', |
|
64 |
'pattern': FILE_PATH_PATTERN, |
|
65 |
}, |
|
66 |
}, |
|
67 |
'required': ['file', 'path'] |
|
68 |
} |
|
44 | 69 | |
45 | 70 | |
46 | 71 |
class CmisConnector(BaseResource): |
... | ... | |
58 | 83 |
fields = super(CmisConnector, self).get_description_fields() |
59 | 84 |
return [(x[0], x[1]) for x in fields if x[0].name != 'password'] |
60 | 85 | |
61 |
@endpoint(methods=['post'], perm='can_access') |
|
62 |
def uploadfile(self, request): |
|
63 |
error, error_msg, data = self._validate_inputs(request.body) |
|
86 |
@endpoint( |
|
87 |
perm='can_access', |
|
88 |
post={ |
|
89 |
'request_body': { |
|
90 |
'schema': { |
|
91 |
'application/json': UPLOAD_SCHEMA, |
|
92 |
} |
|
93 |
} |
|
94 |
}) |
|
95 |
def uploadfile(self, request, post_data): |
|
96 |
error, error_msg, data = self._validate_inputs(post_data) |
|
64 | 97 |
if error: |
65 | 98 |
self.logger.debug("received invalid data: %s" % error_msg) |
66 | 99 |
raise APIError(error_msg, http_status=400) |
... | ... | |
73 | 106 |
data['file_byte_content']) |
74 | 107 |
return {'data': {'properties': doc.properties}} |
75 | 108 | |
76 |
def _validate_inputs(self, body):
|
|
77 |
""" process JSON body
|
|
109 |
def _validate_inputs(self, data):
|
|
110 |
""" process dict
|
|
78 | 111 |
return a tuple (error, error_msg, data) |
79 | 112 |
""" |
80 |
try: |
|
81 |
data = json_loads(body) |
|
82 |
except ValueError as e: |
|
83 |
return True, "could not decode body to json: %s" % e, None |
|
84 |
if 'file' not in data: |
|
85 |
return True, '"file" is required', None |
|
86 |
if 'path' not in data: |
|
87 |
return True, '"path" is required', None |
|
88 |
if not isinstance(data['file'], dict): |
|
89 |
return True, '"file" must be a dict', None |
|
90 |
if not isinstance(data['path'], text_type): |
|
91 |
return True, '"path" must be string', None |
|
92 |
if not RE_FILE_PATH.match(data['path']): |
|
93 |
return True, '"path" must be valid path', None |
|
94 | ||
95 | 113 |
file_ = data['file'] |
96 |
if 'filename' in data: |
|
97 |
if not isinstance(data['filename'], text_type): |
|
98 |
return True, '"filename" must be string', None |
|
99 |
if not RE_FILE_NAME.match(data['filename']): |
|
100 |
return True, '"filename" must be valid file name', None |
|
101 |
else: |
|
102 |
if 'filename' not in file_: |
|
103 |
return True, '"file[\'filename\']" is required', None |
|
104 |
if not isinstance(file_['filename'], text_type): |
|
105 |
return True, '"file[\'filename\']" must be string', None |
|
106 |
if not RE_FILE_NAME.match(file_['filename']): |
|
107 |
return True, '"file[\'filename\']" must be valid file name', None |
|
108 | ||
109 |
if 'content' not in file_: |
|
110 |
return True, '"file[\'content\']" is required', None |
|
111 |
if not isinstance(file_['content'], text_type): |
|
112 |
return True, '"file[\'content\']" must be string', None |
|
114 |
if 'filename' not in file_ and 'filename' not in data: |
|
115 |
return True, '"filename" or "file[\'filename\']" is required', None |
|
116 | ||
113 | 117 |
try: |
114 | 118 |
data['file_byte_content'] = base64.b64decode(file_['content']) |
115 | 119 |
except (TypeError, binascii.Error): |
passerelle/apps/cmis/templates/cmis/cmisconnector_detail.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/service_view.html" %} |
|
2 |
{% load i18n passerelle %} |
|
3 | ||
4 |
{% block endpoints %} |
|
5 |
<ul> |
|
6 |
<li> |
|
7 |
<h4>{% trans 'Upload file' %}</h4> |
|
8 |
{% url "generic-endpoint" connector="cmis" slug=object.slug endpoint="uploadfile" as uploadfile %} |
|
9 |
<p> <strong>POST</strong> <a href="{{uploadfile}}">{{uploadfile}}</a></p> |
|
10 |
<pre> |
|
11 |
data_send = { |
|
12 |
'path': '/a/path', |
|
13 |
'file': { |
|
14 |
'filename': 'test.txt', |
|
15 |
'content': 'ZmlsZSBjb250ZW50', |
|
16 |
'content_type': 'image/jpeg' |
|
17 |
} |
|
18 |
} |
|
19 |
</pre> |
|
20 |
</li> |
|
21 |
</ul> |
|
22 |
{% endblock %} |
|
23 | ||
24 |
{% block security %} |
|
25 |
<p> |
|
26 |
{% trans 'Upload is limited to the following API users:' %} |
|
27 |
</p> |
|
28 |
{% access_rights_table resource=object permission='can_access' %} |
|
29 |
{% endblock %} |
tests/test_cmis.py | ||
---|---|---|
1 | 1 |
import base64 |
2 | 2 |
import httplib2 |
3 |
import re |
|
3 | 4 | |
4 | 5 |
from cmislib import CmisClient |
5 | 6 |
from cmislib.exceptions import CmisException |
... | ... | |
60 | 61 |
assert result_file.exists() |
61 | 62 |
with result_file.open('rb'): |
62 | 63 |
assert result_file.read() == file_content |
63 |
json_result = response.json_body
|
|
64 |
json_result = response.json |
|
64 | 65 |
assert json_result['err'] == 0 |
65 | 66 |
assert json_result['data']['properties'] == {"toto": "tata"} |
66 | 67 | |
... | ... | |
76 | 77 |
assert result_file.exists() |
77 | 78 |
with result_file.open('rb'): |
78 | 79 |
assert result_file.read() == file_content |
79 |
json_result = response.json_body
|
|
80 |
json_result = response.json |
|
80 | 81 |
assert json_result['err'] == 0 |
81 | 82 |
assert json_result['data']['properties'] == {"toto": "tata"} |
82 | 83 | |
... | ... | |
89 | 90 |
"content_type": "image/jpeg"}}, |
90 | 91 |
expect_errors=True) |
91 | 92 |
assert response.status_code == 400 |
92 |
assert response.json_body['err'] == 1
|
|
93 |
assert response.json_body['err_desc'].startswith('"file[\'filename\']" is required')
|
|
93 |
assert response.json['err'] == 1 |
|
94 |
assert response.json['err_desc'].startswith('"filename" or "file[\'filename\']" is required')
|
|
94 | 95 | |
95 | 96 | |
96 | 97 |
def test_uploadfile_error_if_non_string_file_name(app, setup): |
... | ... | |
101 | 102 |
"content_type": "image/jpeg"}}, |
102 | 103 |
expect_errors=True) |
103 | 104 |
assert response.status_code == 400 |
104 |
assert response.json_body['err'] == 1
|
|
105 |
assert response.json_body['err_desc'].startswith('"file[\'filename\']" must be string')
|
|
105 |
assert response.json['err'] == 1 |
|
106 |
assert response.json['err_desc'] == "1 is not of type 'string'"
|
|
106 | 107 | |
107 | 108 |
response = app.post_json( |
108 | 109 |
'/cmis/slug-cmis/uploadfile', |
... | ... | |
112 | 113 |
"filename": 1}, |
113 | 114 |
expect_errors=True) |
114 | 115 |
assert response.status_code == 400 |
115 |
assert response.json_body['err'] == 1
|
|
116 |
assert response.json_body['err_desc'].startswith('"filename" must be string')
|
|
116 |
assert response.json['err'] == 1 |
|
117 |
assert response.json['err_desc'] == "1 is not of type 'string'"
|
|
117 | 118 | |
118 | 119 | |
119 | 120 |
def test_uploadfile_error_if_non_valid_file_name(app, setup): |
... | ... | |
124 | 125 |
"content_type": "image/jpeg"}}, |
125 | 126 |
expect_errors=True) |
126 | 127 |
assert response.status_code == 400 |
127 |
assert response.json_body['err'] == 1
|
|
128 |
assert response.json_body['err_desc'].startswith('"file[\'filename\']" must be valid file name')
|
|
128 |
assert response.json['err'] == 1 |
|
129 |
assert "',.,' does not match " in response.json['err_desc']
|
|
129 | 130 | |
130 | 131 |
response = app.post_json( |
131 | 132 |
'/cmis/slug-cmis/uploadfile', |
... | ... | |
135 | 136 |
"filename": ",.,"}, |
136 | 137 |
expect_errors=True) |
137 | 138 |
assert response.status_code == 400 |
138 |
assert response.json_body['err'] == 1
|
|
139 |
assert response.json_body['err_desc'].startswith('"filename" must be valid file name')
|
|
139 |
assert response.json['err'] == 1 |
|
140 |
assert "',.,' does not match " in response.json['err_desc']
|
|
140 | 141 | |
141 | 142 | |
142 | 143 |
def test_uploadfile_error_if_no_path(app, setup): |
... | ... | |
146 | 147 |
"content_type": "image/jpeg"}}, |
147 | 148 |
expect_errors=True) |
148 | 149 |
assert response.status_code == 400 |
149 |
assert response.json_body['err'] == 1
|
|
150 |
assert response.json_body['err_desc'].startswith('"path" is required')
|
|
150 |
assert response.json['err'] == 1 |
|
151 |
assert response.json['err_desc'] == "'path' is a required property"
|
|
151 | 152 | |
152 | 153 | |
153 | 154 |
def test_uploadfile_error_if_non_string_path(app, setup): |
... | ... | |
158 | 159 |
"content_type": "image/jpeg"}}, |
159 | 160 |
expect_errors=True) |
160 | 161 |
assert response.status_code == 400 |
161 |
assert response.json_body['err'] == 1
|
|
162 |
assert response.json_body['err_desc'].startswith('"path" must be string')
|
|
162 |
assert response.json['err'] == 1 |
|
163 |
assert response.json['err_desc'] == "1 is not of type 'string'"
|
|
163 | 164 | |
164 | 165 | |
165 | 166 |
def test_uploadfile_error_if_no_regular_path(app, setup): |
... | ... | |
170 | 171 |
"content_type": "image/jpeg"}}, |
171 | 172 |
expect_errors=True) |
172 | 173 |
assert response.status_code == 400 |
173 |
assert response.json_body['err'] == 1
|
|
174 |
assert response.json_body['err_desc'].startswith('"path" must be valid path')
|
|
174 |
assert response.json['err'] == 1 |
|
175 |
assert "'no/leading/slash' does not match " in response.json['err_desc']
|
|
175 | 176 | |
176 | 177 | |
177 | 178 |
def test_uploadfile_error_if_no_file_content(app, setup): |
... | ... | |
181 | 182 |
"file": {"filename": 'somefile.txt', "content_type": "image/jpeg"}}, |
182 | 183 |
expect_errors=True) |
183 | 184 |
assert response.status_code == 400 |
184 |
assert response.json_body['err'] == 1
|
|
185 |
assert response.json_body['err_desc'].startswith('"file[\'content\']" is required')
|
|
185 |
assert response.json['err'] == 1 |
|
186 |
assert response.json['err_desc'] == "'content' is a required property"
|
|
186 | 187 | |
187 | 188 | |
188 | 189 |
def test_uploadfile_error_if_non_string_file_content(app, setup): |
... | ... | |
192 | 193 |
"file": {"filename": 'somefile.txt', "content": 1, "content_type": "image/jpeg"}}, |
193 | 194 |
expect_errors=True) |
194 | 195 |
assert response.status_code == 400 |
195 |
assert response.json_body['err'] == 1
|
|
196 |
assert response.json_body['err_desc'].startswith('"file[\'content\']" must be string')
|
|
196 |
assert response.json['err'] == 1 |
|
197 |
assert response.json['err_desc'] == "1 is not of type 'string'"
|
|
197 | 198 | |
198 | 199 | |
199 | 200 |
def test_uploadfile_error_if_no_proper_base64_encoding(app, setup): |
... | ... | |
203 | 204 |
"file": {"filename": 'somefile.txt', "content": "1", "content_type": "image/jpeg"}}, |
204 | 205 |
expect_errors=True) |
205 | 206 |
assert response.status_code == 400 |
206 |
assert response.json_body['err'] == 1 |
|
207 |
assert response.json_body['err_desc'].startswith( |
|
208 |
'"file[\'content\']" must be a valid base64 string') |
|
207 |
assert response.json['err'] == 1 |
|
208 |
assert response.json['err_desc'].startswith('"file[\'content\']" must be a valid base64 string') |
|
209 | 209 | |
210 | 210 | |
211 | 211 |
def test_uploadfile_cmis_gateway_error(app, setup, monkeypatch): |
... | ... | |
220 | 220 |
params={"path": "/some/folder/structure", |
221 | 221 |
"file": {"filename": "file_name", "content": b64encode('aaaa'), |
222 | 222 |
"content_type": "image/jpeg"}}) |
223 |
assert response.json_body['err'] == 1
|
|
224 |
assert response.json_body['err_desc'].startswith("some error")
|
|
223 |
assert response.json['err'] == 1 |
|
224 |
assert response.json['err_desc'].startswith("some error") |
|
225 | 225 | |
226 | 226 | |
227 | 227 |
def test_get_or_create_folder_already_existing(monkeypatch): |
... | ... | |
331 | 331 | |
332 | 332 | |
333 | 333 |
def test_re_file_path(): |
334 |
from passerelle.apps.cmis.models import RE_FILE_PATH |
|
334 |
from passerelle.apps.cmis.models import FILE_PATH_PATTERN |
|
335 |
RE_FILE_PATH = re.compile(FILE_PATH_PATTERN) |
|
335 | 336 |
assert RE_FILE_PATH.match('/') |
336 | 337 |
assert RE_FILE_PATH.match('/some') |
337 | 338 |
assert RE_FILE_PATH.match('/some/path') |
... | ... | |
346 | 347 | |
347 | 348 | |
348 | 349 |
def test_re_file_name(): |
349 |
from passerelle.apps.cmis.models import RE_FILE_NAME |
|
350 |
from passerelle.apps.cmis.models import FILE_NAME_PATTERN |
|
351 |
RE_FILE_NAME = re.compile(FILE_NAME_PATTERN) |
|
350 | 352 |
assert RE_FILE_NAME.match('toto.tata') |
351 | 353 |
assert RE_FILE_NAME.match('TOTO.TATA') |
352 |
- |