Merge lp:~dooferlad/linaro-license-protection/api-updates into lp:~linaro-automation/linaro-license-protection/trunk

Proposed by James Tunnicliffe
Status: Merged
Approved by: Milo Casagrande
Approved revision: 180
Merged at revision: 179
Proposed branch: lp:~dooferlad/linaro-license-protection/api-updates
Merge into: lp:~linaro-automation/linaro-license-protection/trunk
Diff against target: 306 lines (+150/-91)
3 files modified
license_protected_downloads/tests/test_views.py (+24/-0)
license_protected_downloads/views.py (+22/-11)
scripts/download.py (+104/-80)
To merge this branch: bzr merge lp:~dooferlad/linaro-license-protection/api-updates
Reviewer Review Type Date Requested Status
Milo Casagrande (community) Approve
Review via email: mp+153903@code.launchpad.net

Description of the change

Minor update to the API so <server>/api/ls/<path to file> works. Previously a listing would only work for single directory.

Updated download.py. Now it is a full, interactive download script:

download.py <URL of directory>
 - download all files in directory

download <URL of file>
 - download single file

Both of the above require the user to accept each license once. Once a license has been accepted the digest is stored so they don't have to re-accept an unchanged license.

Have split up download.py so the guts of the work is done in a single function, but set up and some messing around with URLs is moved out into a class. This improves readability. Since it is designed as executable documentation, it seems reasonable to take the cruft out of the main function.

To post a comment you must log in.
Revision history for this message
Milo Casagrande (milo) wrote :

Hey James,

thanks for working on this, it looks good to go.
For the download.py file, as long as it is a demo, it looks OK too, otherwise there might be a couple of changes to apply. I hope we can provide a good CLI-client for this in the future.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'license_protected_downloads/tests/test_views.py'
2--- license_protected_downloads/tests/test_views.py 2013-03-12 15:03:35 +0000
3+++ license_protected_downloads/tests/test_views.py 2013-03-18 18:32:28 +0000
4@@ -290,6 +290,30 @@
5
6 self.assertEqual(mtime, file_info["mtime"])
7
8+ def test_api_get_listing_single_file(self):
9+ url = "/api/ls/build-info/snowball-blob.txt"
10+ response = self.client.get(url)
11+ self.assertEqual(response.status_code, 200)
12+
13+ data = json.loads(response.content)["files"]
14+
15+ # Should be a listing for a single file
16+ self.assertEqual(len(data), 1)
17+
18+ # For each file listed, check some key attributes
19+ for file_info in data:
20+ file_path = os.path.join(TESTSERVER_ROOT,
21+ file_info["url"].lstrip("/"))
22+ if file_info["type"] == "folder":
23+ self.assertTrue(os.path.isdir(file_path))
24+ else:
25+ self.assertTrue(os.path.isfile(file_path))
26+
27+ mtime = datetime.fromtimestamp(
28+ os.path.getmtime(file_path)).strftime('%d-%b-%Y %H:%M')
29+
30+ self.assertEqual(mtime, file_info["mtime"])
31+
32 def test_api_get_listing_404(self):
33 url = "/api/ls/buld-info"
34 response = self.client.get(url)
35
36=== modified file 'license_protected_downloads/views.py'
37--- license_protected_downloads/views.py 2013-03-11 15:31:31 +0000
38+++ license_protected_downloads/views.py 2013-03-18 18:32:28 +0000
39@@ -68,23 +68,23 @@
40 files.sort()
41 listing = []
42
43- for file in files:
44- if _hidden_file(file):
45+ for file_name in files:
46+ if _hidden_file(file_name):
47 continue
48
49- name = file
50- file = os.path.join(path, file)
51+ name = file_name
52+ file_name = os.path.join(path, file_name)
53
54- if os.path.exists(file):
55+ if os.path.exists(file_name):
56 mtime = datetime.fromtimestamp(
57- os.path.getmtime(file)).strftime('%d-%b-%Y %H:%M')
58+ os.path.getmtime(file_name)).strftime('%d-%b-%Y %H:%M')
59 else:
60 # If the file we are looking at doesn't exist (broken symlink for
61 # example), it doesn't have a mtime.
62 mtime = 0
63
64 target_type = "other"
65- if os.path.isdir(file):
66+ if os.path.isdir(file_name):
67 target_type = "folder"
68 else:
69 type_tuple = guess_type(name)
70@@ -92,8 +92,8 @@
71 if type_tuple[0].split('/')[0] == "text":
72 target_type = "text"
73
74- if os.path.exists(file):
75- size = os.path.getsize(file)
76+ if os.path.exists(file_name):
77+ size = os.path.getsize(file_name)
78 else:
79 # If the file we are looking at doesn't exist (broken symlink for
80 # example), it doesn't have a size
81@@ -512,11 +512,22 @@
82 target_type = result[0]
83 path = result[1]
84
85- if target_type == "dir":
86+ if target_type:
87+ if target_type == "file":
88+ file_url = url
89+ if file_url[0] != "/":
90+ file_url = "/" + file_url
91+ path = os.path.dirname(path)
92+ url = os.path.dirname(url)
93+
94 listing = dir_list(url, path)
95
96 clean_listing = []
97 for entry in listing:
98+ if target_type == "file" and file_url != entry["url"]:
99+ # If we are getting a listing for a single file, skip the rest
100+ continue
101+
102 if len(entry["license_list"]) == 0:
103 entry["license_list"] = ["Open"]
104
105@@ -545,7 +556,7 @@
106
107 if target_type == "dir":
108 data = json.dumps({"licenses":
109- ["File not found."]})
110+ ["ERROR: License only shown for a single file."]})
111 else:
112 license_digest_list = is_protected(path)
113 license_list = License.objects.all_with_hashes(license_digest_list)
114
115=== modified file 'scripts/download.py'
116--- scripts/download.py 2013-03-12 14:59:02 +0000
117+++ scripts/download.py 2013-03-18 18:32:28 +0000
118@@ -6,84 +6,108 @@
119 import urllib2
120 import os
121 from html2text import html2text
122-
123-# Example of how to use the API to download all files in a directory. This is
124-# written as one procedural script without functions
125-directory_url = "http://localhost:8001/build-info"
126-
127-# Generate the URL that will return the license information. This is the URL
128-# of the file with /api/license prepended to the path.
129-
130-# Unfortunately urlsplit returns an immutable object. Convert it to an array
131-# so we can modify the path section (index 2)
132-parsed_url = [c for c in urlparse.urlsplit(directory_url)]
133-url_path_section = parsed_url[2]
134-
135-parsed_url[2] = "/api/ls" + url_path_section
136-listing_url = urlparse.urlunsplit(parsed_url)
137-
138-u = urllib2.urlopen(listing_url)
139-data = json.loads(u.read())["files"]
140-
141-for file_info in data:
142- if file_info["type"] == "folder":
143- # Skip folders...
144- continue
145-
146- parsed_url[2] = "/api/license" + file_info["url"]
147- license_url = urlparse.urlunsplit(parsed_url)
148-
149- parsed_url[2] = file_info["url"]
150- file_url = urlparse.urlunsplit(parsed_url)
151-
152- # Get the licenses. They are returned as a JSON document in the form:
153- # {"licenses":
154- # [{"text": "<license text>", "digest": "<digest of license>"},
155- # {"text": "<license text>", "digest": "<digest of license>"},
156- # ...
157- # ]}
158- # Each license has a digest associated with it.
159- u = urllib2.urlopen(license_url)
160- data = json.loads(u.read())["licenses"]
161-
162- if data[0] == "Open":
163- headers = {}
164+import sys
165+import xdg.BaseDirectory as xdgBaseDir
166+
167+
168+def download(api_urls, accepted_licenses):
169+ """Example of how to use the API to download a/all files in a directory."""
170+
171+ # Get listing for file(s) pointed to by URL we were given
172+ request = urllib2.urlopen(api_urls.ls())
173+ listing = json.loads(request.read())["files"]
174+
175+ for file_info in listing:
176+ if file_info["type"] == "folder":
177+ # Skip folders...
178+ continue
179+
180+ # Get the licenses. They are returned as a JSON document in the form:
181+ # {"licenses":
182+ # [{"text": "<license text>", "digest": "<digest of license>"},
183+ # {"text": "<license text>", "digest": "<digest of license>"},
184+ # ...
185+ # ]}
186+ # Each license has a digest associated with it.
187+ request = urllib2.urlopen(api_urls.license(file_info["url"]))
188+ licenses = json.loads(request.read())["licenses"]
189+
190+ if licenses[0] == "Open":
191+ headers = {}
192+ else:
193+ # Present each license to the user...
194+ for lic in licenses:
195+ if lic["digest"] not in accepted_licenses:
196+ # Licenses are stored as HTML. Convert them to markdown
197+ # (text) and print it to the terminal.
198+ print html2text(lic["text"])
199+
200+ # Ask the user if they accept the license. If they don't we
201+ # terminate the script.
202+ user_response = raw_input(
203+ "Do you accept this license? (y/N)")
204+ if user_response != "y":
205+ exit(1)
206+
207+ # Remember this license acceptance for another download.
208+ accepted_licenses.append(lic["digest"])
209+
210+ # To accept a license, place the digest in the LICENSE_ACCEPTED
211+ # header. For multiple licenses, they are stored space separated.
212+ digests = [lic["digest"] for lic in licenses]
213+ headers = {"LICENSE_ACCEPTED": " ".join(digests)}
214+
215+ # Once the header has been generated, just download the file.
216+ req = urllib2.urlopen(urllib2.Request(api_urls.file(file_info["url"]),
217+ headers=headers))
218+ with open(os.path.basename(file_info["url"]), 'wb') as fp:
219+ shutil.copyfileobj(req, fp)
220+
221+
222+class ApiUrls():
223+ """Since we want to manipulate URLS, but urlsplit returns an immutable
224+ object this is a convenience object to perform the manipulations for us"""
225+ def __init__(self, input_url):
226+ self.parsed_url = [c for c in urlparse.urlsplit(input_url)]
227+ self.path = self.parsed_url[2]
228+
229+ def ls(self, path=None):
230+ if not path:
231+ path = self.path
232+ self.parsed_url[2] = "/api/ls" + path
233+ return urlparse.urlunsplit(self.parsed_url)
234+
235+ def license(self, path):
236+ self.parsed_url[2] = "/api/license" + path
237+ return urlparse.urlunsplit(self.parsed_url)
238+
239+ def file(self, path):
240+ self.parsed_url[2] = path
241+ return urlparse.urlunsplit(self.parsed_url)
242+
243+
244+if __name__ == '__main__':
245+ if len(sys.argv) != 2:
246+ # Check that a URL has been supplied.
247+ print >> sys.stderr, "Usage: download.py <URL>"
248+ exit(1)
249+
250+ accepted_licenses_path = os.path.join(xdgBaseDir.xdg_data_home,
251+ "linaro",
252+ "accepted_licenses")
253+
254+ # Later we ask the user to accept each license in turn. Store which
255+ # licenses are accepted so the user only has to accept them once.
256+ if os.path.isfile(accepted_licenses_path):
257+ with open(accepted_licenses_path) as accepted_licenses_file:
258+ accepted_licenses = accepted_licenses_file.read().split()
259 else:
260- # If this were a command line client designed to ask the user to accept
261- # each license, you could use this code to ask the user to accept each
262- # license in turn. In this example we store which licenses are accepted
263- # so the user only has to accept them once.
264- if os.path.isfile("accepted_licenses"):
265- with open("accepted_licenses") as accepted_licenses_file:
266- accepted_licenses = accepted_licenses_file.read().split()
267- else:
268- accepted_licenses = []
269-
270- # Present each license to the user...
271- for d in data:
272- if d["digest"] not in accepted_licenses:
273- # Licenses are stored as HTML. Convert them to markdown (text)
274- # and print it to the terminal.
275- print html2text(d["text"])
276-
277- # Ask the user if they accept the license. If they don't we
278- # terminate the script.
279- user_response = raw_input("Do you accept this license? (y/N)")
280- if user_response != "y":
281- exit(1)
282-
283- accepted_licenses.append(d["digest"])
284-
285- # Store the licenses that the user accepted
286- with open("accepted_licenses", "w") as accepted_licenses_file:
287- accepted_licenses_file.write(" ".join(accepted_licenses))
288-
289- # To accept a license, place the digest in the LICENSE_ACCEPTED header.
290- # For multiple licenses, they are stored space separated.
291- digests = [d["digest"] for d in data]
292- headers = {"LICENSE_ACCEPTED": " ".join(digests)}
293-
294- # Once the header has been generated, just download the file.
295- req = urllib2.urlopen(urllib2.Request(file_url, headers=headers))
296- with open(os.path.basename(parsed_url[2]), 'wb') as fp:
297- shutil.copyfileobj(req, fp)
298+ accepted_licenses = []
299+
300+ api_urls = ApiUrls(sys.argv[1])
301+
302+ download(api_urls, accepted_licenses)
303+
304+ # Store the licenses that the user accepted
305+ with open(accepted_licenses_path, "w") as accepted_licenses_file:
306+ accepted_licenses_file.write(" ".join(accepted_licenses))

Subscribers

People subscribed via source and target branches