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
=== modified file 'debian/control'
--- debian/control 2011-02-01 23:53:30 +0000
+++ debian/control 2011-02-11 17:26:30 +0000
@@ -19,7 +19,8 @@
19 python-paste,19 python-paste,
20 python-notify,20 python-notify,
21 python-appindicator,21 python-appindicator,
22 python-xdg22 python-xdg,
23 python-libtorrent
23Provides: python-dmedia24Provides: python-dmedia
24Replaces: python-dmedia (<< ${source:Version})25Replaces: python-dmedia (<< ${source:Version})
25Conflicts: python-dmedia (<< ${source:Version})26Conflicts: python-dmedia (<< ${source:Version})
2627
=== modified file 'dmedia/downloader.py'
--- dmedia/downloader.py 2011-01-21 12:42:47 +0000
+++ dmedia/downloader.py 2011-02-11 17:26:30 +0000
@@ -23,14 +23,17 @@
23Download files in chunks using HTTP Range requests.23Download files in chunks using HTTP Range requests.
24"""24"""
2525
26from os import path
26from base64 import b32encode27from base64 import b32encode
27from urlparse import urlparse28from urlparse import urlparse
28from httplib import HTTPConnection, HTTPSConnection29from httplib import HTTPConnection, HTTPSConnection
29import logging30import logging
31import time
30from . import __version__32from . import __version__
31from .constants import CHUNK_SIZE33from .constants import CHUNK_SIZE, TYPE_ERROR
32from .errors import DownloadFailure34from .errors import DownloadFailure
33from .filestore import HashList, HASH35from .filestore import FileStore, HashList, HASH
36import libtorrent
3437
35USER_AGENT = 'dmedia %s' % __version__38USER_AGENT = 'dmedia %s' % __version__
36log = logging.getLogger()39log = logging.getLogger()
@@ -155,3 +158,50 @@
155 def run(self):158 def run(self):
156 for (i, chash) in enumerate(self.leaves):159 for (i, chash) in enumerate(self.leaves):
157 self.process_leaf(i, chash)160 self.process_leaf(i, chash)
161
162
163class TorrentDownloader(object):
164 def __init__(self, torrent, fs, chash, ext=None):
165 if not isinstance(fs, FileStore):
166 raise TypeError(
167 TYPE_ERROR % ('fs', FileStore, type(fs), fs)
168 )
169 self.torrent = torrent
170 self.fs = fs
171 self.chash = chash
172 self.ext = ext
173
174 def get_tmp(self):
175 tmp = self.fs.temp(self.chash, self.ext, create=True)
176 log.debug('Writting file to %r', tmp)
177 return tmp
178
179 def finalize(self):
180 dst = self.fs.finalize_transfer(self.chash, self.ext)
181 log.debug('Canonical name is %r', dst)
182 return dst
183
184 def run(self):
185 log.info('Downloading torrent %r %r', self.chash, self.ext)
186 tmp = self.get_tmp()
187 session = libtorrent.session()
188 session.listen_on(6881, 6891)
189
190 info = libtorrent.torrent_info(
191 libtorrent.bdecode(self.torrent)
192 )
193
194 torrent = session.add_torrent({
195 'ti': info,
196 'save_path': path.dirname(tmp),
197 })
198
199 while not torrent.is_seed():
200 s = torrent.status()
201 log.debug('Downloaded %d%%', s.progress * 100)
202 time.sleep(2)
203
204 session.remove_torrent(torrent)
205 time.sleep(1)
206
207 return self.finalize()
158208
=== modified file 'dmedia/errors.py'
--- dmedia/errors.py 2011-01-27 16:54:48 +0000
+++ dmedia/errors.py 2011-02-11 17:26:30 +0000
@@ -64,3 +64,7 @@
6464
65class DownloadFailure(DmediaError):65class DownloadFailure(DmediaError):
66 _format = 'leaf %(leaf)d expected %(expected)r; got %(got)r'66 _format = 'leaf %(leaf)d expected %(expected)r; got %(got)r'
67
68
69class IntegrityError(DmediaError):
70 _format = 'got chash %(got)r; expected %(expected)r for %(filename)r'
6771
=== modified file 'dmedia/filestore.py'
--- dmedia/filestore.py 2011-02-07 05:46:11 +0000
+++ dmedia/filestore.py 2011-02-11 17:26:30 +0000
@@ -49,7 +49,8 @@
49from threading import Thread49from threading import Thread
50from Queue import Queue50from Queue import Queue
51from .schema import create_store51from .schema import create_store
52from .errors import AmbiguousPath, DuplicateFile, FileStoreTraversal52from .errors import AmbiguousPath, FileStoreTraversal
53from .errors import DuplicateFile, IntegrityError
53from .constants import LEAF_SIZE, TRANSFERS_DIR, IMPORTS_DIR, TYPE_ERROR, EXT_PAT54from .constants import LEAF_SIZE, TRANSFERS_DIR, IMPORTS_DIR, TYPE_ERROR, EXT_PAT
5455
55B32LENGTH = 32 # Length of base32-encoded hash56B32LENGTH = 32 # Length of base32-encoded hash
@@ -619,6 +620,76 @@
619 fallocate(size, filename)620 fallocate(size, filename)
620 return open(filename, 'r+b')621 return open(filename, 'r+b')
621622
623 def finalize_transfer(self, chash, ext=None):
624 """
625 Move canonically named temporary file to its final canonical location.
626
627 This method will check the content hash of the canonically-named
628 temporary file with content hash *chash* and extension *ext*. If the
629 content hash is correct, it will do an ``os.fchmod()`` to set read-only
630 permissions, and then rename the file into its canonical location.
631
632 If the content hash is incorrect, `IntegrityError` is raised. If the
633 canonical file already exists, `DuplicateFile` is raised. Lastly, if
634 the temporary does not exist, ``IOError`` is raised.
635
636 This method will typically be used with the BitTorrent downloader or
637 similar, in which case the content hash will be known prior to
638 downloading. The downloader will first determine the canonical
639 temporary file name, like this:
640
641 >>> fs = FileStore()
642 >>> tmp = fs.temp('ZR765XWSF6S7JQHLUI4GCG5BHGPE252O', 'mov', create=True)
643 >>> tmp #doctest: +ELLIPSIS
644 '/tmp/store.../transfers/ZR765XWSF6S7JQHLUI4GCG5BHGPE252O.mov'
645
646
647 Then the downloader will write to the temporary file as it's being
648 downloaded:
649
650 >>> from dmedia.tests import sample_mov # Sample .MOV file
651 >>> src_fp = open(sample_mov, 'rb')
652 >>> tmp_fp = open(tmp, 'wb')
653 >>> while True:
654 ... chunk = src_fp.read(2**20) # Read in 1MiB chunks
655 ... if not chunk:
656 ... break
657 ... tmp_fp.write(chunk)
658 ...
659 >>> tmp_fp.close()
660
661
662 Finally, the downloader will move the temporary file into its canonical
663 location:
664
665 >>> dst = fs.finalize_transfer('ZR765XWSF6S7JQHLUI4GCG5BHGPE252O', 'mov')
666 >>> dst #doctest: +ELLIPSIS
667 '/tmp/store.../ZR/765XWSF6S7JQHLUI4GCG5BHGPE252O.mov'
668
669
670 Note above that this method returns the full path of the canonically
671 named file.
672 """
673 # Open temporary file and check content hash:
674 tmp = self.temp(chash, ext)
675 tmp_fp = open(tmp, 'rb')
676 h = HashList(tmp_fp)
677 got = h.run()
678 if got != chash:
679 raise IntegrityError(got=got, expected=chash, filename=tmp_fp.name)
680
681 # Get canonical name, check for duplicate:
682 dst = self.path(chash, ext, create=True)
683 if path.exists(dst):
684 raise DuplicateFile(chash=chash, src=tmp_fp.name, dst=dst)
685
686 # Set file to read-only and rename into canonical location
687 os.fchmod(tmp_fp.fileno(), 0o444)
688 os.rename(tmp_fp.name, dst)
689
690 # Return canonical filename:
691 return dst
692
622 def import_file(self, src_fp, ext=None):693 def import_file(self, src_fp, ext=None):
623 """694 """
624 Atomically copy open file *src_fp* into this file store.695 Atomically copy open file *src_fp* into this file store.
625696
=== modified file 'dmedia/tests/test_downloader.py'
--- dmedia/tests/test_downloader.py 2011-01-21 12:11:33 +0000
+++ dmedia/tests/test_downloader.py 2011-02-11 17:26:30 +0000
@@ -23,11 +23,17 @@
23Unit tests for `dmedia.downloader` module.23Unit tests for `dmedia.downloader` module.
24"""24"""
2525
26import os
27from os import path
26from unittest import TestCase28from unittest import TestCase
27from hashlib import sha129from hashlib import sha1
28from base64 import b32encode30from base64 import b32encode
29from .helpers import raises, TempDir31from .helpers import raises, TempDir
30from dmedia.errors import DownloadFailure32from .helpers import sample_mov, sample_thm
33from .helpers import mov_hash, thm_hash
34from dmedia.constants import TYPE_ERROR, LEAF_SIZE
35from dmedia.errors import DownloadFailure, DuplicateFile, IntegrityError
36from dmedia.filestore import FileStore, HashList
31from dmedia import downloader37from dmedia import downloader
3238
3339
@@ -137,3 +143,130 @@
137 inst = Example(a, b, b)143 inst = Example(a, b, b)
138 self.assertEqual(inst.process_leaf(7, a_hash), a)144 self.assertEqual(inst.process_leaf(7, a_hash), a)
139 self.assertEqual(inst.dst_fp._chunk, a)145 self.assertEqual(inst.dst_fp._chunk, a)
146
147
148class test_TorrentDownloader(TestCase):
149 klass = downloader.TorrentDownloader
150
151 def test_init(self):
152 tmp = TempDir()
153 fs = FileStore(tmp.path)
154
155 e = raises(TypeError, self.klass, '', 17, mov_hash)
156 self.assertEqual(
157 str(e),
158 TYPE_ERROR % ('fs', FileStore, int, 17)
159 )
160
161 inst = self.klass('', fs, mov_hash)
162 self.assertEqual(inst.torrent, '')
163 self.assertTrue(inst.fs is fs)
164 self.assertEqual(inst.chash, mov_hash)
165 self.assertEqual(inst.ext, None)
166
167 inst = self.klass('', fs, mov_hash, ext='mov')
168 self.assertEqual(inst.torrent, '')
169 self.assertTrue(inst.fs is fs)
170 self.assertEqual(inst.chash, mov_hash)
171 self.assertEqual(inst.ext, 'mov')
172
173 def test_get_tmp(self):
174 # Test with ext='mov'
175 tmp = TempDir()
176 fs = FileStore(tmp.path)
177 inst = self.klass('', fs, mov_hash, 'mov')
178 d = tmp.join('transfers')
179 f = tmp.join('transfers', mov_hash + '.mov')
180 self.assertFalse(path.exists(d))
181 self.assertFalse(path.exists(f))
182 self.assertEqual(inst.get_tmp(), f)
183 self.assertTrue(path.isdir(d))
184 self.assertFalse(path.exists(f))
185
186 # Test with ext=None
187 tmp = TempDir()
188 fs = FileStore(tmp.path)
189 inst = self.klass('', fs, mov_hash)
190 d = tmp.join('transfers')
191 f = tmp.join('transfers', mov_hash)
192 self.assertFalse(path.exists(d))
193 self.assertFalse(path.exists(f))
194 self.assertEqual(inst.get_tmp(), f)
195 self.assertTrue(path.isdir(d))
196 self.assertFalse(path.exists(f))
197
198 def test_finalize(self):
199 tmp = TempDir()
200 fs = FileStore(tmp.path)
201 inst = self.klass('', fs, mov_hash, 'mov')
202
203 src_d = tmp.join('transfers')
204 src = tmp.join('transfers', mov_hash + '.mov')
205 dst_d = tmp.join(mov_hash[:2])
206 dst = tmp.join(mov_hash[:2], mov_hash[2:] + '.mov')
207
208 # Test when transfers/ dir doesn't exist:
209 e = raises(IOError, inst.finalize)
210 self.assertFalse(path.exists(src_d))
211 self.assertFalse(path.exists(dst_d))
212 self.assertFalse(path.exists(dst))
213
214 # Test when transfers/ exists but file does not:
215 self.assertEqual(fs.temp(mov_hash, 'mov', create=True), src)
216 self.assertTrue(path.isdir(src_d))
217 e = raises(IOError, inst.finalize)
218 self.assertFalse(path.exists(src))
219 self.assertFalse(path.exists(dst_d))
220 self.assertFalse(path.exists(dst))
221
222 # Test when file has wrong content hash and wrong size:
223 open(src, 'wb').write(open(sample_thm, 'rb').read())
224 e = raises(IntegrityError, inst.finalize)
225 self.assertEqual(e.got, thm_hash)
226 self.assertEqual(e.expected, mov_hash)
227 self.assertEqual(e.filename, src)
228 self.assertFalse(path.exists(dst_d))
229 self.assertFalse(path.exists(dst))
230
231 # Test when file has wrong content hash and *correct* size:
232 fp1 = open(sample_mov, 'rb')
233 fp2 = open(src, 'wb')
234 while True:
235 chunk = fp1.read(LEAF_SIZE)
236 if not chunk:
237 break
238 fp2.write(chunk)
239 fp1.close()
240
241 # Now change final byte at end file:
242 fp2.seek(-1, os.SEEK_END)
243 fp2.write('A')
244 fp2.close()
245 self.assertEqual(path.getsize(sample_mov), path.getsize(src))
246
247 e = raises(IntegrityError, inst.finalize)
248 self.assertEqual(e.got, 'UECTT7A7EIHZ2SGGBMMO5WTTSVU4SUWM')
249 self.assertEqual(e.expected, mov_hash)
250 self.assertEqual(e.filename, src)
251 self.assertFalse(path.exists(dst_d))
252 self.assertFalse(path.exists(dst))
253
254 # Test with correct content hash:
255 fp1 = open(sample_mov, 'rb')
256 fp2 = open(src, 'wb')
257 while True:
258 chunk = fp1.read(LEAF_SIZE)
259 if not chunk:
260 break
261 fp2.write(chunk)
262 fp1.close()
263 fp2.close()
264 self.assertEqual(inst.finalize(), dst)
265 self.assertTrue(path.isdir(src_d))
266 self.assertFalse(path.exists(src))
267 self.assertTrue(path.isdir(dst_d))
268 self.assertTrue(path.isfile(dst))
269
270 # Check content hash of file in canonical location
271 fp = open(dst, 'rb')
272 self.assertEqual(HashList(fp).run(), mov_hash)
140273
=== modified file 'dmedia/tests/test_filestore.py'
--- dmedia/tests/test_filestore.py 2011-02-07 05:46:11 +0000
+++ dmedia/tests/test_filestore.py 2011-02-11 17:26:30 +0000
@@ -34,10 +34,12 @@
34from .helpers import TempDir, TempHome, raises34from .helpers import TempDir, TempHome, raises
35from .helpers import sample_mov, sample_thm35from .helpers import sample_mov, sample_thm
36from .helpers import mov_hash, mov_leaves, mov_qid36from .helpers import mov_hash, mov_leaves, mov_qid
37from dmedia.errors import AmbiguousPath, FileStoreTraversal, DuplicateFile37from .helpers import thm_hash, thm_leaves, thm_qid
38from dmedia.errors import AmbiguousPath, FileStoreTraversal
39from dmedia.errors import DuplicateFile, IntegrityError
38from dmedia.filestore import HashList40from dmedia.filestore import HashList
39from dmedia import filestore, constants, schema41from dmedia import filestore, constants, schema
40from dmedia.constants import TYPE_ERROR, EXT_PAT42from dmedia.constants import TYPE_ERROR, EXT_PAT, LEAF_SIZE
4143
4244
43class test_functions(TestCase):45class test_functions(TestCase):
@@ -892,6 +894,81 @@
892 self.assertEqual(path.dirname(fp.name), imports)894 self.assertEqual(path.dirname(fp.name), imports)
893 self.assertTrue(fp.name.endswith('.mov'))895 self.assertTrue(fp.name.endswith('.mov'))
894896
897 def test_finalize_transfer(self):
898 tmp = TempDir()
899 inst = self.klass(tmp.path)
900
901 src_d = tmp.join('transfers')
902 src = tmp.join('transfers', mov_hash + '.mov')
903 dst_d = tmp.join(mov_hash[:2])
904 dst = tmp.join(mov_hash[:2], mov_hash[2:] + '.mov')
905
906 # Test when transfers/ dir doesn't exist:
907 e = raises(IOError, inst.finalize_transfer, mov_hash, 'mov')
908 self.assertFalse(path.exists(src_d))
909 self.assertFalse(path.exists(dst_d))
910 self.assertFalse(path.exists(dst))
911
912 # Test when transfers/ exists but file does not:
913 self.assertEqual(inst.temp(mov_hash, 'mov', create=True), src)
914 self.assertTrue(path.isdir(src_d))
915 e = raises(IOError, inst.finalize_transfer, mov_hash, 'mov')
916 self.assertFalse(path.exists(src))
917 self.assertFalse(path.exists(dst_d))
918 self.assertFalse(path.exists(dst))
919
920 # Test when file has wrong content hash and wrong size:
921 open(src, 'wb').write(open(sample_thm, 'rb').read())
922 e = raises(IntegrityError, inst.finalize_transfer, mov_hash, 'mov')
923 self.assertEqual(e.got, thm_hash)
924 self.assertEqual(e.expected, mov_hash)
925 self.assertEqual(e.filename, src)
926 self.assertFalse(path.exists(dst_d))
927 self.assertFalse(path.exists(dst))
928
929 # Test when file has wrong content hash and *correct* size:
930 fp1 = open(sample_mov, 'rb')
931 fp2 = open(src, 'wb')
932 while True:
933 chunk = fp1.read(LEAF_SIZE)
934 if not chunk:
935 break
936 fp2.write(chunk)
937 fp1.close()
938
939 # Now change final byte at end file:
940 fp2.seek(-1, os.SEEK_END)
941 fp2.write('A')
942 fp2.close()
943 self.assertEqual(path.getsize(sample_mov), path.getsize(src))
944
945 e = raises(IntegrityError, inst.finalize_transfer, mov_hash, 'mov')
946 self.assertEqual(e.got, 'UECTT7A7EIHZ2SGGBMMO5WTTSVU4SUWM')
947 self.assertEqual(e.expected, mov_hash)
948 self.assertEqual(e.filename, src)
949 self.assertFalse(path.exists(dst_d))
950 self.assertFalse(path.exists(dst))
951
952 # Test with correct content hash:
953 fp1 = open(sample_mov, 'rb')
954 fp2 = open(src, 'wb')
955 while True:
956 chunk = fp1.read(LEAF_SIZE)
957 if not chunk:
958 break
959 fp2.write(chunk)
960 fp1.close()
961 fp2.close()
962 self.assertEqual(inst.finalize_transfer(mov_hash, 'mov'), dst)
963 self.assertTrue(path.isdir(src_d))
964 self.assertFalse(path.exists(src))
965 self.assertTrue(path.isdir(dst_d))
966 self.assertTrue(path.isfile(dst))
967
968 # Check content hash of file in canonical location
969 fp = open(dst, 'rb')
970 self.assertEqual(HashList(fp).run(), mov_hash)
971
895 def test_import_file(self):972 def test_import_file(self):
896 tmp = TempDir()973 tmp = TempDir()
897 src = tmp.join('movie.mov')974 src = tmp.join('movie.mov')
898975
=== added file 'test-torrent.py'
--- test-torrent.py 1970-01-01 00:00:00 +0000
+++ test-torrent.py 2011-02-11 17:26:30 +0000
@@ -0,0 +1,120 @@
1#!/usr/bin/env python
2
3"""
4Tests downloading torrent from:
5
6http://novacut.s3.amazonaws.com/37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS.mov?torrent
7"""
8
9from __future__ import print_function
10
11import sys
12import os
13from os import path
14import time
15import logging
16from base64 import b32decode, b64decode
17from dmedia.downloader import TorrentDownloader
18from dmedia.filestore import FileStore
19
20logging.basicConfig(
21 level=logging.DEBUG,
22 format='\t'.join(['%(levelname)s', '%(message)s'])
23)
24
25# Known size, top hash, and leaf hashes for test video:
26size = 44188757
27
28chash = '37GDNHANX7RCBMBGTYLSIK7TMTUQSKDS'
29
30leaves_b32 = [
31 'TDCPJHYQVEVCTMLIKEQITVMJKSUIETHD',
32 'UAG5HQCLH6PGA4RAYXDEFRNCSZDTMLXU',
33 '2DGBOSUSSDG5OKASXNQTJG3MANGL4H2U',
34 'D4TMBNAQOOFMIWB2ATT2GR7Y262EATVM',
35 'SEYVEQPAVCROXPYZWIXN4YZRHOZV2MWV',
36 'B2I7VCLIVV4LBSRTGSRQNNXDDPWCKNLA',
37]
38
39leaves = [b32decode(l) for l in leaves_b32]
40
41
42# The .torrent file, base64 encoded:
43tdata = """
44ZDg6YW5ub3VuY2U0MjpodHRwOi8vdHJhY2tlci5hbWF6b25hd3MuY29tOjY5NjkvYW5ub3VuY2Ux
45Mzphbm5vdW5jZS1saXN0bGw0MjpodHRwOi8vdHJhY2tlci5hbWF6b25hd3MuY29tOjY5NjkvYW5u
46b3VuY2VlZTQ6aW5mb2Q2Omxlbmd0aGk0NDE4ODc1N2U0Om5hbWUzNjozN0dETkhBTlg3UkNCTUJH
47VFlMU0lLN1RNVFVRU0tEUy5tb3YxMjpwaWVjZSBsZW5ndGhpMjYyMTQ0ZTY6cGllY2VzMzM4MDqr
48KimqIY/Y6lI6kGX+koXSVvVYgXilfN8qnUAk31PJttEN8RXgagd3dLD41PXZjf5KXt84gp1i8Cgh
49pfPOKRi3wBOGwNoq2JCjiHiW4wEobgaePx3alOno8Xl/phRuRoUoK/FgPJ6fm5JRulnrh5lonsmJ
50iShyE+B9N5Kax2lFN5t6B1W+6y51ROzBDep9E8MaBobFyINvHkRsDY0mNgpTpwdtqAiZTOzsj9L4
51YE966dNMTBVXKWC8i5pSEuRN72Bf7wXF9DoOvBaNk1vMpzKzd+GGDRpKYoZcROuyUbEzYMuI+HgE
525CvZGZZH01yVfelfotaMK1ogSShZAserPHEGLEfNwBMn51wSrLGrYiGN770n4O3tLSpmaZ1ZGaE5
53Ot2nZ+ZmSJmcZWF5Uu6e07QONhziX3SYn58rvqON7fJ/mz2GiO+9ARMfv/pc6ElbaVIAEHJy2Yej
545uYIRtaaeraRG/KFgzvuxidwtePSyZzoM+1j/BaTDig+76N1hrei6R8/oUkU+rtn2MjYwaJtH+y5
55/iLp418vHyjqTUsFNGGBh24nJT7LcXhakfkkjmDmCKiZ6SDVbKW47EfpR8lCnwnHC4KXz7l/yddv
56m2Oc6fuUAX11DEffcSclEtPx0/VEtih1uYgoovq6ef4Eg09PwRmEQ1mfy99yCk62WSEefeYWDHq0
57WO7ih+3mPTaimIMp6lClIf3DRszxe2bta3C/D+J4E+NtAsSSzkKn1MK5w+SFxj3ooQmrl1jaYv+I
58QzBeVWb5KRDpzwuIr7LwaeV4/ClqGi1p4c+juohEFoLsrttB0xN9dRZcOtGVwMXqDrz4ukAtla7l
59y+A93K/X60idwWteUlDDiZ8nK//1wmnaS5i1dDhEU+s+3M6JPB/w0pDjqoRFcuc99om0+7GS3E0V
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
109"""
110
111
112# Create a FileStore in ~/.dmedia_test/
113home = path.abspath(os.environ['HOME'])
114base = path.join(home, '.dmedia_test')
115fs = FileStore(base)
116
117t = TorrentDownloader(b64decode(tdata), fs, chash, 'mov')
118t.run()
119
120assert path.getsize(fs.path(chash, 'mov')) == size

Subscribers

People subscribed via source and target branches