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 | 19 | python-paste, | 19 | python-paste, |
6 | 20 | python-notify, | 20 | python-notify, |
7 | 21 | python-appindicator, | 21 | python-appindicator, |
9 | 22 | python-xdg | 22 | python-xdg, |
10 | 23 | python-libtorrent | ||
11 | 23 | Provides: python-dmedia | 24 | Provides: python-dmedia |
12 | 24 | Replaces: python-dmedia (<< ${source:Version}) | 25 | Replaces: python-dmedia (<< ${source:Version}) |
13 | 25 | Conflicts: python-dmedia (<< ${source:Version}) | 26 | Conflicts: python-dmedia (<< ${source:Version}) |
14 | 26 | 27 | ||
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 | 23 | Download files in chunks using HTTP Range requests. | 23 | Download files in chunks using HTTP Range requests. |
20 | 24 | """ | 24 | """ |
21 | 25 | 25 | ||
22 | 26 | from os import path | ||
23 | 26 | from base64 import b32encode | 27 | from base64 import b32encode |
24 | 27 | from urlparse import urlparse | 28 | from urlparse import urlparse |
25 | 28 | from httplib import HTTPConnection, HTTPSConnection | 29 | from httplib import HTTPConnection, HTTPSConnection |
26 | 29 | import logging | 30 | import logging |
27 | 31 | import time | ||
28 | 30 | from . import __version__ | 32 | from . import __version__ |
30 | 31 | from .constants import CHUNK_SIZE | 33 | from .constants import CHUNK_SIZE, TYPE_ERROR |
31 | 32 | from .errors import DownloadFailure | 34 | from .errors import DownloadFailure |
33 | 33 | from .filestore import HashList, HASH | 35 | from .filestore import FileStore, HashList, HASH |
34 | 36 | import libtorrent | ||
35 | 34 | 37 | ||
36 | 35 | USER_AGENT = 'dmedia %s' % __version__ | 38 | USER_AGENT = 'dmedia %s' % __version__ |
37 | 36 | log = logging.getLogger() | 39 | log = logging.getLogger() |
38 | @@ -155,3 +158,50 @@ | |||
39 | 155 | def run(self): | 158 | def run(self): |
40 | 156 | for (i, chash) in enumerate(self.leaves): | 159 | for (i, chash) in enumerate(self.leaves): |
41 | 157 | self.process_leaf(i, chash) | 160 | self.process_leaf(i, chash) |
42 | 161 | |||
43 | 162 | |||
44 | 163 | class TorrentDownloader(object): | ||
45 | 164 | def __init__(self, torrent, fs, chash, ext=None): | ||
46 | 165 | if not isinstance(fs, FileStore): | ||
47 | 166 | raise TypeError( | ||
48 | 167 | TYPE_ERROR % ('fs', FileStore, type(fs), fs) | ||
49 | 168 | ) | ||
50 | 169 | self.torrent = torrent | ||
51 | 170 | self.fs = fs | ||
52 | 171 | self.chash = chash | ||
53 | 172 | self.ext = ext | ||
54 | 173 | |||
55 | 174 | def get_tmp(self): | ||
56 | 175 | tmp = self.fs.temp(self.chash, self.ext, create=True) | ||
57 | 176 | log.debug('Writting file to %r', tmp) | ||
58 | 177 | return tmp | ||
59 | 178 | |||
60 | 179 | def finalize(self): | ||
61 | 180 | dst = self.fs.finalize_transfer(self.chash, self.ext) | ||
62 | 181 | log.debug('Canonical name is %r', dst) | ||
63 | 182 | return dst | ||
64 | 183 | |||
65 | 184 | def run(self): | ||
66 | 185 | log.info('Downloading torrent %r %r', self.chash, self.ext) | ||
67 | 186 | tmp = self.get_tmp() | ||
68 | 187 | session = libtorrent.session() | ||
69 | 188 | session.listen_on(6881, 6891) | ||
70 | 189 | |||
71 | 190 | info = libtorrent.torrent_info( | ||
72 | 191 | libtorrent.bdecode(self.torrent) | ||
73 | 192 | ) | ||
74 | 193 | |||
75 | 194 | torrent = session.add_torrent({ | ||
76 | 195 | 'ti': info, | ||
77 | 196 | 'save_path': path.dirname(tmp), | ||
78 | 197 | }) | ||
79 | 198 | |||
80 | 199 | while not torrent.is_seed(): | ||
81 | 200 | s = torrent.status() | ||
82 | 201 | log.debug('Downloaded %d%%', s.progress * 100) | ||
83 | 202 | time.sleep(2) | ||
84 | 203 | |||
85 | 204 | session.remove_torrent(torrent) | ||
86 | 205 | time.sleep(1) | ||
87 | 206 | |||
88 | 207 | return self.finalize() | ||
89 | 158 | 208 | ||
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 | 64 | 64 | ||
95 | 65 | class DownloadFailure(DmediaError): | 65 | class DownloadFailure(DmediaError): |
96 | 66 | _format = 'leaf %(leaf)d expected %(expected)r; got %(got)r' | 66 | _format = 'leaf %(leaf)d expected %(expected)r; got %(got)r' |
97 | 67 | |||
98 | 68 | |||
99 | 69 | class IntegrityError(DmediaError): | ||
100 | 70 | _format = 'got chash %(got)r; expected %(expected)r for %(filename)r' | ||
101 | 67 | 71 | ||
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 | 49 | from threading import Thread | 49 | from threading import Thread |
107 | 50 | from Queue import Queue | 50 | from Queue import Queue |
108 | 51 | from .schema import create_store | 51 | from .schema import create_store |
110 | 52 | from .errors import AmbiguousPath, DuplicateFile, FileStoreTraversal | 52 | from .errors import AmbiguousPath, FileStoreTraversal |
111 | 53 | from .errors import DuplicateFile, IntegrityError | ||
112 | 53 | from .constants import LEAF_SIZE, TRANSFERS_DIR, IMPORTS_DIR, TYPE_ERROR, EXT_PAT | 54 | from .constants import LEAF_SIZE, TRANSFERS_DIR, IMPORTS_DIR, TYPE_ERROR, EXT_PAT |
113 | 54 | 55 | ||
114 | 55 | B32LENGTH = 32 # Length of base32-encoded hash | 56 | B32LENGTH = 32 # Length of base32-encoded hash |
115 | @@ -619,6 +620,76 @@ | |||
116 | 619 | fallocate(size, filename) | 620 | fallocate(size, filename) |
117 | 620 | return open(filename, 'r+b') | 621 | return open(filename, 'r+b') |
118 | 621 | 622 | ||
119 | 623 | def finalize_transfer(self, chash, ext=None): | ||
120 | 624 | """ | ||
121 | 625 | Move canonically named temporary file to its final canonical location. | ||
122 | 626 | |||
123 | 627 | This method will check the content hash of the canonically-named | ||
124 | 628 | temporary file with content hash *chash* and extension *ext*. If the | ||
125 | 629 | content hash is correct, it will do an ``os.fchmod()`` to set read-only | ||
126 | 630 | permissions, and then rename the file into its canonical location. | ||
127 | 631 | |||
128 | 632 | If the content hash is incorrect, `IntegrityError` is raised. If the | ||
129 | 633 | canonical file already exists, `DuplicateFile` is raised. Lastly, if | ||
130 | 634 | the temporary does not exist, ``IOError`` is raised. | ||
131 | 635 | |||
132 | 636 | This method will typically be used with the BitTorrent downloader or | ||
133 | 637 | similar, in which case the content hash will be known prior to | ||
134 | 638 | downloading. The downloader will first determine the canonical | ||
135 | 639 | temporary file name, like this: | ||
136 | 640 | |||
137 | 641 | >>> fs = FileStore() | ||
138 | 642 | >>> tmp = fs.temp('ZR765XWSF6S7JQHLUI4GCG5BHGPE252O', 'mov', create=True) | ||
139 | 643 | >>> tmp #doctest: +ELLIPSIS | ||
140 | 644 | '/tmp/store.../transfers/ZR765XWSF6S7JQHLUI4GCG5BHGPE252O.mov' | ||
141 | 645 | |||
142 | 646 | |||
143 | 647 | Then the downloader will write to the temporary file as it's being | ||
144 | 648 | downloaded: | ||
145 | 649 | |||
146 | 650 | >>> from dmedia.tests import sample_mov # Sample .MOV file | ||
147 | 651 | >>> src_fp = open(sample_mov, 'rb') | ||
148 | 652 | >>> tmp_fp = open(tmp, 'wb') | ||
149 | 653 | >>> while True: | ||
150 | 654 | ... chunk = src_fp.read(2**20) # Read in 1MiB chunks | ||
151 | 655 | ... if not chunk: | ||
152 | 656 | ... break | ||
153 | 657 | ... tmp_fp.write(chunk) | ||
154 | 658 | ... | ||
155 | 659 | >>> tmp_fp.close() | ||
156 | 660 | |||
157 | 661 | |||
158 | 662 | Finally, the downloader will move the temporary file into its canonical | ||
159 | 663 | location: | ||
160 | 664 | |||
161 | 665 | >>> dst = fs.finalize_transfer('ZR765XWSF6S7JQHLUI4GCG5BHGPE252O', 'mov') | ||
162 | 666 | >>> dst #doctest: +ELLIPSIS | ||
163 | 667 | '/tmp/store.../ZR/765XWSF6S7JQHLUI4GCG5BHGPE252O.mov' | ||
164 | 668 | |||
165 | 669 | |||
166 | 670 | Note above that this method returns the full path of the canonically | ||
167 | 671 | named file. | ||
168 | 672 | """ | ||
169 | 673 | # Open temporary file and check content hash: | ||
170 | 674 | tmp = self.temp(chash, ext) | ||
171 | 675 | tmp_fp = open(tmp, 'rb') | ||
172 | 676 | h = HashList(tmp_fp) | ||
173 | 677 | got = h.run() | ||
174 | 678 | if got != chash: | ||
175 | 679 | raise IntegrityError(got=got, expected=chash, filename=tmp_fp.name) | ||
176 | 680 | |||
177 | 681 | # Get canonical name, check for duplicate: | ||
178 | 682 | dst = self.path(chash, ext, create=True) | ||
179 | 683 | if path.exists(dst): | ||
180 | 684 | raise DuplicateFile(chash=chash, src=tmp_fp.name, dst=dst) | ||
181 | 685 | |||
182 | 686 | # Set file to read-only and rename into canonical location | ||
183 | 687 | os.fchmod(tmp_fp.fileno(), 0o444) | ||
184 | 688 | os.rename(tmp_fp.name, dst) | ||
185 | 689 | |||
186 | 690 | # Return canonical filename: | ||
187 | 691 | return dst | ||
188 | 692 | |||
189 | 622 | def import_file(self, src_fp, ext=None): | 693 | def import_file(self, src_fp, ext=None): |
190 | 623 | """ | 694 | """ |
191 | 624 | Atomically copy open file *src_fp* into this file store. | 695 | Atomically copy open file *src_fp* into this file store. |
192 | 625 | 696 | ||
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 | 23 | Unit tests for `dmedia.downloader` module. | 23 | Unit tests for `dmedia.downloader` module. |
198 | 24 | """ | 24 | """ |
199 | 25 | 25 | ||
200 | 26 | import os | ||
201 | 27 | from os import path | ||
202 | 26 | from unittest import TestCase | 28 | from unittest import TestCase |
203 | 27 | from hashlib import sha1 | 29 | from hashlib import sha1 |
204 | 28 | from base64 import b32encode | 30 | from base64 import b32encode |
205 | 29 | from .helpers import raises, TempDir | 31 | from .helpers import raises, TempDir |
207 | 30 | from dmedia.errors import DownloadFailure | 32 | from .helpers import sample_mov, sample_thm |
208 | 33 | from .helpers import mov_hash, thm_hash | ||
209 | 34 | from dmedia.constants import TYPE_ERROR, LEAF_SIZE | ||
210 | 35 | from dmedia.errors import DownloadFailure, DuplicateFile, IntegrityError | ||
211 | 36 | from dmedia.filestore import FileStore, HashList | ||
212 | 31 | from dmedia import downloader | 37 | from dmedia import downloader |
213 | 32 | 38 | ||
214 | 33 | 39 | ||
215 | @@ -137,3 +143,130 @@ | |||
216 | 137 | inst = Example(a, b, b) | 143 | inst = Example(a, b, b) |
217 | 138 | self.assertEqual(inst.process_leaf(7, a_hash), a) | 144 | self.assertEqual(inst.process_leaf(7, a_hash), a) |
218 | 139 | self.assertEqual(inst.dst_fp._chunk, a) | 145 | self.assertEqual(inst.dst_fp._chunk, a) |
219 | 146 | |||
220 | 147 | |||
221 | 148 | class test_TorrentDownloader(TestCase): | ||
222 | 149 | klass = downloader.TorrentDownloader | ||
223 | 150 | |||
224 | 151 | def test_init(self): | ||
225 | 152 | tmp = TempDir() | ||
226 | 153 | fs = FileStore(tmp.path) | ||
227 | 154 | |||
228 | 155 | e = raises(TypeError, self.klass, '', 17, mov_hash) | ||
229 | 156 | self.assertEqual( | ||
230 | 157 | str(e), | ||
231 | 158 | TYPE_ERROR % ('fs', FileStore, int, 17) | ||
232 | 159 | ) | ||
233 | 160 | |||
234 | 161 | inst = self.klass('', fs, mov_hash) | ||
235 | 162 | self.assertEqual(inst.torrent, '') | ||
236 | 163 | self.assertTrue(inst.fs is fs) | ||
237 | 164 | self.assertEqual(inst.chash, mov_hash) | ||
238 | 165 | self.assertEqual(inst.ext, None) | ||
239 | 166 | |||
240 | 167 | inst = self.klass('', fs, mov_hash, ext='mov') | ||
241 | 168 | self.assertEqual(inst.torrent, '') | ||
242 | 169 | self.assertTrue(inst.fs is fs) | ||
243 | 170 | self.assertEqual(inst.chash, mov_hash) | ||
244 | 171 | self.assertEqual(inst.ext, 'mov') | ||
245 | 172 | |||
246 | 173 | def test_get_tmp(self): | ||
247 | 174 | # Test with ext='mov' | ||
248 | 175 | tmp = TempDir() | ||
249 | 176 | fs = FileStore(tmp.path) | ||
250 | 177 | inst = self.klass('', fs, mov_hash, 'mov') | ||
251 | 178 | d = tmp.join('transfers') | ||
252 | 179 | f = tmp.join('transfers', mov_hash + '.mov') | ||
253 | 180 | self.assertFalse(path.exists(d)) | ||
254 | 181 | self.assertFalse(path.exists(f)) | ||
255 | 182 | self.assertEqual(inst.get_tmp(), f) | ||
256 | 183 | self.assertTrue(path.isdir(d)) | ||
257 | 184 | self.assertFalse(path.exists(f)) | ||
258 | 185 | |||
259 | 186 | # Test with ext=None | ||
260 | 187 | tmp = TempDir() | ||
261 | 188 | fs = FileStore(tmp.path) | ||
262 | 189 | inst = self.klass('', fs, mov_hash) | ||
263 | 190 | d = tmp.join('transfers') | ||
264 | 191 | f = tmp.join('transfers', mov_hash) | ||
265 | 192 | self.assertFalse(path.exists(d)) | ||
266 | 193 | self.assertFalse(path.exists(f)) | ||
267 | 194 | self.assertEqual(inst.get_tmp(), f) | ||
268 | 195 | self.assertTrue(path.isdir(d)) | ||
269 | 196 | self.assertFalse(path.exists(f)) | ||
270 | 197 | |||
271 | 198 | def test_finalize(self): | ||
272 | 199 | tmp = TempDir() | ||
273 | 200 | fs = FileStore(tmp.path) | ||
274 | 201 | inst = self.klass('', fs, mov_hash, 'mov') | ||
275 | 202 | |||
276 | 203 | src_d = tmp.join('transfers') | ||
277 | 204 | src = tmp.join('transfers', mov_hash + '.mov') | ||
278 | 205 | dst_d = tmp.join(mov_hash[:2]) | ||
279 | 206 | dst = tmp.join(mov_hash[:2], mov_hash[2:] + '.mov') | ||
280 | 207 | |||
281 | 208 | # Test when transfers/ dir doesn't exist: | ||
282 | 209 | e = raises(IOError, inst.finalize) | ||
283 | 210 | self.assertFalse(path.exists(src_d)) | ||
284 | 211 | self.assertFalse(path.exists(dst_d)) | ||
285 | 212 | self.assertFalse(path.exists(dst)) | ||
286 | 213 | |||
287 | 214 | # Test when transfers/ exists but file does not: | ||
288 | 215 | self.assertEqual(fs.temp(mov_hash, 'mov', create=True), src) | ||
289 | 216 | self.assertTrue(path.isdir(src_d)) | ||
290 | 217 | e = raises(IOError, inst.finalize) | ||
291 | 218 | self.assertFalse(path.exists(src)) | ||
292 | 219 | self.assertFalse(path.exists(dst_d)) | ||
293 | 220 | self.assertFalse(path.exists(dst)) | ||
294 | 221 | |||
295 | 222 | # Test when file has wrong content hash and wrong size: | ||
296 | 223 | open(src, 'wb').write(open(sample_thm, 'rb').read()) | ||
297 | 224 | e = raises(IntegrityError, inst.finalize) | ||
298 | 225 | self.assertEqual(e.got, thm_hash) | ||
299 | 226 | self.assertEqual(e.expected, mov_hash) | ||
300 | 227 | self.assertEqual(e.filename, src) | ||
301 | 228 | self.assertFalse(path.exists(dst_d)) | ||
302 | 229 | self.assertFalse(path.exists(dst)) | ||
303 | 230 | |||
304 | 231 | # Test when file has wrong content hash and *correct* size: | ||
305 | 232 | fp1 = open(sample_mov, 'rb') | ||
306 | 233 | fp2 = open(src, 'wb') | ||
307 | 234 | while True: | ||
308 | 235 | chunk = fp1.read(LEAF_SIZE) | ||
309 | 236 | if not chunk: | ||
310 | 237 | break | ||
311 | 238 | fp2.write(chunk) | ||
312 | 239 | fp1.close() | ||
313 | 240 | |||
314 | 241 | # Now change final byte at end file: | ||
315 | 242 | fp2.seek(-1, os.SEEK_END) | ||
316 | 243 | fp2.write('A') | ||
317 | 244 | fp2.close() | ||
318 | 245 | self.assertEqual(path.getsize(sample_mov), path.getsize(src)) | ||
319 | 246 | |||
320 | 247 | e = raises(IntegrityError, inst.finalize) | ||
321 | 248 | self.assertEqual(e.got, 'UECTT7A7EIHZ2SGGBMMO5WTTSVU4SUWM') | ||
322 | 249 | self.assertEqual(e.expected, mov_hash) | ||
323 | 250 | self.assertEqual(e.filename, src) | ||
324 | 251 | self.assertFalse(path.exists(dst_d)) | ||
325 | 252 | self.assertFalse(path.exists(dst)) | ||
326 | 253 | |||
327 | 254 | # Test with correct content hash: | ||
328 | 255 | fp1 = open(sample_mov, 'rb') | ||
329 | 256 | fp2 = open(src, 'wb') | ||
330 | 257 | while True: | ||
331 | 258 | chunk = fp1.read(LEAF_SIZE) | ||
332 | 259 | if not chunk: | ||
333 | 260 | break | ||
334 | 261 | fp2.write(chunk) | ||
335 | 262 | fp1.close() | ||
336 | 263 | fp2.close() | ||
337 | 264 | self.assertEqual(inst.finalize(), dst) | ||
338 | 265 | self.assertTrue(path.isdir(src_d)) | ||
339 | 266 | self.assertFalse(path.exists(src)) | ||
340 | 267 | self.assertTrue(path.isdir(dst_d)) | ||
341 | 268 | self.assertTrue(path.isfile(dst)) | ||
342 | 269 | |||
343 | 270 | # Check content hash of file in canonical location | ||
344 | 271 | fp = open(dst, 'rb') | ||
345 | 272 | self.assertEqual(HashList(fp).run(), mov_hash) | ||
346 | 140 | 273 | ||
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 | 34 | from .helpers import TempDir, TempHome, raises | 34 | from .helpers import TempDir, TempHome, raises |
352 | 35 | from .helpers import sample_mov, sample_thm | 35 | from .helpers import sample_mov, sample_thm |
353 | 36 | from .helpers import mov_hash, mov_leaves, mov_qid | 36 | from .helpers import mov_hash, mov_leaves, mov_qid |
355 | 37 | from dmedia.errors import AmbiguousPath, FileStoreTraversal, DuplicateFile | 37 | from .helpers import thm_hash, thm_leaves, thm_qid |
356 | 38 | from dmedia.errors import AmbiguousPath, FileStoreTraversal | ||
357 | 39 | from dmedia.errors import DuplicateFile, IntegrityError | ||
358 | 38 | from dmedia.filestore import HashList | 40 | from dmedia.filestore import HashList |
359 | 39 | from dmedia import filestore, constants, schema | 41 | from dmedia import filestore, constants, schema |
361 | 40 | from dmedia.constants import TYPE_ERROR, EXT_PAT | 42 | from dmedia.constants import TYPE_ERROR, EXT_PAT, LEAF_SIZE |
362 | 41 | 43 | ||
363 | 42 | 44 | ||
364 | 43 | class test_functions(TestCase): | 45 | class test_functions(TestCase): |
365 | @@ -892,6 +894,81 @@ | |||
366 | 892 | self.assertEqual(path.dirname(fp.name), imports) | 894 | self.assertEqual(path.dirname(fp.name), imports) |
367 | 893 | self.assertTrue(fp.name.endswith('.mov')) | 895 | self.assertTrue(fp.name.endswith('.mov')) |
368 | 894 | 896 | ||
369 | 897 | def test_finalize_transfer(self): | ||
370 | 898 | tmp = TempDir() | ||
371 | 899 | inst = self.klass(tmp.path) | ||
372 | 900 | |||
373 | 901 | src_d = tmp.join('transfers') | ||
374 | 902 | src = tmp.join('transfers', mov_hash + '.mov') | ||
375 | 903 | dst_d = tmp.join(mov_hash[:2]) | ||
376 | 904 | dst = tmp.join(mov_hash[:2], mov_hash[2:] + '.mov') | ||
377 | 905 | |||
378 | 906 | # Test when transfers/ dir doesn't exist: | ||
379 | 907 | e = raises(IOError, inst.finalize_transfer, mov_hash, 'mov') | ||
380 | 908 | self.assertFalse(path.exists(src_d)) | ||
381 | 909 | self.assertFalse(path.exists(dst_d)) | ||
382 | 910 | self.assertFalse(path.exists(dst)) | ||
383 | 911 | |||
384 | 912 | # Test when transfers/ exists but file does not: | ||
385 | 913 | self.assertEqual(inst.temp(mov_hash, 'mov', create=True), src) | ||
386 | 914 | self.assertTrue(path.isdir(src_d)) | ||
387 | 915 | e = raises(IOError, inst.finalize_transfer, mov_hash, 'mov') | ||
388 | 916 | self.assertFalse(path.exists(src)) | ||
389 | 917 | self.assertFalse(path.exists(dst_d)) | ||
390 | 918 | self.assertFalse(path.exists(dst)) | ||
391 | 919 | |||
392 | 920 | # Test when file has wrong content hash and wrong size: | ||
393 | 921 | open(src, 'wb').write(open(sample_thm, 'rb').read()) | ||
394 | 922 | e = raises(IntegrityError, inst.finalize_transfer, mov_hash, 'mov') | ||
395 | 923 | self.assertEqual(e.got, thm_hash) | ||
396 | 924 | self.assertEqual(e.expected, mov_hash) | ||
397 | 925 | self.assertEqual(e.filename, src) | ||
398 | 926 | self.assertFalse(path.exists(dst_d)) | ||
399 | 927 | self.assertFalse(path.exists(dst)) | ||
400 | 928 | |||
401 | 929 | # Test when file has wrong content hash and *correct* size: | ||
402 | 930 | fp1 = open(sample_mov, 'rb') | ||
403 | 931 | fp2 = open(src, 'wb') | ||
404 | 932 | while True: | ||
405 | 933 | chunk = fp1.read(LEAF_SIZE) | ||
406 | 934 | if not chunk: | ||
407 | 935 | break | ||
408 | 936 | fp2.write(chunk) | ||
409 | 937 | fp1.close() | ||
410 | 938 | |||
411 | 939 | # Now change final byte at end file: | ||
412 | 940 | fp2.seek(-1, os.SEEK_END) | ||
413 | 941 | fp2.write('A') | ||
414 | 942 | fp2.close() | ||
415 | 943 | self.assertEqual(path.getsize(sample_mov), path.getsize(src)) | ||
416 | 944 | |||
417 | 945 | e = raises(IntegrityError, inst.finalize_transfer, mov_hash, 'mov') | ||
418 | 946 | self.assertEqual(e.got, 'UECTT7A7EIHZ2SGGBMMO5WTTSVU4SUWM') | ||
419 | 947 | self.assertEqual(e.expected, mov_hash) | ||
420 | 948 | self.assertEqual(e.filename, src) | ||
421 | 949 | self.assertFalse(path.exists(dst_d)) | ||
422 | 950 | self.assertFalse(path.exists(dst)) | ||
423 | 951 | |||
424 | 952 | # Test with correct content hash: | ||
425 | 953 | fp1 = open(sample_mov, 'rb') | ||
426 | 954 | fp2 = open(src, 'wb') | ||
427 | 955 | while True: | ||
428 | 956 | chunk = fp1.read(LEAF_SIZE) | ||
429 | 957 | if not chunk: | ||
430 | 958 | break | ||
431 | 959 | fp2.write(chunk) | ||
432 | 960 | fp1.close() | ||
433 | 961 | fp2.close() | ||
434 | 962 | self.assertEqual(inst.finalize_transfer(mov_hash, 'mov'), dst) | ||
435 | 963 | self.assertTrue(path.isdir(src_d)) | ||
436 | 964 | self.assertFalse(path.exists(src)) | ||
437 | 965 | self.assertTrue(path.isdir(dst_d)) | ||
438 | 966 | self.assertTrue(path.isfile(dst)) | ||
439 | 967 | |||
440 | 968 | # Check content hash of file in canonical location | ||
441 | 969 | fp = open(dst, 'rb') | ||
442 | 970 | self.assertEqual(HashList(fp).run(), mov_hash) | ||
443 | 971 | |||
444 | 895 | def test_import_file(self): | 972 | def test_import_file(self): |
445 | 896 | tmp = TempDir() | 973 | tmp = TempDir() |
446 | 897 | src = tmp.join('movie.mov') | 974 | src = tmp.join('movie.mov') |
447 | 898 | 975 | ||
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 | 1 | #!/usr/bin/env python | ||
453 | 2 | |||
454 | 3 | """ | ||
455 | 4 | Tests downloading torrent from: | ||
456 | 5 | |||
457 | 6 | http://novacut.s3.amazonaws.com/37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS.mov?torrent | ||
458 | 7 | """ | ||
459 | 8 | |||
460 | 9 | from __future__ import print_function | ||
461 | 10 | |||
462 | 11 | import sys | ||
463 | 12 | import os | ||
464 | 13 | from os import path | ||
465 | 14 | import time | ||
466 | 15 | import logging | ||
467 | 16 | from base64 import b32decode, b64decode | ||
468 | 17 | from dmedia.downloader import TorrentDownloader | ||
469 | 18 | from dmedia.filestore import FileStore | ||
470 | 19 | |||
471 | 20 | logging.basicConfig( | ||
472 | 21 | level=logging.DEBUG, | ||
473 | 22 | format='\t'.join(['%(levelname)s', '%(message)s']) | ||
474 | 23 | ) | ||
475 | 24 | |||
476 | 25 | # Known size, top hash, and leaf hashes for test video: | ||
477 | 26 | size = 44188757 | ||
478 | 27 | |||
479 | 28 | chash = '37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS' | ||
480 | 29 | |||
481 | 30 | leaves_b32 = [ | ||
482 | 31 | 'TDCPJHYQVEVCTMLIKEQITVMJKSUIETHD', | ||
483 | 32 | 'UAG5HQCLH6PGA4RAYXDEFRNCSZDTMLXU', | ||
484 | 33 | '2DGBOSUSSDG5OKASXNQTJG3MANGL4H2U', | ||
485 | 34 | 'D4TMBNAQOOFMIWB2ATT2GR7Y262EATVM', | ||
486 | 35 | 'SEYVEQPAVCROXPYZWIXN4YZRHOZV2MWV', | ||
487 | 36 | 'B2I7VCLIVV4LBSRTGSRQNNXDDPWCKNLA', | ||
488 | 37 | ] | ||
489 | 38 | |||
490 | 39 | leaves = [b32decode(l) for l in leaves_b32] | ||
491 | 40 | |||
492 | 41 | |||
493 | 42 | # The .torrent file, base64 encoded: | ||
494 | 43 | tdata = """ | ||
495 | 44 | ZDg6YW5ub3VuY2U0MjpodHRwOi8vdHJhY2tlci5hbWF6b25hd3MuY29tOjY5NjkvYW5ub3VuY2Ux | ||
496 | 45 | Mzphbm5vdW5jZS1saXN0bGw0MjpodHRwOi8vdHJhY2tlci5hbWF6b25hd3MuY29tOjY5NjkvYW5u | ||
497 | 46 | b3VuY2VlZTQ6aW5mb2Q2Omxlbmd0aGk0NDE4ODc1N2U0Om5hbWUzNjozN0dETkhBTlg3UkNCTUJH | ||
498 | 47 | VFlMU0lLN1RNVFVRU0tEUy5tb3YxMjpwaWVjZSBsZW5ndGhpMjYyMTQ0ZTY6cGllY2VzMzM4MDqr | ||
499 | 48 | KimqIY/Y6lI6kGX+koXSVvVYgXilfN8qnUAk31PJttEN8RXgagd3dLD41PXZjf5KXt84gp1i8Cgh | ||
500 | 49 | pfPOKRi3wBOGwNoq2JCjiHiW4wEobgaePx3alOno8Xl/phRuRoUoK/FgPJ6fm5JRulnrh5lonsmJ | ||
501 | 50 | iShyE+B9N5Kax2lFN5t6B1W+6y51ROzBDep9E8MaBobFyINvHkRsDY0mNgpTpwdtqAiZTOzsj9L4 | ||
502 | 51 | YE966dNMTBVXKWC8i5pSEuRN72Bf7wXF9DoOvBaNk1vMpzKzd+GGDRpKYoZcROuyUbEzYMuI+HgE | ||
503 | 52 | 5CvZGZZH01yVfelfotaMK1ogSShZAserPHEGLEfNwBMn51wSrLGrYiGN770n4O3tLSpmaZ1ZGaE5 | ||
504 | 53 | Ot2nZ+ZmSJmcZWF5Uu6e07QONhziX3SYn58rvqON7fJ/mz2GiO+9ARMfv/pc6ElbaVIAEHJy2Yej | ||
505 | 54 | 5uYIRtaaeraRG/KFgzvuxidwtePSyZzoM+1j/BaTDig+76N1hrei6R8/oUkU+rtn2MjYwaJtH+y5 | ||
506 | 55 | /iLp418vHyjqTUsFNGGBh24nJT7LcXhakfkkjmDmCKiZ6SDVbKW47EfpR8lCnwnHC4KXz7l/yddv | ||
507 | 56 | m2Oc6fuUAX11DEffcSclEtPx0/VEtih1uYgoovq6ef4Eg09PwRmEQ1mfy99yCk62WSEefeYWDHq0 | ||
508 | 57 | WO7ih+3mPTaimIMp6lClIf3DRszxe2bta3C/D+J4E+NtAsSSzkKn1MK5w+SFxj3ooQmrl1jaYv+I | ||
509 | 58 | QzBeVWb5KRDpzwuIr7LwaeV4/ClqGi1p4c+juohEFoLsrttB0xN9dRZcOtGVwMXqDrz4ukAtla7l | ||
510 | 59 | y+A93K/X60idwWteUlDDiZ8nK//1wmnaS5i1dDhEU+s+3M6JPB/w0pDjqoRFcuc99om0+7GS3E0V | ||
511 | 60 | NH+/yazeA9il4sJ5qpl8frN1SCevhoGQqg604YEQwyZpuqy8vjIwbRDwPr4ly56F75wcxpPOaWZd | ||
512 | 61 | xRqf5Sp53mDSTV3lEg7Jkm+H8n1o+fjVR5dnqoiYmQuepriBnJKcMLZx1ENnD04b1QD04oM4A/hX | ||
513 | 62 | FSjHjO2/fR4Q/uQXxKliv5WSdvvXSkTyC09eSXjR7OTLG7PHEkBNTlLkBOGDZR7DzUClCIHozS5V | ||
514 | 63 | qSSy/qTdmcLFwsU/LRAMMTUfb9H/JpN0olIO47A5MQvVWO3t2bh/FWcgr+yNODBKBPP/W4YSsjKa | ||
515 | 64 | bJ2HBfRhePuiij/DfEGga6qfyHSmOM2lX9u58JW6IlWfTxWMNfaC4p7KZA7T3rnO7QOpRqss3lGy | ||
516 | 65 | W6XWoJcCGF1jt6NztG67aRBAEv6n60KMKjEq6I5b8xTnHYaLjqlYxRr0ayzq/dJV+fev8W2gArfH | ||
517 | 66 | cRLv6VhGBmyaMBCxxinHjs6IaQj+Grn0SH5bWHKKPwV/pB+aFykce845bwzpNpIlOanWfiNo5lRC | ||
518 | 67 | wlBNx/XPMMERs3rOohkshp35bsYd+Y4tbuk/sDSJdcU9kq6lAMzc8WTIltmJ3kShD1YFGGlu9jJN | ||
519 | 68 | h3KNoUUYLHPvCLx4yN7JzNr15X+J8mUh07sSLgprmTbw4oj2WgZZujpx54I8j7ynYM6w4sqshmya | ||
520 | 69 | FLLzkMnv2HaJtG1kwtzGX+S2Pn6kp/JH0I5JU/lAM61SOprOyBV4SeJHCrEmL8BPHZzPLj3mF84S | ||
521 | 70 | gEMijEK0pdk3dvmy/9JlmwiUqC8g+0POAfsZirubaG1c55HQ1hKdSrZOZZbyzdGfwFDndOP3JXFY | ||
522 | 71 | miQLAH90mmd5JKR9+LrRv4nYH0HC092uf4smKzcFUYHpy0eJd3/UJKU64uE3z4X3JeZcSaboESMu | ||
523 | 72 | 6j7nzbk/ZEvQKZ/R/LJ93c90Hk8GMB3OenwZai0LMSMmCwlxGyvAJUuheZ+1jD8+uH7Ryz1znadg | ||
524 | 73 | L8U5sTXkFKscH7ctlTKQUSyFWECb1szevd+4nOqpEfty55GoN1VOBsj7uLe0TQFSqryUEqSbhBUK | ||
525 | 74 | 5ZUlt/5orrDTyw4q4zxMXV3t+swhiJ3/xh1USwpELpdP5BsExmhpW7p9WScj17GqojfzHEgMrS90 | ||
526 | 75 | 591i8/CiRs/XRFpn1j4AeUFRam7XU0f5o5QpjSwQo9/Icf40xU+DYwaCRHYnM1gvtsBJjRWBGShC | ||
527 | 76 | 38wFJ80VQJDq1jjYeFL99SY6n33OqBZPW+/EX0LU7pzepxWgvl1RnsaoLjvQzbFdj+akBgQWtrJd | ||
528 | 77 | W5JCKH3qNq0Y9f1hRhUXAWLE0UMW1IlCDwaNnPYm/kKyoilYoxi4pj9xWGEaI+mOhrRZbvBXIzMp | ||
529 | 78 | LKIiPx/VWg/2B3fWw4VWmAWQN66J26j8imTUAO74Uk9iUbU4htax0VU5pCWguU6peqCfXfOrCfDb | ||
530 | 79 | +CIQP/2OzU52R+k9VzPTMdH+WfXqXD/whq0azSm8davU3zI/XQ0A6ASEw7jdwj8dW0jTQCPMWGRD | ||
531 | 80 | lngE1sW8a6Gf37PtjGLrCfllnrYQmiJeDom23VbubxjGFi4lsUikXakh8yQauEN0Omx1D3FO86sn | ||
532 | 81 | u45XAq6OO9Ifh68R5D6VtpSv7Wi7JVibICqWqfxMunbf5NOuET7Nk4yPIwzKHr1+JFxY6B3CeZP6 | ||
533 | 82 | K8KBYmamGcFTgfsnvPS+4sodae66hPJUSDE/Hjsn/fvNLqOoqIu+3jDcL7EgSNaC0iVQuG0M/sOR | ||
534 | 83 | +Rj1Kzg04sHJz0QjdmH9940aR/VgssLGhTS2GQcdCZeabRX0rZY3k7zfXSMP+I9N0jQpV/2fI+oD | ||
535 | 84 | GdPwv1CcQWn+0e00sU7UeY5Qqap0M9/GNR8C4w2TjINHqbIAPOxCVIQo+aeqpbe6Es/CJkBpxFj7 | ||
536 | 85 | U6TJf3yMCWy0vhoYetWD0gBAab6stDwnQeRNPUjqN96qhLUFqyZgPNuWvQPpjXYrJS0xSFN59LVV | ||
537 | 86 | s8FhEU+lagNpRgPaUsqMdoxx9x7l3COesxU5Hl7znezKL+YY6TBJQIbCnCjAWNmv6t8Jj92rON+M | ||
538 | 87 | /TER3Q7Pzhy9TJFY1Mq6sjEslx+6V1uhYEG0amQ3cZ217ffK4i3Y4kFrFtRfEYCvCiXogTqVz4yG | ||
539 | 88 | 3JmsyJrokSpyRDmP6d2EXHybFUGi5PGgh1+jgHuHK65beaez/Ldd8WXOfxxjCukQ2/ZAaycoTGYL | ||
540 | 89 | qv2FJab3ce10JQFZgxKGtK3OaIfprSGP1yqpfWX2/WwBv3g3j00gW4b/a8UpOeS0L4usuf6HrK00 | ||
541 | 90 | TUArAegiFfikLUZqTcDlp0T6X0XIZ1xgZ/NKUFY8Ooi+YRwSzDmblBLuYVArpjldSjeuilWPI1Dn | ||
542 | 91 | xmSm7BgYrI7pMG2NYlBMGvntMFFK6705OD6d4a2UKg9Tzt4LU8eah2XWYnpjAO+730bmjFr5rHy7 | ||
543 | 92 | +KVdaSkGfjJ4G8zjKa3qwapFtGj5lbczHtpXlYW1YiRfDPdgr7IGSQ9yfAlEwJIAhi17OIPck4y3 | ||
544 | 93 | 9dQei5XdPDrpzO6E7bWtA/5TJkOjeKAOgiO5m9L7sssdKBqPOmrljXHmZTQc0RbUFXtteLrNq7uD | ||
545 | 94 | af8tDdlsWJSlG/qxhNSFHz7YcKxw9huM5pyFbTRmiIVVFq3rQ38zAES5RjSAlSs0Bw/zhUNUvmmq | ||
546 | 95 | Z99T7JDcRzU2rfTNOZ7zno0xXhnCGAGoeFclIw+TqFVW2mLSl2WUrkdhE5YiYDq0sRT64ajlg3tF | ||
547 | 96 | rhvVK0AQuWNHm5LzF4gJq/eksf3iQyDVDCZlXQIoGnSzwDqA4dKHU/vm7yv7lmxbfeiKGlGNiK8r | ||
548 | 97 | PrIcNRMRFcjry/FbDEJlLvHLGdFIVvGxbXA/4R6bfj32fSLNWqq14Vps53r8qmDIdTfmQMSmREu2 | ||
549 | 98 | N/0HwBtxKlF0pVtU68wuxubqceZF5Y3d4K/R+8pGDWz5j6Fa6qBb3Bqoa9TrVkQhuoN7XGTOKE/u | ||
550 | 99 | NlX4Y7gROs6g/pe37U+eTTRIkiGgcGFQSZOktTCkAlpzCgQeIgWfQf3GiKDuy6x9ISHqnqROboHA | ||
551 | 100 | JBTbsz6stH3+57ihMiWJ3uOTcivqrJ8PpjKb0ISxdR+XMw3baXklnN9b6fIXVw2BlfqORJAwRjz5 | ||
552 | 101 | i51/MICGnBdIs24CAb/SJQgezzspKHURdCo9Cr7wgH5S/9NheuP3ChjnjhkcIq6iIBRgMx6cQo/u | ||
553 | 102 | xl3m6OmpZ6qyy6JLpoNJECjUW4MQB/uXpGE8xZEbPddYewOAFGRP4pub0FTBJx7tpMyE8VxGV31A | ||
554 | 103 | 529Iw0KqKFK1efvtMQyCd/BTkxqjHIk5ahfEcKkIv06fjdXCRngISP4LKjOcZYeb/Ltb58pIAA12 | ||
555 | 104 | 2xEAiTe74jzv4jxw2mBAAEBy6JZCCQhvfXRadNoJrWs0W4WDsKIXUtfN2tqPIE1KGgnYj6GRVi4g | ||
556 | 105 | IQAiJ41Gb6sQUl7iyQbJ6gEOP0wo1vnIH7udeN3AMND+rJlT9sEuNJPwHRjjMm7ThgOumOjv6m5w | ||
557 | 106 | 9Nfq2yu22LozPsNqDhbmrRSS5IhvtmYdll5iEC+EFt6L8Jpf4CtsHVmQhnX0ePk5JA7/EzYSoaId | ||
558 | 107 | oo1fdItR4cLxIFVRK+YFGjEyOngtYW16LWJ1Y2tldDc6bm92YWN1dDk6eC1hbXota2V5MzY6MzdH | ||
559 | 108 | RE5IQU5YN1JDQk1CR1RZTFNJSzdUTVRVUVNLRFMubW92ZWU= | ||
560 | 109 | """ | ||
561 | 110 | |||
562 | 111 | |||
563 | 112 | # Create a FileStore in ~/.dmedia_test/ | ||
564 | 113 | home = path.abspath(os.environ['HOME']) | ||
565 | 114 | base = path.join(home, '.dmedia_test') | ||
566 | 115 | fs = FileStore(base) | ||
567 | 116 | |||
568 | 117 | t = TorrentDownloader(b64decode(tdata), fs, chash, 'mov') | ||
569 | 118 | t.run() | ||
570 | 119 | |||
571 | 120 | 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