]> git.phdru.name Git - extfs.d.git/blob - torrent
Feat(torrent): Try harder to guess encoding
[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+ just put the script in $HOME/[.local/share/].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 For mc 4.7+ run this "cd" command in the Midnight Commander (in the "bindings"
14 file the command is "%cd"): cd file/torrent://; In older versions it is
15 cd file#torrent, where "file" is the name of your torrent metafile.
16
17 See detailed installation instructions at
18 https://phdru.name/Software/mc/torrent_INSTALL.html.
19
20 The VFS lists all files and directories from the torrent metafile; all files
21 appear empty, of course, but the sizes are shown. Filenames are reencoded from
22 the metafile's encoding/codepage to the current locale.
23
24 Along with the files/directories in the torrent metafile the VFS also presents
25 meta information - in the form of files in .META directory. The size and
26 contents of these files are taken from the corresponding fields in the torrent
27 metafile. The script doesn't check if the torrent consists of a .META file or
28 directory (quite unlikely).
29
30 Date/time for all directories/files is set to the value of 'creation date'
31 field, if it exists; if not date/time is set to the last modification time of
32 the torrent file itself.
33
34 The filesystem is, naturally, read-only.
35
36 """
37
38 __version__ = "1.2.4"
39 __author__ = "Oleg Broytman <phd@phdru.name>"
40 __copyright__ = "Copyright (C) 2010-2018 PhiloSoft Design"
41 __license__ = "GPL"
42
43
44 from datetime import datetime
45 from os.path import dirname, getmtime
46 import sys
47 from time import localtime, asctime
48 from eff_bdecode import decode
49
50 try:
51     import locale
52     use_locale = True
53 except ImportError:
54     use_locale = False
55
56 if use_locale:
57     # Get the default charset.
58     try:
59         lcAll = locale.getdefaultlocale()
60     except locale.Error, err:
61         print >>sys.stderr, "WARNING:", err
62         lcAll = []
63
64     if len(lcAll) == 2:
65         default_encoding = lcAll[1]
66     else:
67         try:
68             default_encoding = locale.getpreferredencoding()
69         except locale.Error, err:
70             print >>sys.stderr, "WARNING:", err
71             default_encoding = sys.getdefaultencoding()
72 else:
73     default_encoding = sys.getdefaultencoding()
74
75 import logging
76 logger = logging.getLogger('torrent-mcextfs')
77 log_err_handler = logging.StreamHandler(sys.stderr)
78 logger.addHandler(log_err_handler)
79 logger.setLevel(logging.INFO)
80
81 if len(sys.argv) < 3:
82     logger.critical("""\
83 Torrent Virtual FileSystem for Midnight Commander version %s
84 Author: %s
85 %s
86
87 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
88 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
89                     __version__, __author__, __copyright__)
90     sys.exit(1)
91
92 locale.setlocale(locale.LC_ALL, '')
93
94
95 def mctorrent_list():
96     """List the entire VFS"""
97
98     if 'info' not in torrent:
99         torrent_error('Info absent')
100
101     info = torrent['info']
102     if 'name' not in info and 'name.utf-8' not in info:
103         torrent_error('Unknown name')
104
105     codepage = torrent.get('codepage', None)
106     encoding = torrent.get('encoding', None)
107     if not encoding and codepage:
108         encoding = str(codepage)
109
110     name = info['name']
111     name_utf8 = info.get('name.utf-8', None)
112
113     dt = None
114     if 'files' in info:
115         files = info['files']
116         paths = []
117         for file in files:
118             if 'path' not in file and 'path.utf-8' not in file:
119                 torrent_error('Unknown path')
120             if 'length' not in file:
121                 torrent_error('Unknown length')
122             if 'path.utf-8' in file:
123                 if name_utf8:
124                     path = '/'.join([name_utf8] + file['path.utf-8'])
125                     if default_encoding != 'utf-8':
126                         path = path.decode('utf-8', 'replace').encode(
127                             default_encoding, 'replace')
128                 else:
129                     _name_utf8 = name
130                     if encoding and (encoding != 'utf-8'):
131                         _name_utf8 = _name_utf8.decode(
132                             encoding, 'replace').encode('utf-8', 'replace')
133                     path = '/'.join([_name_utf8] + file['path.utf-8'])
134                     if default_encoding != 'utf-8':
135                         path = path.decode('utf-8', 'replace').encode(
136                             default_encoding, 'replace')
137             else:
138                 if name_utf8:
139                     path = file['path']
140                     if encoding and (encoding != 'utf-8'):
141                         path = path.decode(encoding, 'replace').encode(
142                             'utf-8', 'replace')
143                     path = '/'.join([name_utf8] + path)
144                     if default_encoding != 'utf-8':
145                         path = path.decode('utf-8', 'replace').encode(
146                             default_encoding, 'replace')
147                 else:
148                     path = '/'.join([name] + file['path'])
149                     if encoding and (default_encoding != encoding):
150                         path = path.decode(encoding, 'replace').encode(
151                             default_encoding, 'replace')
152             length = file['length']
153             paths.append((path, length))
154     else:  # One-file torrent
155         if 'length' not in info:
156             torrent_error('Unknown length')
157         length = info['length']
158         if name_utf8:
159             if default_encoding != 'utf-8':
160                 name = name_utf8.decode('utf-8', 'replace').encode(
161                     default_encoding, 'replace')
162         elif encoding and (default_encoding != encoding):
163             name = name.decode(encoding, 'replace').encode(
164                 default_encoding, 'replace')
165         paths = [(name, length)]
166
167     meta = []
168     for name in 'announce', 'announce-list', 'codepage', 'comment', \
169                 'created by', 'creation date', 'encoding', \
170                 'nodes', 'publisher', 'publisher-url':
171         if name == 'comment' and 'comment.utf-8' in torrent:
172             data = torrent['comment.utf-8'].decode('utf-8').encode(
173                 default_encoding, 'replace')
174             meta.append(('.META/' + name, len(data)))
175         elif name in torrent:
176             if name == 'announce-list':
177                 data = decode_announce_list(torrent[name])
178             elif name == 'codepage':
179                 data = str(torrent[name])
180             elif name == 'creation date':
181                 dt = torrent[name]
182                 data = decode_datetime_asc(dt)
183                 dt = decode_datetime(dt)
184             elif name == 'nodes':
185                 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
186                 data = '\n'.join(data)
187             else:
188                 data = torrent[name]
189             meta.append(('.META/' + name, len(data)))
190
191     if 'private' in info:
192         meta.append(('.META/private', 1))
193
194     if 'piece length' in info:
195         meta.append(('.META/piece length', len(str(info['piece length']))))
196
197     paths += meta
198     dirs = set()
199     for name, size in paths:
200         if '/' in name:
201             dirs.add(dirname(name))
202
203     if not dt:
204         dt = decode_datetime(getmtime(sys.argv[2]))
205
206     for name in sorted(dirs):
207         print "dr-xr-xr-x 1 user group 0 %s %s" % (dt, name)
208
209     for name, size in sorted(paths):
210         print "-r--r--r-- 1 user group %d %s %s" % (size, dt, name)
211
212
213 def mctorrent_copyout():
214     """Extract a file from the VFS"""
215
216     torrent_filename = sys.argv[3]
217     real_filename = sys.argv[4]
218     data = None
219
220     for name in 'announce', 'announce-list', 'codepage', 'comment', \
221                 'created by', 'creation date', 'encoding', \
222                 'nodes', 'publisher', 'publisher-url':
223         if name == 'comment' and 'comment.utf-8' in torrent:
224             data = torrent['comment.utf-8'].decode('utf-8').encode(
225                 default_encoding, 'replace')
226         elif torrent_filename == '.META/' + name:
227             if name in torrent:
228                 if name == 'announce-list':
229                     data = decode_announce_list(torrent[name])
230                 elif name == 'codepage':
231                     data = str(torrent[name])
232                 elif name == 'creation date':
233                     data = decode_datetime_asc(torrent[name])
234                 elif name == 'nodes':
235                     data = ['%s:%s' % (host, port)
236                             for host, port in torrent[name]]
237                     data = '\n'.join(data)
238                 else:
239                     data = str(torrent[name])
240             else:
241                 torrent_error('Unknown ' + name)
242             break
243
244     if torrent_filename in ('.META/private', '.META/piece length'):
245         if 'info' not in torrent:
246             torrent_error('Info absent')
247         info = torrent['info']
248         if torrent_filename == '.META/private':
249             if 'private' not in info:
250                 torrent_error('Info absent')
251         if torrent_filename == '.META/piece length':
252             if 'piece length' not in info:
253                 torrent_error('Info absent')
254         data = str(info[torrent_filename[len('.META/'):]])
255
256     if not torrent_filename.startswith('.META/'):
257         data = ''
258
259     if data is None:
260         torrent_error('Unknown file name')
261     else:
262         outfile = open(real_filename, 'w')
263         outfile.write(data)
264         outfile.close()
265
266
267 def mctorrent_copyin():
268     """Put a file to the VFS"""
269     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
270
271
272 def mctorrent_rm():
273     """Remove a file from the VFS"""
274     sys.exit("Torrent VFS doesn't support removing files/directories "
275              "(read-only filesystem)")
276
277
278 mctorrent_rmdir = mctorrent_rm
279
280
281 def mctorrent_mkdir():
282     """Create a directory in the VFS"""
283     sys.exit("Torrent VFS doesn't support creating directories "
284              "(read-only filesystem)")
285
286
287 def torrent_error(error_str):
288     logger.critical("Error parsing the torrent metafile: %s", error_str)
289     sys.exit(1)
290
291
292 def decode_torrent():
293     try:
294         torrent_file = open(sys.argv[2], 'r')
295         data = torrent_file.read()
296         torrent_file.close()
297         return decode(data)
298     except IOError, error_str:
299         torrent_error(error_str)
300
301
302 def decode_datetime_asc(dt):
303     try:
304         return asctime(localtime(float(dt)))
305     except ValueError:
306         return datetime.max.ctime()
307
308
309 def decode_datetime(dt):
310     try:
311         Y, m, d, H, M = localtime(float(dt))[0:5]
312     except ValueError:
313         return datetime.max.ctime()
314     else:
315         return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
316
317
318 def decode_announce_list(announce):
319     return '\n'.join(l[0] for l in announce if l)
320
321
322 command = sys.argv[1]
323 procname = "mctorrent_" + command
324
325 g = globals()
326 if procname not in g:
327     logger.critical("Unknown command %s", command)
328     sys.exit(1)
329
330 torrent = decode_torrent()
331
332 try:
333     g[procname]()
334 except SystemExit:
335     raise
336 except:
337     logger.exception("Error during run")