##// END OF EJS Templates
git-lfs: don't store oid under repo-path....
marcink -
r181:58195c38 default
parent child Browse files
Show More
@@ -1,276 +1,276 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import re
19 19 import logging
20 20 from wsgiref.util import FileWrapper
21 21
22 22 import simplejson as json
23 23 from pyramid.config import Configurator
24 24 from pyramid.response import Response, FileIter
25 25 from pyramid.httpexceptions import (
26 26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
27 27 HTTPUnprocessableEntity)
28 28
29 29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
30 30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
31 31 from vcsserver.utils import safe_int
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
37 37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
38 38
39 39
40 40 def write_response_error(http_exception, text=None):
41 41 content_type = 'application/json'
42 42 _exception = http_exception(content_type=content_type)
43 43 _exception.content_type = content_type
44 44 if text:
45 45 _exception.body = json.dumps({'message': text})
46 46 log.debug('LFS: writing response of type %s to client with text:%s',
47 47 http_exception, text)
48 48 return _exception
49 49
50 50
51 51 class AuthHeaderRequired(object):
52 52 """
53 53 Decorator to check if request has proper auth-header
54 54 """
55 55
56 56 def __call__(self, func):
57 57 return get_cython_compat_decorator(self.__wrapper, func)
58 58
59 59 def __wrapper(self, func, *fargs, **fkwargs):
60 60 request = fargs[1]
61 61 auth = request.authorization
62 62 if not auth:
63 63 return write_response_error(HTTPForbidden)
64 64 return func(*fargs[1:], **fkwargs)
65 65
66 66
67 67 # views
68 68
69 69 def lfs_objects(request):
70 70 # indicate not supported, V1 API
71 71 log.warning('LFS: v1 api not supported, reporting it back to client')
72 72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
73 73
74 74
75 75 @AuthHeaderRequired()
76 76 def lfs_objects_batch(request):
77 77 """
78 78 The client sends the following information to the Batch endpoint to transfer some objects:
79 79
80 80 operation - Should be download or upload.
81 81 transfers - An optional Array of String identifiers for transfer
82 82 adapters that the client has configured. If omitted, the basic
83 83 transfer adapter MUST be assumed by the server.
84 84 objects - An Array of objects to download.
85 85 oid - String OID of the LFS object.
86 86 size - Integer byte size of the LFS object. Must be at least zero.
87 87 """
88 88 auth = request.authorization
89 89
90 90 repo = request.matchdict.get('repo')
91 91
92 92 data = request.json
93 93 operation = data.get('operation')
94 94 if operation not in ('download', 'upload'):
95 95 log.debug('LFS: unsupported operation:%s', operation)
96 96 return write_response_error(
97 97 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
98 98
99 99 if 'objects' not in data:
100 100 log.debug('LFS: missing objects data')
101 101 return write_response_error(
102 102 HTTPBadRequest, 'missing objects data')
103 103
104 104 log.debug('LFS: handling operation of type: %s', operation)
105 105
106 106 objects = []
107 107 for o in data['objects']:
108 108 try:
109 109 oid = o['oid']
110 110 obj_size = o['size']
111 111 except KeyError:
112 112 log.exception('LFS, failed to extract data')
113 113 return write_response_error(
114 114 HTTPBadRequest, 'unsupported data in objects')
115 115
116 116 obj_data = {'oid': oid}
117 117
118 118 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid)
119 119 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo)
120 120 store = LFSOidStore(
121 repo, oid, store_location=request.registry.git_lfs_store_path)
121 oid, repo, store_location=request.registry.git_lfs_store_path)
122 122 handler = OidHandler(
123 123 store, repo, auth, oid, obj_size, obj_data,
124 124 obj_href, obj_verify_href)
125 125
126 126 # this verifies also OIDs
127 127 actions, errors = handler.exec_operation(operation)
128 128 if errors:
129 129 log.warning('LFS: got following errors: %s', errors)
130 130 obj_data['errors'] = errors
131 131
132 132 if actions:
133 133 obj_data['actions'] = actions
134 134
135 135 obj_data['size'] = obj_size
136 136 obj_data['authenticated'] = True
137 137 objects.append(obj_data)
138 138
139 139 result = {'objects': objects, 'transfer': 'basic'}
140 140 log.debug('LFS Response %s', safe_result(result))
141 141
142 142 return result
143 143
144 144
145 145 def lfs_objects_oid_upload(request):
146 146 repo = request.matchdict.get('repo')
147 147 oid = request.matchdict.get('oid')
148 148 store = LFSOidStore(
149 repo, oid, store_location=request.registry.git_lfs_store_path)
149 oid, repo, store_location=request.registry.git_lfs_store_path)
150 150 engine = store.get_engine(mode='wb')
151 151 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
152 152 with engine as f:
153 153 for chunk in FileWrapper(request.body_file_seekable, blksize=64 * 1024):
154 154 f.write(chunk)
155 155
156 156 return {'upload': 'ok'}
157 157
158 158
159 159 def lfs_objects_oid_download(request):
160 160 repo = request.matchdict.get('repo')
161 161 oid = request.matchdict.get('oid')
162 162
163 163 store = LFSOidStore(
164 repo, oid, store_location=request.registry.git_lfs_store_path)
164 oid, repo, store_location=request.registry.git_lfs_store_path)
165 165 if not store.has_oid():
166 166 log.debug('LFS: oid %s does not exists in store', oid)
167 167 return write_response_error(
168 168 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
169 169
170 170 # TODO(marcink): support range header ?
171 171 # Range: bytes=0-, `bytes=(\d+)\-.*`
172 172
173 173 f = open(store.oid_path, 'rb')
174 174 response = Response(
175 175 content_type='application/octet-stream', app_iter=FileIter(f))
176 176 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
177 177 return response
178 178
179 179
180 180 def lfs_objects_verify(request):
181 181 repo = request.matchdict.get('repo')
182 182
183 183 data = request.json
184 184 oid = data.get('oid')
185 185 size = safe_int(data.get('size'))
186 186
187 187 if not (oid and size):
188 188 return write_response_error(
189 189 HTTPBadRequest, 'missing oid and size in request data')
190 190
191 store = LFSOidStore(repo, oid,
192 store_location=request.registry.git_lfs_store_path)
191 store = LFSOidStore(
192 oid, repo, store_location=request.registry.git_lfs_store_path)
193 193 if not store.has_oid():
194 194 log.debug('LFS: oid %s does not exists in store', oid)
195 195 return write_response_error(
196 196 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
197 197
198 198 store_size = store.size_oid()
199 199 if store_size != size:
200 200 msg = 'requested file size mismatch store size:%s requested:%s' % (
201 201 store_size, size)
202 202 return write_response_error(
203 203 HTTPUnprocessableEntity, msg)
204 204
205 205 return {'message': {'size': 'ok', 'in_store': 'ok'}}
206 206
207 207
208 208 def lfs_objects_lock(request):
209 209 return write_response_error(
210 210 HTTPNotImplemented, 'GIT LFS locking api not supported')
211 211
212 212
213 213 def not_found(request):
214 214 return write_response_error(
215 215 HTTPNotFound, 'request path not found')
216 216
217 217
218 218 def lfs_disabled(request):
219 219 return write_response_error(
220 220 HTTPNotImplemented, 'GIT LFS disabled for this repo')
221 221
222 222
223 223 def git_lfs_app(config):
224 224
225 225 # v1 API deprecation endpoint
226 226 config.add_route('lfs_objects',
227 227 '/{repo:.*?[^/]}/info/lfs/objects')
228 228 config.add_view(lfs_objects, route_name='lfs_objects',
229 229 request_method='POST', renderer='json')
230 230
231 231 # locking API
232 232 config.add_route('lfs_objects_lock',
233 233 '/{repo:.*?[^/]}/info/lfs/locks')
234 234 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
235 235 request_method=('POST', 'GET'), renderer='json')
236 236
237 237 config.add_route('lfs_objects_lock_verify',
238 238 '/{repo:.*?[^/]}/info/lfs/locks/verify')
239 239 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
240 240 request_method=('POST', 'GET'), renderer='json')
241 241
242 242 # batch API
243 243 config.add_route('lfs_objects_batch',
244 244 '/{repo:.*?[^/]}/info/lfs/objects/batch')
245 245 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
246 246 request_method='POST', renderer='json')
247 247
248 248 # oid upload/download API
249 249 config.add_route('lfs_objects_oid',
250 250 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
251 251 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
252 252 request_method='PUT', renderer='json')
253 253 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
254 254 request_method='GET', renderer='json')
255 255
256 256 # verification API
257 257 config.add_route('lfs_objects_verify',
258 258 '/{repo:.*?[^/]}/info/lfs/verify')
259 259 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
260 260 request_method='POST', renderer='json')
261 261
262 262 # not found handler for API
263 263 config.add_notfound_view(not_found, renderer='json')
264 264
265 265
266 266 def create_app(git_lfs_enabled, git_lfs_store_path):
267 267 config = Configurator()
268 268 if git_lfs_enabled:
269 269 config.include(git_lfs_app)
270 270 config.registry.git_lfs_store_path = git_lfs_store_path
271 271 else:
272 272 # not found handler for API, reporting disabled LFS support
273 273 config.add_notfound_view(lfs_disabled, renderer='json')
274 274
275 275 app = config.make_wsgi_app()
276 276 return app
@@ -1,167 +1,166 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import shutil
20 20 import logging
21 21 from collections import OrderedDict
22 22
23 23 log = logging.getLogger(__name__)
24 24
25 25
26 26 class OidHandler(object):
27 27
28 28 def __init__(self, store, repo_name, auth, oid, obj_size, obj_data, obj_href,
29 29 obj_verify_href=None):
30 30 self.current_store = store
31 31 self.repo_name = repo_name
32 32 self.auth = auth
33 33 self.oid = oid
34 34 self.obj_size = obj_size
35 35 self.obj_data = obj_data
36 36 self.obj_href = obj_href
37 37 self.obj_verify_href = obj_verify_href
38 38
39 39 def get_store(self, mode=None):
40 40 return self.current_store
41 41
42 42 def get_auth(self):
43 43 """returns auth header for re-use in upload/download"""
44 44 return " ".join(self.auth)
45 45
46 46 def download(self):
47 47
48 48 store = self.get_store()
49 49 response = None
50 50 has_errors = None
51 51
52 52 if not store.has_oid():
53 53 # error reply back to client that something is wrong with dl
54 54 err_msg = 'object: {} does not exist in store'.format(store.oid)
55 55 has_errors = OrderedDict(
56 56 error=OrderedDict(
57 57 code=404,
58 58 message=err_msg
59 59 )
60 60 )
61 61
62 62 download_action = OrderedDict(
63 63 href=self.obj_href,
64 64 header=OrderedDict([("Authorization", self.get_auth())])
65 65 )
66 66 if not has_errors:
67 67 response = OrderedDict(download=download_action)
68 68 return response, has_errors
69 69
70 70 def upload(self, skip_existing=True):
71 71 """
72 72 Write upload action for git-lfs server
73 73 """
74 74
75 75 store = self.get_store()
76 76 response = None
77 77 has_errors = None
78 78
79 79 # verify if we have the OID before, if we do, reply with empty
80 80 if store.has_oid():
81 81 log.debug('LFS: store already has oid %s', store.oid)
82 82 if skip_existing:
83 83 log.debug('LFS: skipping further action as oid is existing')
84 84 return response, has_errors
85 85
86 86 upload_action = OrderedDict(
87 87 href=self.obj_href,
88 88 header=OrderedDict([("Authorization", self.get_auth())])
89 89 )
90 90 if not has_errors:
91 91 response = OrderedDict(upload=upload_action)
92 92 # if specified in handler, return the verification endpoint
93 93 if self.obj_verify_href:
94 94 verify_action = OrderedDict(
95 95 href=self.obj_verify_href,
96 96 header=OrderedDict([("Authorization", self.get_auth())])
97 97 )
98 98 response['verify'] = verify_action
99 99 return response, has_errors
100 100
101 101 def exec_operation(self, operation, *args, **kwargs):
102 102 handler = getattr(self, operation)
103 103 log.debug('LFS: handling request using %s handler', handler)
104 104 return handler(*args, **kwargs)
105 105
106 106
107 107 class LFSOidStore(object):
108 108
109 def __init__(self, repo, oid, store_location=None):
110 self._store = store_location or self.get_default_store()
109 def __init__(self, oid, repo, store_location=None):
111 110 self.oid = oid
112 111 self.repo = repo
113 self.store_path = os.path.join(self._store, repo)
112 self.store_path = store_location or self.get_default_store()
114 113 self.tmp_oid_path = os.path.join(self.store_path, oid + '.tmp')
115 114 self.oid_path = os.path.join(self.store_path, oid)
116 115 self.fd = None
117 116
118 117 def get_engine(self, mode):
119 118 """
120 119 engine = .get_engine(mode='wb')
121 120 with engine as f:
122 121 f.write('...')
123 122 """
124 123
125 124 class StoreEngine(object):
126 125 def __init__(self, mode, store_path, oid_path, tmp_oid_path):
127 126 self.mode = mode
128 127 self.store_path = store_path
129 128 self.oid_path = oid_path
130 129 self.tmp_oid_path = tmp_oid_path
131 130
132 131 def __enter__(self):
133 132 if not os.path.isdir(self.store_path):
134 133 os.makedirs(self.store_path)
135 134
136 135 # TODO(marcink): maybe write metadata here with size/oid ?
137 136 fd = open(self.tmp_oid_path, self.mode)
138 137 self.fd = fd
139 138 return fd
140 139
141 140 def __exit__(self, exc_type, exc_value, traceback):
142 141 # close tmp file, and rename to final destination
143 142 self.fd.close()
144 143 shutil.move(self.tmp_oid_path, self.oid_path)
145 144
146 145 return StoreEngine(
147 146 mode, self.store_path, self.oid_path, self.tmp_oid_path)
148 147
149 148 def get_default_store(self):
150 149 """
151 150 Default store, consistent with defaults of Mercurial large files store
152 151 which is /home/username/.cache/largefiles
153 152 """
154 153 user_home = os.path.expanduser("~")
155 154 return os.path.join(user_home, '.cache', 'lfs-store')
156 155
157 156 def has_oid(self):
158 157 return os.path.exists(os.path.join(self.store_path, self.oid))
159 158
160 159 def size_oid(self):
161 160 size = -1
162 161
163 162 if self.has_oid():
164 163 oid = os.path.join(self.store_path, self.oid)
165 164 size = os.stat(oid).st_size
166 165
167 return size No newline at end of file
166 return size
@@ -1,233 +1,237 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import pytest
20 20 from webtest.app import TestApp as WebObTestApp
21 21
22 22 from vcsserver.git_lfs.app import create_app
23 23
24 24
25 25 @pytest.fixture(scope='function')
26 26 def git_lfs_app(tmpdir):
27 27 custom_app = WebObTestApp(create_app(
28 28 git_lfs_enabled=True, git_lfs_store_path=str(tmpdir)))
29 29 custom_app._store = str(tmpdir)
30 30 return custom_app
31 31
32 32
33 33 @pytest.fixture()
34 34 def http_auth():
35 35 return {'HTTP_AUTHORIZATION': "Basic XXXXX"}
36 36
37 37
38 38 class TestLFSApplication(object):
39 39
40 40 def test_app_wrong_path(self, git_lfs_app):
41 41 git_lfs_app.get('/repo/info/lfs/xxx', status=404)
42 42
43 43 def test_app_deprecated_endpoint(self, git_lfs_app):
44 44 response = git_lfs_app.post('/repo/info/lfs/objects', status=501)
45 45 assert response.status_code == 501
46 46 assert response.json == {u'message': u'LFS: v1 api not supported'}
47 47
48 48 def test_app_lock_verify_api_not_available(self, git_lfs_app):
49 49 response = git_lfs_app.post('/repo/info/lfs/locks/verify', status=501)
50 50 assert response.status_code == 501
51 51 assert response.json == {
52 52 u'message': u'GIT LFS locking api not supported'}
53 53
54 54 def test_app_lock_api_not_available(self, git_lfs_app):
55 55 response = git_lfs_app.post('/repo/info/lfs/locks', status=501)
56 56 assert response.status_code == 501
57 57 assert response.json == {
58 58 u'message': u'GIT LFS locking api not supported'}
59 59
60 60 def test_app_batch_api_missing_auth(self, git_lfs_app,):
61 61 git_lfs_app.post_json(
62 62 '/repo/info/lfs/objects/batch', params={}, status=403)
63 63
64 64 def test_app_batch_api_unsupported_operation(self, git_lfs_app, http_auth):
65 65 response = git_lfs_app.post_json(
66 66 '/repo/info/lfs/objects/batch', params={}, status=400,
67 67 extra_environ=http_auth)
68 68 assert response.json == {
69 69 u'message': u'unsupported operation mode: `None`'}
70 70
71 71 def test_app_batch_api_missing_objects(self, git_lfs_app, http_auth):
72 72 response = git_lfs_app.post_json(
73 73 '/repo/info/lfs/objects/batch', params={'operation': 'download'},
74 74 status=400, extra_environ=http_auth)
75 75 assert response.json == {
76 76 u'message': u'missing objects data'}
77 77
78 78 def test_app_batch_api_unsupported_data_in_objects(
79 79 self, git_lfs_app, http_auth):
80 80 params = {'operation': 'download',
81 81 'objects': [{}]}
82 82 response = git_lfs_app.post_json(
83 83 '/repo/info/lfs/objects/batch', params=params, status=400,
84 84 extra_environ=http_auth)
85 85 assert response.json == {
86 86 u'message': u'unsupported data in objects'}
87 87
88 88 def test_app_batch_api_download_missing_object(
89 89 self, git_lfs_app, http_auth):
90 90 params = {'operation': 'download',
91 91 'objects': [{'oid': '123', 'size': '1024'}]}
92 92 response = git_lfs_app.post_json(
93 93 '/repo/info/lfs/objects/batch', params=params,
94 94 extra_environ=http_auth)
95 95
96 96 expected_objects = [
97 97 {u'authenticated': True,
98 98 u'errors': {u'error': {
99 99 u'code': 404,
100 100 u'message': u'object: 123 does not exist in store'}},
101 101 u'oid': u'123',
102 102 u'size': u'1024'}
103 103 ]
104 104 assert response.json == {
105 105 'objects': expected_objects, 'transfer': 'basic'}
106 106
107 107 def test_app_batch_api_download(self, git_lfs_app, http_auth):
108 108 oid = '456'
109 oid_path = os.path.join(git_lfs_app._store, 'repo', oid)
110 os.makedirs(os.path.dirname(oid_path))
109 oid_path = os.path.join(git_lfs_app._store, oid)
110 if not os.path.isdir(os.path.dirname(oid_path)):
111 os.makedirs(os.path.dirname(oid_path))
111 112 with open(oid_path, 'wb') as f:
112 113 f.write('OID_CONTENT')
113 114
114 115 params = {'operation': 'download',
115 116 'objects': [{'oid': oid, 'size': '1024'}]}
116 117 response = git_lfs_app.post_json(
117 118 '/repo/info/lfs/objects/batch', params=params,
118 119 extra_environ=http_auth)
119 120
120 121 expected_objects = [
121 122 {u'authenticated': True,
122 123 u'actions': {
123 124 u'download': {
124 125 u'header': {u'Authorization': u'Basic XXXXX'},
125 126 u'href': u'http://localhost/repo/info/lfs/objects/456'},
126 127 },
127 128 u'oid': u'456',
128 129 u'size': u'1024'}
129 130 ]
130 131 assert response.json == {
131 132 'objects': expected_objects, 'transfer': 'basic'}
132 133
133 134 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
134 135 params = {'operation': 'upload',
135 136 'objects': [{'oid': '123', 'size': '1024'}]}
136 137 response = git_lfs_app.post_json(
137 138 '/repo/info/lfs/objects/batch', params=params,
138 139 extra_environ=http_auth)
139 140 expected_objects = [
140 141 {u'authenticated': True,
141 142 u'actions': {
142 143 u'upload': {
143 144 u'header': {u'Authorization': u'Basic XXXXX'},
144 145 u'href': u'http://localhost/repo/info/lfs/objects/123'},
145 146 u'verify': {
146 147 u'header': {u'Authorization': u'Basic XXXXX'},
147 148 u'href': u'http://localhost/repo/info/lfs/verify'}
148 149 },
149 150 u'oid': u'123',
150 151 u'size': u'1024'}
151 152 ]
152 153 assert response.json == {
153 154 'objects': expected_objects, 'transfer': 'basic'}
154 155
155 156 def test_app_verify_api_missing_data(self, git_lfs_app):
156 157 params = {'oid': 'missing',}
157 158 response = git_lfs_app.post_json(
158 159 '/repo/info/lfs/verify', params=params,
159 160 status=400)
160 161
161 162 assert response.json == {
162 163 u'message': u'missing oid and size in request data'}
163 164
164 165 def test_app_verify_api_missing_obj(self, git_lfs_app):
165 166 params = {'oid': 'missing', 'size': '1024'}
166 167 response = git_lfs_app.post_json(
167 168 '/repo/info/lfs/verify', params=params,
168 169 status=404)
169 170
170 171 assert response.json == {
171 172 u'message': u'oid `missing` does not exists in store'}
172 173
173 174 def test_app_verify_api_size_mismatch(self, git_lfs_app):
174 175 oid = 'existing'
175 oid_path = os.path.join(git_lfs_app._store, 'repo', oid)
176 os.makedirs(os.path.dirname(oid_path))
176 oid_path = os.path.join(git_lfs_app._store, oid)
177 if not os.path.isdir(os.path.dirname(oid_path)):
178 os.makedirs(os.path.dirname(oid_path))
177 179 with open(oid_path, 'wb') as f:
178 180 f.write('OID_CONTENT')
179 181
180 182 params = {'oid': oid, 'size': '1024'}
181 183 response = git_lfs_app.post_json(
182 184 '/repo/info/lfs/verify', params=params, status=422)
183 185
184 186 assert response.json == {
185 187 u'message': u'requested file size mismatch '
186 188 u'store size:11 requested:1024'}
187 189
188 190 def test_app_verify_api(self, git_lfs_app):
189 191 oid = 'existing'
190 oid_path = os.path.join(git_lfs_app._store, 'repo', oid)
191 os.makedirs(os.path.dirname(oid_path))
192 oid_path = os.path.join(git_lfs_app._store, oid)
193 if not os.path.isdir(os.path.dirname(oid_path)):
194 os.makedirs(os.path.dirname(oid_path))
192 195 with open(oid_path, 'wb') as f:
193 196 f.write('OID_CONTENT')
194 197
195 198 params = {'oid': oid, 'size': 11}
196 199 response = git_lfs_app.post_json(
197 200 '/repo/info/lfs/verify', params=params)
198 201
199 202 assert response.json == {
200 203 u'message': {u'size': u'ok', u'in_store': u'ok'}}
201 204
202 205 def test_app_download_api_oid_not_existing(self, git_lfs_app):
203 206 oid = 'missing'
204 207
205 208 response = git_lfs_app.get(
206 209 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
207 210
208 211 assert response.json == {
209 212 u'message': u'requested file with oid `missing` not found in store'}
210 213
211 214 def test_app_download_api(self, git_lfs_app):
212 215 oid = 'existing'
213 oid_path = os.path.join(git_lfs_app._store, 'repo', oid)
214 os.makedirs(os.path.dirname(oid_path))
216 oid_path = os.path.join(git_lfs_app._store, oid)
217 if not os.path.isdir(os.path.dirname(oid_path)):
218 os.makedirs(os.path.dirname(oid_path))
215 219 with open(oid_path, 'wb') as f:
216 220 f.write('OID_CONTENT')
217 221
218 222 response = git_lfs_app.get(
219 223 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
220 224 assert response
221 225
222 226 def test_app_upload(self, git_lfs_app):
223 227 oid = 'uploaded'
224 228
225 229 response = git_lfs_app.put(
226 230 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
227 231
228 232 assert response.json == {u'upload': u'ok'}
229 233
230 234 # verify that we actually wrote that OID
231 oid_path = os.path.join(git_lfs_app._store, 'repo', oid)
235 oid_path = os.path.join(git_lfs_app._store, oid)
232 236 assert os.path.isfile(oid_path)
233 237 assert 'CONTENT' == open(oid_path).read()
@@ -1,121 +1,123 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
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 General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import pytest
20 20 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
21 21
22 22
23 23 @pytest.fixture()
24 24 def lfs_store(tmpdir):
25 25 repo = 'test'
26 26 oid = '123456789'
27 store = LFSOidStore(repo=repo, oid=oid, store_location=str(tmpdir))
27 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
28 28 return store
29 29
30 30
31 31 @pytest.fixture()
32 32 def oid_handler(lfs_store):
33 33 store = lfs_store
34 34 repo = store.repo
35 35 oid = store.oid
36 36
37 37 oid_handler = OidHandler(
38 38 store=store, repo_name=repo, auth=('basic', 'xxxx'),
39 39 oid=oid,
40 40 obj_size='1024', obj_data={}, obj_href='http://localhost/handle_oid',
41 41 obj_verify_href='http://localhost/verify')
42 42 return oid_handler
43 43
44 44
45 45 class TestOidHandler(object):
46 46
47 47 @pytest.mark.parametrize('exec_action', [
48 48 'download',
49 49 'upload',
50 50 ])
51 51 def test_exec_action(self, exec_action, oid_handler):
52 52 handler = oid_handler.exec_operation(exec_action)
53 53 assert handler
54 54
55 55 def test_exec_action_undefined(self, oid_handler):
56 56 with pytest.raises(AttributeError):
57 57 oid_handler.exec_operation('wrong')
58 58
59 59 def test_download_oid_not_existing(self, oid_handler):
60 60 response, has_errors = oid_handler.exec_operation('download')
61 61
62 62 assert response is None
63 63 assert has_errors['error'] == {
64 64 'code': 404,
65 65 'message': 'object: 123456789 does not exist in store'}
66 66
67 67 def test_download_oid(self, oid_handler):
68 68 store = oid_handler.get_store()
69 if not os.path.isdir(os.path.dirname(store.oid_path)):
70 os.makedirs(os.path.dirname(store.oid_path))
69 71
70 os.makedirs(os.path.dirname(store.oid_path))
71 72 with open(store.oid_path, 'wb') as f:
72 73 f.write('CONTENT')
73 74
74 75 response, has_errors = oid_handler.exec_operation('download')
75 76
76 77 assert has_errors is None
77 78 assert response['download'] == {
78 79 'header': {'Authorization': 'basic xxxx'},
79 80 'href': 'http://localhost/handle_oid'
80 81 }
81 82
82 83 def test_upload_oid_that_exists(self, oid_handler):
83 84 store = oid_handler.get_store()
85 if not os.path.isdir(os.path.dirname(store.oid_path)):
86 os.makedirs(os.path.dirname(store.oid_path))
84 87
85 os.makedirs(os.path.dirname(store.oid_path))
86 88 with open(store.oid_path, 'wb') as f:
87 89 f.write('CONTENT')
88 90
89 91 response, has_errors = oid_handler.exec_operation('upload')
90 92 assert has_errors is None
91 93 assert response is None
92 94
93 95 def test_upload_oid(self, oid_handler):
94 96 response, has_errors = oid_handler.exec_operation('upload')
95 97 assert has_errors is None
96 98 assert response['upload'] == {
97 99 'header': {'Authorization': 'basic xxxx'},
98 100 'href': 'http://localhost/handle_oid'
99 101 }
100 102
101 103
102 104 class TestLFSStore(object):
103 105 def test_write_oid(self, lfs_store):
104 106 oid_location = lfs_store.oid_path
105 107
106 108 assert not os.path.isfile(oid_location)
107 109
108 110 engine = lfs_store.get_engine(mode='wb')
109 111 with engine as f:
110 112 f.write('CONTENT')
111 113
112 114 assert os.path.isfile(oid_location)
113 115
114 116 def test_detect_has_oid(self, lfs_store):
115 117
116 118 assert lfs_store.has_oid() is False
117 119 engine = lfs_store.get_engine(mode='wb')
118 120 with engine as f:
119 121 f.write('CONTENT')
120 122
121 123 assert lfs_store.has_oid() is True No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now