]> git.phdru.name Git - extfs.d.git/blob - torrent
a6685f0d3165d8627d611fa4105e4043ab5f5076
[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.3"
39 __author__ = "Oleg Broytman <phd@phdru.name>"
40 __copyright__ = "Copyright (C) 2010-2015 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 )
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(default_encoding, 'replace')
127                 else:
128                     _name_utf8 = name
129                     if encoding and (encoding != 'utf-8'):
130                         _name_utf8 = _name_utf8.decode(encoding, 'replace').encode('utf-8', 'replace')
131                     path = '/'.join([_name_utf8] + file['path.utf-8'])
132                     if default_encoding != 'utf-8':
133                         path = path.decode('utf-8', 'replace').encode(default_encoding, 'replace')
134             else:
135                 if name_utf8:
136                     path = file['path']
137                     if encoding and (encoding != 'utf-8'):
138                         path = path.decode(encoding, 'replace').encode('utf-8', 'replace')
139                     path = '/'.join([name_utf8] + path)
140                     if default_encoding != 'utf-8':
141                         path = path.decode('utf-8', 'replace').encode(default_encoding, 'replace')
142                 else:
143                     path = '/'.join([name] + file['path'])
144                     if encoding and (default_encoding != encoding):
145                         path = path.decode(encoding, 'replace').encode(default_encoding, 'replace')
146             length = file['length']
147             paths.append((path, length))
148     else:  # One-file torrent
149         if 'length' not in info:
150             torrent_error('Unknown length')
151         length = info['length']
152         if name_utf8:
153             if default_encoding != 'utf-8':
154                 name = name_utf8.decode('utf-8', 'replace').encode(default_encoding, 'replace')
155         elif encoding and (default_encoding != encoding):
156             name = name.decode(encoding, 'replace').encode(default_encoding, 'replace')
157         paths = [(name, length)]
158
159     meta = []
160     for name in 'announce', 'announce-list', 'codepage', 'comment', \
161                 'created by', 'creation date', 'encoding', \
162                 'nodes', 'publisher', 'publisher-url':
163         if name == 'comment' and 'comment.utf-8' in torrent:
164             data = torrent['comment.utf-8'].decode('utf-8').encode(default_encoding, 'replace')
165             meta.append(('.META/' + name, len(data)))
166         elif name in torrent:
167             if name == 'announce-list':
168                 data = decode_announce_list(torrent[name])
169             elif name == 'codepage':
170                 data = str(torrent[name])
171             elif name == 'creation date':
172                 dt = torrent[name]
173                 data = decode_datetime_asc(dt)
174                 dt = decode_datetime(dt)
175             elif name == 'nodes':
176                 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
177                 data = '\n'.join(data)
178             else:
179                 data = torrent[name]
180             meta.append(('.META/' + name, len(data)))
181
182     if 'private' in info:
183         meta.append(('.META/private', 1))
184
185     if 'piece length' in info:
186         meta.append(('.META/piece length', len(str(info['piece length']))))
187
188     paths += meta
189     dirs = set()
190     for name, size in paths:
191         if '/' in name:
192             dirs.add(dirname(name))
193
194     if not dt:
195         dt = decode_datetime(getmtime(sys.argv[2]))
196
197     for name in sorted(dirs):
198         print "dr-xr-xr-x 1 user group 0 %s %s" % (dt, name)
199
200     for name, size in sorted(paths):
201         print "-r--r--r-- 1 user group %d %s %s" % (size, dt, name)
202
203
204 def mctorrent_copyout():
205     """Extract a file from the VFS"""
206
207     torrent_filename = sys.argv[3]
208     real_filename = sys.argv[4]
209     data = None
210
211     for name in 'announce', 'announce-list', 'codepage', 'comment', \
212                 'created by', 'creation date', 'encoding', \
213                 'nodes', 'publisher', 'publisher-url':
214         if name == 'comment' and 'comment.utf-8' in torrent:
215             data = torrent['comment.utf-8'].decode('utf-8').encode(default_encoding, 'replace')
216         elif torrent_filename == '.META/' + name:
217             if name in torrent:
218                 if name == 'announce-list':
219                     data = decode_announce_list(torrent[name])
220                 elif name == 'codepage':
221                     data = str(torrent[name])
222                 elif name == 'creation date':
223                     data = decode_datetime_asc(torrent[name])
224                 elif name == 'nodes':
225                     data = ['%s:%s' % (host, port) for host, port in torrent[name]]
226                     data = '\n'.join(data)
227                 else:
228                     data = str(torrent[name])
229             else:
230                 torrent_error('Unknown ' + name)
231             break
232
233     if torrent_filename in ('.META/private', '.META/piece length'):
234         if 'info' not in torrent:
235             torrent_error('Info absent')
236         info = torrent['info']
237         if torrent_filename == '.META/private':
238             if 'private' not in info:
239                 torrent_error('Info absent')
240         if torrent_filename == '.META/piece length':
241             if 'piece length' not in info:
242                 torrent_error('Info absent')
243         data = str(info[torrent_filename[len('.META/'):]])
244
245     if not torrent_filename.startswith('.META/'):
246         data = ''
247
248     if data is None:
249         torrent_error('Unknown file name')
250     else:
251         outfile = open(real_filename, 'w')
252         outfile.write(data)
253         outfile.close()
254
255
256 def mctorrent_copyin():
257     """Put a file to the VFS"""
258     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
259
260 def mctorrent_rm():
261     """Remove a file from the VFS"""
262     sys.exit("Torrent VFS doesn't support removing files/directories (read-only filesystem)")
263
264 mctorrent_rmdir = mctorrent_rm
265
266 def mctorrent_mkdir():
267     """Create a directory in the VFS"""
268     sys.exit("Torrent VFS doesn't support creating directories (read-only filesystem)")
269
270
271 def torrent_error(error_str):
272     logger.critical("Error parsing the torrent metafile: %s", error_str)
273     sys.exit(1)
274
275 def decode_torrent():
276     try:
277         torrent_file = open(sys.argv[2], 'r')
278         data = torrent_file.read()
279         torrent_file.close()
280         return decode(data)
281     except IOError, error_str:
282         torrent_error(error_str)
283
284
285 def decode_datetime_asc(dt):
286     return asctime(localtime(float(dt)))
287
288 def decode_datetime(dt):
289     Y, m, d, H, M = localtime(float(dt))[0:5]
290     return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
291
292 def decode_announce_list(announce):
293     return '\n'.join(l[0] for l in announce)
294
295
296 command = sys.argv[1]
297 procname = "mctorrent_" + command
298
299 g = globals()
300 if not g.has_key(procname):
301     logger.critical("Unknown command %s", command)
302     sys.exit(1)
303
304 torrent = decode_torrent()
305
306 try:
307     g[procname]()
308 except SystemExit:
309     raise
310 except:
311     logger.exception("Error during run")