Merge lp:~jderose/dmedia/bittorrent into lp:dmedia
- bittorrent
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 155 |
Proposed branch: | lp:~jderose/dmedia/bittorrent |
Merge into: | lp:dmedia |
Diff against target: |
571 lines (+463/-7) 7 files modified
debian/control (+2/-1) dmedia/downloader.py (+52/-2) dmedia/errors.py (+4/-0) dmedia/filestore.py (+72/-1) dmedia/tests/test_downloader.py (+134/-1) dmedia/tests/test_filestore.py (+79/-2) test-torrent.py (+120/-0) |
To merge this branch: | bzr merge lp:~jderose/dmedia/bittorrent |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jason Gerard DeRose | Approve | ||
Paul Hummer (community) | Needs Fixing | ||
Martin Owens (community) | Approve | ||
Review via email: mp+49429@code.launchpad.net |
Commit message
Description of the change
* Adds FileStore.
* Adds basic but working dmedia.
* Adds test-torrent.py script which will download a 44MB video file.
You run the test script like this:
$ ./test-torrent.py
It will create a FileStore in ~/.dmedia_test/, and will download this torrent:
http://
The test-torrent.py script will be removed in the near future.
Note that TorrentDownloader and Downloader (http) are deliberately crude... I'm adding several rough download/upload backends so I can get a feel for what abstract API captures them well. We want a download Manager that can dispatches download Workers generically without needing to understand specifics of download backend.
Next I'm going to add rough S3 upload/download. And then I will design the abstract API, make download Manager and download Worker all shiny.
- 168. By Jason Gerard DeRose
-
Fixed outdated comment
Jason Gerard DeRose (jderose) wrote : | # |
Martin Owens (doctormo) wrote : | # |
It looks good, needs a bit of documentation. But otherwise a good patch.
Paul Hummer (rockstar) wrote : | # |
<rockstar> jderose, any reason you're using relative imports?
<jderose> because i'm partial to them... i like that it makes it perefectly clear which modules are internal to package in question
<jderose> although i know python community seems divided on this
<rockstar> Isn't that what PEP 8's module ordering is for?
<rockstar> I think PEP 8 is kinda the standard among the community.
<jderose> yes, but unless you know all the modules/packages being used, it can be difficult to quickly spot where imports outside python stdlib start, where imports internal to package start
<rockstar> There's a blank line there, right?
<rockstar> import stdlib
<rockstar>
<rockstar> import thirdparty
<rockstar>
<rockstar> import dmedia
<jderose> ah, mabye i haven't ready pep 8 recently enough
<rockstar> I don't think it's PEP 8 specifically, but another PEP in the spirit of PEP 8.
<jderose> hmm, might be one i haven't read
<rockstar> jderose, also, my eyes twitch every time I see a function or class without a docstring. :)
<jderose> rockstar: in TorrentDownloader? that one i made note about... i'm feeling out what common/abstract API needs to be for download backends, so i didn't spend time with docstrings as API will change in next day or too
<jderose> which i know is an excuse, but things are moving oh so quickly
<jderose> and of course there are a lot of missing docstrings... sometimes i wait till i feel like the API is mature enough, cocepts are clear enough to merit detailed explanation
<rockstar> Maybe a docstring that says "XXX: Put some docstrings here."
<jderose> see, another fancy sounding excuse! ^^^ :P
<jderose> true, i should do that... good idea
<rockstar> I'm willing to bet you'll get a better feeling of API clarity when you add docstrings.
<rockstar> If you can't explain it, of course it's not clear. :)
<jderose> sometimes... although sometimes i've found that if i write detailed docstrings too quickly, there is a certain friction to changing API... you protect work in docstrings at expense of improving API
<rockstar> Hm, maybe that's a workflow difference between you and I.
<jderose> i'm all about programing as sculpture... whittle away, refactor, take a step back
<jderose> rockstar: could be you have a better attention span than i do :)
<rockstar> Although if anyone writes against the dmedia libs, they deserve whatever they get.
<rockstar> jderose, why are your test case classes starting with test_ ?
<jderose> i've made it pretty clear that things are in very heavy flux still... a long term quality api is more important that trying to make only backword compatible too early on, IMHO
<jderose> rockstar: ah, habit from using nosetests for so long
<rockstar> jderose, nose finds Test*
* jderose is really getting grilled by rockstar... honored to merit such a review :P
<rockstar> jderose, "grilled" isn't the word I'd want to use. :)
<jderose> rockstar: i know it's not PEP8 standard, but i like visual clarity of test_FooBar better than TestFooBar... helps eye more quiclky zero in on class name, IMHO
<jderose> i meant grilled playfully... appreciate the attention to detail
<jderose> it was like 30 seconds tell you said "PEP 8"... so you're a g...
Jason Gerard DeRose (jderose) wrote : | # |
As discussed with rockstar, I'm going to fix the import best practices in a separate merge, so I'm approving this.
doctormo and rockstar, thanks for the reviews!
Preview Diff
1 | === modified file 'debian/control' |
2 | --- debian/control 2011-02-01 23:53:30 +0000 |
3 | +++ debian/control 2011-02-11 17:26:30 +0000 |
4 | @@ -19,7 +19,8 @@ |
5 | python-paste, |
6 | python-notify, |
7 | python-appindicator, |
8 | - python-xdg |
9 | + python-xdg, |
10 | + python-libtorrent |
11 | Provides: python-dmedia |
12 | Replaces: python-dmedia (<< ${source:Version}) |
13 | Conflicts: python-dmedia (<< ${source:Version}) |
14 | |
15 | === modified file 'dmedia/downloader.py' |
16 | --- dmedia/downloader.py 2011-01-21 12:42:47 +0000 |
17 | +++ dmedia/downloader.py 2011-02-11 17:26:30 +0000 |
18 | @@ -23,14 +23,17 @@ |
19 | Download files in chunks using HTTP Range requests. |
20 | """ |
21 | |
22 | +from os import path |
23 | from base64 import b32encode |
24 | from urlparse import urlparse |
25 | from httplib import HTTPConnection, HTTPSConnection |
26 | import logging |
27 | +import time |
28 | from . import __version__ |
29 | -from .constants import CHUNK_SIZE |
30 | +from .constants import CHUNK_SIZE, TYPE_ERROR |
31 | from .errors import DownloadFailure |
32 | -from .filestore import HashList, HASH |
33 | +from .filestore import FileStore, HashList, HASH |
34 | +import libtorrent |
35 | |
36 | USER_AGENT = 'dmedia %s' % __version__ |
37 | log = logging.getLogger() |
38 | @@ -155,3 +158,50 @@ |
39 | def run(self): |
40 | for (i, chash) in enumerate(self.leaves): |
41 | self.process_leaf(i, chash) |
42 | + |
43 | + |
44 | +class TorrentDownloader(object): |
45 | + def __init__(self, torrent, fs, chash, ext=None): |
46 | + if not isinstance(fs, FileStore): |
47 | + raise TypeError( |
48 | + TYPE_ERROR % ('fs', FileStore, type(fs), fs) |
49 | + ) |
50 | + self.torrent = torrent |
51 | + self.fs = fs |
52 | + self.chash = chash |
53 | + self.ext = ext |
54 | + |
55 | + def get_tmp(self): |
56 | + tmp = self.fs.temp(self.chash, self.ext, create=True) |
57 | + log.debug('Writting file to %r', tmp) |
58 | + return tmp |
59 | + |
60 | + def finalize(self): |
61 | + dst = self.fs.finalize_transfer(self.chash, self.ext) |
62 | + log.debug('Canonical name is %r', dst) |
63 | + return dst |
64 | + |
65 | + def run(self): |
66 | + log.info('Downloading torrent %r %r', self.chash, self.ext) |
67 | + tmp = self.get_tmp() |
68 | + session = libtorrent.session() |
69 | + session.listen_on(6881, 6891) |
70 | + |
71 | + info = libtorrent.torrent_info( |
72 | + libtorrent.bdecode(self.torrent) |
73 | + ) |
74 | + |
75 | + torrent = session.add_torrent({ |
76 | + 'ti': info, |
77 | + 'save_path': path.dirname(tmp), |
78 | + }) |
79 | + |
80 | + while not torrent.is_seed(): |
81 | + s = torrent.status() |
82 | + log.debug('Downloaded %d%%', s.progress * 100) |
83 | + time.sleep(2) |
84 | + |
85 | + session.remove_torrent(torrent) |
86 | + time.sleep(1) |
87 | + |
88 | + return self.finalize() |
89 | |
90 | === modified file 'dmedia/errors.py' |
91 | --- dmedia/errors.py 2011-01-27 16:54:48 +0000 |
92 | +++ dmedia/errors.py 2011-02-11 17:26:30 +0000 |
93 | @@ -64,3 +64,7 @@ |
94 | |
95 | class DownloadFailure(DmediaError): |
96 | _format = 'leaf %(leaf)d expected %(expected)r; got %(got)r' |
97 | + |
98 | + |
99 | +class IntegrityError(DmediaError): |
100 | + _format = 'got chash %(got)r; expected %(expected)r for %(filename)r' |
101 | |
102 | === modified file 'dmedia/filestore.py' |
103 | --- dmedia/filestore.py 2011-02-07 05:46:11 +0000 |
104 | +++ dmedia/filestore.py 2011-02-11 17:26:30 +0000 |
105 | @@ -49,7 +49,8 @@ |
106 | from threading import Thread |
107 | from Queue import Queue |
108 | from .schema import create_store |
109 | -from .errors import AmbiguousPath, DuplicateFile, FileStoreTraversal |
110 | +from .errors import AmbiguousPath, FileStoreTraversal |
111 | +from .errors import DuplicateFile, IntegrityError |
112 | from .constants import LEAF_SIZE, TRANSFERS_DIR, IMPORTS_DIR, TYPE_ERROR, EXT_PAT |
113 | |
114 | B32LENGTH = 32 # Length of base32-encoded hash |
115 | @@ -619,6 +620,76 @@ |
116 | fallocate(size, filename) |
117 | return open(filename, 'r+b') |
118 | |
119 | + def finalize_transfer(self, chash, ext=None): |
120 | + """ |
121 | + Move canonically named temporary file to its final canonical location. |
122 | + |
123 | + This method will check the content hash of the canonically-named |
124 | + temporary file with content hash *chash* and extension *ext*. If the |
125 | + content hash is correct, it will do an ``os.fchmod()`` to set read-only |
126 | + permissions, and then rename the file into its canonical location. |
127 | + |
128 | + If the content hash is incorrect, `IntegrityError` is raised. If the |
129 | + canonical file already exists, `DuplicateFile` is raised. Lastly, if |
130 | + the temporary does not exist, ``IOError`` is raised. |
131 | + |
132 | + This method will typically be used with the BitTorrent downloader or |
133 | + similar, in which case the content hash will be known prior to |
134 | + downloading. The downloader will first determine the canonical |
135 | + temporary file name, like this: |
136 | + |
137 | + >>> fs = FileStore() |
138 | + >>> tmp = fs.temp('ZR765XWSF6S7JQHLUI4GCG5BHGPE252O', 'mov', create=True) |
139 | + >>> tmp #doctest: +ELLIPSIS |
140 | + '/tmp/store.../transfers/ZR765XWSF6S7JQHLUI4GCG5BHGPE252O.mov' |
141 | + |
142 | + |
143 | + Then the downloader will write to the temporary file as it's being |
144 | + downloaded: |
145 | + |
146 | + >>> from dmedia.tests import sample_mov # Sample .MOV file |
147 | + >>> src_fp = open(sample_mov, 'rb') |
148 | + >>> tmp_fp = open(tmp, 'wb') |
149 | + >>> while True: |
150 | + ... chunk = src_fp.read(2**20) # Read in 1MiB chunks |
151 | + ... if not chunk: |
152 | + ... break |
153 | + ... tmp_fp.write(chunk) |
154 | + ... |
155 | + >>> tmp_fp.close() |
156 | + |
157 | + |
158 | + Finally, the downloader will move the temporary file into its canonical |
159 | + location: |
160 | + |
161 | + >>> dst = fs.finalize_transfer('ZR765XWSF6S7JQHLUI4GCG5BHGPE252O', 'mov') |
162 | + >>> dst #doctest: +ELLIPSIS |
163 | + '/tmp/store.../ZR/765XWSF6S7JQHLUI4GCG5BHGPE252O.mov' |
164 | + |
165 | + |
166 | + Note above that this method returns the full path of the canonically |
167 | + named file. |
168 | + """ |
169 | + # Open temporary file and check content hash: |
170 | + tmp = self.temp(chash, ext) |
171 | + tmp_fp = open(tmp, 'rb') |
172 | + h = HashList(tmp_fp) |
173 | + got = h.run() |
174 | + if got != chash: |
175 | + raise IntegrityError(got=got, expected=chash, filename=tmp_fp.name) |
176 | + |
177 | + # Get canonical name, check for duplicate: |
178 | + dst = self.path(chash, ext, create=True) |
179 | + if path.exists(dst): |
180 | + raise DuplicateFile(chash=chash, src=tmp_fp.name, dst=dst) |
181 | + |
182 | + # Set file to read-only and rename into canonical location |
183 | + os.fchmod(tmp_fp.fileno(), 0o444) |
184 | + os.rename(tmp_fp.name, dst) |
185 | + |
186 | + # Return canonical filename: |
187 | + return dst |
188 | + |
189 | def import_file(self, src_fp, ext=None): |
190 | """ |
191 | Atomically copy open file *src_fp* into this file store. |
192 | |
193 | === modified file 'dmedia/tests/test_downloader.py' |
194 | --- dmedia/tests/test_downloader.py 2011-01-21 12:11:33 +0000 |
195 | +++ dmedia/tests/test_downloader.py 2011-02-11 17:26:30 +0000 |
196 | @@ -23,11 +23,17 @@ |
197 | Unit tests for `dmedia.downloader` module. |
198 | """ |
199 | |
200 | +import os |
201 | +from os import path |
202 | from unittest import TestCase |
203 | from hashlib import sha1 |
204 | from base64 import b32encode |
205 | from .helpers import raises, TempDir |
206 | -from dmedia.errors import DownloadFailure |
207 | +from .helpers import sample_mov, sample_thm |
208 | +from .helpers import mov_hash, thm_hash |
209 | +from dmedia.constants import TYPE_ERROR, LEAF_SIZE |
210 | +from dmedia.errors import DownloadFailure, DuplicateFile, IntegrityError |
211 | +from dmedia.filestore import FileStore, HashList |
212 | from dmedia import downloader |
213 | |
214 | |
215 | @@ -137,3 +143,130 @@ |
216 | inst = Example(a, b, b) |
217 | self.assertEqual(inst.process_leaf(7, a_hash), a) |
218 | self.assertEqual(inst.dst_fp._chunk, a) |
219 | + |
220 | + |
221 | +class test_TorrentDownloader(TestCase): |
222 | + klass = downloader.TorrentDownloader |
223 | + |
224 | + def test_init(self): |
225 | + tmp = TempDir() |
226 | + fs = FileStore(tmp.path) |
227 | + |
228 | + e = raises(TypeError, self.klass, '', 17, mov_hash) |
229 | + self.assertEqual( |
230 | + str(e), |
231 | + TYPE_ERROR % ('fs', FileStore, int, 17) |
232 | + ) |
233 | + |
234 | + inst = self.klass('', fs, mov_hash) |
235 | + self.assertEqual(inst.torrent, '') |
236 | + self.assertTrue(inst.fs is fs) |
237 | + self.assertEqual(inst.chash, mov_hash) |
238 | + self.assertEqual(inst.ext, None) |
239 | + |
240 | + inst = self.klass('', fs, mov_hash, ext='mov') |
241 | + self.assertEqual(inst.torrent, '') |
242 | + self.assertTrue(inst.fs is fs) |
243 | + self.assertEqual(inst.chash, mov_hash) |
244 | + self.assertEqual(inst.ext, 'mov') |
245 | + |
246 | + def test_get_tmp(self): |
247 | + # Test with ext='mov' |
248 | + tmp = TempDir() |
249 | + fs = FileStore(tmp.path) |
250 | + inst = self.klass('', fs, mov_hash, 'mov') |
251 | + d = tmp.join('transfers') |
252 | + f = tmp.join('transfers', mov_hash + '.mov') |
253 | + self.assertFalse(path.exists(d)) |
254 | + self.assertFalse(path.exists(f)) |
255 | + self.assertEqual(inst.get_tmp(), f) |
256 | + self.assertTrue(path.isdir(d)) |
257 | + self.assertFalse(path.exists(f)) |
258 | + |
259 | + # Test with ext=None |
260 | + tmp = TempDir() |
261 | + fs = FileStore(tmp.path) |
262 | + inst = self.klass('', fs, mov_hash) |
263 | + d = tmp.join('transfers') |
264 | + f = tmp.join('transfers', mov_hash) |
265 | + self.assertFalse(path.exists(d)) |
266 | + self.assertFalse(path.exists(f)) |
267 | + self.assertEqual(inst.get_tmp(), f) |
268 | + self.assertTrue(path.isdir(d)) |
269 | + self.assertFalse(path.exists(f)) |
270 | + |
271 | + def test_finalize(self): |
272 | + tmp = TempDir() |
273 | + fs = FileStore(tmp.path) |
274 | + inst = self.klass('', fs, mov_hash, 'mov') |
275 | + |
276 | + src_d = tmp.join('transfers') |
277 | + src = tmp.join('transfers', mov_hash + '.mov') |
278 | + dst_d = tmp.join(mov_hash[:2]) |
279 | + dst = tmp.join(mov_hash[:2], mov_hash[2:] + '.mov') |
280 | + |
281 | + # Test when transfers/ dir doesn't exist: |
282 | + e = raises(IOError, inst.finalize) |
283 | + self.assertFalse(path.exists(src_d)) |
284 | + self.assertFalse(path.exists(dst_d)) |
285 | + self.assertFalse(path.exists(dst)) |
286 | + |
287 | + # Test when transfers/ exists but file does not: |
288 | + self.assertEqual(fs.temp(mov_hash, 'mov', create=True), src) |
289 | + self.assertTrue(path.isdir(src_d)) |
290 | + e = raises(IOError, inst.finalize) |
291 | + self.assertFalse(path.exists(src)) |
292 | + self.assertFalse(path.exists(dst_d)) |
293 | + self.assertFalse(path.exists(dst)) |
294 | + |
295 | + # Test when file has wrong content hash and wrong size: |
296 | + open(src, 'wb').write(open(sample_thm, 'rb').read()) |
297 | + e = raises(IntegrityError, inst.finalize) |
298 | + self.assertEqual(e.got, thm_hash) |
299 | + self.assertEqual(e.expected, mov_hash) |
300 | + self.assertEqual(e.filename, src) |
301 | + self.assertFalse(path.exists(dst_d)) |
302 | + self.assertFalse(path.exists(dst)) |
303 | + |
304 | + # Test when file has wrong content hash and *correct* size: |
305 | + fp1 = open(sample_mov, 'rb') |
306 | + fp2 = open(src, 'wb') |
307 | + while True: |
308 | + chunk = fp1.read(LEAF_SIZE) |
309 | + if not chunk: |
310 | + break |
311 | + fp2.write(chunk) |
312 | + fp1.close() |
313 | + |
314 | + # Now change final byte at end file: |
315 | + fp2.seek(-1, os.SEEK_END) |
316 | + fp2.write('A') |
317 | + fp2.close() |
318 | + self.assertEqual(path.getsize(sample_mov), path.getsize(src)) |
319 | + |
320 | + e = raises(IntegrityError, inst.finalize) |
321 | + self.assertEqual(e.got, 'UECTT7A7EIHZ2SGGBMMO5WTTSVU4SUWM') |
322 | + self.assertEqual(e.expected, mov_hash) |
323 | + self.assertEqual(e.filename, src) |
324 | + self.assertFalse(path.exists(dst_d)) |
325 | + self.assertFalse(path.exists(dst)) |
326 | + |
327 | + # Test with correct content hash: |
328 | + fp1 = open(sample_mov, 'rb') |
329 | + fp2 = open(src, 'wb') |
330 | + while True: |
331 | + chunk = fp1.read(LEAF_SIZE) |
332 | + if not chunk: |
333 | + break |
334 | + fp2.write(chunk) |
335 | + fp1.close() |
336 | + fp2.close() |
337 | + self.assertEqual(inst.finalize(), dst) |
338 | + self.assertTrue(path.isdir(src_d)) |
339 | + self.assertFalse(path.exists(src)) |
340 | + self.assertTrue(path.isdir(dst_d)) |
341 | + self.assertTrue(path.isfile(dst)) |
342 | + |
343 | + # Check content hash of file in canonical location |
344 | + fp = open(dst, 'rb') |
345 | + self.assertEqual(HashList(fp).run(), mov_hash) |
346 | |
347 | === modified file 'dmedia/tests/test_filestore.py' |
348 | --- dmedia/tests/test_filestore.py 2011-02-07 05:46:11 +0000 |
349 | +++ dmedia/tests/test_filestore.py 2011-02-11 17:26:30 +0000 |
350 | @@ -34,10 +34,12 @@ |
351 | from .helpers import TempDir, TempHome, raises |
352 | from .helpers import sample_mov, sample_thm |
353 | from .helpers import mov_hash, mov_leaves, mov_qid |
354 | -from dmedia.errors import AmbiguousPath, FileStoreTraversal, DuplicateFile |
355 | +from .helpers import thm_hash, thm_leaves, thm_qid |
356 | +from dmedia.errors import AmbiguousPath, FileStoreTraversal |
357 | +from dmedia.errors import DuplicateFile, IntegrityError |
358 | from dmedia.filestore import HashList |
359 | from dmedia import filestore, constants, schema |
360 | -from dmedia.constants import TYPE_ERROR, EXT_PAT |
361 | +from dmedia.constants import TYPE_ERROR, EXT_PAT, LEAF_SIZE |
362 | |
363 | |
364 | class test_functions(TestCase): |
365 | @@ -892,6 +894,81 @@ |
366 | self.assertEqual(path.dirname(fp.name), imports) |
367 | self.assertTrue(fp.name.endswith('.mov')) |
368 | |
369 | + def test_finalize_transfer(self): |
370 | + tmp = TempDir() |
371 | + inst = self.klass(tmp.path) |
372 | + |
373 | + src_d = tmp.join('transfers') |
374 | + src = tmp.join('transfers', mov_hash + '.mov') |
375 | + dst_d = tmp.join(mov_hash[:2]) |
376 | + dst = tmp.join(mov_hash[:2], mov_hash[2:] + '.mov') |
377 | + |
378 | + # Test when transfers/ dir doesn't exist: |
379 | + e = raises(IOError, inst.finalize_transfer, mov_hash, 'mov') |
380 | + self.assertFalse(path.exists(src_d)) |
381 | + self.assertFalse(path.exists(dst_d)) |
382 | + self.assertFalse(path.exists(dst)) |
383 | + |
384 | + # Test when transfers/ exists but file does not: |
385 | + self.assertEqual(inst.temp(mov_hash, 'mov', create=True), src) |
386 | + self.assertTrue(path.isdir(src_d)) |
387 | + e = raises(IOError, inst.finalize_transfer, mov_hash, 'mov') |
388 | + self.assertFalse(path.exists(src)) |
389 | + self.assertFalse(path.exists(dst_d)) |
390 | + self.assertFalse(path.exists(dst)) |
391 | + |
392 | + # Test when file has wrong content hash and wrong size: |
393 | + open(src, 'wb').write(open(sample_thm, 'rb').read()) |
394 | + e = raises(IntegrityError, inst.finalize_transfer, mov_hash, 'mov') |
395 | + self.assertEqual(e.got, thm_hash) |
396 | + self.assertEqual(e.expected, mov_hash) |
397 | + self.assertEqual(e.filename, src) |
398 | + self.assertFalse(path.exists(dst_d)) |
399 | + self.assertFalse(path.exists(dst)) |
400 | + |
401 | + # Test when file has wrong content hash and *correct* size: |
402 | + fp1 = open(sample_mov, 'rb') |
403 | + fp2 = open(src, 'wb') |
404 | + while True: |
405 | + chunk = fp1.read(LEAF_SIZE) |
406 | + if not chunk: |
407 | + break |
408 | + fp2.write(chunk) |
409 | + fp1.close() |
410 | + |
411 | + # Now change final byte at end file: |
412 | + fp2.seek(-1, os.SEEK_END) |
413 | + fp2.write('A') |
414 | + fp2.close() |
415 | + self.assertEqual(path.getsize(sample_mov), path.getsize(src)) |
416 | + |
417 | + e = raises(IntegrityError, inst.finalize_transfer, mov_hash, 'mov') |
418 | + self.assertEqual(e.got, 'UECTT7A7EIHZ2SGGBMMO5WTTSVU4SUWM') |
419 | + self.assertEqual(e.expected, mov_hash) |
420 | + self.assertEqual(e.filename, src) |
421 | + self.assertFalse(path.exists(dst_d)) |
422 | + self.assertFalse(path.exists(dst)) |
423 | + |
424 | + # Test with correct content hash: |
425 | + fp1 = open(sample_mov, 'rb') |
426 | + fp2 = open(src, 'wb') |
427 | + while True: |
428 | + chunk = fp1.read(LEAF_SIZE) |
429 | + if not chunk: |
430 | + break |
431 | + fp2.write(chunk) |
432 | + fp1.close() |
433 | + fp2.close() |
434 | + self.assertEqual(inst.finalize_transfer(mov_hash, 'mov'), dst) |
435 | + self.assertTrue(path.isdir(src_d)) |
436 | + self.assertFalse(path.exists(src)) |
437 | + self.assertTrue(path.isdir(dst_d)) |
438 | + self.assertTrue(path.isfile(dst)) |
439 | + |
440 | + # Check content hash of file in canonical location |
441 | + fp = open(dst, 'rb') |
442 | + self.assertEqual(HashList(fp).run(), mov_hash) |
443 | + |
444 | def test_import_file(self): |
445 | tmp = TempDir() |
446 | src = tmp.join('movie.mov') |
447 | |
448 | === added file 'test-torrent.py' |
449 | --- test-torrent.py 1970-01-01 00:00:00 +0000 |
450 | +++ test-torrent.py 2011-02-11 17:26:30 +0000 |
451 | @@ -0,0 +1,120 @@ |
452 | +#!/usr/bin/env python |
453 | + |
454 | +""" |
455 | +Tests downloading torrent from: |
456 | + |
457 | +http://novacut.s3.amazonaws.com/37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS.mov?torrent |
458 | +""" |
459 | + |
460 | +from __future__ import print_function |
461 | + |
462 | +import sys |
463 | +import os |
464 | +from os import path |
465 | +import time |
466 | +import logging |
467 | +from base64 import b32decode, b64decode |
468 | +from dmedia.downloader import TorrentDownloader |
469 | +from dmedia.filestore import FileStore |
470 | + |
471 | +logging.basicConfig( |
472 | + level=logging.DEBUG, |
473 | + format='\t'.join(['%(levelname)s', '%(message)s']) |
474 | +) |
475 | + |
476 | +# Known size, top hash, and leaf hashes for test video: |
477 | +size = 44188757 |
478 | + |
479 | +chash = '37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS' |
480 | + |
481 | +leaves_b32 = [ |
482 | + 'TDCPJHYQVEVCTMLIKEQITVMJKSUIETHD', |
483 | + 'UAG5HQCLH6PGA4RAYXDEFRNCSZDTMLXU', |
484 | + '2DGBOSUSSDG5OKASXNQTJG3MANGL4H2U', |
485 | + 'D4TMBNAQOOFMIWB2ATT2GR7Y262EATVM', |
486 | + 'SEYVEQPAVCROXPYZWIXN4YZRHOZV2MWV', |
487 | + 'B2I7VCLIVV4LBSRTGSRQNNXDDPWCKNLA', |
488 | +] |
489 | + |
490 | +leaves = [b32decode(l) for l in leaves_b32] |
491 | + |
492 | + |
493 | +# The .torrent file, base64 encoded: |
494 | +tdata = """ |
495 | +ZDg6YW5ub3VuY2U0MjpodHRwOi8vdHJhY2tlci5hbWF6b25hd3MuY29tOjY5NjkvYW5ub3VuY2Ux |
496 | +Mzphbm5vdW5jZS1saXN0bGw0MjpodHRwOi8vdHJhY2tlci5hbWF6b25hd3MuY29tOjY5NjkvYW5u |
497 | +b3VuY2VlZTQ6aW5mb2Q2Omxlbmd0aGk0NDE4ODc1N2U0Om5hbWUzNjozN0dETkhBTlg3UkNCTUJH |
498 | +VFlMU0lLN1RNVFVRU0tEUy5tb3YxMjpwaWVjZSBsZW5ndGhpMjYyMTQ0ZTY6cGllY2VzMzM4MDqr |
499 | +KimqIY/Y6lI6kGX+koXSVvVYgXilfN8qnUAk31PJttEN8RXgagd3dLD41PXZjf5KXt84gp1i8Cgh |
500 | +pfPOKRi3wBOGwNoq2JCjiHiW4wEobgaePx3alOno8Xl/phRuRoUoK/FgPJ6fm5JRulnrh5lonsmJ |
501 | +iShyE+B9N5Kax2lFN5t6B1W+6y51ROzBDep9E8MaBobFyINvHkRsDY0mNgpTpwdtqAiZTOzsj9L4 |
502 | +YE966dNMTBVXKWC8i5pSEuRN72Bf7wXF9DoOvBaNk1vMpzKzd+GGDRpKYoZcROuyUbEzYMuI+HgE |
503 | +5CvZGZZH01yVfelfotaMK1ogSShZAserPHEGLEfNwBMn51wSrLGrYiGN770n4O3tLSpmaZ1ZGaE5 |
504 | +Ot2nZ+ZmSJmcZWF5Uu6e07QONhziX3SYn58rvqON7fJ/mz2GiO+9ARMfv/pc6ElbaVIAEHJy2Yej |
505 | +5uYIRtaaeraRG/KFgzvuxidwtePSyZzoM+1j/BaTDig+76N1hrei6R8/oUkU+rtn2MjYwaJtH+y5 |
506 | +/iLp418vHyjqTUsFNGGBh24nJT7LcXhakfkkjmDmCKiZ6SDVbKW47EfpR8lCnwnHC4KXz7l/yddv |
507 | +m2Oc6fuUAX11DEffcSclEtPx0/VEtih1uYgoovq6ef4Eg09PwRmEQ1mfy99yCk62WSEefeYWDHq0 |
508 | +WO7ih+3mPTaimIMp6lClIf3DRszxe2bta3C/D+J4E+NtAsSSzkKn1MK5w+SFxj3ooQmrl1jaYv+I |
509 | +QzBeVWb5KRDpzwuIr7LwaeV4/ClqGi1p4c+juohEFoLsrttB0xN9dRZcOtGVwMXqDrz4ukAtla7l |
510 | +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 |
560 | +""" |
561 | + |
562 | + |
563 | +# Create a FileStore in ~/.dmedia_test/ |
564 | +home = path.abspath(os.environ['HOME']) |
565 | +base = path.join(home, '.dmedia_test') |
566 | +fs = FileStore(base) |
567 | + |
568 | +t = TorrentDownloader(b64decode(tdata), fs, chash, 'mov') |
569 | +t.run() |
570 | + |
571 | +assert path.getsize(fs.path(chash, 'mov')) == size |
Ah, note that you'll need to install python-libtorrent to test this stuff. This should install everything you need:
sudo apt-get install python- desktopcouch- records totem libimage- exiftool- perl python-webkit gir1.2-webkit-1.0 python-paste python-notify python-appindicator python-xdg python-libtorrent