##// END OF EJS Templates
file-store: always calculate sha256 and metadata.
marcink -
r3455:4171fa2a default
parent child Browse files
Show More
@@ -1,188 +1,211 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 time
23 import shutil
23 import shutil
24 import hashlib
24
25
25 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
26 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store.extensions import resolve_extensions
28 from rhodecode.apps.file_store.extensions import resolve_extensions
28 from rhodecode.apps.file_store.exceptions import FileNotAllowedException
29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException
29
30
30 METADATA_VER = 'v1'
31 METADATA_VER = 'v1'
31
32
32
33
33 class LocalFileStorage(object):
34 class LocalFileStorage(object):
34
35
35 @classmethod
36 @classmethod
36 def resolve_name(cls, name, directory):
37 def resolve_name(cls, name, directory):
37 """
38 """
38 Resolves a unique name and the correct path. If a filename
39 Resolves a unique name and the correct path. If a filename
39 for that path already exists then a numeric prefix with values > 0 will be
40 for that path already exists then a numeric prefix with values > 0 will be
40 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
41 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
41
42
42 :param name: base name of file
43 :param name: base name of file
43 :param directory: absolute directory path
44 :param directory: absolute directory path
44 """
45 """
45
46
46 basename, ext = os.path.splitext(name)
47 basename, ext = os.path.splitext(name)
47 counter = 0
48 counter = 0
48 while True:
49 while True:
49 name = '%s-%d%s' % (basename, counter, ext)
50 name = '%s-%d%s' % (basename, counter, ext)
50
51
51 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
52 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
52 sub_store = cls._sub_store_from_filename(basename)
53 sub_store = cls._sub_store_from_filename(basename)
53 sub_store_path = os.path.join(directory, sub_store)
54 sub_store_path = os.path.join(directory, sub_store)
54 if not os.path.exists(sub_store_path):
55 if not os.path.exists(sub_store_path):
55 os.makedirs(sub_store_path)
56 os.makedirs(sub_store_path)
56
57
57 path = os.path.join(sub_store_path, name)
58 path = os.path.join(sub_store_path, name)
58 if not os.path.exists(path):
59 if not os.path.exists(path):
59 return name, path
60 return name, path
60 counter += 1
61 counter += 1
61
62
62 @classmethod
63 @classmethod
63 def _sub_store_from_filename(cls, filename):
64 def _sub_store_from_filename(cls, filename):
64 return filename[:2]
65 return filename[:2]
65
66
67 @classmethod
68 def calculate_path_hash(cls, file_path):
69 """
70 Efficient calculation of file_path sha256 sum
71
72 :param file_path:
73 :return: sha256sum
74 """
75 digest = hashlib.sha256()
76 with open(file_path, 'rb') as f:
77 for chunk in iter(lambda: f.read(1024 * 100), b""):
78 digest.update(chunk)
79
80 return digest.hexdigest()
81
66 def __init__(self, base_path, extension_groups=None):
82 def __init__(self, base_path, extension_groups=None):
67
83
68 """
84 """
69 Local file storage
85 Local file storage
70
86
71 :param base_path: the absolute base path where uploads are stored
87 :param base_path: the absolute base path where uploads are stored
72 :param extension_groups: extensions string
88 :param extension_groups: extensions string
73 """
89 """
74
90
75 extension_groups = extension_groups or ['any']
91 extension_groups = extension_groups or ['any']
76 self.base_path = base_path
92 self.base_path = base_path
77 self.extensions = resolve_extensions([], groups=extension_groups)
93 self.extensions = resolve_extensions([], groups=extension_groups)
78
94
79 def store_path(self, filename):
95 def store_path(self, filename):
80 """
96 """
81 Returns absolute file path of the filename, joined to the
97 Returns absolute file path of the filename, joined to the
82 base_path.
98 base_path.
83
99
84 :param filename: base name of file
100 :param filename: base name of file
85 """
101 """
86 sub_store = self._sub_store_from_filename(filename)
102 sub_store = self._sub_store_from_filename(filename)
87 return os.path.join(self.base_path, sub_store, filename)
103 return os.path.join(self.base_path, sub_store, filename)
88
104
89 def delete(self, filename):
105 def delete(self, filename):
90 """
106 """
91 Deletes the filename. Filename is resolved with the
107 Deletes the filename. Filename is resolved with the
92 absolute path based on base_path. If file does not exist,
108 absolute path based on base_path. If file does not exist,
93 returns **False**, otherwise **True**
109 returns **False**, otherwise **True**
94
110
95 :param filename: base name of file
111 :param filename: base name of file
96 """
112 """
97 if self.exists(filename):
113 if self.exists(filename):
98 os.remove(self.store_path(filename))
114 os.remove(self.store_path(filename))
99 return True
115 return True
100 return False
116 return False
101
117
102 def exists(self, filename):
118 def exists(self, filename):
103 """
119 """
104 Checks if file exists. Resolves filename's absolute
120 Checks if file exists. Resolves filename's absolute
105 path based on base_path.
121 path based on base_path.
106
122
107 :param filename: base name of file
123 :param filename: base name of file
108 """
124 """
109 return os.path.exists(self.store_path(filename))
125 return os.path.exists(self.store_path(filename))
110
126
111 def filename_allowed(self, filename, extensions=None):
127 def filename_allowed(self, filename, extensions=None):
112 """Checks if a filename has an allowed extension
128 """Checks if a filename has an allowed extension
113
129
114 :param filename: base name of file
130 :param filename: base name of file
115 :param extensions: iterable of extensions (or self.extensions)
131 :param extensions: iterable of extensions (or self.extensions)
116 """
132 """
117 _, ext = os.path.splitext(filename)
133 _, ext = os.path.splitext(filename)
118 return self.extension_allowed(ext, extensions)
134 return self.extension_allowed(ext, extensions)
119
135
120 def extension_allowed(self, ext, extensions=None):
136 def extension_allowed(self, ext, extensions=None):
121 """
137 """
122 Checks if an extension is permitted. Both e.g. ".jpg" and
138 Checks if an extension is permitted. Both e.g. ".jpg" and
123 "jpg" can be passed in. Extension lookup is case-insensitive.
139 "jpg" can be passed in. Extension lookup is case-insensitive.
124
140
125 :param ext: extension to check
141 :param ext: extension to check
126 :param extensions: iterable of extensions to validate against (or self.extensions)
142 :param extensions: iterable of extensions to validate against (or self.extensions)
127 """
143 """
128
144
129 extensions = extensions or self.extensions
145 extensions = extensions or self.extensions
130 if not extensions:
146 if not extensions:
131 return True
147 return True
132 if ext.startswith('.'):
148 if ext.startswith('.'):
133 ext = ext[1:]
149 ext = ext[1:]
134 return ext.lower() in extensions
150 return ext.lower() in extensions
135
151
136 def save_file(self, file_obj, filename, directory=None, extensions=None,
152 def save_file(self, file_obj, filename, directory=None, extensions=None,
137 metadata=None, **kwargs):
153 extra_metadata=None, **kwargs):
138 """
154 """
139 Saves a file object to the uploads location.
155 Saves a file object to the uploads location.
140 Returns the resolved filename, i.e. the directory +
156 Returns the resolved filename, i.e. the directory +
141 the (randomized/incremented) base name.
157 the (randomized/incremented) base name.
142
158
143 :param file_obj: **cgi.FieldStorage** object (or similar)
159 :param file_obj: **cgi.FieldStorage** object (or similar)
144 :param filename: original filename
160 :param filename: original filename
145 :param directory: relative path of sub-directory
161 :param directory: relative path of sub-directory
146 :param extensions: iterable of allowed extensions, if not default
162 :param extensions: iterable of allowed extensions, if not default
147 :param metadata: JSON metadata to store next to the file with .meta suffix
163 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
148 :returns: modified filename
149 """
164 """
150
165
151 extensions = extensions or self.extensions
166 extensions = extensions or self.extensions
152
167
153 if not self.filename_allowed(filename, extensions):
168 if not self.filename_allowed(filename, extensions):
154 raise FileNotAllowedException()
169 raise FileNotAllowedException()
155
170
156 if directory:
171 if directory:
157 dest_directory = os.path.join(self.base_path, directory)
172 dest_directory = os.path.join(self.base_path, directory)
158 else:
173 else:
159 dest_directory = self.base_path
174 dest_directory = self.base_path
160
175
161 if not os.path.exists(dest_directory):
176 if not os.path.exists(dest_directory):
162 os.makedirs(dest_directory)
177 os.makedirs(dest_directory)
163
178
164 filename = utils.uid_filename(filename)
179 filename = utils.uid_filename(filename)
165
180
181 # resolve also produces special sub-dir for file optimized store
166 filename, path = self.resolve_name(filename, dest_directory)
182 filename, path = self.resolve_name(filename, dest_directory)
183 stored_file_dir = os.path.dirname(path)
167
184
168 file_obj.seek(0)
185 file_obj.seek(0)
169
186
170 with open(path, "wb") as dest:
187 with open(path, "wb") as dest:
171 shutil.copyfileobj(file_obj, dest)
188 shutil.copyfileobj(file_obj, dest)
172
189
173 if metadata:
190 metadata = {}
174 size = os.stat(path).st_size
191 if extra_metadata:
175 metadata.update(
192 metadata = extra_metadata
176 {"size": size,
193
177 "time": time.time(),
194 size = os.stat(path).st_size
178 "meta_ver": METADATA_VER})
195 file_hash = self.calculate_path_hash(path)
179
196
180 stored_file_path = os.path.dirname(path)
197 metadata.update(
181 filename_meta = filename + '.meta'
198 {"filename": filename,
182 with open(os.path.join(stored_file_path, filename_meta), "wb") as dest_meta:
199 "size": size,
183 dest_meta.write(json.dumps(metadata))
200 "time": time.time(),
201 "sha256": file_hash,
202 "meta_ver": METADATA_VER})
203
204 filename_meta = filename + '.meta'
205 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
206 dest_meta.write(json.dumps(metadata))
184
207
185 if directory:
208 if directory:
186 filename = os.path.join(directory, filename)
209 filename = os.path.join(directory, filename)
187
210
188 return filename, metadata
211 return filename, metadata
@@ -1,111 +1,111 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-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 import os
20 import os
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.ext_json import json
23 from rhodecode.lib.ext_json import json
24 from rhodecode.tests import TestController
24 from rhodecode.tests import TestController
25 from rhodecode.apps.file_store import utils, config_keys
25 from rhodecode.apps.file_store import utils, config_keys
26
26
27
27
28 def route_path(name, params=None, **kwargs):
28 def route_path(name, params=None, **kwargs):
29 import urllib
29 import urllib
30
30
31 base_url = {
31 base_url = {
32 'upload_file': '/_file_store/upload',
32 'upload_file': '/_file_store/upload',
33 'download_file': '/_file_store/download/{fid}',
33 'download_file': '/_file_store/download/{fid}',
34
34
35 }[name].format(**kwargs)
35 }[name].format(**kwargs)
36
36
37 if params:
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 return base_url
39 return base_url
40
40
41
41
42 class TestFileStoreViews(TestController):
42 class TestFileStoreViews(TestController):
43
43
44 @pytest.mark.parametrize("fid, content, exists", [
44 @pytest.mark.parametrize("fid, content, exists", [
45 ('abcde-0.jpg', "xxxxx", True),
45 ('abcde-0.jpg', "xxxxx", True),
46 ('abcde-0.exe', "1234567", True),
46 ('abcde-0.exe', "1234567", True),
47 ('abcde-0.jpg', "xxxxx", False),
47 ('abcde-0.jpg', "xxxxx", False),
48 ])
48 ])
49 def test_get_files_from_store(self, fid, content, exists, tmpdir):
49 def test_get_files_from_store(self, fid, content, exists, tmpdir):
50 self.log_user()
50 self.log_user()
51 store_path = self.app._pyramid_settings[config_keys.store_path]
51 store_path = self.app._pyramid_settings[config_keys.store_path]
52
52
53 if exists:
53 if exists:
54 status = 200
54 status = 200
55 store = utils.get_file_storage({config_keys.store_path: store_path})
55 store = utils.get_file_storage({config_keys.store_path: store_path})
56 filesystem_file = os.path.join(str(tmpdir), fid)
56 filesystem_file = os.path.join(str(tmpdir), fid)
57 with open(filesystem_file, 'wb') as f:
57 with open(filesystem_file, 'wb') as f:
58 f.write(content)
58 f.write(content)
59
59
60 with open(filesystem_file, 'rb') as f:
60 with open(filesystem_file, 'rb') as f:
61 fid, metadata = store.save_file(f, fid, metadata={'filename': fid})
61 fid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
62
62
63 else:
63 else:
64 status = 404
64 status = 404
65
65
66 response = self.app.get(route_path('download_file', fid=fid), status=status)
66 response = self.app.get(route_path('download_file', fid=fid), status=status)
67
67
68 if exists:
68 if exists:
69 assert response.text == content
69 assert response.text == content
70 file_store_path = os.path.dirname(store.resolve_name(fid, store_path)[1])
70 file_store_path = os.path.dirname(store.resolve_name(fid, store_path)[1])
71 metadata_file = os.path.join(file_store_path, fid + '.meta')
71 metadata_file = os.path.join(file_store_path, fid + '.meta')
72 assert os.path.exists(metadata_file)
72 assert os.path.exists(metadata_file)
73 with open(metadata_file, 'rb') as f:
73 with open(metadata_file, 'rb') as f:
74 json_data = json.loads(f.read())
74 json_data = json.loads(f.read())
75
75
76 assert json_data
76 assert json_data
77 assert 'size' in json_data
77 assert 'size' in json_data
78
78
79 def test_upload_files_without_content_to_store(self):
79 def test_upload_files_without_content_to_store(self):
80 self.log_user()
80 self.log_user()
81 response = self.app.post(
81 response = self.app.post(
82 route_path('upload_file'),
82 route_path('upload_file'),
83 params={'csrf_token': self.csrf_token},
83 params={'csrf_token': self.csrf_token},
84 status=200)
84 status=200)
85
85
86 assert response.json == {
86 assert response.json == {
87 u'error': u'store_file data field is missing',
87 u'error': u'store_file data field is missing',
88 u'access_path': None,
88 u'access_path': None,
89 u'store_fid': None}
89 u'store_fid': None}
90
90
91 def test_upload_files_bogus_content_to_store(self):
91 def test_upload_files_bogus_content_to_store(self):
92 self.log_user()
92 self.log_user()
93 response = self.app.post(
93 response = self.app.post(
94 route_path('upload_file'),
94 route_path('upload_file'),
95 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
95 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
96 status=200)
96 status=200)
97
97
98 assert response.json == {
98 assert response.json == {
99 u'error': u'filename cannot be read from the data field',
99 u'error': u'filename cannot be read from the data field',
100 u'access_path': None,
100 u'access_path': None,
101 u'store_fid': None}
101 u'store_fid': None}
102
102
103 def test_upload_content_to_store(self):
103 def test_upload_content_to_store(self):
104 self.log_user()
104 self.log_user()
105 response = self.app.post(
105 response = self.app.post(
106 route_path('upload_file'),
106 route_path('upload_file'),
107 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
107 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
108 params={'csrf_token': self.csrf_token},
108 params={'csrf_token': self.csrf_token},
109 status=200)
109 status=200)
110
110
111 assert response.json['store_fid']
111 assert response.json['store_fid']
@@ -1,96 +1,95 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 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 (CSRFRequired, NotAnonymous)
33 from rhodecode.lib.auth import (CSRFRequired, NotAnonymous)
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class FileStoreView(BaseAppView):
38 class FileStoreView(BaseAppView):
39 upload_key = 'store_file'
39 upload_key = 'store_file'
40
40
41 def load_default_context(self):
41 def load_default_context(self):
42 c = self._get_local_tmpl_context()
42 c = self._get_local_tmpl_context()
43 self.storage = utils.get_file_storage(self.request.registry.settings)
43 self.storage = utils.get_file_storage(self.request.registry.settings)
44 return c
44 return c
45
45
46 @NotAnonymous()
46 @NotAnonymous()
47 @CSRFRequired()
47 @CSRFRequired()
48 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
48 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
49 def upload_file(self):
49 def upload_file(self):
50 self.load_default_context()
50 self.load_default_context()
51 file_obj = self.request.POST.get(self.upload_key)
51 file_obj = self.request.POST.get(self.upload_key)
52
52
53 if file_obj is None:
53 if file_obj is None:
54 return {'store_fid': None,
54 return {'store_fid': None,
55 'access_path': None,
55 'access_path': None,
56 'error': '{} data field is missing'.format(self.upload_key)}
56 'error': '{} data field is missing'.format(self.upload_key)}
57
57
58 if not hasattr(file_obj, 'filename'):
58 if not hasattr(file_obj, 'filename'):
59 return {'store_fid': None,
59 return {'store_fid': None,
60 'access_path': None,
60 'access_path': None,
61 'error': 'filename cannot be read from the data field'}
61 'error': 'filename cannot be read from the data field'}
62
62
63 filename = file_obj.filename
63 filename = file_obj.filename
64
64
65 metadata = {
65 metadata = {
66 'filename': filename,
67 'size': '', # filled by save_file
68 'user_uploaded': {'username': self._rhodecode_user.username,
66 'user_uploaded': {'username': self._rhodecode_user.username,
69 'user_id': self._rhodecode_user.user_id,
67 'user_id': self._rhodecode_user.user_id,
70 'ip': self._rhodecode_user.ip_addr}}
68 'ip': self._rhodecode_user.ip_addr}}
71 try:
69 try:
72 store_fid, metadata = self.storage.save_file(file_obj.file, filename, metadata=metadata)
70 store_fid, metadata = self.storage.save_file(
71 file_obj.file, filename, extra_metadata=metadata)
73 except FileNotAllowedException:
72 except FileNotAllowedException:
74 return {'store_fid': None,
73 return {'store_fid': None,
75 'access_path': None,
74 'access_path': None,
76 'error': 'File {} is not allowed.'.format(filename)}
75 'error': 'File {} is not allowed.'.format(filename)}
77
76
78 except FileOverSizeException:
77 except FileOverSizeException:
79 return {'store_fid': None,
78 return {'store_fid': None,
80 'access_path': None,
79 'access_path': None,
81 'error': 'File {} is exceeding allowed limit.'.format(filename)}
80 'error': 'File {} is exceeding allowed limit.'.format(filename)}
82
81
83 return {'store_fid': store_fid,
82 return {'store_fid': store_fid,
84 'access_path': h.route_path('download_file', fid=store_fid)}
83 'access_path': h.route_path('download_file', fid=store_fid)}
85
84
86 @view_config(route_name='download_file')
85 @view_config(route_name='download_file')
87 def download_file(self):
86 def download_file(self):
88 self.load_default_context()
87 self.load_default_context()
89 file_uid = self.request.matchdict['fid']
88 file_uid = self.request.matchdict['fid']
90 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
89 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
91 if not self.storage.exists(file_uid):
90 if not self.storage.exists(file_uid):
92 log.debug('File with FID:%s not found in the store', file_uid)
91 log.debug('File with FID:%s not found in the store', file_uid)
93 raise HTTPNotFound()
92 raise HTTPNotFound()
94
93
95 file_path = self.storage.store_path(file_uid)
94 file_path = self.storage.store_path(file_uid)
96 return FileResponse(file_path)
95 return FileResponse(file_path)
General Comments 0
You need to be logged in to leave comments. Login now