Documented the fact that the script can be put in $HOME/.mc/extfs.d
[extfs.d.git] / torrent
1 #! /usr/bin/env python
2 """Torrent Virtual FileSystem for Midnight Commander
3
4 The script requires Midnight Commander 3.1+
5 (http://www.midnight-commander.org/), Python 2.4+ (http://www.python.org/),
6 module eff_bdecode.py (http://effbot.org/zone/bencode.htm).
7
8 For mc 4.7+ put the script in $HOME/.mc/extfs.d.
9 For older versions put it in /usr/[local/][lib|share]/mc/extfs
10 and add a line "torrent" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini.
11 Make the script executable.
12
13 Run this "cd" command in the Midnight Commander (in the "bindings" file the
14 command is "%cd"): cd file#torrent, where "file" is the name of your torrent
15 metafile. The VFS lists all files and directories from the torrent metafile;
16 all files appear empty, of course, but the sizes are shown. Filenames are
17 reencoded from the metafile's encoding/codepage to the current locale.
18
19 Along with the files/directories in the torrent metafile the VFS also presents
20 meta information - in the form of files in .META directory. The size and
21 contents of these files are taken from the corresponding fields in the torrent
22 metafile. The script doesn't check if the torrent consists of a .META file or
23 directory (quite unlikely).
24
25 Date/time for all files is set to midnight of the 1st January of the current
26 year. The filesystem is, naturally, read-only.
27
28 """
29
30 __version__ = "1.1.1"
31 __author__ = "Oleg Broytman <phd@phdru.name>"
32 __copyright__ = "Copyright (C) 2010-2013 PhiloSoft Design"
33 __license__ = "GPL"
34
35 import locale, sys, os
36 from eff_bdecode import decode
37
38 import logging
39 logger = logging.getLogger('torrent-mcextfs')
40 log_err_handler = logging.StreamHandler(sys.stderr)
41 logger.addHandler(log_err_handler)
42 logger.setLevel(logging.INFO)
43
44 if len(sys.argv) < 3:
45     logger.critical("""\
46 Torrent Virtual FileSystem for Midnight Commander version %s
47 Author: %s
48 %s
49
50 This is not a program. Put the script in $HOME/.mc/extfs.d or
51 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
52    __version__, __author__, __copyright__
53 )
54     sys.exit(1)
55
56
57 locale.setlocale(locale.LC_ALL, '')
58 charset = locale.getpreferredencoding()
59
60
61 def mctorrent_list():
62     """List the entire VFS"""
63
64     if 'info' not in torrent:
65         torrent_error('Info absent')
66
67     info = torrent['info']
68     if 'name' not in info and 'name.utf-8' not in info:
69         torrent_error('Unknown name')
70
71     codepage = torrent.get('codepage', None)
72     encoding = torrent.get('encoding', None)
73     if not encoding and codepage:
74         encoding = str(codepage)
75
76     name = info['name']
77     name_utf8 = info.get('name.utf-8', None)
78
79     if 'files' in info:
80         files = info['files']
81         paths = []
82         for file in files:
83             if 'path' not in file and 'path.utf-8' not in file:
84                 torrent_error('Unknown path')
85             if 'length' not in file:
86                 torrent_error('Unknown length')
87             if 'path.utf-8' in file:
88                 if name_utf8:
89                     path = '/'.join([name_utf8] + file['path.utf-8'])
90                     if charset and (charset != 'utf-8'):
91                         path = path.decode('utf-8', 'replace').encode(charset, 'replace')
92                 else:
93                     _name_utf8 = name
94                     if encoding and (encoding != 'utf-8'):
95                         _name_utf8 = _name_utf8.decode(encoding, 'replace').encode('utf-8', 'replace')
96                     path = '/'.join([_name_utf8] + file['path.utf-8'])
97                     if charset and (charset != 'utf-8'):
98                         path = path.decode('utf-8', 'replace').encode(charset, 'replace')
99             else:
100                 if name_utf8:
101                     path = file['path']
102                     if encoding and (encoding != 'utf-8'):
103                         path = path.decode(encoding, 'replace').encode('utf-8', 'replace')
104                     path = '/'.join([name_utf8] + path)
105                     if charset and (charset != 'utf-8'):
106                         path = path.decode('utf-8', 'replace').encode(charset, 'replace')
107                 else:
108                     path = '/'.join([name] + file['path'])
109                     if charset and encoding and (charset != encoding):
110                         path = path.decode(encoding, 'replace').encode(charset, 'replace')
111             length = file['length']
112             paths.append((path, length))
113     else: # One-file torrent
114         if 'length' not in info:
115             torrent_error('Unknown length')
116         length = info['length']
117         if name_utf8:
118             if charset and (charset != 'utf-8'):
119                 name = name_utf8.decode('utf-8', 'replace').encode(charset, 'replace')
120         elif charset and encoding and (charset != encoding):
121             name = name.decode(encoding, 'replace').encode(charset, 'replace')
122         paths = [(name, length)]
123
124     meta = []
125     for name in 'announce', 'announce-list', 'codepage', 'comment', \
126                 'created by', 'creation date', 'encoding', \
127                 'nodes', 'publisher', 'publisher-url':
128         if name == 'comment' and 'comment.utf-8' in torrent:
129             data = torrent['comment.utf-8'].decode('utf-8').encode(charset, 'replace')
130             meta.append(('.META/' + name, len(data)))
131         elif name in torrent:
132             if name == 'announce-list':
133                 data = decode_announce_list(torrent[name])
134             elif name == 'codepage':
135                 data = str(torrent[name])
136             elif name == 'creation date':
137                 data = decode_datetime(torrent[name])
138             elif name == 'nodes':
139                 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
140                 data = '\n'.join(data)
141             else:
142                 data = torrent[name]
143             meta.append(('.META/' + name, len(data)))
144
145     if 'private' in info:
146         meta.append(('.META/private', 1))
147
148     if 'piece length' in info:
149         meta.append(('.META/piece length', len(str(info['piece length']))))
150
151     for name, size in paths + meta:
152         print "-r--r--r-- 1 user group %d Jan 1 00:00 %s" % (size, name)
153
154
155 def mctorrent_copyout():
156     """Extract a file from the VFS"""
157
158     torrent_filename = sys.argv[3]
159     real_filename = sys.argv[4]
160     data = None
161
162     for name in 'announce', 'announce-list', 'codepage', 'comment', \
163                 'created by', 'creation date', 'encoding', \
164                 'nodes', 'publisher', 'publisher-url':
165         if name == 'comment' and 'comment.utf-8' in torrent:
166             data = torrent['comment.utf-8'].decode('utf-8').encode(charset, 'replace')
167             meta.append(('.META/' + name, len(data)))
168         elif torrent_filename == '.META/' + name:
169             if name in torrent:
170                 if name == 'announce-list':
171                     data = decode_announce_list(torrent[name])
172                 elif name == 'codepage':
173                     data = str(torrent[name])
174                 elif name == 'creation date':
175                     data = decode_datetime(torrent[name])
176                 elif name == 'nodes':
177                     data = ['%s:%s' % (host, port) for host, port in torrent[name]]
178                     data = '\n'.join(data)
179                 else:
180                     data = str(torrent[name])
181             else:
182                 torrent_error('Unknown ' + name)
183             break
184
185     if torrent_filename in ('.META/private', '.META/piece length'):
186         if 'info' not in torrent:
187             torrent_error('Info absent')
188         info = torrent['info']
189         if torrent_filename == '.META/private':
190             if 'private' not in info:
191                 torrent_error('Info absent')
192         if torrent_filename == '.META/piece length':
193             if 'piece length' not in info:
194                 torrent_error('Info absent')
195         data = str(info[torrent_filename[len('.META/'):]])
196
197     if not torrent_filename.startswith('.META/'):
198         data = ''
199
200     if data is None:
201         torrent_error('Unknown file name')
202     else:
203         outfile = open(real_filename, 'w')
204         outfile.write(data)
205         outfile.close()
206
207
208 def mctorrent_copyin():
209     """Put a file to the VFS"""
210     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
211
212 def mctorrent_rm():
213     """Remove a file from the VFS"""
214     sys.exit("Torrent VFS doesn't support removing files/directories (read-only filesystem)")
215
216 mctorrent_rmdir = mctorrent_rm
217
218 def mctorrent_mkdir():
219     """Create a directory in the VFS"""
220     sys.exit("Torrent VFS doesn't support creating directories (read-only filesystem)")
221
222
223 def torrent_error(error_str):
224     logger.critical("Error parsing the torrent metafile: %s", error_str)
225     sys.exit(1)
226
227 def decode_torrent():
228     try:
229         torrent_file = open(sys.argv[2], 'r')
230         data = torrent_file.read()
231         torrent_file.close()
232         return decode(data)
233     except IOError, error_str:
234         torrent_error(error_str)
235
236
237 def decode_datetime(dt):
238     from time import localtime, asctime
239     the_time = float(dt)
240     l_now = localtime(the_time)
241     return asctime(l_now)
242
243 def decode_announce_list(announce):
244     return '\n'.join(l[0] for l in announce)
245
246
247 command = sys.argv[1]
248 procname = "mctorrent_" + command
249
250 g = globals()
251 if not g.has_key(procname):
252     logger.critical("Unknown command %s", command)
253     sys.exit(1)
254
255 torrent = decode_torrent()
256
257 try:
258     g[procname]()
259 except SystemExit:
260     raise
261 except:
262     logger.exception("Error during run")