Merge lp:~dooferlad/linaro-license-protection/push_support into lp:~linaro-automation/linaro-license-protection/trunk
- push_support
- Merge into trunk
Proposed by
James Tunnicliffe
Status: | Merged |
---|---|
Approved by: | Milo Casagrande |
Approved revision: | 203 |
Merged at revision: | 207 |
Proposed branch: | lp:~dooferlad/linaro-license-protection/push_support |
Merge into: | lp:~linaro-automation/linaro-license-protection/trunk |
Diff against target: |
566 lines (+390/-25) 8 files modified
README (+27/-0) license_protected_downloads/common.py (+18/-0) license_protected_downloads/models.py (+4/-0) license_protected_downloads/tests/test_views.py (+144/-1) license_protected_downloads/uploads.py (+117/-0) license_protected_downloads/views.py (+40/-21) settings.py (+34/-3) urls.py (+6/-0) |
To merge this branch: | bzr merge lp:~dooferlad/linaro-license-protection/push_support |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Milo Casagrande (community) | Approve | ||
Review via email: mp+167817@code.launchpad.net |
Commit message
Description of the change
Adds to the API to allow file uploads.
settings.py has a blank master key by default, which prevents any API keys from being issued, thus disabling the new functionality. The master key should, of course, be generated on the server that this is installed on and only shared with trusted parties.
To post a comment you must log in.
- 199. By James Tunnicliffe
-
Make full dir path for uploads
Expect local_settings.py
If user supplies a valid key, default protection to OPEN. - 200. By James Tunnicliffe
-
Uploads are now downloadable if the upload path isn't the same as one of the shared directories!
- 201. By James Tunnicliffe
-
Updated to make downloads work from inside the upload directory.
- 202. By James Tunnicliffe
-
Added auto-generation of local_settings (if needed) and updated README
- 203. By James Tunnicliffe
-
Cosmetic fixes
Revision history for this message
Milo Casagrande (milo) wrote : | # |
Looks good to go for me!
Thanks James!
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'README' |
2 | --- README 2013-06-03 14:05:59 +0000 |
3 | +++ README 2013-06-14 17:43:26 +0000 |
4 | @@ -21,6 +21,7 @@ |
5 | (as used by Launchpad.net) and Atlassian Crowd are supported). |
6 | * post-processing of all uploads (a script that manages uploads) |
7 | * per-IP pass-through (for automatic services like a test framework) |
8 | + * key based file uploads into a private area. |
9 | |
10 | Background |
11 | ---------- |
12 | @@ -79,6 +80,32 @@ |
13 | so it may be inaccessible to the general public. |
14 | |
15 | |
16 | +Publishing API |
17 | +-------------- |
18 | +To upload a file using the publishing API you need a key. This can be obtained |
19 | +by the query <server>/api/request_key?<MASTER_API_KEY> where MASTER_API_KEY |
20 | +is stored in local_settings.py. |
21 | + |
22 | +Now you can use HTTP PUT requests to push up files: |
23 | +curl -F file=@<file> -F key=<key> <server>/path/to/store/file |
24 | + |
25 | +And this file can be downloaded from: |
26 | +<server>/path/to/store/file?key=<key> |
27 | + |
28 | + * By default API key protected files have no further license protection. |
29 | + * You can upload multiple files with the same key |
30 | + * Directories are automatically created |
31 | + * File listings still work (as long as you append ?key=<key> to the URL) |
32 | + |
33 | +Once you have finished with the files, you can delete all files associated |
34 | +with a temporary API key by deleting the key: |
35 | + |
36 | +curl <server>/api/delete_key?key=<key> |
37 | + |
38 | +Uploading files to a public directory is not supported. This is planned for |
39 | +the future. |
40 | + |
41 | + |
42 | Build-Info support |
43 | ------------------ |
44 | |
45 | |
46 | === added file 'license_protected_downloads/common.py' |
47 | --- license_protected_downloads/common.py 1970-01-01 00:00:00 +0000 |
48 | +++ license_protected_downloads/common.py 2013-06-14 17:43:26 +0000 |
49 | @@ -0,0 +1,18 @@ |
50 | +import os |
51 | + |
52 | +def safe_path_join(base_path, *paths): |
53 | + """os.path.join with check that result is inside base_path. |
54 | + |
55 | + Checks that the generated path doesn't end up outside the target |
56 | + directory, so server accesses stay where we expect them. |
57 | + """ |
58 | + |
59 | + target_path = os.path.join(base_path, *paths) |
60 | + |
61 | + if not target_path.startswith(base_path): |
62 | + return None |
63 | + |
64 | + if not os.path.normpath(target_path) == target_path.rstrip("/"): |
65 | + return None |
66 | + |
67 | + return target_path |
68 | |
69 | === modified file 'license_protected_downloads/models.py' |
70 | --- license_protected_downloads/models.py 2012-08-06 11:53:16 +0000 |
71 | +++ license_protected_downloads/models.py 2013-06-14 17:43:26 +0000 |
72 | @@ -26,3 +26,7 @@ |
73 | |
74 | def __unicode__(self): |
75 | return self.digest |
76 | + |
77 | + |
78 | +class APIKeyStore(models.Model): |
79 | + key = models.CharField(max_length=80) |
80 | |
81 | === modified file 'license_protected_downloads/tests/test_views.py' |
82 | --- license_protected_downloads/tests/test_views.py 2013-06-05 13:12:53 +0000 |
83 | +++ license_protected_downloads/tests/test_views.py 2013-06-14 17:43:26 +0000 |
84 | @@ -10,7 +10,8 @@ |
85 | import urllib2 |
86 | import urlparse |
87 | import json |
88 | - |
89 | +import random |
90 | +import shutil |
91 | from mock import Mock |
92 | |
93 | from license_protected_downloads import bzr_version |
94 | @@ -34,9 +35,19 @@ |
95 | self.old_served_paths = settings.SERVED_PATHS |
96 | settings.SERVED_PATHS = [os.path.join(THIS_DIRECTORY, |
97 | "testserver_root")] |
98 | + self.old_upload_path = settings.UPLOAD_PATH |
99 | + settings.UPLOAD_PATH = os.path.join(THIS_DIRECTORY, |
100 | + "test_upload_root") |
101 | + if not os.path.isdir(settings.UPLOAD_PATH): |
102 | + os.makedirs(settings.UPLOAD_PATH) |
103 | + self.old_master_api_key = settings.MASTER_API_KEY |
104 | + settings.MASTER_API_KEY = "1234abcd" |
105 | |
106 | def tearDown(self): |
107 | settings.SERVED_PATHS = self.old_served_paths |
108 | + settings.MASTER_API_KEY = self.old_master_api_key |
109 | + os.rmdir(settings.UPLOAD_PATH) |
110 | + settings.UPLOAD_PATH = self.old_upload_path |
111 | |
112 | |
113 | class ViewTests(BaseServeViewTest): |
114 | @@ -839,6 +850,138 @@ |
115 | # Shouldn't be able to escape served paths... |
116 | self.assertEqual(response.status_code, 404) |
117 | |
118 | + def test_get_key(self): |
119 | + response = self.client.get("http://testserver/api/request_key", |
120 | + data={"key": settings.MASTER_API_KEY}) |
121 | + |
122 | + self.assertEqual(response.status_code, 200) |
123 | + # Don't care what the key is, as long as it isn't blank |
124 | + self.assertRegexpMatches(response.content, "\S+") |
125 | + |
126 | + def test_get_key_api_disabled(self): |
127 | + settings.MASTER_API_KEY = "" |
128 | + response = self.client.get("http://testserver/api/request_key", |
129 | + data={"key": settings.MASTER_API_KEY}) |
130 | + |
131 | + self.assertEqual(response.status_code, 403) |
132 | + |
133 | + def test_get_key_post_and_get_file(self): |
134 | + response = self.client.get("http://testserver/api/request_key", |
135 | + data={"key": settings.MASTER_API_KEY}) |
136 | + |
137 | + self.assertEqual(response.status_code, 200) |
138 | + # Don't care what the key is, as long as it isn't blank |
139 | + self.assertRegexpMatches(response.content, "\S+") |
140 | + key = response.content |
141 | + |
142 | + # Now write a file so we can upload it |
143 | + file_content = "test_get_key_post_and_get_file" |
144 | + file_root = "/tmp" |
145 | + |
146 | + tmp_file_name = os.path.join( |
147 | + file_root, |
148 | + self.make_temporary_file(file_content)) |
149 | + |
150 | + # Send the file |
151 | + with open(tmp_file_name) as f: |
152 | + response = self.client.post("http://testserver/file_name", |
153 | + data={"key": key, "file": f}) |
154 | + self.assertEqual(response.status_code, 200) |
155 | + |
156 | + # Check the upload worked by reading the file back from its uploaded |
157 | + # location |
158 | + uploaded_file_path = os.path.join(settings.UPLOAD_PATH, |
159 | + key, |
160 | + "file_name") |
161 | + with open(uploaded_file_path) as f: |
162 | + self.assertEqual(f.read(), file_content) |
163 | + |
164 | + # Test we can fetch the newly uploaded file if we present the key |
165 | + response = self.client.get("http://testserver/file_name", |
166 | + data={"key": key}) |
167 | + self.assertEqual(response.status_code, 200) |
168 | + |
169 | + response = self.client.get("http://testserver/file_name") |
170 | + self.assertNotEqual(response.status_code, 200) |
171 | + |
172 | + # Delete the files generated by the test |
173 | + shutil.rmtree(os.path.join(settings.UPLOAD_PATH, key)) |
174 | + |
175 | + def test_post_file_no_key(self): |
176 | + file_content = "test_post_file_no_key" |
177 | + file_root = "/tmp" |
178 | + |
179 | + tmp_file_name = os.path.join( |
180 | + file_root, |
181 | + self.make_temporary_file(file_content)) |
182 | + |
183 | + # Try to upload a file without a key. |
184 | + with open(tmp_file_name) as f: |
185 | + response = self.client.post("http://testserver/file_name", |
186 | + data={"file": f}) |
187 | + self.assertEqual(response.status_code, 500) |
188 | + |
189 | + # Make sure the file didn't get created. |
190 | + self.assertFalse(os.path.isfile(os.path.join(settings.UPLOAD_PATH, |
191 | + "file_name"))) |
192 | + |
193 | + def test_post_file_random_key(self): |
194 | + key = "%030x" % random.randrange(256**15) |
195 | + file_content = "test_post_file_random_key" |
196 | + file_root = "/tmp" |
197 | + |
198 | + tmp_file_name = os.path.join( |
199 | + file_root, |
200 | + self.make_temporary_file(file_content)) |
201 | + |
202 | + # Try to upload a file with a randomly generated key. |
203 | + with open(tmp_file_name) as f: |
204 | + response = self.client.post("http://testserver/file_name", |
205 | + data={"key": key, "file": f}) |
206 | + self.assertEqual(response.status_code, 500) |
207 | + |
208 | + # Make sure the file didn't get created. |
209 | + self.assertFalse(os.path.isfile(os.path.join(settings.UPLOAD_PATH, |
210 | + key, |
211 | + "file_name"))) |
212 | + |
213 | + def test_api_delete_key(self): |
214 | + response = self.client.get("http://testserver/api/request_key", |
215 | + data={"key": settings.MASTER_API_KEY}) |
216 | + |
217 | + self.assertEqual(response.status_code, 200) |
218 | + # Don't care what the key is, as long as it isn't blank |
219 | + self.assertRegexpMatches(response.content, "\S+") |
220 | + key = response.content |
221 | + file_content = "test_api_delete_key" |
222 | + file_root = "/tmp" |
223 | + |
224 | + tmp_file_name = os.path.join( |
225 | + file_root, |
226 | + self.make_temporary_file(file_content)) |
227 | + |
228 | + with open(tmp_file_name) as f: |
229 | + response = self.client.post("http://testserver/file_name", |
230 | + data={"key": key, "file": f}) |
231 | + self.assertEqual(response.status_code, 200) |
232 | + |
233 | + self.assertTrue(os.path.isfile(os.path.join(settings.UPLOAD_PATH, |
234 | + key, |
235 | + "file_name"))) |
236 | + |
237 | + # Release the key, the files should be deleted |
238 | + response = self.client.get("http://testserver/api/delete_key", |
239 | + data={"key": key}) |
240 | + self.assertEqual(response.status_code, 200) |
241 | + self.assertFalse(os.path.isfile(os.path.join(settings.UPLOAD_PATH, |
242 | + key, |
243 | + "file_name"))) |
244 | + |
245 | + # Key shouldn't work after released |
246 | + response = self.client.get("http://testserver/file_name", |
247 | + data={"key": key}) |
248 | + self.assertNotEqual(response.status_code, 200) |
249 | + |
250 | |
251 | class HowtoViewTests(BaseServeViewTest): |
252 | def test_no_howtos(self): |
253 | |
254 | === added file 'license_protected_downloads/uploads.py' |
255 | --- license_protected_downloads/uploads.py 1970-01-01 00:00:00 +0000 |
256 | +++ license_protected_downloads/uploads.py 2013-06-14 17:43:26 +0000 |
257 | @@ -0,0 +1,117 @@ |
258 | +from django.views.decorators.csrf import csrf_exempt |
259 | +from django.http import ( |
260 | + HttpResponse, |
261 | + HttpResponseForbidden, |
262 | + HttpResponseServerError |
263 | +) |
264 | +from django import forms |
265 | +import random |
266 | +from django.conf import settings |
267 | +import os |
268 | +import shutil |
269 | +from models import APIKeyStore |
270 | +from common import safe_path_join |
271 | + |
272 | + |
273 | +class UploadFileForm(forms.Form): |
274 | + file = forms.FileField() |
275 | + |
276 | + |
277 | +def upload_target_path(path, key): |
278 | + """Quick path handling function. |
279 | + |
280 | + Checks that the generated path doesn't end up outside the target directory, |
281 | + so you can't set path to start with "/" and upload to anywhere. |
282 | + """ |
283 | + base_path = os.path.join(settings.UPLOAD_PATH, key) |
284 | + return safe_path_join(base_path, path) |
285 | + |
286 | + |
287 | +@csrf_exempt |
288 | +def file_server_post(request, path): |
289 | + """ Handle post requests. |
290 | + |
291 | + All post requests must be accompanied by a valid key. If not, the upload |
292 | + will be ignored. |
293 | + |
294 | + Files are stored in a private directory that can not be accessed via the |
295 | + web interface unless you have the same key. |
296 | + """ |
297 | + if not ("key" in request.POST and |
298 | + APIKeyStore.objects.filter(key=request.POST["key"])): |
299 | + return HttpResponseServerError("Invalid key") |
300 | + |
301 | + form = UploadFileForm(request.POST, request.FILES) |
302 | + if not form.is_valid() or not path: |
303 | + return HttpResponseServerError("Invalid call") |
304 | + |
305 | + path = upload_target_path(path, request.POST["key"]) |
306 | + |
307 | + # Create directory if required |
308 | + dirname = os.path.dirname(path) |
309 | + if not os.path.isdir(dirname): |
310 | + os.makedirs(dirname) |
311 | + |
312 | + with open(path, "wb") as destination: |
313 | + for chunk in request.FILES["file"].chunks(): |
314 | + destination.write(chunk) |
315 | + |
316 | + return HttpResponse("OK") |
317 | + |
318 | + |
319 | +def api_request_key(request): |
320 | + if("key" in request.GET and |
321 | + request.GET["key"] == settings.MASTER_API_KEY and |
322 | + settings.MASTER_API_KEY): |
323 | + |
324 | + # Generate a new, random key. |
325 | + key = "%030x" % random.randrange(256**15) |
326 | + while APIKeyStore.objects.filter(key=key): |
327 | + key = "%030x" % random.randrange(256**15) |
328 | + |
329 | + api_key = APIKeyStore(key=key) |
330 | + api_key.save() |
331 | + return HttpResponse(key) |
332 | + |
333 | + return HttpResponseForbidden() |
334 | + |
335 | +def api_delete_key(request): |
336 | + if "key" not in request.GET: |
337 | + return HttpResponseServerError("Invalid key") |
338 | + |
339 | + key = request.GET["key"] |
340 | + api_key = APIKeyStore.objects.filter(key=key) |
341 | + |
342 | + if not api_key: |
343 | + return HttpResponseServerError("Invalid key") |
344 | + |
345 | + # Delete key from database and all files associated with it |
346 | + api_key.delete() |
347 | + shutil.rmtree(os.path.join(settings.UPLOAD_PATH, key)) |
348 | + |
349 | + return HttpResponse("OK") |
350 | + |
351 | +def api_push_to_server(request): |
352 | + # TODO: Upload files from this machine to another linaro-licence-protection |
353 | + # node. |
354 | + """ |
355 | + Something like: |
356 | + |
357 | + if request.GET["target"] in settings.REMOTE_SERVERS: |
358 | + remote_server = settings.REMOTE_SERVERS[request.GET["target"]] |
359 | + |
360 | + remote_server should contain: |
361 | + { |
362 | + "key": "...", |
363 | + "url": "...", |
364 | + } |
365 | + |
366 | + now just POST files from this machine to the specified URL/KEY. |
367 | + |
368 | + Possibly add some magic to POST endpoint (file_server_post) to allow |
369 | + (some users??) uploads to a public path: |
370 | + |
371 | + POST snapshots.linaro.org/path/to/file?key="key"&public=true |
372 | + |
373 | + """ |
374 | + pass |
375 | |
376 | === modified file 'license_protected_downloads/views.py' |
377 | --- license_protected_downloads/views.py 2013-06-06 13:24:17 +0000 |
378 | +++ license_protected_downloads/views.py 2013-06-14 17:43:26 +0000 |
379 | @@ -19,6 +19,7 @@ |
380 | from django.shortcuts import render_to_response, redirect |
381 | from django.template import RequestContext |
382 | from django.utils.encoding import smart_str, iri_to_uri |
383 | +from django.views.decorators.csrf import csrf_exempt |
384 | |
385 | import bzr_version |
386 | from buildinfo import BuildInfo, IncorrectDataFormatException |
387 | @@ -28,7 +29,9 @@ |
388 | import importlib |
389 | group_auth_modules = [importlib.import_module(m) for m in settings.GROUP_AUTH_MODULES] |
390 | from BeautifulSoup import BeautifulSoup |
391 | +from uploads import file_server_post |
392 | import config |
393 | +from common import safe_path_join |
394 | from group_auth_common import GroupAuthError |
395 | |
396 | |
397 | @@ -132,28 +135,22 @@ |
398 | return listing |
399 | |
400 | |
401 | -def safe_path_join(base_path, *paths): |
402 | - """os.path.join with check that result is inside base_path. |
403 | +def test_path(path, served_paths=None): |
404 | + """Check that path points to something we can serve up. |
405 | |
406 | - Checks that the generated path doesn't end up outside the target |
407 | - directory, so server accesses stay where we expect them. |
408 | + served_paths can be provided to overwrite settings.SERVED_PATHS. This is |
409 | + used for uploaded files, which may not be shared in the server root. |
410 | """ |
411 | |
412 | - target_path = os.path.join(base_path, *paths) |
413 | - |
414 | - if not target_path.startswith(base_path): |
415 | - return None |
416 | - |
417 | - if not os.path.normpath(target_path) == target_path.rstrip("/"): |
418 | - return None |
419 | - |
420 | - return target_path |
421 | - |
422 | - |
423 | -def test_path(path): |
424 | - |
425 | - for basepath in settings.SERVED_PATHS: |
426 | + if served_paths is None: |
427 | + served_paths = settings.SERVED_PATHS |
428 | + else: |
429 | + if not isinstance(served_paths, list): |
430 | + served_paths = [served_paths] |
431 | + |
432 | + for basepath in served_paths: |
433 | fullpath = safe_path_join(basepath, path) |
434 | + |
435 | if fullpath is None: |
436 | return None |
437 | |
438 | @@ -443,11 +440,32 @@ |
439 | return response |
440 | |
441 | |
442 | +@csrf_exempt |
443 | def file_server(request, path): |
444 | """Serve up a file / directory listing or license page as required""" |
445 | path = iri_to_uri(path) |
446 | + |
447 | + # Intercept post requests and send them to file_server_post. |
448 | + if request.method == "POST": |
449 | + return file_server_post(request, path) |
450 | + |
451 | + # GET requests are handled by file_server_get |
452 | + elif request.method == "GET": |
453 | + return file_server_get(request, path) |
454 | + |
455 | + |
456 | +def file_server_get(request, path): |
457 | + |
458 | url = path |
459 | - result = test_path(path) |
460 | + |
461 | + # if key is in request.GET["key"] then need to mod path and give |
462 | + # access to a per-key directory. |
463 | + if "key" in request.GET: |
464 | + path = os.path.join(request.GET["key"], path) |
465 | + result = test_path(path, settings.UPLOAD_PATH) |
466 | + else: |
467 | + result = test_path(path) |
468 | + |
469 | if not result: |
470 | raise Http404 |
471 | |
472 | @@ -522,8 +540,9 @@ |
473 | if not file_listed(path, url): |
474 | raise Http404 |
475 | |
476 | - if get_client_ip(request) in config.INTERNAL_HOSTS or\ |
477 | - is_whitelisted(os.path.join('/', url)): |
478 | + if (get_client_ip(request) in config.INTERNAL_HOSTS or |
479 | + is_whitelisted(os.path.join('/', url)) or |
480 | + "key" in request.GET): # If user has a key, default to open |
481 | digests = 'OPEN' |
482 | else: |
483 | digests = is_protected(path) |
484 | |
485 | === modified file 'settings.py' |
486 | --- settings.py 2013-06-10 16:03:50 +0000 |
487 | +++ settings.py 2013-06-14 17:43:26 +0000 |
488 | @@ -30,6 +30,8 @@ |
489 | } |
490 | } |
491 | |
492 | +FILE_UPLOAD_PERMISSIONS = 0644 |
493 | + |
494 | TIME_ZONE = None |
495 | |
496 | # Language code for this installation. All choices can be found here: |
497 | @@ -80,9 +82,6 @@ |
498 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', |
499 | ) |
500 | |
501 | -# Make this unique, and don't share it with anybody. |
502 | -SECRET_KEY = 'lkye^=q_i(#jies7^cz#anqq(1g0k$luy5^1jr2nk=g#inet(n' |
503 | - |
504 | # List of callables that know how to import templates from various sources. |
505 | TEMPLATE_LOADERS = ( |
506 | 'django.template.loaders.filesystem.Loader', |
507 | @@ -183,6 +182,7 @@ |
508 | } |
509 | |
510 | SERVED_PATHS = [os.path.join(PROJECT_ROOT, "sampleroot")] |
511 | +UPLOAD_PATH = os.path.join(PROJECT_ROOT, "sample_upload_root") |
512 | |
513 | TEMPLATE_CONTEXT_PROCESSORS = ( |
514 | 'django.contrib.messages.context_processors.messages', |
515 | @@ -224,3 +224,34 @@ |
516 | "init.css": "http://www.linaro.org/remote/css/init.css", |
517 | "remote.css": "http://www.linaro.org/remote/css/remote.css", |
518 | } |
519 | + |
520 | +MASTER_API_KEY = "" |
521 | + |
522 | +# Try to import local_settings. If it doesn't exist, generate it. It contains |
523 | +# SECRET_KEY (to keep it secret). |
524 | +try: |
525 | + from local_settings import * |
526 | +except ImportError: |
527 | + import random |
528 | + |
529 | + # Create local_settings with random SECRET_KEY and MASTER_API_KEY |
530 | + char_selection = '0123456789abcdefghijklmnopqrstuvwxyz' \ |
531 | + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
532 | + char_selection_with_punctuation = char_selection + '!@#$%^&*(-_=+)' |
533 | + |
534 | + # SECRET_KEY contains anything but whitespace |
535 | + secret_key = ''.join(random.sample(char_selection_with_punctuation, 50)) |
536 | + local_settings_content = "SECRET_KEY = '%s'\n" % secret_key |
537 | + |
538 | + # At the moment the publishing API is still in development so it is |
539 | + # disabled... |
540 | + if False: |
541 | + # MASTER_API_KEY contains characters that don't have to be % encoded |
542 | + # in an HTTP URL. |
543 | + master_api_key = ''.join(random.sample(char_selection, 50)) |
544 | + local_settings_content += "MASTER_API_KEY = '%s'\n" % master_api_key |
545 | + |
546 | + with open(os.path.join(PROJECT_ROOT, "local_settings.py"), "w") as f: |
547 | + f.write(local_settings_content) |
548 | + |
549 | + from local_settings import * |
550 | |
551 | === modified file 'urls.py' |
552 | --- urls.py 2013-03-11 15:31:31 +0000 |
553 | +++ urls.py 2013-06-14 17:43:26 +0000 |
554 | @@ -52,6 +52,12 @@ |
555 | url(r'^api/license/(?P<path>.*)$', |
556 | 'license_protected_downloads.views.get_license_api'), |
557 | |
558 | + url(r'^api/request_key$', |
559 | + 'license_protected_downloads.uploads.api_request_key'), |
560 | + |
561 | + url(r'^api/delete_key$', |
562 | + 'license_protected_downloads.uploads.api_delete_key'), |
563 | + |
564 | # Catch-all. We always return a file (or try to) if it exists. |
565 | # This handler does that. |
566 | url(r'(?P<path>.*)', 'license_protected_downloads.views.file_server'), |
Hi James!
Awesome progress! Everything looks good, there are only two definitely minor comments and one question from me.
First comes the question.
To upload a file there is no "api/" URL, we just use the catch-all urls.py rule and differentiate on GET/POST. Is that right?
So, an imaginary URL like 'https:/ /snapshots. l.o/a_big_ file.zip', with my API key and file contents as data, and a POST request, should upload the file. Is it correct?
=== added file 'license_ protected_ downloads/ common. py' protected_ downloads/ common. py 1970-01-01 00:00:00 +0000 protected_ downloads/ common. py 2013-06-14 12:41:40 +0000 join(base_ path, *paths):
--- license_
+++ license_
@@ -0,0 +1,18 @@
+import os
+
+def safe_path_
+ """os.path.join with with check that result is inside base_path.
s/with with/with
=== modified file 'license_ protected_ downloads/ views.py'
+
+ # if key is in request.GET["key"] then need to mod path and give
+ # access to a per-key directory.
+ if "key" in request.GET :
Extra space before the colon?
=== modified file 'settings.py' CONTEXT_ PROCESSORS = ( contrib. messages. context_ processors. messages' , www.linaro. org/remote/ css/init. css", www.linaro. org/remote/ css/remote. css",
--- settings.py 2013-06-10 16:03:50 +0000
+++ settings.py 2013-06-14 12:41:40 +0000
TEMPLATE_
'django.
@@ -224,3 +227,7 @@
"init.css": "http://
"remote.css": "http://
}
+
+MASTER_API_KEY = ""
+
+from local_settings import *
Maybe is worth adding some comments here about the MASTER_API_KEY, and also update the installation/ deployment notes.
Approving since those things can be fixed during merge (and the questions are just to make sure I understood the implementation correctly).
Thanks.