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