##// END OF EJS Templates
API: added basic upload api for the file storage
marcink -
r3437:206694bc default
parent child Browse files
Show More
@@ -1,414 +1,481 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import inspect
21 import inspect
22 import logging
22 import logging
23 import itertools
23 import itertools
24 import base64
25
26 from pyramid import compat
24
27
25 from rhodecode.api import (
28 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
29 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
27
30
28 from rhodecode.api.utils import (
31 from rhodecode.api.utils import (
29 Optional, OAttr, has_superadmin_permission, get_user_or_error)
32 Optional, OAttr, has_superadmin_permission, get_user_or_error)
30 from rhodecode.lib.utils import repo2db_mapper
33 from rhodecode.lib.utils import repo2db_mapper
31 from rhodecode.lib import system_info
34 from rhodecode.lib import system_info
32 from rhodecode.lib import user_sessions
35 from rhodecode.lib import user_sessions
33 from rhodecode.lib import exc_tracking
36 from rhodecode.lib import exc_tracking
34 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.utils2 import safe_int
38 from rhodecode.lib.utils2 import safe_int
36 from rhodecode.model.db import UserIpMap
39 from rhodecode.model.db import UserIpMap
37 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.settings import VcsSettingsModel
41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.apps.upload_store import utils
43 from rhodecode.apps.upload_store.exceptions import FileNotAllowedException, \
44 FileOverSizeException
39
45
40 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
41
47
42
48
43 @jsonrpc_method()
49 @jsonrpc_method()
44 def get_server_info(request, apiuser):
50 def get_server_info(request, apiuser):
45 """
51 """
46 Returns the |RCE| server information.
52 Returns the |RCE| server information.
47
53
48 This includes the running version of |RCE| and all installed
54 This includes the running version of |RCE| and all installed
49 packages. This command takes the following options:
55 packages. This command takes the following options:
50
56
51 :param apiuser: This is filled automatically from the |authtoken|.
57 :param apiuser: This is filled automatically from the |authtoken|.
52 :type apiuser: AuthUser
58 :type apiuser: AuthUser
53
59
54 Example output:
60 Example output:
55
61
56 .. code-block:: bash
62 .. code-block:: bash
57
63
58 id : <id_given_in_input>
64 id : <id_given_in_input>
59 result : {
65 result : {
60 'modules': [<module name>,...]
66 'modules': [<module name>,...]
61 'py_version': <python version>,
67 'py_version': <python version>,
62 'platform': <platform type>,
68 'platform': <platform type>,
63 'rhodecode_version': <rhodecode version>
69 'rhodecode_version': <rhodecode version>
64 }
70 }
65 error : null
71 error : null
66 """
72 """
67
73
68 if not has_superadmin_permission(apiuser):
74 if not has_superadmin_permission(apiuser):
69 raise JSONRPCForbidden()
75 raise JSONRPCForbidden()
70
76
71 server_info = ScmModel().get_server_info(request.environ)
77 server_info = ScmModel().get_server_info(request.environ)
72 # rhodecode-index requires those
78 # rhodecode-index requires those
73
79
74 server_info['index_storage'] = server_info['search']['value']['location']
80 server_info['index_storage'] = server_info['search']['value']['location']
75 server_info['storage'] = server_info['storage']['value']['path']
81 server_info['storage'] = server_info['storage']['value']['path']
76
82
77 return server_info
83 return server_info
78
84
79
85
80 @jsonrpc_method()
86 @jsonrpc_method()
81 def get_repo_store(request, apiuser):
87 def get_repo_store(request, apiuser):
82 """
88 """
83 Returns the |RCE| repository storage information.
89 Returns the |RCE| repository storage information.
84
90
85 :param apiuser: This is filled automatically from the |authtoken|.
91 :param apiuser: This is filled automatically from the |authtoken|.
86 :type apiuser: AuthUser
92 :type apiuser: AuthUser
87
93
88 Example output:
94 Example output:
89
95
90 .. code-block:: bash
96 .. code-block:: bash
91
97
92 id : <id_given_in_input>
98 id : <id_given_in_input>
93 result : {
99 result : {
94 'modules': [<module name>,...]
100 'modules': [<module name>,...]
95 'py_version': <python version>,
101 'py_version': <python version>,
96 'platform': <platform type>,
102 'platform': <platform type>,
97 'rhodecode_version': <rhodecode version>
103 'rhodecode_version': <rhodecode version>
98 }
104 }
99 error : null
105 error : null
100 """
106 """
101
107
102 if not has_superadmin_permission(apiuser):
108 if not has_superadmin_permission(apiuser):
103 raise JSONRPCForbidden()
109 raise JSONRPCForbidden()
104
110
105 path = VcsSettingsModel().get_repos_location()
111 path = VcsSettingsModel().get_repos_location()
106 return {"path": path}
112 return {"path": path}
107
113
108
114
109 @jsonrpc_method()
115 @jsonrpc_method()
110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
116 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
111 """
117 """
112 Displays the IP Address as seen from the |RCE| server.
118 Displays the IP Address as seen from the |RCE| server.
113
119
114 * This command displays the IP Address, as well as all the defined IP
120 * This command displays the IP Address, as well as all the defined IP
115 addresses for the specified user. If the ``userid`` is not set, the
121 addresses for the specified user. If the ``userid`` is not set, the
116 data returned is for the user calling the method.
122 data returned is for the user calling the method.
117
123
118 This command can only be run using an |authtoken| with admin rights to
124 This command can only be run using an |authtoken| with admin rights to
119 the specified repository.
125 the specified repository.
120
126
121 This command takes the following options:
127 This command takes the following options:
122
128
123 :param apiuser: This is filled automatically from |authtoken|.
129 :param apiuser: This is filled automatically from |authtoken|.
124 :type apiuser: AuthUser
130 :type apiuser: AuthUser
125 :param userid: Sets the userid for which associated IP Address data
131 :param userid: Sets the userid for which associated IP Address data
126 is returned.
132 is returned.
127 :type userid: Optional(str or int)
133 :type userid: Optional(str or int)
128
134
129 Example output:
135 Example output:
130
136
131 .. code-block:: bash
137 .. code-block:: bash
132
138
133 id : <id_given_in_input>
139 id : <id_given_in_input>
134 result : {
140 result : {
135 "server_ip_addr": "<ip_from_clien>",
141 "server_ip_addr": "<ip_from_clien>",
136 "user_ips": [
142 "user_ips": [
137 {
143 {
138 "ip_addr": "<ip_with_mask>",
144 "ip_addr": "<ip_with_mask>",
139 "ip_range": ["<start_ip>", "<end_ip>"],
145 "ip_range": ["<start_ip>", "<end_ip>"],
140 },
146 },
141 ...
147 ...
142 ]
148 ]
143 }
149 }
144
150
145 """
151 """
146 if not has_superadmin_permission(apiuser):
152 if not has_superadmin_permission(apiuser):
147 raise JSONRPCForbidden()
153 raise JSONRPCForbidden()
148
154
149 userid = Optional.extract(userid, evaluate_locals=locals())
155 userid = Optional.extract(userid, evaluate_locals=locals())
150 userid = getattr(userid, 'user_id', userid)
156 userid = getattr(userid, 'user_id', userid)
151
157
152 user = get_user_or_error(userid)
158 user = get_user_or_error(userid)
153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
159 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
154 return {
160 return {
155 'server_ip_addr': request.rpc_ip_addr,
161 'server_ip_addr': request.rpc_ip_addr,
156 'user_ips': ips
162 'user_ips': ips
157 }
163 }
158
164
159
165
160 @jsonrpc_method()
166 @jsonrpc_method()
161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
167 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
162 """
168 """
163 Triggers a rescan of the specified repositories.
169 Triggers a rescan of the specified repositories.
164
170
165 * If the ``remove_obsolete`` option is set, it also deletes repositories
171 * If the ``remove_obsolete`` option is set, it also deletes repositories
166 that are found in the database but not on the file system, so called
172 that are found in the database but not on the file system, so called
167 "clean zombies".
173 "clean zombies".
168
174
169 This command can only be run using an |authtoken| with admin rights to
175 This command can only be run using an |authtoken| with admin rights to
170 the specified repository.
176 the specified repository.
171
177
172 This command takes the following options:
178 This command takes the following options:
173
179
174 :param apiuser: This is filled automatically from the |authtoken|.
180 :param apiuser: This is filled automatically from the |authtoken|.
175 :type apiuser: AuthUser
181 :type apiuser: AuthUser
176 :param remove_obsolete: Deletes repositories from the database that
182 :param remove_obsolete: Deletes repositories from the database that
177 are not found on the filesystem.
183 are not found on the filesystem.
178 :type remove_obsolete: Optional(``True`` | ``False``)
184 :type remove_obsolete: Optional(``True`` | ``False``)
179
185
180 Example output:
186 Example output:
181
187
182 .. code-block:: bash
188 .. code-block:: bash
183
189
184 id : <id_given_in_input>
190 id : <id_given_in_input>
185 result : {
191 result : {
186 'added': [<added repository name>,...]
192 'added': [<added repository name>,...]
187 'removed': [<removed repository name>,...]
193 'removed': [<removed repository name>,...]
188 }
194 }
189 error : null
195 error : null
190
196
191 Example error output:
197 Example error output:
192
198
193 .. code-block:: bash
199 .. code-block:: bash
194
200
195 id : <id_given_in_input>
201 id : <id_given_in_input>
196 result : null
202 result : null
197 error : {
203 error : {
198 'Error occurred during rescan repositories action'
204 'Error occurred during rescan repositories action'
199 }
205 }
200
206
201 """
207 """
202 if not has_superadmin_permission(apiuser):
208 if not has_superadmin_permission(apiuser):
203 raise JSONRPCForbidden()
209 raise JSONRPCForbidden()
204
210
205 try:
211 try:
206 rm_obsolete = Optional.extract(remove_obsolete)
212 rm_obsolete = Optional.extract(remove_obsolete)
207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
213 added, removed = repo2db_mapper(ScmModel().repo_scan(),
208 remove_obsolete=rm_obsolete)
214 remove_obsolete=rm_obsolete)
209 return {'added': added, 'removed': removed}
215 return {'added': added, 'removed': removed}
210 except Exception:
216 except Exception:
211 log.exception('Failed to run repo rescann')
217 log.exception('Failed to run repo rescann')
212 raise JSONRPCError(
218 raise JSONRPCError(
213 'Error occurred during rescan repositories action'
219 'Error occurred during rescan repositories action'
214 )
220 )
215
221
216
222
217 @jsonrpc_method()
223 @jsonrpc_method()
218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
224 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
219 """
225 """
220 Triggers a session cleanup action.
226 Triggers a session cleanup action.
221
227
222 If the ``older_then`` option is set, only sessions that hasn't been
228 If the ``older_then`` option is set, only sessions that hasn't been
223 accessed in the given number of days will be removed.
229 accessed in the given number of days will be removed.
224
230
225 This command can only be run using an |authtoken| with admin rights to
231 This command can only be run using an |authtoken| with admin rights to
226 the specified repository.
232 the specified repository.
227
233
228 This command takes the following options:
234 This command takes the following options:
229
235
230 :param apiuser: This is filled automatically from the |authtoken|.
236 :param apiuser: This is filled automatically from the |authtoken|.
231 :type apiuser: AuthUser
237 :type apiuser: AuthUser
232 :param older_then: Deletes session that hasn't been accessed
238 :param older_then: Deletes session that hasn't been accessed
233 in given number of days.
239 in given number of days.
234 :type older_then: Optional(int)
240 :type older_then: Optional(int)
235
241
236 Example output:
242 Example output:
237
243
238 .. code-block:: bash
244 .. code-block:: bash
239
245
240 id : <id_given_in_input>
246 id : <id_given_in_input>
241 result: {
247 result: {
242 "backend": "<type of backend>",
248 "backend": "<type of backend>",
243 "sessions_removed": <number_of_removed_sessions>
249 "sessions_removed": <number_of_removed_sessions>
244 }
250 }
245 error : null
251 error : null
246
252
247 Example error output:
253 Example error output:
248
254
249 .. code-block:: bash
255 .. code-block:: bash
250
256
251 id : <id_given_in_input>
257 id : <id_given_in_input>
252 result : null
258 result : null
253 error : {
259 error : {
254 'Error occurred during session cleanup'
260 'Error occurred during session cleanup'
255 }
261 }
256
262
257 """
263 """
258 if not has_superadmin_permission(apiuser):
264 if not has_superadmin_permission(apiuser):
259 raise JSONRPCForbidden()
265 raise JSONRPCForbidden()
260
266
261 older_then = safe_int(Optional.extract(older_then)) or 60
267 older_then = safe_int(Optional.extract(older_then)) or 60
262 older_than_seconds = 60 * 60 * 24 * older_then
268 older_than_seconds = 60 * 60 * 24 * older_then
263
269
264 config = system_info.rhodecode_config().get_value()['value']['config']
270 config = system_info.rhodecode_config().get_value()['value']['config']
265 session_model = user_sessions.get_session_handler(
271 session_model = user_sessions.get_session_handler(
266 config.get('beaker.session.type', 'memory'))(config)
272 config.get('beaker.session.type', 'memory'))(config)
267
273
268 backend = session_model.SESSION_TYPE
274 backend = session_model.SESSION_TYPE
269 try:
275 try:
270 cleaned = session_model.clean_sessions(
276 cleaned = session_model.clean_sessions(
271 older_than_seconds=older_than_seconds)
277 older_than_seconds=older_than_seconds)
272 return {'sessions_removed': cleaned, 'backend': backend}
278 return {'sessions_removed': cleaned, 'backend': backend}
273 except user_sessions.CleanupCommand as msg:
279 except user_sessions.CleanupCommand as msg:
274 return {'cleanup_command': msg.message, 'backend': backend}
280 return {'cleanup_command': msg.message, 'backend': backend}
275 except Exception as e:
281 except Exception as e:
276 log.exception('Failed session cleanup')
282 log.exception('Failed session cleanup')
277 raise JSONRPCError(
283 raise JSONRPCError(
278 'Error occurred during session cleanup'
284 'Error occurred during session cleanup'
279 )
285 )
280
286
281
287
282 @jsonrpc_method()
288 @jsonrpc_method()
283 def get_method(request, apiuser, pattern=Optional('*')):
289 def get_method(request, apiuser, pattern=Optional('*')):
284 """
290 """
285 Returns list of all available API methods. By default match pattern
291 Returns list of all available API methods. By default match pattern
286 os "*" but any other pattern can be specified. eg *comment* will return
292 os "*" but any other pattern can be specified. eg *comment* will return
287 all methods with comment inside them. If just single method is matched
293 all methods with comment inside them. If just single method is matched
288 returned data will also include method specification
294 returned data will also include method specification
289
295
290 This command can only be run using an |authtoken| with admin rights to
296 This command can only be run using an |authtoken| with admin rights to
291 the specified repository.
297 the specified repository.
292
298
293 This command takes the following options:
299 This command takes the following options:
294
300
295 :param apiuser: This is filled automatically from the |authtoken|.
301 :param apiuser: This is filled automatically from the |authtoken|.
296 :type apiuser: AuthUser
302 :type apiuser: AuthUser
297 :param pattern: pattern to match method names against
303 :param pattern: pattern to match method names against
298 :type pattern: Optional("*")
304 :type pattern: Optional("*")
299
305
300 Example output:
306 Example output:
301
307
302 .. code-block:: bash
308 .. code-block:: bash
303
309
304 id : <id_given_in_input>
310 id : <id_given_in_input>
305 "result": [
311 "result": [
306 "changeset_comment",
312 "changeset_comment",
307 "comment_pull_request",
313 "comment_pull_request",
308 "comment_commit"
314 "comment_commit"
309 ]
315 ]
310 error : null
316 error : null
311
317
312 .. code-block:: bash
318 .. code-block:: bash
313
319
314 id : <id_given_in_input>
320 id : <id_given_in_input>
315 "result": [
321 "result": [
316 "comment_commit",
322 "comment_commit",
317 {
323 {
318 "apiuser": "<RequiredType>",
324 "apiuser": "<RequiredType>",
319 "comment_type": "<Optional:u'note'>",
325 "comment_type": "<Optional:u'note'>",
320 "commit_id": "<RequiredType>",
326 "commit_id": "<RequiredType>",
321 "message": "<RequiredType>",
327 "message": "<RequiredType>",
322 "repoid": "<RequiredType>",
328 "repoid": "<RequiredType>",
323 "request": "<RequiredType>",
329 "request": "<RequiredType>",
324 "resolves_comment_id": "<Optional:None>",
330 "resolves_comment_id": "<Optional:None>",
325 "status": "<Optional:None>",
331 "status": "<Optional:None>",
326 "userid": "<Optional:<OptionalAttr:apiuser>>"
332 "userid": "<Optional:<OptionalAttr:apiuser>>"
327 }
333 }
328 ]
334 ]
329 error : null
335 error : null
330 """
336 """
331 if not has_superadmin_permission(apiuser):
337 if not has_superadmin_permission(apiuser):
332 raise JSONRPCForbidden()
338 raise JSONRPCForbidden()
333
339
334 pattern = Optional.extract(pattern)
340 pattern = Optional.extract(pattern)
335
341
336 matches = find_methods(request.registry.jsonrpc_methods, pattern)
342 matches = find_methods(request.registry.jsonrpc_methods, pattern)
337
343
338 args_desc = []
344 args_desc = []
339 if len(matches) == 1:
345 if len(matches) == 1:
340 func = matches[matches.keys()[0]]
346 func = matches[matches.keys()[0]]
341
347
342 argspec = inspect.getargspec(func)
348 argspec = inspect.getargspec(func)
343 arglist = argspec[0]
349 arglist = argspec[0]
344 defaults = map(repr, argspec[3] or [])
350 defaults = map(repr, argspec[3] or [])
345
351
346 default_empty = '<RequiredType>'
352 default_empty = '<RequiredType>'
347
353
348 # kw arguments required by this method
354 # kw arguments required by this method
349 func_kwargs = dict(itertools.izip_longest(
355 func_kwargs = dict(itertools.izip_longest(
350 reversed(arglist), reversed(defaults), fillvalue=default_empty))
356 reversed(arglist), reversed(defaults), fillvalue=default_empty))
351 args_desc.append(func_kwargs)
357 args_desc.append(func_kwargs)
352
358
353 return matches.keys() + args_desc
359 return matches.keys() + args_desc
354
360
355
361
356 @jsonrpc_method()
362 @jsonrpc_method()
357 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
363 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
358 """
364 """
359 Stores sent exception inside the built-in exception tracker in |RCE| server.
365 Stores sent exception inside the built-in exception tracker in |RCE| server.
360
366
361 This command can only be run using an |authtoken| with admin rights to
367 This command can only be run using an |authtoken| with admin rights to
362 the specified repository.
368 the specified repository.
363
369
364 This command takes the following options:
370 This command takes the following options:
365
371
366 :param apiuser: This is filled automatically from the |authtoken|.
372 :param apiuser: This is filled automatically from the |authtoken|.
367 :type apiuser: AuthUser
373 :type apiuser: AuthUser
368
374
369 :param exc_data_json: JSON data with exception e.g
375 :param exc_data_json: JSON data with exception e.g
370 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
376 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
371 :type exc_data_json: JSON data
377 :type exc_data_json: JSON data
372
378
373 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
379 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
374 :type prefix: Optional("rhodecode")
380 :type prefix: Optional("rhodecode")
375
381
376 Example output:
382 Example output:
377
383
378 .. code-block:: bash
384 .. code-block:: bash
379
385
380 id : <id_given_in_input>
386 id : <id_given_in_input>
381 "result": {
387 "result": {
382 "exc_id": 139718459226384,
388 "exc_id": 139718459226384,
383 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
389 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
384 }
390 }
385 error : null
391 error : null
386 """
392 """
387 if not has_superadmin_permission(apiuser):
393 if not has_superadmin_permission(apiuser):
388 raise JSONRPCForbidden()
394 raise JSONRPCForbidden()
389
395
390 prefix = Optional.extract(prefix)
396 prefix = Optional.extract(prefix)
391 exc_id = exc_tracking.generate_id()
397 exc_id = exc_tracking.generate_id()
392
398
393 try:
399 try:
394 exc_data = json.loads(exc_data_json)
400 exc_data = json.loads(exc_data_json)
395 except Exception:
401 except Exception:
396 log.error('Failed to parse JSON: %r', exc_data_json)
402 log.error('Failed to parse JSON: %r', exc_data_json)
397 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
403 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
398 'Please make sure it contains a valid JSON.')
404 'Please make sure it contains a valid JSON.')
399
405
400 try:
406 try:
401 exc_traceback = exc_data['exc_traceback']
407 exc_traceback = exc_data['exc_traceback']
402 exc_type_name = exc_data['exc_type_name']
408 exc_type_name = exc_data['exc_type_name']
403 except KeyError as err:
409 except KeyError as err:
404 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
410 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
405 'in exc_data_json field. Missing: {}'.format(err))
411 'in exc_data_json field. Missing: {}'.format(err))
406
412
407 exc_tracking._store_exception(
413 exc_tracking._store_exception(
408 exc_id=exc_id, exc_traceback=exc_traceback,
414 exc_id=exc_id, exc_traceback=exc_traceback,
409 exc_type_name=exc_type_name, prefix=prefix)
415 exc_type_name=exc_type_name, prefix=prefix)
410
416
411 exc_url = request.route_url(
417 exc_url = request.route_url(
412 'admin_settings_exception_tracker_show', exception_id=exc_id)
418 'admin_settings_exception_tracker_show', exception_id=exc_id)
413 return {'exc_id': exc_id, 'exc_url': exc_url}
419 return {'exc_id': exc_id, 'exc_url': exc_url}
414
420
421
422 @jsonrpc_method()
423 def upload_file(request, apiuser, filename, content):
424 """
425 Upload API for the file_store
426
427 Example usage from CLI::
428 rhodecode-api --instance-name=enterprise-1 upload_file "{\"content\": \"$(cat image.jpg | base64)\", \"filename\":\"image.jpg\"}"
429
430
431 This command can only be run using an |authtoken| with admin rights to
432 the specified repository.
433
434 This command takes the following options:
435
436 :param apiuser: This is filled automatically from the |authtoken|.
437 :type apiuser: AuthUser
438 :param filename: name of the file uploaded
439 :type filename: str
440 :param content: base64 encoded content of the uploaded file
441 :type prefix: str
442
443 Example output:
444
445 .. code-block:: bash
446
447 id : <id_given_in_input>
448 result: {
449 "access_path": "/_file_store/download/84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg",
450 "access_path_fqn": "http://server.domain.com/_file_store/download/84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg",
451 "store_fid": "84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg"
452 }
453 error : null
454 """
455 if not has_superadmin_permission(apiuser):
456 raise JSONRPCForbidden()
457
458 storage = utils.get_file_storage(request.registry.settings)
459
460 try:
461 file_obj = compat.NativeIO(base64.decodestring(content))
462 except Exception as exc:
463 raise JSONRPCError('File `{}` content decoding error: {}.'.format(filename, exc))
464
465 metadata = {
466 'filename': filename,
467 'size': '', # filled by save_file
468 'user_uploaded': {'username': apiuser.username,
469 'user_id': apiuser.user_id,
470 'ip': apiuser.ip_addr}}
471 try:
472 store_fid = storage.save_file(file_obj, filename, metadata=metadata)
473 except FileNotAllowedException:
474 raise JSONRPCError('File `{}` is not allowed.'.format(filename))
475
476 except FileOverSizeException:
477 raise JSONRPCError('File `{}` is exceeding allowed limit.'.format(filename))
478
479 return {'store_fid': store_fid,
480 'access_path_fqn': request.route_url('download_file', fid=store_fid),
481 'access_path': request.route_path('download_file', fid=store_fid)}
@@ -1,167 +1,171 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import time
22 import shutil
23 import shutil
23
24
24 from rhodecode.lib.ext_json import json
25 from rhodecode.lib.ext_json import json
25 from rhodecode.apps.upload_store import utils
26 from rhodecode.apps.upload_store import utils
26 from rhodecode.apps.upload_store.extensions import resolve_extensions
27 from rhodecode.apps.upload_store.extensions import resolve_extensions
27 from rhodecode.apps.upload_store.exceptions import FileNotAllowedException
28 from rhodecode.apps.upload_store.exceptions import FileNotAllowedException
28
29
30 METADATA_VER = 'v1'
31
29
32
30 class LocalFileStorage(object):
33 class LocalFileStorage(object):
31
34
32 @classmethod
35 @classmethod
33 def resolve_name(cls, name, directory):
36 def resolve_name(cls, name, directory):
34 """
37 """
35 Resolves a unique name and the correct path. If a filename
38 Resolves a unique name and the correct path. If a filename
36 for that path already exists then a numeric prefix with values > 0 will be
39 for that path already exists then a numeric prefix with values > 0 will be
37 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
40 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
38
41
39 :param name: base name of file
42 :param name: base name of file
40 :param directory: absolute directory path
43 :param directory: absolute directory path
41 """
44 """
42
45
43 basename, ext = os.path.splitext(name)
46 basename, ext = os.path.splitext(name)
44 counter = 0
47 counter = 0
45 while True:
48 while True:
46 name = '%s-%d%s' % (basename, counter, ext)
49 name = '%s-%d%s' % (basename, counter, ext)
47 path = os.path.join(directory, name)
50 path = os.path.join(directory, name)
48 if not os.path.exists(path):
51 if not os.path.exists(path):
49 return name, path
52 return name, path
50 counter += 1
53 counter += 1
51
54
52 def __init__(self, base_path, extension_groups=None):
55 def __init__(self, base_path, extension_groups=None):
53
56
54 """
57 """
55 Local file storage
58 Local file storage
56
59
57 :param base_path: the absolute base path where uploads are stored
60 :param base_path: the absolute base path where uploads are stored
58 :param extension_groups: extensions string
61 :param extension_groups: extensions string
59 """
62 """
60
63
61 extension_groups = extension_groups or ['any']
64 extension_groups = extension_groups or ['any']
62 self.base_path = base_path
65 self.base_path = base_path
63 self.extensions = resolve_extensions([], groups=extension_groups)
66 self.extensions = resolve_extensions([], groups=extension_groups)
64
67
65 def store_path(self, filename):
68 def store_path(self, filename):
66 """
69 """
67 Returns absolute file path of the filename, joined to the
70 Returns absolute file path of the filename, joined to the
68 base_path.
71 base_path.
69
72
70 :param filename: base name of file
73 :param filename: base name of file
71 """
74 """
72 return os.path.join(self.base_path, filename)
75 return os.path.join(self.base_path, filename)
73
76
74 def delete(self, filename):
77 def delete(self, filename):
75 """
78 """
76 Deletes the filename. Filename is resolved with the
79 Deletes the filename. Filename is resolved with the
77 absolute path based on base_path. If file does not exist,
80 absolute path based on base_path. If file does not exist,
78 returns **False**, otherwise **True**
81 returns **False**, otherwise **True**
79
82
80 :param filename: base name of file
83 :param filename: base name of file
81 """
84 """
82 if self.exists(filename):
85 if self.exists(filename):
83 os.remove(self.store_path(filename))
86 os.remove(self.store_path(filename))
84 return True
87 return True
85 return False
88 return False
86
89
87 def exists(self, filename):
90 def exists(self, filename):
88 """
91 """
89 Checks if file exists. Resolves filename's absolute
92 Checks if file exists. Resolves filename's absolute
90 path based on base_path.
93 path based on base_path.
91
94
92 :param filename: base name of file
95 :param filename: base name of file
93 """
96 """
94 return os.path.exists(self.store_path(filename))
97 return os.path.exists(self.store_path(filename))
95
98
96 def filename_allowed(self, filename, extensions=None):
99 def filename_allowed(self, filename, extensions=None):
97 """Checks if a filename has an allowed extension
100 """Checks if a filename has an allowed extension
98
101
99 :param filename: base name of file
102 :param filename: base name of file
100 :param extensions: iterable of extensions (or self.extensions)
103 :param extensions: iterable of extensions (or self.extensions)
101 """
104 """
102 _, ext = os.path.splitext(filename)
105 _, ext = os.path.splitext(filename)
103 return self.extension_allowed(ext, extensions)
106 return self.extension_allowed(ext, extensions)
104
107
105 def extension_allowed(self, ext, extensions=None):
108 def extension_allowed(self, ext, extensions=None):
106 """
109 """
107 Checks if an extension is permitted. Both e.g. ".jpg" and
110 Checks if an extension is permitted. Both e.g. ".jpg" and
108 "jpg" can be passed in. Extension lookup is case-insensitive.
111 "jpg" can be passed in. Extension lookup is case-insensitive.
109
112
110 :param extensions: iterable of extensions (or self.extensions)
113 :param extensions: iterable of extensions (or self.extensions)
111 """
114 """
112
115
113 extensions = extensions or self.extensions
116 extensions = extensions or self.extensions
114 if not extensions:
117 if not extensions:
115 return True
118 return True
116 if ext.startswith('.'):
119 if ext.startswith('.'):
117 ext = ext[1:]
120 ext = ext[1:]
118 return ext.lower() in extensions
121 return ext.lower() in extensions
119
122
120 def save_file(self, file_obj, filename, directory=None, extensions=None,
123 def save_file(self, file_obj, filename, directory=None, extensions=None,
121 metadata=None, **kwargs):
124 metadata=None, **kwargs):
122 """
125 """
123 Saves a file object to the uploads location.
126 Saves a file object to the uploads location.
124 Returns the resolved filename, i.e. the directory +
127 Returns the resolved filename, i.e. the directory +
125 the (randomized/incremented) base name.
128 the (randomized/incremented) base name.
126
129
127 :param file_obj: **cgi.FieldStorage** object (or similar)
130 :param file_obj: **cgi.FieldStorage** object (or similar)
128 :param filename: original filename
131 :param filename: original filename
129 :param directory: relative path of sub-directory
132 :param directory: relative path of sub-directory
130 :param extensions: iterable of allowed extensions, if not default
133 :param extensions: iterable of allowed extensions, if not default
131 :param metadata: JSON metadata to store next to the file with .meta suffix
134 :param metadata: JSON metadata to store next to the file with .meta suffix
132 :returns: modified filename
135 :returns: modified filename
133 """
136 """
134
137
135 extensions = extensions or self.extensions
138 extensions = extensions or self.extensions
136
139
137 if not self.filename_allowed(filename, extensions):
140 if not self.filename_allowed(filename, extensions):
138 raise FileNotAllowedException()
141 raise FileNotAllowedException()
139
142
140 if directory:
143 if directory:
141 dest_directory = os.path.join(self.base_path, directory)
144 dest_directory = os.path.join(self.base_path, directory)
142 else:
145 else:
143 dest_directory = self.base_path
146 dest_directory = self.base_path
144
147
145 if not os.path.exists(dest_directory):
148 if not os.path.exists(dest_directory):
146 os.makedirs(dest_directory)
149 os.makedirs(dest_directory)
147
150
148 filename = utils.uid_filename(filename)
151 filename = utils.uid_filename(filename)
149
152
150 filename, path = self.resolve_name(filename, dest_directory)
153 filename, path = self.resolve_name(filename, dest_directory)
151 filename_meta = filename + '.meta'
154 filename_meta = filename + '.meta'
152
155
153 file_obj.seek(0)
156 file_obj.seek(0)
154
157
155 with open(path, "wb") as dest:
158 with open(path, "wb") as dest:
156 shutil.copyfileobj(file_obj, dest)
159 shutil.copyfileobj(file_obj, dest)
157
160
158 if metadata:
161 if metadata:
159 size = os.stat(path).st_size
162 size = os.stat(path).st_size
160 metadata.update({'size': size})
163 metadata.update({'size': size, "time": time.time(),
164 "meta_ver": METADATA_VER})
161 with open(os.path.join(dest_directory, filename_meta), "wb") as dest_meta:
165 with open(os.path.join(dest_directory, filename_meta), "wb") as dest_meta:
162 dest_meta.write(json.dumps(metadata))
166 dest_meta.write(json.dumps(metadata))
163
167
164 if directory:
168 if directory:
165 filename = os.path.join(directory, filename)
169 filename = os.path.join(directory, filename)
166
170
167 return filename
171 return filename
General Comments 0
You need to be logged in to leave comments. Login now