~launchpad-pqm/launchpad/devel

13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
1
Librarian Access
2
================
3
4
The librarian is a file storage service for launchpad. Conceptually
5
similar to other file storage API's like S3, it is used to store binary
6
or large content - bug attachments, package builds, images and so on.
7
8
Content in the librarian can be exposed at different urls. To expose
9
some content use a LibraryFileAlias. Private content is supported as
10
well - for that tokens are added to permit access for a limited time by
11
a client - each time a client attempts to dereference a private
12
LibraryFileAlias a token is emitted.
13
14
15
Deployment notes
16
================
17
18
(These may seem a bit out of place - they are, but they need to be
19
written down somewhere, and the deployment choices inform the
20
implementation choices).
21
22
The basics are simple: The librarian talks to clients. However
23
restricted file access makes things a little more complex. As the
24
librarian itself doesn't do SSL processing, and we want restricted files
25
to be kept confidential the librarian will need a hint from the SSL
26
front end that SSL was in fact used. The semi standard header Front-End-
27
Https can be used for this if we filter it in incoming requests from
28
clients.
29
30
31
setUp
32
-----
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
33
12393.27.5 by Danilo Segan
Merge with latest accordionoverlay.
34
    >>> from canonical.database.sqlbase import session_store
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
35
    >>> from lp.services.librarian.model import TimeLimitedToken
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
36
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
37
38
High Level
39
----------
4989.3.2 by Francis J. Lacoste
Convert moin header.
40
11716.1.12 by Curtis Hovey
Sorted imports in doctests.
41
    >>> from StringIO import StringIO
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
42
    >>> from lp.services.librarian.interfaces import (
12393.27.5 by Danilo Segan
Merge with latest accordionoverlay.
43
    ...     ILibraryFileAliasSet)
4195.1.12 by Brad Crittenden
Post-review changes
44
    >>> data = 'This is some data'
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
45
46
We can create LibraryFileAliases using the ILibraryFileAliasSet utility.
4195.1.1 by Brad Crittenden
Implement upload and management of files associated with a product release.
47
This name is a mouthful, but is consistent with the rest of our naming.
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
48
4195.1.12 by Brad Crittenden
Post-review changes
49
    >>> lfas = getUtility(ILibraryFileAliasSet)
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
50
    >>> from lp.services.librarian.interfaces import NEVER_EXPIRES
4195.1.12 by Brad Crittenden
Post-review changes
51
    >>> alias = lfas.create(
52
    ...     'text.txt', len(data), StringIO(data), 'text/plain', NEVER_EXPIRES
53
    ...     )
54
    >>> alias.mimetype
55
    u'text/plain'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
56
4195.1.12 by Brad Crittenden
Post-review changes
57
    >>> alias.filename
58
    u'text.txt'
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
59
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
60
We may wish to set an expiry timestamp on the file. The NEVER_EXPIRES
61
constant means the file will never be removed from the Librarian, and
62
because of this should probably never be used.
63
4195.1.12 by Brad Crittenden
Post-review changes
64
    >>> alias.expires == NEVER_EXPIRES
65
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
66
5911.2.19 by Francis J. Lacoste
Style based on salgado's review.
67
    >>> alias = lfas.create(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
68
    ...     'text.txt', len(data), StringIO(data), 'text/plain')
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
69
70
The default expiry of None means the file will expire a few days after
71
it is no longer referenced in the database.
72
4195.1.12 by Brad Crittenden
Post-review changes
73
    >>> alias.expires is None
74
    True
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
75
4989.3.3 by Francis J. Lacoste
Expose date_created column in interface.
76
The creation timestamp of the LibraryFileAlias is available in the
77
date_created attribute.
78
79
    >>> alias.date_created
80
    datetime.datetime(...)
81
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
82
We can retrieve the LibraryFileAlias we just created using its ID or
83
sha1.
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
84
4195.1.12 by Brad Crittenden
Post-review changes
85
    >>> org_alias_id = alias.id
86
    >>> alias = lfas[org_alias_id]
87
    >>> alias.id == org_alias_id
88
    True
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
89
4195.1.12 by Brad Crittenden
Post-review changes
90
    >>> org_alias_id in [a.id for a in lfas.findBySHA1(alias.content.sha1)]
91
    True
3691.164.16 by Guilherme Salgado
Lots of fixes and tests suggested by Bjorn
92
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
93
We can get its URL too
94
14605.1.1 by Curtis Hovey
Moved canonical.config to lp.services.
95
    >>> from lp.services.config import config
4195.1.12 by Brad Crittenden
Post-review changes
96
    >>> import re
97
    >>> re.search(
13980.2.3 by Jeroen Vermeulen
Some undetected lint, and some caused by auto-formatting.
98
    ...     r'^%s\d+/text.txt$' % config.librarian.download_url,
99
    ...     alias.http_url
100
    ...     ) is not None
4195.1.12 by Brad Crittenden
Post-review changes
101
    True
3691.9.26 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/76743 Refactor of LibraryFileAlias.[secure_]url
102
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
103
Librarian also serves the same file through https, we use this for
104
branding and similar inline-presented objects which would trigger
105
security warnings on https pages otherwise.
3691.9.26 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/76743 Refactor of LibraryFileAlias.[secure_]url
106
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
107
    >>> re.search(r'^https://.+/\d+/text.txt$', alias.https_url) is not None
4195.1.12 by Brad Crittenden
Post-review changes
108
    True
3691.9.26 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/76743 Refactor of LibraryFileAlias.[secure_]url
109
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
110
And we even have a convenient method which returns either the http URL
111
or the https one, depending on a config value.
3691.9.26 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/76743 Refactor of LibraryFileAlias.[secure_]url
112
5773.4.6 by Curtis Hovey
Added support for vhost to lazr and launchpad.
113
    >>> config.vhosts.use_https
4195.1.12 by Brad Crittenden
Post-review changes
114
    False
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
115
4195.1.12 by Brad Crittenden
Post-review changes
116
    >>> re.search(
13980.2.3 by Jeroen Vermeulen
Some undetected lint, and some caused by auto-formatting.
117
    ...     r'^%s\d+/text.txt$' % config.librarian.download_url,
118
    ...     alias.getURL()
119
    ...     ) is not None
4195.1.12 by Brad Crittenden
Post-review changes
120
    True
3691.9.26 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/76743 Refactor of LibraryFileAlias.[secure_]url
121
5773.4.10 by Curtis Hovey
Revisions per review.
122
    >>> from textwrap import dedent
123
    >>> test_data = dedent("""
13980.2.3 by Jeroen Vermeulen
Some undetected lint, and some caused by auto-formatting.
124
    ...     [librarian]
125
    ...     use_https: true
126
    ...     """)
5773.4.10 by Curtis Hovey
Revisions per review.
127
    >>> config.push('test', test_data)
4195.1.12 by Brad Crittenden
Post-review changes
128
    >>> re.search(
13980.2.3 by Jeroen Vermeulen
Some undetected lint, and some caused by auto-formatting.
129
    ...     r'^https://.+/\d+/text.txt$', alias.https_url
130
    ...     ) is not None
4195.1.12 by Brad Crittenden
Post-review changes
131
    True
3691.9.26 by Guilherme Salgado
Fix https://launchpad.net/launchpad/+bug/76743 Refactor of LibraryFileAlias.[secure_]url
132
5723.9.13 by Brad Crittenden
Corrected HTTP_X_SCHEME in the doc test.
133
However, we can force the use of HTTP by setting the 'HTTP_X_SCHEME'
5723.9.1 by Brad Crittenden
Redirect LibraryFileAliases based on the X-SCHEME request variable to allow serving them over HTTP even when the site is configured for HTTPS
134
header in the request to 'http', even when 'use_https' is True.
135
11716.1.12 by Curtis Hovey
Sorted imports in doctests.
136
    >>> from zope.component import getMultiAdapter
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
137
    >>> from lp.services.webapp.servers import LaunchpadTestRequest
5723.9.1 by Brad Crittenden
Redirect LibraryFileAliases based on the X-SCHEME request variable to allow serving them over HTTP even when the site is configured for HTTPS
138
    >>> from urlparse import urlparse
139
    >>> request = LaunchpadTestRequest(
13980.2.3 by Jeroen Vermeulen
Some undetected lint, and some caused by auto-formatting.
140
    ...     environ={'REQUEST_METHOD': 'GET', 'HTTP_X_SCHEME': 'http'})
5723.9.1 by Brad Crittenden
Redirect LibraryFileAliases based on the X-SCHEME request variable to allow serving them over HTTP even when the site is configured for HTTPS
141
    >>> view = getMultiAdapter((alias,request), name='+index')
142
    >>> view.initialize()
143
    >>> print urlparse(request.response.getHeader('Location'))[0]
144
    http
145
146
When the incoming scheme is 'https' then the redirect scheme is
147
unaffected.
148
149
    >>> request = LaunchpadTestRequest(
13980.2.3 by Jeroen Vermeulen
Some undetected lint, and some caused by auto-formatting.
150
    ...     environ={'REQUEST_METHOD': 'GET', 'HTTP_X_SCHEME': 'https'})
5723.9.1 by Brad Crittenden
Redirect LibraryFileAliases based on the X-SCHEME request variable to allow serving them over HTTP even when the site is configured for HTTPS
151
    >>> view = getMultiAdapter((alias,request), name='+index')
152
    >>> view.initialize()
153
    >>> print urlparse(request.response.getHeader('Location'))[0]
154
    https
3564.6.38 by Diogo Matsubara
Fixes bug 30370 (Graphics from Librarian over HTTP cause browser warnings on Launchpad over HTTPS)
155
5723.9.8 by Brad Crittenden
Reset use_https to the original value as expected by subsequent tests.
156
Reset 'use_https' to its original state.
157
5773.4.6 by Curtis Hovey
Added support for vhost to lazr and launchpad.
158
    >>> test_config_data = config.pop('test')
5723.9.8 by Brad Crittenden
Reset use_https to the original value as expected by subsequent tests.
159
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
160
However, we can't access its contents until we have committed
161
4195.1.12 by Brad Crittenden
Post-review changes
162
    >>> alias.open()
163
    Traceback (most recent call last):
164
        [...]
165
    LookupError: ...
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
166
167
Once we commit the transaction, LibraryFileAliases can be accessed like
168
files.
169
4195.1.12 by Brad Crittenden
Post-review changes
170
    >>> import transaction
171
    >>> transaction.commit()
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
172
4195.1.12 by Brad Crittenden
Post-review changes
173
    >>> alias.open()
174
    >>> alias.read()
175
    'This is some data'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
176
4195.1.12 by Brad Crittenden
Post-review changes
177
    >>> alias.close()
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
178
179
We can also read it in chunks.
180
4195.1.12 by Brad Crittenden
Post-review changes
181
    >>> alias.open()
182
    >>> alias.read(2)
183
    'Th'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
184
4195.1.12 by Brad Crittenden
Post-review changes
185
    >>> alias.read(6)
186
    'is is '
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
187
4195.1.12 by Brad Crittenden
Post-review changes
188
    >>> alias.read()
189
    'some data'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
190
4195.1.12 by Brad Crittenden
Post-review changes
191
    >>> alias.close()
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
192
193
If you don't want to read the file in chunks you can neglect to call
194
open() and close().
195
4195.1.12 by Brad Crittenden
Post-review changes
196
    >>> alias.read()
197
    'This is some data'
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
198
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
199
Each alias also has an expiry date associated with it, the default of
200
None meaning the file will expire a few days after nothing references it
201
any more:
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
202
4195.1.12 by Brad Crittenden
Post-review changes
203
    >>> alias.expires is None
204
    True
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
205
6422.1.1 by Muharem Hrnjadovic
bug fixed
206
Closing an alias repeatedly and/or without opening it beforehand is
207
tolerated and will not result in exceptions being raised.
208
209
    >>> alias.close()
210
    >>> alias.close()
211
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
212
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
213
Low Level
214
---------
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
215
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
216
We can also use the ILibrarianClient Utility directly to store and
217
access files in the Librarian.
1654 by Canonical.com Patch Queue Manager
MailStorage [r=stevea]
218
14606.2.3 by William Grant
canonical.librarian.interfaces -> lp.services.librarian.interfaces.client
219
    >>> from lp.services.librarian.interfaces.client import ILibrarianClient
4195.1.12 by Brad Crittenden
Post-review changes
220
    >>> client = getUtility(ILibrarianClient)
221
    >>> aid = client.addFile(
222
    ...     'text.txt', len(data), StringIO(data), 'text/plain', NEVER_EXPIRES
223
    ...     )
224
    >>> transaction.commit()
225
    >>> f = client.getFileByAlias(aid)
226
    >>> f.read()
227
    'This is some data'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
228
4195.1.12 by Brad Crittenden
Post-review changes
229
    >>> url = client.getURLForAlias(aid)
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
230
    >>> re.search(
231
    ...     r'^%s\d+/text.txt$' % config.librarian.download_url, url
232
    ...     ) is not None
4195.1.12 by Brad Crittenden
Post-review changes
233
    True
2398 by Canonical.com Patch Queue Manager
Librarian buildd_download_url support. r=spiv
234
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
235
When secure=True, the returned url has the id as part of the domain name
236
and the protocol is https:
7675.809.18 by Robert Collins
Extend the private librarian concept to have unique domains to get unique security contexts on browsers.
237
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
238
    >>> expected = r'^https://i%d\..+:\d+/%d/text.txt$' % (aid, aid)
239
    >>> found = client.getURLForAlias(aid, secure=True)
240
    >>> re.search(expected, found) is not None
7675.809.18 by Robert Collins
Extend the private librarian concept to have unique domains to get unique security contexts on browsers.
241
    True
242
11502.1.1 by Robert Collins
Instrument librarian access via timelines.
243
Librarian reads are logged in the request timeline.
244
13931.2.1 by Steve Kowalik
Chip away at canonical.lazr a little more.
245
    >>> from lazr.restful.utils import get_current_browser_request
11502.1.1 by Robert Collins
Instrument librarian access via timelines.
246
    >>> from lp.services.timeline.requesttimeline import get_request_timeline
247
    >>> request = get_current_browser_request()
248
    >>> timeline = get_request_timeline(request)
249
    >>> f = client.getFileByAlias(aid)
250
    >>> action = timeline.actions[-1]
251
    >>> action.category
252
    'librarian-connection'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
253
11502.1.1 by Robert Collins
Instrument librarian access via timelines.
254
    >>> action.detail.endswith('/text.txt')
255
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
256
11502.1.1 by Robert Collins
Instrument librarian access via timelines.
257
    >>> _unused = f.read()
258
    >>> action = timeline.actions[-1]
259
    >>> action.category
260
    'librarian-read'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
261
11502.1.1 by Robert Collins
Instrument librarian access via timelines.
262
    >>> action.detail.endswith('/text.txt')
263
    True
264
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
265
At this level we can also reverse the transactional semantics by using
266
the remoteAddFile instead of the addFile method. In this case, the
267
database rows are added by the Librarian, which means that the file is
268
downloadable immediately and will exist even if the client transaction
269
rolls back. However, the records in the database will not be visible to
270
the client until it begins a new transaction.
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
271
4195.1.12 by Brad Crittenden
Post-review changes
272
    >>> url = client.remoteAddFile(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
273
    ...     'text.txt', len(data), StringIO(data), 'text/plain')
4195.1.12 by Brad Crittenden
Post-review changes
274
    >>> print url
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
275
    http://.../text.txt
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
276
4195.1.12 by Brad Crittenden
Post-review changes
277
    >>> from urllib2 import urlopen
278
    >>> urlopen(url).read()
279
    'This is some data'
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
280
281
If we abort the transaction, it is still in there
282
4195.1.12 by Brad Crittenden
Post-review changes
283
    >>> transaction.abort()
284
    >>> urlopen(url).read()
285
    'This is some data'
2449 by Canonical.com Patch Queue Manager
[r=spiv] script librarian logging
286
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
287
You can also set the expiry date on the file this way too:
288
8499.2.4 by Guilherme Salgado
A couple improvements as suggested by Martin
289
    >>> from datetime import date, datetime
4195.1.12 by Brad Crittenden
Post-review changes
290
    >>> from pytz import utc
291
    >>> url = client.remoteAddFile(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
292
    ...     'text.txt', len(data), StringIO(data), 'text/plain',
293
    ...     expires=datetime(2005,9,1,12,0,0, tzinfo=utc))
4195.1.12 by Brad Crittenden
Post-review changes
294
    >>> transaction.abort()
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
295
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
296
To check the expiry is set, we need to extract the alias id from the
297
URL. remoteAddFile deliberatly returns the URL instead of the alias id
298
because, except for test cases, the URL is the only thing useful
299
(because the client can't see the database records yet).
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
300
4195.1.12 by Brad Crittenden
Post-review changes
301
    >>> import re
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
302
    >>> match = re.search('/(\d+)/', url)
4195.1.12 by Brad Crittenden
Post-review changes
303
    >>> alias_id = int(match.group(1))
304
    >>> alias = lfas[alias_id]
305
    >>> print alias.expires.isoformat()
306
    2005-09-01T12:00:00+00:00
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
307
4989.3.2 by Francis J. Lacoste
Convert moin header.
308
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
309
Restricted Librarian
310
--------------------
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
311
312
Some files should not be generally available publicly. If you know the
313
URL, any file can be retrieved directly from the librarian. For this
5911.2.19 by Francis J. Lacoste
Style based on salgado's review.
314
reason, there is a restricted librarian to which access is restricted
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
315
(at the system-level). This means that only Launchpad has direct access
316
to the host. You use the IRestrictedLibrarianClient to access this
317
librarian.
318
319
    >>> from zope.interface.verify import verifyObject
14606.2.3 by William Grant
canonical.librarian.interfaces -> lp.services.librarian.interfaces.client
320
    >>> from lp.services.librarian.interfaces.client import IRestrictedLibrarianClient
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
321
    >>> restricted_client = getUtility(IRestrictedLibrarianClient)
322
    >>> verifyObject(IRestrictedLibrarianClient, restricted_client)
323
    True
324
325
File alias uploaded through the restricted librarian have the restricted
326
attribute set.
327
328
    >>> private_content = 'This is private data.'
329
    >>> private_file_id = restricted_client.addFile(
330
    ...     'private.txt', len(private_content), StringIO(private_content),
331
    ...     'text/plain')
332
    >>> file_alias = getUtility(ILibraryFileAliasSet)[private_file_id]
333
    >>> file_alias.restricted
334
    True
335
12670.2.13 by William Grant
Fix tests.
336
    >>> transaction.commit()
337
    >>> file_alias.open()
338
    >>> print file_alias.read()
339
    This is private data.
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
340
12670.2.13 by William Grant
Fix tests.
341
    >>> file_alias.close()
342
343
Restricted files are accessible with HTTP on a private domain.
344
5911.2.10 by Francis J. Lacoste
Use proper client in LibraryFileAlias.
345
    >>> print file_alias.http_url
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
346
    http://.../private.txt
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
347
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
348
    >>> file_alias.http_url.startswith(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
349
    ...     config.librarian.restricted_download_url)
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
350
    True
5911.2.10 by Francis J. Lacoste
Use proper client in LibraryFileAlias.
351
12670.2.13 by William Grant
Fix tests.
352
They can also be accessed externally using a time-limited token appended
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
353
to their private_url. Possession of a token is sufficient to grant
354
access to a file, regardless of who is logged in. getURL can be asked to
355
provide such a token.
12670.2.13 by William Grant
Fix tests.
356
357
    >>> token_url = file_alias.getURL(include_token=True)
358
    >>> print token_url
359
    https://i...restricted.../private.txt?token=...
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
360
12670.2.13 by William Grant
Fix tests.
361
    >>> token_url.startswith('https://i%d.restricted.' % file_alias.id)
362
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
363
12670.2.13 by William Grant
Fix tests.
364
    >>> private_path = TimeLimitedToken.url_to_token_path(
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
365
    ...        file_alias.private_url)
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
366
    >>> token_url.endswith(session_store().find(
367
    ...     TimeLimitedToken, path=private_path).any().token)
12670.2.13 by William Grant
Fix tests.
368
    True
369
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
370
LibraryFileAliasView doesn't work on restricted files. This is a
371
temporary measure until we're sure no restricted files leak into the
372
traversal hierarchy.
12670.2.13 by William Grant
Fix tests.
373
374
    >>> view = getMultiAdapter((file_alias, request), name='+index')
375
    >>> view.initialize()
376
    Traceback (most recent call last):
377
    ...
378
    AssertionError
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
379
5911.2.19 by Francis J. Lacoste
Style based on salgado's review.
380
If you try to retrieve this file through the standard ILibrarianClient,
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
381
you'll get a DownloadFailed error.
382
383
    >>> client.getFileByAlias(private_file_id)
384
    Traceback (most recent call last):
385
      ...
5911.2.6 by Francis J. Lacoste
Prevent retrieving restricted file alias through wrong client.
386
    DownloadFailed: Alias ... cannot be downloaded from this client.
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
387
388
    >>> client.getURLForAlias(private_file_id)
389
    Traceback (most recent call last):
390
      ...
5911.2.6 by Francis J. Lacoste
Prevent retrieving restricted file alias through wrong client.
391
    DownloadFailed: Alias ... cannot be downloaded from this client.
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
392
393
But using the restricted librarian will work:
394
395
    >>> restricted_client.getFileByAlias(private_file_id)
14606.2.2 by William Grant
Move canonical.librarian.{client,utils} to lp.services.librarian.
396
    <lp.services.librarian.client._File...>
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
397
398
    >>> file_url = restricted_client.getURLForAlias(private_file_id)
399
    >>> print file_url
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
400
    http://.../private.txt
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
401
402
Trying to access that file directly on the normal librarian will fail
403
(by switching the port)
404
405
    >>> sneaky_url = file_url.replace(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
406
    ...     config.librarian.restricted_download_url,
407
    ...     config.librarian.download_url)
5911.2.7 by Francis J. Lacoste
Add configuration for restricted librarian.
408
    >>> urlopen(sneaky_url).read()
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
409
    Traceback (most recent call last):
410
      ...
8697.27.21 by Gary Poster
switch back to HTTPError: this is using urlopen, not testbrowser.
411
    HTTPError: HTTP Error 404: Not Found
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
412
413
But downloading it from the restricted host, will work.
414
415
    >>> print urlopen(file_url).read()
416
    This is private data.
417
418
Trying to retrieve a non-restricted file from the restricted librarian
419
also fails:
420
421
    >>> public_content = 'This is public data.'
422
    >>> public_file_id = getUtility(ILibrarianClient).addFile(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
423
    ...     'public.txt', len(public_content), StringIO(public_content),
424
    ...     'text/plain')
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
425
    >>> file_alias = getUtility(ILibraryFileAliasSet)[public_file_id]
426
    >>> file_alias.restricted
5911.2.6 by Francis J. Lacoste
Prevent retrieving restricted file alias through wrong client.
427
    False
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
428
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
429
    >>> transaction.commit()
430
431
    >>> restricted_client.getURLForAlias(public_file_id)
432
    Traceback (most recent call last):
433
      ...
434
    DownloadFailed: ...
435
436
    >>> restricted_client.getFileByAlias(public_file_id)
437
    Traceback (most recent call last):
438
      ...
439
    DownloadFailed: ...
440
5911.2.19 by Francis J. Lacoste
Style based on salgado's review.
441
The remoteAddFile() on the restricted client, also creates a restricted
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
442
file:
443
444
    >>> url = restricted_client.remoteAddFile(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
445
    ...     'another-private.txt', len(private_content),
446
    ...     StringIO(private_content), 'text/plain')
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
447
    >>> print url
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
448
    http://.../another-private.txt
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
449
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
450
    >>> url.startswith(config.librarian.restricted_download_url)
451
    True
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
452
5911.2.12 by Francis J. Lacoste
Make restrited remoteAddFile() work.
453
The file can then immediately be retrieved:
454
455
    >>> print urlopen(url).read()
456
    This is private data.
457
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
458
Another way to create a restricted file is by using the restricted
459
parameter to ILibraryFileAliasSet:
460
461
    >>> restricted_file = getUtility(ILibraryFileAliasSet).create(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
462
    ...     'yet-another-private.txt', len(private_content),
463
    ...     StringIO(private_content), 'text/plain', restricted=True)
5911.2.3 by Francis J. Lacoste
Add initial tests for the restricted librarian.
464
    >>> restricted_file.restricted
465
    True
466
5911.2.13 by Francis J. Lacoste
Make sure that searching for a SHA doesn't leak the restricted files. As a bonus, actually make getAliases() work.
467
Even if one has the SHA1 of the file, searching the librarian for it
468
will only return the file if it was in the same restriction space.
469
470
So searching for the private content on the public librarian will fail:
471
472
    >>> transaction.commit()
473
    >>> search_query = "search?digest=%s" % restricted_file.content.sha1
474
    >>> print urlopen(config.librarian.download_url + search_query).read()
475
    0
476
477
But on the restricted server, this will work:
478
479
    >>> result = urlopen(
480
    ...     config.librarian.restricted_download_url + search_query).read()
481
    >>> result = result.splitlines()
482
    >>> print result[0]
483
    3
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
484
6642.1.9 by Celso Providelo
fixing test failures.
485
    >>> sorted(file_path.split('/')[1] for file_path in result[1:])
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
486
    ['another-private.txt', 'private.txt', 'yet-another-private.txt']
487
488
489
Odds and Sods
490
-------------
491
492
An UploadFailed will be raised if you try to create a file with no
493
content
1743 by Canonical.com Patch Queue Manager
[trivial] Librarian upload shouldn't hang if passed an empty file
494
4195.1.12 by Brad Crittenden
Post-review changes
495
    >>> client.addFile('test.txt', 0, StringIO('hello'), 'text/plain')
496
    Traceback (most recent call last):
497
        [...]
498
    UploadFailed: Invalid length: 0
1817 by Canonical.com Patch Queue Manager
Add an assertion to catch mismatches between stated file size and bytes read, avoiding a hang. r=SteveA
499
12225.2.1 by Robert Collins
Allow zero length files in the librarian - the server supports them and has since a rev in the early 2000 range.
500
If you really want a zero length file you can do it:
501
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
502
    >>> aid = client.addFile(
503
    ...     'test.txt', 0, StringIO(''), 'text/plain', allow_zero_length=True)
12225.2.1 by Robert Collins
Allow zero length files in the librarian - the server supports them and has since a rev in the early 2000 range.
504
    >>> transaction.commit()
505
    >>> f = client.getFileByAlias(aid)
506
    >>> f.read()
507
    ''
508
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
509
An AssertionError will be raised if the number of bytes that could be
510
read from the file don't match the declared size.
1743 by Canonical.com Patch Queue Manager
[trivial] Librarian upload shouldn't hang if passed an empty file
511
4195.1.12 by Brad Crittenden
Post-review changes
512
    >>> client.addFile('test.txt', 42, StringIO(''), 'text/plain')
513
    Traceback (most recent call last):
514
        [...]
515
    AssertionError: size is 42, but 0 were read from the file
1743 by Canonical.com Patch Queue Manager
[trivial] Librarian upload shouldn't hang if passed an empty file
516
2326 by Canonical.com Patch Queue Manager
[r=BjornT] Fix for bug 1785: Librarian doesn't handle unicode filenames properly. Plus more tests.
517
Filenames with spaces in them work.
518
12393.27.5 by Danilo Segan
Merge with latest accordionoverlay.
519
    >>> aid = client.addFile(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
520
    ...     'hot dog', len(data), StringIO(data), 'text/plain')
4195.1.12 by Brad Crittenden
Post-review changes
521
    >>> transaction.commit()
522
    >>> f = client.getFileByAlias(aid)
523
    >>> f.read()
524
    'This is some data'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
525
4195.1.12 by Brad Crittenden
Post-review changes
526
    >>> url = client.getURLForAlias(aid)
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
527
    >>> re.search(r'/\d+/hot%20dog$', url) is not None
4195.1.12 by Brad Crittenden
Post-review changes
528
    True
2326 by Canonical.com Patch Queue Manager
[r=BjornT] Fix for bug 1785: Librarian doesn't handle unicode filenames properly. Plus more tests.
529
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
530
Unicode file names work.  Note that the filename in the resulting URL is
531
encoded as UTF-8.
2326 by Canonical.com Patch Queue Manager
[r=BjornT] Fix for bug 1785: Librarian doesn't handle unicode filenames properly. Plus more tests.
532
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
533
    >>> aid = client.addFile(
534
    ...     u'Yow\N{INTERROBANG}', len(data), StringIO(data), 'text/plain')
4195.1.12 by Brad Crittenden
Post-review changes
535
    >>> transaction.commit()
536
    >>> f = client.getFileByAlias(aid)
537
    >>> f.read()
538
    'This is some data'
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
539
4195.1.12 by Brad Crittenden
Post-review changes
540
    >>> url = client.getURLForAlias(aid)
3691.57.70 by Stuart Bishop
Fix some librarian.txt tests
541
    >>> re.search(r'/\d+/Yow%E2%80%BD$', url) is not None
4195.1.12 by Brad Crittenden
Post-review changes
542
    True
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
543
544
Files will get garbage collected on production systems as per
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
545
LibrarianGarbageCollection. If you request the URL of a deleted file,
546
you will be given None
2764 by Canonical.com Patch Queue Manager
[r=spiv] LibrarianGarbageCollection client API
547
4195.1.12 by Brad Crittenden
Post-review changes
548
    >>> alias = lfas[36]
7675.415.4 by Abel Deuring
adjust storm classes to no longer define and use the dropped columns LibraryFileContent.deleted, LibraryFileContent.datemirrored (exception: librarian_gc.py is not yet fixed); add a property 'deleted' to LibraryFileAlias; replace usage of LibraryFileAlias.content.deleted by LibraryFileAlias.deleted; fix failing tests, except test_gc.py
549
    >>> alias.deleted
4195.1.12 by Brad Crittenden
Post-review changes
550
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
551
4195.1.12 by Brad Crittenden
Post-review changes
552
    >>> alias.http_url is None
553
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
554
4195.1.12 by Brad Crittenden
Post-review changes
555
    >>> alias.https_url is None
556
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
557
4195.1.12 by Brad Crittenden
Post-review changes
558
    >>> alias.getURL() is None
559
    True
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
560
4195.1.12 by Brad Crittenden
Post-review changes
561
    >>> client.getURLForAlias(alias.id) is None
562
    True
4195.1.1 by Brad Crittenden
Implement upload and management of files associated with a product release.
563
564
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
565
Default View
566
------------
4989.3.2 by Francis J. Lacoste
Convert moin header.
567
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
568
A librarian file has a default view that should redirect to the download
569
URL.
4195.1.1 by Brad Crittenden
Implement upload and management of files associated with a product release.
570
4195.1.12 by Brad Crittenden
Post-review changes
571
    >>> from zope.component import getMultiAdapter
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
572
    >>> from lp.services.webapp.servers import LaunchpadTestRequest
4195.1.12 by Brad Crittenden
Post-review changes
573
    >>> req = LaunchpadTestRequest()
574
    >>> alias = lfas.create(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
575
    ...     'text2.txt', len(data), StringIO(data), 'text/plain',
576
    ...     NEVER_EXPIRES)
4195.1.12 by Brad Crittenden
Post-review changes
577
    >>> transaction.commit()
12670.2.13 by William Grant
Fix tests.
578
    >>> lfa_view = getMultiAdapter((alias, req), name='+index')
4195.1.12 by Brad Crittenden
Post-review changes
579
    >>> lfa_view.initialize()
12670.2.13 by William Grant
Fix tests.
580
    >>> req.response.getHeader("Location") == alias.getURL()
4195.1.12 by Brad Crittenden
Post-review changes
581
    True
7077.2.12 by Celso Providelo
testing StreamOrRedirectLibraryFileAliasView.
582
583
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
584
File views setup
585
----------------
7077.2.12 by Celso Providelo
testing StreamOrRedirectLibraryFileAliasView.
586
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
587
We need some files to test different ways of accessing them.
7077.2.12 by Celso Providelo
testing StreamOrRedirectLibraryFileAliasView.
588
589
    >>> filename = 'public.txt'
590
    >>> content = 'PUBLIC'
591
    >>> public_file = getUtility(ILibraryFileAliasSet).create(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
592
    ...     filename, len(content), StringIO(content), 'text/plain',
593
    ...     NEVER_EXPIRES, restricted=False)
7077.2.12 by Celso Providelo
testing StreamOrRedirectLibraryFileAliasView.
594
595
    >>> filename = 'restricted.txt'
596
    >>> content = 'RESTRICTED'
597
    >>> restricted_file = getUtility(ILibraryFileAliasSet).create(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
598
    ...     filename, len(content), StringIO(content), 'text/plain',
599
    ...     NEVER_EXPIRES, restricted=True)
7077.2.12 by Celso Providelo
testing StreamOrRedirectLibraryFileAliasView.
600
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
601
    # Create a new LibraryFileAlias not referencing any LibraryFileContent
602
    # record. Such records are considered as being deleted.
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
603
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
604
    >>> from lp.services.librarian.model import LibraryFileAlias
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
605
    >>> from lp.services.webapp.interfaces import (
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
606
    ...     IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
607
608
    >>> store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
609
    >>> deleted_file = LibraryFileAlias(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
610
    ...     content=None, filename='deleted.txt', mimetype='text/plain')
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
611
    >>> ignore = store.add(deleted_file)
612
7077.2.16 by Celso Providelo
applying review comments, r=bac.
613
Commit the just-created files.
7077.2.12 by Celso Providelo
testing StreamOrRedirectLibraryFileAliasView.
614
615
    >>> from canonical.database.sqlbase import commit
616
    >>> commit()
617
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
618
    >>> deleted_file = getUtility(ILibraryFileAliasSet)[deleted_file.id]
619
    >>> print deleted_file.deleted
620
    True
621
7675.809.30 by Robert Collins
Rename url to path in TimeLimitedToken.
622
Clear out existing tokens.
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
623
12393.27.5 by Danilo Segan
Merge with latest accordionoverlay.
624
    >>> _ = session_store().find(TimeLimitedToken).remove()
7675.809.9 by Robert Collins
Implement a RedirectPerhapsWithTokenLibraryFileAliasView which will be the replacement view to use once the librarian server honours tokens.
625
7675.809.24 by Robert Collins
Make the public restricted librarian facility be controlled by a feature flag in the appservers.
626
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
627
LibraryFileAliasMD5View
628
-----------------------
8520.4.2 by Curtis Hovey
Added the file name to the md5 file.
629
630
The MD5 summary for a file can be downloaded. The text file contains the
631
hash and file name.
632
633
    >>> view = create_view(public_file, '+md5')
634
    >>> print view.render()
635
    cd0c6092d6a6874f379fe4827ed1db8b public.txt
636
637
    >>> print view.request.response.getHeader('Content-type')
638
    text/plain
639
640
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
641
Download counts
642
---------------
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
643
644
The download counts for librarian files are stored in the
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
645
LibraryFileDownloadCount table, broken down by day and country, but
646
there's also a 'hits' attribute on ILibraryFileAlias, which holds the
647
total number of times that file has been downloaded.
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
648
7675.86.5 by Guilherme Salgado
Fix a typo
649
The count starts at 0, and cannot be changed directly.
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
650
651
    >>> public_file.hits
652
    0
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
653
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
654
    >>> public_file.hits = 10
655
    Traceback (most recent call last):
656
    ...
657
    ForbiddenAttribute: ...
658
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
659
To change that, we have to use the updateDownloadCount() method, which
660
takes care of creating/updating the necessary LibraryFileDownloadCount
661
entries.
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
662
8356.1.13 by Leonard Richardson
Moved in Spokenin and Country.
663
    >>> from lp.services.worlddata.interfaces.country import ICountrySet
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
664
    >>> country_set = getUtility(ICountrySet)
8499.2.4 by Guilherme Salgado
A couple improvements as suggested by Martin
665
    >>> november_1st_2006 = date(2006, 11, 1)
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
666
    >>> brazil = country_set['BR']
667
    >>> public_file.updateDownloadCount(november_1st_2006, brazil, count=1)
668
    >>> public_file.hits
669
    1
670
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
671
This was the first hit for that file from Brazil on 2006 November first,
672
so a new LibraryFileDownloadCount was created.
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
673
14578.2.1 by William Grant
Move librarian stuff from canonical.launchpad to lp.services.librarian. canonical.librarian remains untouched.
674
    >>> from lp.services.librarian.model import (
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
675
    ...        LibraryFileDownloadCount)
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
676
    >>> from storm.locals import Store
677
    >>> store = Store.of(public_file)
678
    >>> brazil_entry = store.find(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
679
    ...     LibraryFileDownloadCount, libraryfilealias=public_file,
680
    ...     country=brazil, day=november_1st_2006).one()
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
681
    >>> brazil_entry.count
682
    1
683
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
684
Below we simulate a hit from Japan on that same day, which will also
685
create a new LibraryFileDownloadCount.
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
686
7675.86.3 by Guilherme Salgado
Expose ILibraryFileAlias.hits and implement its updateDownloadCount() method.
687
    >>> japan = country_set['JP']
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
688
    >>> public_file.updateDownloadCount(november_1st_2006, japan, count=3)
689
    >>> public_file.hits
690
    4
691
692
    >>> japan_entry = store.find(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
693
    ...     LibraryFileDownloadCount, libraryfilealias=public_file,
694
    ...     country=japan, day=november_1st_2006).one()
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
695
    >>> japan_entry.count
696
    3
697
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
698
If there's another hit from Brazil on the same day, the existing entry
699
will be updated.
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
700
701
    >>> public_file.updateDownloadCount(november_1st_2006, brazil, count=2)
702
    >>> public_file.hits
703
    6
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
704
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
705
    >>> brazil_entry.count
706
    3
707
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
708
If the hit happened on a different day, a separate entry would be
709
created.
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
710
8499.2.4 by Guilherme Salgado
A couple improvements as suggested by Martin
711
    >>> november_2nd_2006 = date(2006, 11, 2)
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
712
    >>> public_file.updateDownloadCount(november_2nd_2006, brazil, count=10)
713
    >>> public_file.hits
714
    16
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
715
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
716
    >>> brazil_entry2 = store.find(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
717
    ...     LibraryFileDownloadCount, libraryfilealias=public_file,
718
    ...     country=brazil, day=november_2nd_2006).one()
7675.86.7 by Guilherme Salgado
A few more tests, as requested by Danilo
719
    >>> brazil_entry2.count
720
    10
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
721
8499.2.5 by Guilherme Salgado
A few changes suggested by Michael
722
    >>> last_downloaded_date = november_2nd_2006
8499.2.2 by Guilherme Salgado
Show download counts on the productrelease +index page
723
724
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
725
Time to last download
726
---------------------
8499.2.4 by Guilherme Salgado
A couple improvements as suggested by Martin
727
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
728
The .last_downloaded property gives us the time delta from today to the
729
day that file was last downloaded, or None if it's never been
730
downloaded.
8499.2.4 by Guilherme Salgado
A couple improvements as suggested by Martin
731
732
    >>> today = datetime.now(utc).date()
8499.2.5 by Guilherme Salgado
A few changes suggested by Michael
733
    >>> public_file.last_downloaded == today - last_downloaded_date
8499.2.4 by Guilherme Salgado
A couple improvements as suggested by Martin
734
    True
8499.2.2 by Guilherme Salgado
Show download counts on the productrelease +index page
735
736
    >>> content = 'something'
737
    >>> brand_new_file = getUtility(ILibraryFileAliasSet).create(
13980.2.5 by Jeroen Vermeulen
Fixed up bad doctest indentaiton introduced by formatdoctest. Thanks Benji for taking on this review.
738
    ...     'new.txt', len(content), StringIO(content), 'text/plain',
739
    ...     NEVER_EXPIRES, restricted=False)
8499.2.2 by Guilherme Salgado
Show download counts on the productrelease +index page
740
    >>> print brand_new_file.last_downloaded
741
    None