##// END OF EJS Templates
file-store: expose additional headers, and content-disposiztion for nicer downloads
milka -
r4609:a3bacd67 stable
parent child Browse files
Show More
@@ -1,195 +1,205 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import logging
20 import logging
21
21
22 from pyramid.view import view_config
22 from pyramid.view import view_config
23 from pyramid.response import FileResponse
23 from pyramid.response import FileResponse
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25
25
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.exceptions import (
28 from rhodecode.apps.file_store.exceptions import (
29 FileNotAllowedException, FileOverSizeException)
29 FileNotAllowedException, FileOverSizeException)
30
30
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 LoginRequired)
35 LoginRequired)
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 from rhodecode.model.db import Session, FileStore, UserApiKeys
37 from rhodecode.model.db import Session, FileStore, UserApiKeys
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class FileStoreView(BaseAppView):
42 class FileStoreView(BaseAppView):
43 upload_key = 'store_file'
43 upload_key = 'store_file'
44
44
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
47 self.storage = utils.get_file_storage(self.request.registry.settings)
47 self.storage = utils.get_file_storage(self.request.registry.settings)
48 return c
48 return c
49
49
50 def _guess_type(self, file_name):
50 def _guess_type(self, file_name):
51 """
51 """
52 Our own type guesser for mimetypes using the rich DB
52 Our own type guesser for mimetypes using the rich DB
53 """
53 """
54 if not hasattr(self, 'db'):
54 if not hasattr(self, 'db'):
55 self.db = get_mimetypes_db()
55 self.db = get_mimetypes_db()
56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
57 return _content_type, _encoding
57 return _content_type, _encoding
58
58
59 def _serve_file(self, file_uid):
59 def _serve_file(self, file_uid):
60
61 if not self.storage.exists(file_uid):
60 if not self.storage.exists(file_uid):
62 store_path = self.storage.store_path(file_uid)
61 store_path = self.storage.store_path(file_uid)
63 log.debug('File with FID:%s not found in the store under `%s`',
62 log.debug('File with FID:%s not found in the store under `%s`',
64 file_uid, store_path)
63 file_uid, store_path)
65 raise HTTPNotFound()
64 raise HTTPNotFound()
66
65
67 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
66 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
68 if not db_obj:
67 if not db_obj:
69 raise HTTPNotFound()
68 raise HTTPNotFound()
70
69
71 # private upload for user
70 # private upload for user
72 if db_obj.check_acl and db_obj.scope_user_id:
71 if db_obj.check_acl and db_obj.scope_user_id:
73 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
72 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
74 db_obj.scope_user_id)
73 db_obj.scope_user_id)
75 user = db_obj.user
74 user = db_obj.user
76 if self._rhodecode_db_user.user_id != user.user_id:
75 if self._rhodecode_db_user.user_id != user.user_id:
77 log.warning('Access to file store object forbidden')
76 log.warning('Access to file store object forbidden')
78 raise HTTPNotFound()
77 raise HTTPNotFound()
79
78
80 # scoped to repository permissions
79 # scoped to repository permissions
81 if db_obj.check_acl and db_obj.scope_repo_id:
80 if db_obj.check_acl and db_obj.scope_repo_id:
82 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
81 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
83 db_obj.scope_repo_id)
82 db_obj.scope_repo_id)
84 repo = db_obj.repo
83 repo = db_obj.repo
85 perm_set = ['repository.read', 'repository.write', 'repository.admin']
84 perm_set = ['repository.read', 'repository.write', 'repository.admin']
86 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
85 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
87 if not has_perm:
86 if not has_perm:
88 log.warning('Access to file store object `%s` forbidden', file_uid)
87 log.warning('Access to file store object `%s` forbidden', file_uid)
89 raise HTTPNotFound()
88 raise HTTPNotFound()
90
89
91 # scoped to repository group permissions
90 # scoped to repository group permissions
92 if db_obj.check_acl and db_obj.scope_repo_group_id:
91 if db_obj.check_acl and db_obj.scope_repo_group_id:
93 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
92 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
94 db_obj.scope_repo_group_id)
93 db_obj.scope_repo_group_id)
95 repo_group = db_obj.repo_group
94 repo_group = db_obj.repo_group
96 perm_set = ['group.read', 'group.write', 'group.admin']
95 perm_set = ['group.read', 'group.write', 'group.admin']
97 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
96 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
98 if not has_perm:
97 if not has_perm:
99 log.warning('Access to file store object `%s` forbidden', file_uid)
98 log.warning('Access to file store object `%s` forbidden', file_uid)
100 raise HTTPNotFound()
99 raise HTTPNotFound()
101
100
102 FileStore.bump_access_counter(file_uid)
101 FileStore.bump_access_counter(file_uid)
103
102
104 file_path = self.storage.store_path(file_uid)
103 file_path = self.storage.store_path(file_uid)
105 content_type = 'application/octet-stream'
104 content_type = 'application/octet-stream'
106 content_encoding = None
105 content_encoding = None
107
106
108 _content_type, _encoding = self._guess_type(file_path)
107 _content_type, _encoding = self._guess_type(file_path)
109 if _content_type:
108 if _content_type:
110 content_type = _content_type
109 content_type = _content_type
111
110
112 # For file store we don't submit any session data, this logic tells the
111 # For file store we don't submit any session data, this logic tells the
113 # Session lib to skip it
112 # Session lib to skip it
114 setattr(self.request, '_file_response', True)
113 setattr(self.request, '_file_response', True)
115 return FileResponse(file_path, request=self.request,
114 response = FileResponse(
115 file_path, request=self.request,
116 content_type=content_type, content_encoding=content_encoding)
116 content_type=content_type, content_encoding=content_encoding)
117
117
118 file_name = db_obj.file_display_name
119
120 response.headers["Content-Disposition"] = (
121 'attachment; filename="{}"'.format(str(file_name))
122 )
123 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
124 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
125 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
126 return response
127
118 @LoginRequired()
128 @LoginRequired()
119 @NotAnonymous()
129 @NotAnonymous()
120 @CSRFRequired()
130 @CSRFRequired()
121 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
131 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
122 def upload_file(self):
132 def upload_file(self):
123 self.load_default_context()
133 self.load_default_context()
124 file_obj = self.request.POST.get(self.upload_key)
134 file_obj = self.request.POST.get(self.upload_key)
125
135
126 if file_obj is None:
136 if file_obj is None:
127 return {'store_fid': None,
137 return {'store_fid': None,
128 'access_path': None,
138 'access_path': None,
129 'error': '{} data field is missing'.format(self.upload_key)}
139 'error': '{} data field is missing'.format(self.upload_key)}
130
140
131 if not hasattr(file_obj, 'filename'):
141 if not hasattr(file_obj, 'filename'):
132 return {'store_fid': None,
142 return {'store_fid': None,
133 'access_path': None,
143 'access_path': None,
134 'error': 'filename cannot be read from the data field'}
144 'error': 'filename cannot be read from the data field'}
135
145
136 filename = file_obj.filename
146 filename = file_obj.filename
137
147
138 metadata = {
148 metadata = {
139 'user_uploaded': {'username': self._rhodecode_user.username,
149 'user_uploaded': {'username': self._rhodecode_user.username,
140 'user_id': self._rhodecode_user.user_id,
150 'user_id': self._rhodecode_user.user_id,
141 'ip': self._rhodecode_user.ip_addr}}
151 'ip': self._rhodecode_user.ip_addr}}
142 try:
152 try:
143 store_uid, metadata = self.storage.save_file(
153 store_uid, metadata = self.storage.save_file(
144 file_obj.file, filename, extra_metadata=metadata)
154 file_obj.file, filename, extra_metadata=metadata)
145 except FileNotAllowedException:
155 except FileNotAllowedException:
146 return {'store_fid': None,
156 return {'store_fid': None,
147 'access_path': None,
157 'access_path': None,
148 'error': 'File {} is not allowed.'.format(filename)}
158 'error': 'File {} is not allowed.'.format(filename)}
149
159
150 except FileOverSizeException:
160 except FileOverSizeException:
151 return {'store_fid': None,
161 return {'store_fid': None,
152 'access_path': None,
162 'access_path': None,
153 'error': 'File {} is exceeding allowed limit.'.format(filename)}
163 'error': 'File {} is exceeding allowed limit.'.format(filename)}
154
164
155 try:
165 try:
156 entry = FileStore.create(
166 entry = FileStore.create(
157 file_uid=store_uid, filename=metadata["filename"],
167 file_uid=store_uid, filename=metadata["filename"],
158 file_hash=metadata["sha256"], file_size=metadata["size"],
168 file_hash=metadata["sha256"], file_size=metadata["size"],
159 file_description=u'upload attachment',
169 file_description=u'upload attachment',
160 check_acl=False, user_id=self._rhodecode_user.user_id
170 check_acl=False, user_id=self._rhodecode_user.user_id
161 )
171 )
162 Session().add(entry)
172 Session().add(entry)
163 Session().commit()
173 Session().commit()
164 log.debug('Stored upload in DB as %s', entry)
174 log.debug('Stored upload in DB as %s', entry)
165 except Exception:
175 except Exception:
166 log.exception('Failed to store file %s', filename)
176 log.exception('Failed to store file %s', filename)
167 return {'store_fid': None,
177 return {'store_fid': None,
168 'access_path': None,
178 'access_path': None,
169 'error': 'File {} failed to store in DB.'.format(filename)}
179 'error': 'File {} failed to store in DB.'.format(filename)}
170
180
171 return {'store_fid': store_uid,
181 return {'store_fid': store_uid,
172 'access_path': h.route_path('download_file', fid=store_uid)}
182 'access_path': h.route_path('download_file', fid=store_uid)}
173
183
174 # ACL is checked by scopes, if no scope the file is accessible to all
184 # ACL is checked by scopes, if no scope the file is accessible to all
175 @view_config(route_name='download_file')
185 @view_config(route_name='download_file')
176 def download_file(self):
186 def download_file(self):
177 self.load_default_context()
187 self.load_default_context()
178 file_uid = self.request.matchdict['fid']
188 file_uid = self.request.matchdict['fid']
179 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
189 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
180 return self._serve_file(file_uid)
190 return self._serve_file(file_uid)
181
191
182 # in addition to @LoginRequired ACL is checked by scopes
192 # in addition to @LoginRequired ACL is checked by scopes
183 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
193 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
184 @NotAnonymous()
194 @NotAnonymous()
185 @view_config(route_name='download_file_by_token')
195 @view_config(route_name='download_file_by_token')
186 def download_file_by_token(self):
196 def download_file_by_token(self):
187 """
197 """
188 Special view that allows to access the download file by special URL that
198 Special view that allows to access the download file by special URL that
189 is stored inside the URL.
199 is stored inside the URL.
190
200
191 http://example.com/_file_store/token-download/TOKEN/FILE_UID
201 http://example.com/_file_store/token-download/TOKEN/FILE_UID
192 """
202 """
193 self.load_default_context()
203 self.load_default_context()
194 file_uid = self.request.matchdict['fid']
204 file_uid = self.request.matchdict['fid']
195 return self._serve_file(file_uid)
205 return self._serve_file(file_uid)
General Comments 0
You need to be logged in to leave comments. Login now