#! /usr/bin/env python """Torrent Virtual FileSystem for Midnight Commander The script requires Midnight Commander 3.1+ (http://www.midnight-commander.org/), Python 2.4+ (http://www.python.org/), module eff_bdecode.py (http://effbot.org/zone/bencode.htm). For mc 4.7+ just put the script in $HOME/[.local/share/].mc/extfs.d. For older versions put it in /usr/[local/][lib|share]/mc/extfs and add a line "torrent" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini. Make the script executable. For mc 4.7+ run this "cd" command in the Midnight Commander (in the "bindings" file the command is "%cd"): cd file/torrent://; In older versions it is cd file#torrent, where "file" is the name of your torrent metafile. The VFS lists all files and directories from the torrent metafile; all files appear empty, of course, but the sizes are shown. Filenames are reencoded from the metafile's encoding/codepage to the current locale. Along with the files/directories in the torrent metafile the VFS also presents meta information - in the form of files in .META directory. The size and contents of these files are taken from the corresponding fields in the torrent metafile. The script doesn't check if the torrent consists of a .META file or directory (quite unlikely). Date/time for all files is set to midnight of the 1st January of the current year. The filesystem is, naturally, read-only. """ __version__ = "1.2.1" __author__ = "Oleg Broytman " __copyright__ = "Copyright (C) 2010-2013 PhiloSoft Design" __license__ = "GPL" import sys from eff_bdecode import decode try: import locale use_locale = True except ImportError: use_locale = False if use_locale: # Get the default charset. try: lcAll = locale.getdefaultlocale() except locale.Error, err: print >>sys.stderr, "WARNING:", err lcAll = [] if len(lcAll) == 2: default_encoding = lcAll[1] else: try: default_encoding = locale.getpreferredencoding() except locale.Error, err: print >>sys.stderr, "WARNING:", err default_encoding = sys.getdefaultencoding() else: default_encoding = sys.getdefaultencoding() import logging logger = logging.getLogger('torrent-mcextfs') log_err_handler = logging.StreamHandler(sys.stderr) logger.addHandler(log_err_handler) logger.setLevel(logging.INFO) if len(sys.argv) < 3: logger.critical("""\ Torrent Virtual FileSystem for Midnight Commander version %s Author: %s %s This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or /usr/[local/][lib|share]/mc/extfs. For more information read the source!""", __version__, __author__, __copyright__ ) sys.exit(1) locale.setlocale(locale.LC_ALL, '') def mctorrent_list(): """List the entire VFS""" if 'info' not in torrent: torrent_error('Info absent') info = torrent['info'] if 'name' not in info and 'name.utf-8' not in info: torrent_error('Unknown name') codepage = torrent.get('codepage', None) encoding = torrent.get('encoding', None) if not encoding and codepage: encoding = str(codepage) name = info['name'] name_utf8 = info.get('name.utf-8', None) if 'files' in info: files = info['files'] paths = [] for file in files: if 'path' not in file and 'path.utf-8' not in file: torrent_error('Unknown path') if 'length' not in file: torrent_error('Unknown length') if 'path.utf-8' in file: if name_utf8: path = '/'.join([name_utf8] + file['path.utf-8']) if default_encoding != 'utf-8': path = path.decode('utf-8', 'replace').encode(default_encoding, 'replace') else: _name_utf8 = name if encoding and (encoding != 'utf-8'): _name_utf8 = _name_utf8.decode(encoding, 'replace').encode('utf-8', 'replace') path = '/'.join([_name_utf8] + file['path.utf-8']) if default_encoding != 'utf-8': path = path.decode('utf-8', 'replace').encode(default_encoding, 'replace') else: if name_utf8: path = file['path'] if encoding and (encoding != 'utf-8'): path = path.decode(encoding, 'replace').encode('utf-8', 'replace') path = '/'.join([name_utf8] + path) if default_encoding != 'utf-8': path = path.decode('utf-8', 'replace').encode(default_encoding, 'replace') else: path = '/'.join([name] + file['path']) if encoding and (default_encoding != encoding): path = path.decode(encoding, 'replace').encode(default_encoding, 'replace') length = file['length'] paths.append((path, length)) else: # One-file torrent if 'length' not in info: torrent_error('Unknown length') length = info['length'] if name_utf8: if default_encoding != 'utf-8': name = name_utf8.decode('utf-8', 'replace').encode(default_encoding, 'replace') elif encoding and (default_encoding != encoding): name = name.decode(encoding, 'replace').encode(default_encoding, 'replace') paths = [(name, length)] meta = [] for name in 'announce', 'announce-list', 'codepage', 'comment', \ 'created by', 'creation date', 'encoding', \ 'nodes', 'publisher', 'publisher-url': if name == 'comment' and 'comment.utf-8' in torrent: data = torrent['comment.utf-8'].decode('utf-8').encode(default_encoding, 'replace') meta.append(('.META/' + name, len(data))) elif name in torrent: if name == 'announce-list': data = decode_announce_list(torrent[name]) elif name == 'codepage': data = str(torrent[name]) elif name == 'creation date': data = decode_datetime(torrent[name]) elif name == 'nodes': data = ['%s:%s' % (host, port) for host, port in torrent[name]] data = '\n'.join(data) else: data = torrent[name] meta.append(('.META/' + name, len(data))) if 'private' in info: meta.append(('.META/private', 1)) if 'piece length' in info: meta.append(('.META/piece length', len(str(info['piece length'])))) for name, size in paths + meta: print "-r--r--r-- 1 user group %d Jan 1 00:00 %s" % (size, name) def mctorrent_copyout(): """Extract a file from the VFS""" torrent_filename = sys.argv[3] real_filename = sys.argv[4] data = None for name in 'announce', 'announce-list', 'codepage', 'comment', \ 'created by', 'creation date', 'encoding', \ 'nodes', 'publisher', 'publisher-url': if name == 'comment' and 'comment.utf-8' in torrent: data = torrent['comment.utf-8'].decode('utf-8').encode(default_encoding, 'replace') elif torrent_filename == '.META/' + name: if name in torrent: if name == 'announce-list': data = decode_announce_list(torrent[name]) elif name == 'codepage': data = str(torrent[name]) elif name == 'creation date': data = decode_datetime(torrent[name]) elif name == 'nodes': data = ['%s:%s' % (host, port) for host, port in torrent[name]] data = '\n'.join(data) else: data = str(torrent[name]) else: torrent_error('Unknown ' + name) break if torrent_filename in ('.META/private', '.META/piece length'): if 'info' not in torrent: torrent_error('Info absent') info = torrent['info'] if torrent_filename == '.META/private': if 'private' not in info: torrent_error('Info absent') if torrent_filename == '.META/piece length': if 'piece length' not in info: torrent_error('Info absent') data = str(info[torrent_filename[len('.META/'):]]) if not torrent_filename.startswith('.META/'): data = '' if data is None: torrent_error('Unknown file name') else: outfile = open(real_filename, 'w') outfile.write(data) outfile.close() def mctorrent_copyin(): """Put a file to the VFS""" sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)") def mctorrent_rm(): """Remove a file from the VFS""" sys.exit("Torrent VFS doesn't support removing files/directories (read-only filesystem)") mctorrent_rmdir = mctorrent_rm def mctorrent_mkdir(): """Create a directory in the VFS""" sys.exit("Torrent VFS doesn't support creating directories (read-only filesystem)") def torrent_error(error_str): logger.critical("Error parsing the torrent metafile: %s", error_str) sys.exit(1) def decode_torrent(): try: torrent_file = open(sys.argv[2], 'r') data = torrent_file.read() torrent_file.close() return decode(data) except IOError, error_str: torrent_error(error_str) def decode_datetime(dt): from time import localtime, asctime the_time = float(dt) l_now = localtime(the_time) return asctime(l_now) def decode_announce_list(announce): return '\n'.join(l[0] for l in announce) command = sys.argv[1] procname = "mctorrent_" + command g = globals() if not g.has_key(procname): logger.critical("Unknown command %s", command) sys.exit(1) torrent = decode_torrent() try: g[procname]() except SystemExit: raise except: logger.exception("Error during run")