#!/usr/bin/env python """Syntax: /path/to/lazytorrent.py remotehost:dir mntpoint localfile.torrent... Expose the content of all the 'localfile.torrent' via PyFuse to the directory 'mntpoint'. If no torrent is specified, it defaults to '*.torrent'. When accessing the files, the content is first looked up in local files (as usual) and then in remote files. The remote files come from 'remotehost:remotedir'. This path must contain the same .torrent file somewhere (typically fully downloaded). In case the 'remotedir' is only a parent directory, the real remote directory is looked up by searching for the same .torrent name. """ import sys, os import py from handler import Handler from objectfs import ObjectFs, SymLink sys.path.append('/home/arigo/python/bt') import btlib, checktorrent TOTAL_DOWNLOADED = 0 # ____________________________________________________________ class TorrentFile(object): def __init__(self, tinfo, name, start, length): self.tinfo = tinfo self.name = name self.start = start self.length = length def size(self): return self.length def read(self): return OpenFile(self) class OpenFile(object): _close = os.close def __init__(self, torrentfile): make_links(torrentfile.name) self.torrentfile = torrentfile self.fdlocal = os.open(torrentfile.name, os.O_RDONLY | os.O_CREAT, 0666) self.pos = 0 def __del__(self): self._close(self.fdlocal) def seek(self, pos): os.lseek(self.fdlocal, pos, 0) self.pos = pos def read(self, count): abspos = self.torrentfile.start + self.pos blocksize = self.torrentfile.tinfo.piecelength for blocknum in range(abspos // blocksize, (abspos + count - 1) // blocksize + 1): self.torrentfile.tinfo.loadblock(blocknum) return readall(self.fdlocal, count) def readall(fd, count): blocks = [] while count > 0: data = os.read(fd, count) if not data: break blocks.append(data) count -= len(data) return ''.join(blocks) class TInfoWithRemote(checktorrent.TInfo): def set_remote_info(self, gw, remotedir): self._gw = gw self._remotedir = remotedir def open_remote_link(self): self.channel = self._gw.remote_exec(""" import os def handle_OPEN(f, filename): if f is not None: f.close() filename = os.path.join(remotedir, filename) f = open(filename, 'rb') return f def handle_R(f, start, end): f.seek(start) data = f.read(end-start) fragments.append(data) return f def handle_OK(f): channel.send(''.join(fragments)) del fragments[:] return f remotedir = channel.receive() fragments = [] f = None for args in channel: fn = globals()['handle_' + args[0]] f = fn(f, *args[1:]) """) self.channel.send(self._remotedir) self._remote_file_index = None self._remote_n = [] def query_data(self, n): if n in self.checked: return try: intervals = self.piece_intervals[n] except IndexError: return if not self.check(n): if not hasattr(self, '_remote_n'): self.open_remote_link() for file_index, start, end in intervals: if file_index != self._remote_file_index: filename, _ = self.files[file_index] print 'remote opening:', filename self.channel.send(('OPEN', filename)) self._remote_file_index = file_index self.channel.send(('R', start, end)) self.channel.send(('OK',)) self._remote_n.append(n) def process_answer(self): global TOTAL_DOWNLOADED n = self._remote_n.pop(0) piecedata = self.channel.receive() pieceorg = 0 for file_index, start, end in self.piece_intervals[n]: name, _ = self.files[file_index] try: f = open(name, 'r+b', 0) except IOError: make_links(name) f = open(name, 'w+b', 0) f.seek(start) f.write(piecedata[pieceorg : pieceorg + (end-start)]) f.close() pieceorg += end-start assert pieceorg == len(piecedata) TOTAL_DOWNLOADED += pieceorg self.checked[n] = True def loadblock(self, n): self.query_data(n) self.query_data(n+1) while not self.checked[n]: self.process_answer() def make_links(name): dirname = '' while '/' in name: dir, name = name.split('/', 1) dirname = os.path.join(dirname, dir) if not os.path.exists(dirname): os.symlink('.', dirname) # ____________________________________________________________ def find_remote_torrents(torrentfiles, gw, remotedir): channel = gw.remote_exec(""" import os, stat class Found(Exception): def __init__(self, fullname): self.fullname = fullname def search(dir): for name in os.listdir(dir): fullname = os.path.join(dir, name) st = os.lstat(fullname) if stat.S_ISREG(st.st_mode): if name == torrentfile: raise Found(fullname) elif stat.S_ISDIR(st.st_mode): search(fullname) startdir = channel.receive() for torrentfile in channel: try: search(startdir) except Found, f: channel.send(f.fullname) else: channel.send(None) """) channel.send(remotedir) for torrentfile in torrentfiles: channel.send(os.path.basename(torrentfile)) remotetorrents = [channel.receive() for _ in torrentfiles] return remotetorrents def make_root(root, torrentfile): f = open(torrentfile, 'rb') metainfo = btlib.bdecode(f.read()) f.close() info = metainfo['info'] tinfo = TInfoWithRemote(info) start = 0 for name, length in tinfo.files: while '/' in name: dirname, name = name.split('/', 1) if dirname not in root: root[dirname] = SymLink('.') root[name] = TorrentFile(tinfo, name, start, length) start += length return tinfo def update_root(tinfo, gw, remotetorrent): remoteroot = os.path.dirname(remotetorrent) tinfo.set_remote_info(gw, remoteroot) def serve(root, mountpoint): handler = Handler(mountpoint, ObjectFs(root), max_read=256*1024) handler.loop_forever() def main(remote, mountpoint, torrentfiles): assert ':' in remote root = {} tinfos = [] for torrentfile in torrentfiles: tinfos.append(make_root(root, torrentfile)) remotehost, remotedir = remote.split(':', 1) gw = py.execnet.SshGateway(remotehost) remotetorrents = find_remote_torrents(torrentfiles, gw, remotedir) for tinfo, remotetorrent, tf in zip(tinfos, remotetorrents, torrentfiles): if remotetorrent is not None: print '=====', remotehost + ':' + remotetorrent, '=====' update_root(tinfo, gw, remotetorrent) else: print '=====', '(%s not found)' % (os.path.basename(tf),) try: serve(root, mountpoint) finally: print 'DONE: downloaded %.1f MB' % (TOTAL_DOWNLOADED / (1024.0*1024),) if __name__ == '__main__': remote, mountpoint = sys.argv[1:3] torrentfiles = sys.argv[3:] if not torrentfiles: for fn in os.listdir('.'): if fn.endswith('.torrent'): torrentfiles.append(fn) assert torrentfiles, "no .torrent file found" torrentfiles.sort() main(remote, mountpoint, torrentfiles)