##// 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 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import logging
21 21
22 22 from pyramid.view import view_config
23 23 from pyramid.response import FileResponse
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.exceptions import (
29 29 FileNotAllowedException, FileOverSizeException)
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.auth import (
34 34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 35 LoginRequired)
36 36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 37 from rhodecode.model.db import Session, FileStore, UserApiKeys
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class FileStoreView(BaseAppView):
43 43 upload_key = 'store_file'
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 self.storage = utils.get_file_storage(self.request.registry.settings)
48 48 return c
49 49
50 50 def _guess_type(self, file_name):
51 51 """
52 52 Our own type guesser for mimetypes using the rich DB
53 53 """
54 54 if not hasattr(self, 'db'):
55 55 self.db = get_mimetypes_db()
56 56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
57 57 return _content_type, _encoding
58 58
59 59 def _serve_file(self, file_uid):
60
61 60 if not self.storage.exists(file_uid):
62 61 store_path = self.storage.store_path(file_uid)
63 62 log.debug('File with FID:%s not found in the store under `%s`',
64 63 file_uid, store_path)
65 64 raise HTTPNotFound()
66 65
67 66 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
68 67 if not db_obj:
69 68 raise HTTPNotFound()
70 69
71 70 # private upload for user
72 71 if db_obj.check_acl and db_obj.scope_user_id:
73 72 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
74 73 db_obj.scope_user_id)
75 74 user = db_obj.user
76 75 if self._rhodecode_db_user.user_id != user.user_id:
77 76 log.warning('Access to file store object forbidden')
78 77 raise HTTPNotFound()
79 78
80 79 # scoped to repository permissions
81 80 if db_obj.check_acl and db_obj.scope_repo_id:
82 81 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
83 82 db_obj.scope_repo_id)
84 83 repo = db_obj.repo
85 84 perm_set = ['repository.read', 'repository.write', 'repository.admin']
86 85 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
87 86 if not has_perm:
88 87 log.warning('Access to file store object `%s` forbidden', file_uid)
89 88 raise HTTPNotFound()
90 89
91 90 # scoped to repository group permissions
92 91 if db_obj.check_acl and db_obj.scope_repo_group_id:
93 92 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
94 93 db_obj.scope_repo_group_id)
95 94 repo_group = db_obj.repo_group
96 95 perm_set = ['group.read', 'group.write', 'group.admin']
97 96 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
98 97 if not has_perm:
99 98 log.warning('Access to file store object `%s` forbidden', file_uid)
100 99 raise HTTPNotFound()
101 100
102 101 FileStore.bump_access_counter(file_uid)
103 102
104 103 file_path = self.storage.store_path(file_uid)
105 104 content_type = 'application/octet-stream'
106 105 content_encoding = None
107 106
108 107 _content_type, _encoding = self._guess_type(file_path)
109 108 if _content_type:
110 109 content_type = _content_type
111 110
112 111 # For file store we don't submit any session data, this logic tells the
113 112 # Session lib to skip it
114 113 setattr(self.request, '_file_response', True)
115 return FileResponse(file_path, request=self.request,
116 content_type=content_type, content_encoding=content_encoding)
114 response = FileResponse(
115 file_path, request=self.request,
116 content_type=content_type, content_encoding=content_encoding)
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
117 127
118 128 @LoginRequired()
119 129 @NotAnonymous()
120 130 @CSRFRequired()
121 131 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
122 132 def upload_file(self):
123 133 self.load_default_context()
124 134 file_obj = self.request.POST.get(self.upload_key)
125 135
126 136 if file_obj is None:
127 137 return {'store_fid': None,
128 138 'access_path': None,
129 139 'error': '{} data field is missing'.format(self.upload_key)}
130 140
131 141 if not hasattr(file_obj, 'filename'):
132 142 return {'store_fid': None,
133 143 'access_path': None,
134 144 'error': 'filename cannot be read from the data field'}
135 145
136 146 filename = file_obj.filename
137 147
138 148 metadata = {
139 149 'user_uploaded': {'username': self._rhodecode_user.username,
140 150 'user_id': self._rhodecode_user.user_id,
141 151 'ip': self._rhodecode_user.ip_addr}}
142 152 try:
143 153 store_uid, metadata = self.storage.save_file(
144 154 file_obj.file, filename, extra_metadata=metadata)
145 155 except FileNotAllowedException:
146 156 return {'store_fid': None,
147 157 'access_path': None,
148 158 'error': 'File {} is not allowed.'.format(filename)}
149 159
150 160 except FileOverSizeException:
151 161 return {'store_fid': None,
152 162 'access_path': None,
153 163 'error': 'File {} is exceeding allowed limit.'.format(filename)}
154 164
155 165 try:
156 166 entry = FileStore.create(
157 167 file_uid=store_uid, filename=metadata["filename"],
158 168 file_hash=metadata["sha256"], file_size=metadata["size"],
159 169 file_description=u'upload attachment',
160 170 check_acl=False, user_id=self._rhodecode_user.user_id
161 171 )
162 172 Session().add(entry)
163 173 Session().commit()
164 174 log.debug('Stored upload in DB as %s', entry)
165 175 except Exception:
166 176 log.exception('Failed to store file %s', filename)
167 177 return {'store_fid': None,
168 178 'access_path': None,
169 179 'error': 'File {} failed to store in DB.'.format(filename)}
170 180
171 181 return {'store_fid': store_uid,
172 182 'access_path': h.route_path('download_file', fid=store_uid)}
173 183
174 184 # ACL is checked by scopes, if no scope the file is accessible to all
175 185 @view_config(route_name='download_file')
176 186 def download_file(self):
177 187 self.load_default_context()
178 188 file_uid = self.request.matchdict['fid']
179 189 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
180 190 return self._serve_file(file_uid)
181 191
182 192 # in addition to @LoginRequired ACL is checked by scopes
183 193 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
184 194 @NotAnonymous()
185 195 @view_config(route_name='download_file_by_token')
186 196 def download_file_by_token(self):
187 197 """
188 198 Special view that allows to access the download file by special URL that
189 199 is stored inside the URL.
190 200
191 201 http://example.com/_file_store/token-download/TOKEN/FILE_UID
192 202 """
193 203 self.load_default_context()
194 204 file_uid = self.request.matchdict['fid']
195 205 return self._serve_file(file_uid)
General Comments 0
You need to be logged in to leave comments. Login now