Merge lp:~dooferlad/linaro-license-protection/public_push_support into lp:~linaro-automation/linaro-license-protection/trunk
- public_push_support
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Milo Casagrande |
Approved revision: | 204 |
Merged at revision: | 209 |
Proposed branch: | lp:~dooferlad/linaro-license-protection/public_push_support |
Merge into: | lp:~linaro-automation/linaro-license-protection/trunk |
Diff against target: |
210 lines (+90/-17) 4 files modified
license_protected_downloads/models.py (+1/-0) license_protected_downloads/tests/test_views.py (+53/-0) license_protected_downloads/uploads.py (+18/-4) license_protected_downloads/views.py (+18/-13) |
To merge this branch: | bzr merge lp:~dooferlad/linaro-license-protection/public_push_support |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Milo Casagrande (community) | Approve | ||
Review via email: mp+175319@code.launchpad.net |
Commit message
Description of the change
* Tidy up a few bits from the old publishing code (so the old API should function against upload paths - untested)
* Should work for public publishing - when getting a key, have &public=true on the end of the URL.
* Haven't updated the docs, just found that explaining a proposed change took more effort than writing it!
Milo Casagrande (milo) wrote : | # |
Milo Casagrande (milo) wrote : | # |
Needs fixing, at least for the try-finally as said above.
James Tunnicliffe (dooferlad) wrote : | # |
On 22 July 2013 16:04, Milo Casagrande <email address hidden> wrote:
> Hi James,
>
> overall it looks good, a couple of suggestion follows.
>
> On Wed, Jul 17, 2013 at 5:29 PM, James Tunnicliffe
> <email address hidden> wrote:
>>
>> + def test_get_
>> + response = self.client.get("http://
>> + data={"key": settings.
>> + "public": ""})
>> +
>> + self.assertEqua
>> + # Don't care what the key is, as long as it isn't blank
>> + self.assertRege
>> + key = response.content
>> +
>> + # Now write a file so we can upload it
>> + file_content = "test_get_
>> + file_root = "/tmp"
>> +
>> + tmp_file_name = os.path.join(
>> + file_root,
>> + self.make_
>> +
>> + buildinfo_content = "\n".join([
>> + "Format-Version: 0.1",
>> + "Files-Pattern: *",
>> + "Build-Name: test",
>> + "License-Type: open"])
>> + tmp_build_info = os.path.join(
>> + file_root,
>> + self.make_
>> +
>> + # Send the files
>> + with open(tmp_file_name) as f:
>> + response = self.client.post("http://
>> + data={"key": key, "file": f})
>> + self.assertEqua
>> +
>> + with open(tmp_
>> + response = self.client.post("http://
>> + data={"key": key, "file": f})
>> + self.assertEqua
>> +
>> + # Check the upload worked by reading the file back from its uploaded
>> + # location
>> + uploaded_file_path = os.path.
>> + "file_name")
>> +
>> + with open(uploaded_
>> + self.assertEqua
>> +
>> + # Test we can fetch the newly uploaded file
>> + response = self.client.get("http://
>> + self.assertEqua
>> +
>> + # Delete the files generated by the test
>> + shutil.
>
>
> Wouldn't it be better to have all the logic of this test method inside
> a try-finally?
> I'm concerned with an error that can happen, for whatever reason,
> somewhere before the call to shutil.rmtree, leaving cruft on the file
> system.
Good point. Will clean that up.
>> === modified file 'license_
>>
>>
>> @@ -41,11 +44,14 @@
>> APIKeyStore.
>> return HttpResponseSer
>>
>> + api_key = APIKeyStore.
>> +
>
> Does the call to APIKeyStore.
> The pro...
James Tunnicliffe (dooferlad) wrote : | # |
Actually I should put the tests in a new class with a tearDown that
removes extra directories - it is called even if there is an
exception.
On 22 July 2013 16:31, James Tunnicliffe <email address hidden> wrote:
> On 22 July 2013 16:04, Milo Casagrande <email address hidden> wrote:
>> Hi James,
>>
>> overall it looks good, a couple of suggestion follows.
>>
>> On Wed, Jul 17, 2013 at 5:29 PM, James Tunnicliffe
>> <email address hidden> wrote:
>>>
>>> + def test_get_
>>> + response = self.client.get("http://
>>> + data={"key": settings.
>>> + "public": ""})
>>> +
>>> + self.assertEqua
>>> + # Don't care what the key is, as long as it isn't blank
>>> + self.assertRege
>>> + key = response.content
>>> +
>>> + # Now write a file so we can upload it
>>> + file_content = "test_get_
>>> + file_root = "/tmp"
>>> +
>>> + tmp_file_name = os.path.join(
>>> + file_root,
>>> + self.make_
>>> +
>>> + buildinfo_content = "\n".join([
>>> + "Format-Version: 0.1",
>>> + "Files-Pattern: *",
>>> + "Build-Name: test",
>>> + "License-Type: open"])
>>> + tmp_build_info = os.path.join(
>>> + file_root,
>>> + self.make_
>>> +
>>> + # Send the files
>>> + with open(tmp_file_name) as f:
>>> + response = self.client.post("http://
>>> + data={"key": key, "file": f})
>>> + self.assertEqua
>>> +
>>> + with open(tmp_
>>> + response = self.client.post("http://
>>> + data={"key": key, "file": f})
>>> + self.assertEqua
>>> +
>>> + # Check the upload worked by reading the file back from its uploaded
>>> + # location
>>> + uploaded_file_path = os.path.
>>> + "file_name")
>>> +
>>> + with open(uploaded_
>>> + self.assertEqua
>>> +
>>> + # Test we can fetch the newly uploaded file
>>> + response = self.client.get("http://
>>> + self.assertEqua
>>> +
>>> + # Delete the files generated by the test
>>> + shutil.
>>
>>
>> Wouldn't it be better to have all the logic of this test method inside
>> a try-finally?
>> I'm concerned with an error that can happen, for whatever reason,
>> somewhere before the call to shutil.rmtree, leaving cruft on the file
>> system.
>
> Good point. Will clean that up.
>
>>> === modified file 'license_
>>>
>>>
>>> @@ -41,...
Milo Casagrande (milo) wrote : | # |
On Mon, Jul 22, 2013 at 5:34 PM, James Tunnicliffe
<email address hidden> wrote:
>> Does the call to APIKeyStore.
>> The problem is that the other solution I have in mind means to always
>> call APIKeyStore.
>> False.
>
> You couldn't make the call - if request.POST["key"] didn't exist, it
> would raise an exception. If you had no filter for the key value, the
> filter would match everything, which would be a bad thing.
Righto, it is the same key.
On Mon, Jul 22, 2013 at 5:37 PM, James Tunnicliffe
<email address hidden> wrote:
> Actually I should put the tests in a new class with a tearDown that
> removes extra directories - it is called even if there is an
> exception.
Even better!
--
Milo Casagrande | Automation Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs
Milo Casagrande (milo) wrote : | # |
Spoke with James about this merge proposal: I will merge it and fix test issue discovered.
Preview Diff
1 | === modified file 'license_protected_downloads/models.py' | |||
2 | --- license_protected_downloads/models.py 2013-06-05 13:16:41 +0000 | |||
3 | +++ license_protected_downloads/models.py 2013-07-17 15:28:26 +0000 | |||
4 | @@ -30,3 +30,4 @@ | |||
5 | 30 | 30 | ||
6 | 31 | class APIKeyStore(models.Model): | 31 | class APIKeyStore(models.Model): |
7 | 32 | key = models.CharField(max_length=80) | 32 | key = models.CharField(max_length=80) |
8 | 33 | public = models.BooleanField() | ||
9 | 33 | 34 | ||
10 | === modified file 'license_protected_downloads/tests/test_views.py' | |||
11 | --- license_protected_downloads/tests/test_views.py 2013-06-14 12:39:29 +0000 | |||
12 | +++ license_protected_downloads/tests/test_views.py 2013-07-17 15:28:26 +0000 | |||
13 | @@ -907,6 +907,59 @@ | |||
14 | 907 | # Delete the files generated by the test | 907 | # Delete the files generated by the test |
15 | 908 | shutil.rmtree(os.path.join(settings.UPLOAD_PATH, key)) | 908 | shutil.rmtree(os.path.join(settings.UPLOAD_PATH, key)) |
16 | 909 | 909 | ||
17 | 910 | def test_get_public_key_post_and_get_file(self): | ||
18 | 911 | response = self.client.get("http://testserver/api/request_key", | ||
19 | 912 | data={"key": settings.MASTER_API_KEY, | ||
20 | 913 | "public": ""}) | ||
21 | 914 | |||
22 | 915 | self.assertEqual(response.status_code, 200) | ||
23 | 916 | # Don't care what the key is, as long as it isn't blank | ||
24 | 917 | self.assertRegexpMatches(response.content, "\S+") | ||
25 | 918 | key = response.content | ||
26 | 919 | |||
27 | 920 | # Now write a file so we can upload it | ||
28 | 921 | file_content = "test_get_key_post_and_get_file" | ||
29 | 922 | file_root = "/tmp" | ||
30 | 923 | |||
31 | 924 | tmp_file_name = os.path.join( | ||
32 | 925 | file_root, | ||
33 | 926 | self.make_temporary_file(file_content)) | ||
34 | 927 | |||
35 | 928 | buildinfo_content = "\n".join([ | ||
36 | 929 | "Format-Version: 0.1", | ||
37 | 930 | "Files-Pattern: *", | ||
38 | 931 | "Build-Name: test", | ||
39 | 932 | "License-Type: open"]) | ||
40 | 933 | tmp_build_info = os.path.join( | ||
41 | 934 | file_root, | ||
42 | 935 | self.make_temporary_file(buildinfo_content)) | ||
43 | 936 | |||
44 | 937 | # Send the files | ||
45 | 938 | with open(tmp_file_name) as f: | ||
46 | 939 | response = self.client.post("http://testserver/pub/file_name", | ||
47 | 940 | data={"key": key, "file": f}) | ||
48 | 941 | self.assertEqual(response.status_code, 200) | ||
49 | 942 | |||
50 | 943 | with open(tmp_build_info) as f: | ||
51 | 944 | response = self.client.post("http://testserver/pub/BUILD-INFO.txt", | ||
52 | 945 | data={"key": key, "file": f}) | ||
53 | 946 | self.assertEqual(response.status_code, 200) | ||
54 | 947 | |||
55 | 948 | # Check the upload worked by reading the file back from its uploaded | ||
56 | 949 | # location | ||
57 | 950 | uploaded_file_path = os.path.join(settings.SERVED_PATHS[0], | ||
58 | 951 | "file_name") | ||
59 | 952 | |||
60 | 953 | with open(uploaded_file_path) as f: | ||
61 | 954 | self.assertEqual(f.read(), file_content) | ||
62 | 955 | |||
63 | 956 | # Test we can fetch the newly uploaded file | ||
64 | 957 | response = self.client.get("http://testserver/pub/file_name") | ||
65 | 958 | self.assertEqual(response.status_code, 200) | ||
66 | 959 | |||
67 | 960 | # Delete the files generated by the test | ||
68 | 961 | shutil.rmtree(os.path.join(settings.SERVED_PATHS[0], "pub")) | ||
69 | 962 | |||
70 | 910 | def test_post_file_no_key(self): | 963 | def test_post_file_no_key(self): |
71 | 911 | file_content = "test_post_file_no_key" | 964 | file_content = "test_post_file_no_key" |
72 | 912 | file_root = "/tmp" | 965 | file_root = "/tmp" |
73 | 913 | 966 | ||
74 | === modified file 'license_protected_downloads/uploads.py' | |||
75 | --- license_protected_downloads/uploads.py 2013-06-11 15:03:01 +0000 | |||
76 | +++ license_protected_downloads/uploads.py 2013-07-17 15:28:26 +0000 | |||
77 | @@ -17,13 +17,16 @@ | |||
78 | 17 | file = forms.FileField() | 17 | file = forms.FileField() |
79 | 18 | 18 | ||
80 | 19 | 19 | ||
82 | 20 | def upload_target_path(path, key): | 20 | def upload_target_path(path, key, public): |
83 | 21 | """Quick path handling function. | 21 | """Quick path handling function. |
84 | 22 | 22 | ||
85 | 23 | Checks that the generated path doesn't end up outside the target directory, | 23 | Checks that the generated path doesn't end up outside the target directory, |
86 | 24 | so you can't set path to start with "/" and upload to anywhere. | 24 | so you can't set path to start with "/" and upload to anywhere. |
87 | 25 | """ | 25 | """ |
89 | 26 | base_path = os.path.join(settings.UPLOAD_PATH, key) | 26 | if public: |
90 | 27 | base_path = os.path.join(settings.SERVED_PATHS[0]) | ||
91 | 28 | else: | ||
92 | 29 | base_path = os.path.join(settings.UPLOAD_PATH, key) | ||
93 | 27 | return safe_path_join(base_path, path) | 30 | return safe_path_join(base_path, path) |
94 | 28 | 31 | ||
95 | 29 | 32 | ||
96 | @@ -41,11 +44,14 @@ | |||
97 | 41 | APIKeyStore.objects.filter(key=request.POST["key"])): | 44 | APIKeyStore.objects.filter(key=request.POST["key"])): |
98 | 42 | return HttpResponseServerError("Invalid key") | 45 | return HttpResponseServerError("Invalid key") |
99 | 43 | 46 | ||
100 | 47 | api_key = APIKeyStore.objects.filter(key=request.POST["key"]) | ||
101 | 48 | |||
102 | 44 | form = UploadFileForm(request.POST, request.FILES) | 49 | form = UploadFileForm(request.POST, request.FILES) |
103 | 45 | if not form.is_valid() or not path: | 50 | if not form.is_valid() or not path: |
104 | 46 | return HttpResponseServerError("Invalid call") | 51 | return HttpResponseServerError("Invalid call") |
105 | 47 | 52 | ||
107 | 48 | path = upload_target_path(path, request.POST["key"]) | 53 | path = upload_target_path( |
108 | 54 | path, request.POST["key"], public=api_key[0].public) | ||
109 | 49 | 55 | ||
110 | 50 | # Create directory if required | 56 | # Create directory if required |
111 | 51 | dirname = os.path.dirname(path) | 57 | dirname = os.path.dirname(path) |
112 | @@ -69,7 +75,15 @@ | |||
113 | 69 | while APIKeyStore.objects.filter(key=key): | 75 | while APIKeyStore.objects.filter(key=key): |
114 | 70 | key = "%030x" % random.randrange(256**15) | 76 | key = "%030x" % random.randrange(256**15) |
115 | 71 | 77 | ||
117 | 72 | api_key = APIKeyStore(key=key) | 78 | # Look for a hint of sanity in the value given to public, but don't |
118 | 79 | # care about it too much. | ||
119 | 80 | yes = ["", "y", "yes", "true", "1"] | ||
120 | 81 | if "public" in request.GET: | ||
121 | 82 | public = request.GET["public"].lower() in yes | ||
122 | 83 | else: | ||
123 | 84 | public = False | ||
124 | 85 | |||
125 | 86 | api_key = APIKeyStore(key=key, public=public) | ||
126 | 73 | api_key.save() | 87 | api_key.save() |
127 | 74 | return HttpResponse(key) | 88 | return HttpResponse(key) |
128 | 75 | 89 | ||
129 | 76 | 90 | ||
130 | === modified file 'license_protected_downloads/views.py' | |||
131 | --- license_protected_downloads/views.py 2013-06-19 09:51:06 +0000 | |||
132 | +++ license_protected_downloads/views.py 2013-07-17 15:28:26 +0000 | |||
133 | @@ -24,7 +24,7 @@ | |||
134 | 24 | import bzr_version | 24 | import bzr_version |
135 | 25 | from buildinfo import BuildInfo, IncorrectDataFormatException | 25 | from buildinfo import BuildInfo, IncorrectDataFormatException |
136 | 26 | from render_text_files import RenderTextFiles | 26 | from render_text_files import RenderTextFiles |
138 | 27 | from models import License | 27 | from models import License, APIKeyStore |
139 | 28 | # Load group auth "plugin" dynamically | 28 | # Load group auth "plugin" dynamically |
140 | 29 | import importlib | 29 | import importlib |
141 | 30 | group_auth_modules = [importlib.import_module(m) for m in settings.GROUP_AUTH_MODULES] | 30 | group_auth_modules = [importlib.import_module(m) for m in settings.GROUP_AUTH_MODULES] |
142 | @@ -136,13 +136,25 @@ | |||
143 | 136 | return listing | 136 | return listing |
144 | 137 | 137 | ||
145 | 138 | 138 | ||
147 | 139 | def test_path(path, served_paths=None): | 139 | def test_path(path, request, served_paths=None): |
148 | 140 | """Check that path points to something we can serve up. | 140 | """Check that path points to something we can serve up. |
149 | 141 | 141 | ||
150 | 142 | served_paths can be provided to overwrite settings.SERVED_PATHS. This is | 142 | served_paths can be provided to overwrite settings.SERVED_PATHS. This is |
151 | 143 | used for uploaded files, which may not be shared in the server root. | 143 | used for uploaded files, which may not be shared in the server root. |
152 | 144 | """ | 144 | """ |
153 | 145 | 145 | ||
154 | 146 | # if key is in request.GET["key"] then need to mod path and give | ||
155 | 147 | # access to a per-key directory. | ||
156 | 148 | if "key" in request.GET: | ||
157 | 149 | key_details = APIKeyStore.objects.filter(key=request.GET["key"]) | ||
158 | 150 | if key_details: | ||
159 | 151 | path = os.path.join(request.GET["key"], path) | ||
160 | 152 | |||
161 | 153 | # Private uploads are in a separate path (or can be), so set | ||
162 | 154 | # served_paths as needed. | ||
163 | 155 | if key_details[0].public == False: | ||
164 | 156 | served_paths = settings.UPLOAD_PATH | ||
165 | 157 | |||
166 | 146 | if served_paths is None: | 158 | if served_paths is None: |
167 | 147 | served_paths = settings.SERVED_PATHS | 159 | served_paths = settings.SERVED_PATHS |
168 | 148 | else: | 160 | else: |
169 | @@ -458,14 +470,7 @@ | |||
170 | 458 | def file_server_get(request, path): | 470 | def file_server_get(request, path): |
171 | 459 | 471 | ||
172 | 460 | url = path | 472 | url = path |
181 | 461 | 473 | result = test_path(path, request) | |
174 | 462 | # if key is in request.GET["key"] then need to mod path and give | ||
175 | 463 | # access to a per-key directory. | ||
176 | 464 | if "key" in request.GET: | ||
177 | 465 | path = os.path.join(request.GET["key"], path) | ||
178 | 466 | result = test_path(path, settings.UPLOAD_PATH) | ||
179 | 467 | else: | ||
180 | 468 | result = test_path(path) | ||
182 | 469 | 474 | ||
183 | 470 | if not result: | 475 | if not result: |
184 | 471 | raise Http404 | 476 | raise Http404 |
185 | @@ -572,7 +577,7 @@ | |||
186 | 572 | 577 | ||
187 | 573 | def get_textile_files(request): | 578 | def get_textile_files(request): |
188 | 574 | 579 | ||
190 | 575 | result = test_path(request.GET.get("path")) | 580 | result = test_path(request.GET.get("path"), request) |
191 | 576 | if not result: | 581 | if not result: |
192 | 577 | raise Http404 | 582 | raise Http404 |
193 | 578 | 583 | ||
194 | @@ -600,7 +605,7 @@ | |||
195 | 600 | def list_files_api(request, path): | 605 | def list_files_api(request, path): |
196 | 601 | path = iri_to_uri(path) | 606 | path = iri_to_uri(path) |
197 | 602 | url = path | 607 | url = path |
199 | 603 | result = test_path(path) | 608 | result = test_path(path, request) |
200 | 604 | if not result: | 609 | if not result: |
201 | 605 | raise Http404 | 610 | raise Http404 |
202 | 606 | 611 | ||
203 | @@ -643,7 +648,7 @@ | |||
204 | 643 | 648 | ||
205 | 644 | def get_license_api(request, path): | 649 | def get_license_api(request, path): |
206 | 645 | path = iri_to_uri(path) | 650 | path = iri_to_uri(path) |
208 | 646 | result = test_path(path) | 651 | result = test_path(path, request) |
209 | 647 | if not result: | 652 | if not result: |
210 | 648 | raise Http404 | 653 | raise Http404 |
211 | 649 | 654 |
Hi James,
overall it looks good, a couple of suggestion follows.
On Wed, Jul 17, 2013 at 5:29 PM, James Tunnicliffe public_ key_post_ and_get_ file(self) : testserver/ api/request_ key", MASTER_ API_KEY, l(response. status_ code, 200) xpMatches( response. content, "\S+") key_post_ and_get_ file" temporary_ file(file_ content) ) temporary_ file(buildinfo_ content) ) testserver/ pub/file_ name", l(response. status_ code, 200) build_info) as f: testserver/ pub/BUILD- INFO.txt", l(response. status_ code, 200) join(settings. SERVED_ PATHS[0] , file_path) as f: l(f.read( ), file_content) testserver/ pub/file_ name") l(response. status_ code, 200) rmtree( os.path. join(settings. SERVED_ PATHS[0] , "pub"))
<email address hidden> wrote:
>
> + def test_get_
> + response = self.client.get("http://
> + data={"key": settings.
> + "public": ""})
> +
> + self.assertEqua
> + # Don't care what the key is, as long as it isn't blank
> + self.assertRege
> + key = response.content
> +
> + # Now write a file so we can upload it
> + file_content = "test_get_
> + file_root = "/tmp"
> +
> + tmp_file_name = os.path.join(
> + file_root,
> + self.make_
> +
> + buildinfo_content = "\n".join([
> + "Format-Version: 0.1",
> + "Files-Pattern: *",
> + "Build-Name: test",
> + "License-Type: open"])
> + tmp_build_info = os.path.join(
> + file_root,
> + self.make_
> +
> + # Send the files
> + with open(tmp_file_name) as f:
> + response = self.client.post("http://
> + data={"key": key, "file": f})
> + self.assertEqua
> +
> + with open(tmp_
> + response = self.client.post("http://
> + data={"key": key, "file": f})
> + self.assertEqua
> +
> + # Check the upload worked by reading the file back from its uploaded
> + # location
> + uploaded_file_path = os.path.
> + "file_name")
> +
> + with open(uploaded_
> + self.assertEqua
> +
> + # Test we can fetch the newly uploaded file
> + response = self.client.get("http://
> + self.assertEqua
> +
> + # Delete the files generated by the test
> + shutil.
Wouldn't it be better to have all the logic of this test method inside
a try-finally?
I'm concerned with an error that can happen, for whatever reason,
somewhere before the call to shutil.rmtree, leaving cruft on the file
system.
> === modified file 'license_ protected_ downloads/ uploads. py' objects. filter( key=request. POST["key" ])): verError( "Invalid key") objects. filter( key=request. POST["key" ])
>
>
> @@ -41,11 +44,14 @@
> APIKeyStore.
> return HttpResponseSer
>
> + api_key = APIKeyStore.
> +
Does the call to APIKeyStore. objects. filter get cached in some way? objects. filter even if "key in request.POST" is
The problem is that the other solution I have in mind means to always
call APIKeyStore.
False.
--
Milo Casagrande | Automation Engineer
Linaro.org <www.li...