Merge lp:~jderose/dmedia/bittorrent into lp:dmedia

Proposed by Jason Gerard DeRose
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
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

Description of the change

* Adds FileStore.finalize_transfer() to capture an important pattern some downloaders will need (in particular the bittorrent downloader)

* Adds basic but working dmedia.downloader.TorrentDownloader that uses python-libtorrent... this is what the bug is all about

* 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://novacut.s3.amazonaws.com/37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS.mov?torrent

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.

To post a comment you must log in.
lp:~jderose/dmedia/bittorrent updated
168. By Jason Gerard DeRose

Fixed outdated comment

Revision history for this message
Jason Gerard DeRose (jderose) wrote :

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

Revision history for this message
Martin Owens (doctormo) wrote :

It looks good, needs a bit of documentation. But otherwise a good patch.

review: Approve
Revision history for this message
Paul Hummer (rockstar) wrote :
Download full text (4.8 KiB)

<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...

Read more...

review: Needs Fixing
Revision history for this message
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!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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
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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

Subscribers

People subscribed via source and target branches