Fixed minor bugs
[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.0"
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 /usr/[local/][lib|share]/mc/extfs.
51 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         if name in torrent:
128             if name == 'announce-list':
129                 data = decode_list(torrent[name])
130             elif name == 'codepage':
131                 data = str(torrent[name])
132             elif name == 'creation date':
133                 data = decode_datetime(torrent[name])
134             else:
135                 data = torrent[name]
136             meta.append(('.META/' + name, len(data)))
137
138     if 'private' in info:
139         meta.append(('.META/private', 1))
140
141     if 'piece length' in info:
142         meta.append(('.META/piece length', len(str(info['piece length']))))
143
144     for name, size in paths + meta:
145         print "-r--r--r-- 1 user group %d Jan 1 00:00 %s" % (size, name)
146
147
148 def mctorrent_copyout():
149     """Extract a file from the VFS"""
150
151     torrent_filename = sys.argv[3]
152     real_filename = sys.argv[4]
153     data = None
154
155     for name in 'announce', 'announce-list', 'codepage', 'comment', \
156                 'created by', 'creation date', 'encoding':
157         if torrent_filename == '.META/' + name:
158             if name in torrent:
159                 if name == 'announce-list':
160                     data = decode_list(torrent[name])
161                 elif name == 'codepage':
162                     data = str(torrent[name])
163                 elif name == 'creation date':
164                     data = decode_datetime(torrent[name])
165                 else:
166                     data = str(torrent[name])
167             else:
168                 torrent_error('Unknown ' + name)
169             break
170
171     if torrent_filename in ('.META/private', '.META/piece length'):
172         if 'info' not in torrent:
173             torrent_error('Info absent')
174         info = torrent['info']
175         if torrent_filename == '.META/private':
176             if 'private' not in info:
177                 torrent_error('Info absent')
178         if torrent_filename == '.META/piece length':
179             if 'piece length' not in info:
180                 torrent_error('Info absent')
181         data = str(info[torrent_filename[len('.META/'):]])
182
183     if not torrent_filename.startswith('.META/'):
184         data = ''
185
186     if data is None:
187         torrent_error('Unknown file name')
188     else:
189         outfile = open(real_filename, 'w')
190         outfile.write(data)
191         outfile.close()
192
193
194 def mctorrent_copyin():
195     """Put a file to the VFS"""
196     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
197
198 def mctorrent_rm():
199     """Remove a file from the VFS"""
200     sys.exit("Torrent VFS doesn't support removing files/directories (read-only filesystem)")
201
202 mctorrent_rmdir = mctorrent_rm
203
204 def mctorrent_mkdir():
205     """Create a directory in the VFS"""
206     sys.exit("Torrent VFS doesn't support creating directories (read-only filesystem)")
207
208
209 def torrent_error(error_str):
210     logger.critical("Error parsing the torrent metafile: %s", error_str)
211     sys.exit(1)
212
213 def decode_torrent():
214     try:
215         torrent_file = open(sys.argv[2], 'r')
216         data = torrent_file.read()
217         torrent_file.close()
218         return decode(data)
219     except IOError, error_str:
220         torrent_error(error_str)
221
222
223 def decode_datetime(dt):
224     from time import localtime, asctime
225     the_time = float(dt)
226     l_now = localtime(the_time)
227     return asctime(l_now)
228
229 def decode_list(announce):
230     return '\n'.join(l[0] for l in announce)
231
232
233 command = sys.argv[1]
234 procname = "mctorrent_" + command
235
236 g = globals()
237 if not g.has_key(procname):
238     logger.critical("Unknown command %s", command)
239     sys.exit(1)
240
241 torrent = decode_torrent()
242
243 try:
244     g[procname]()
245 except SystemExit:
246     raise
247 except:
248     logger.exception("Error during run")