1 #! /usr/bin/env python3
2 """Torrent Virtual FileSystem for Midnight Commander
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).
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.
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.
17 See detailed installation instructions at
18 https://phdru.name/Software/mc/torrent_INSTALL.html.
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.
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).
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.
34 The filesystem is, naturally, read-only.
39 __author__ = "Oleg Broytman <phd@phdru.name>"
40 __copyright__ = "Copyright (C) 2010-2023 PhiloSoft Design"
44 from datetime import datetime
45 from os.path import dirname, getmtime
47 from time import localtime, asctime
48 from eff_bdecode import decode
57 # Get the default charset.
59 if sys.version_info[:2] < (3, 11):
60 lcAll = locale.getdefaultlocale()
63 except locale.Error as err:
64 #print("WARNING:", err, file=sys.stderr)
68 default_encoding = lcAll[1]
71 default_encoding = locale.getpreferredencoding()
72 except locale.Error as err:
73 #print("WARNING:", err, file=sys.stderr)
74 default_encoding = sys.getdefaultencoding()
76 default_encoding = sys.getdefaultencoding()
79 logger = logging.getLogger('torrent-mcextfs')
80 log_err_handler = logging.StreamHandler(sys.stderr)
81 logger.addHandler(log_err_handler)
82 logger.setLevel(logging.INFO)
86 Torrent Virtual FileSystem for Midnight Commander version %s
90 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
91 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
92 __version__, __author__, __copyright__)
95 locale.setlocale(locale.LC_ALL, '')
97 PY3 = (sys.version_info[0] >= 3)
100 sys.stdout.buffer.write(s.encode(default_encoding, 'replace') + b'\n')
103 sys.stdout.write(s + '\n')
106 def mctorrent_list():
107 """List the entire VFS"""
109 info = torrent['info']
110 if 'name' not in info and 'name.utf-8' not in info:
111 torrent_error('Unknown name')
113 codepage = torrent.get('codepage', None)
114 encoding = torrent.get('encoding', None)
115 if not encoding and codepage:
116 encoding = str(codepage)
119 name_utf8 = info.get('name.utf-8', None)
123 files = info['files']
126 if 'path' not in file and 'path.utf-8' not in file:
127 torrent_error('Unknown path')
128 if 'length' not in file:
129 torrent_error('Unknown length')
130 if 'path.utf-8' in file:
132 path = '/'.join([name_utf8] + file['path.utf-8'])
134 path = '/'.join([name] + file['path.utf-8'])
137 path = '/'.join([name_utf8] + path)
139 path = '/'.join([name] + file['path'])
140 length = file['length']
141 paths.append((path, length))
142 else: # One-file torrent
143 if 'length' not in info:
144 torrent_error('Unknown length')
145 length = info['length']
148 paths = [(name, length)]
151 for name in 'announce', 'announce-list', 'codepage', 'comment', \
152 'created by', 'creation date', 'encoding', \
153 'nodes', 'publisher', 'publisher-url':
154 if name == 'comment' and 'comment.utf-8' in torrent:
155 data = torrent['comment.utf-8']
156 meta.append(('.META/' + name, len(data)))
157 elif name in torrent:
158 if name == 'announce-list':
159 data = decode_announce_list(torrent[name])
160 elif name == 'codepage':
161 data = str(torrent[name])
162 elif name == 'creation date':
164 data = decode_datetime_asc(dt)
165 dt = decode_datetime(dt)
166 elif name == 'nodes':
167 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
168 data = '\n'.join(data)
171 meta.append(('.META/' + name, len(data)))
173 if 'private' in info:
174 meta.append(('.META/private', 1))
176 if 'piece length' in info:
177 meta.append(('.META/piece length', len(str(info['piece length']))))
181 for name, size in paths:
183 dirs.add(dirname(name))
186 dt = decode_datetime(getmtime(sys.argv[2]))
188 for name in sorted(dirs):
189 output("dr-xr-xr-x 1 user group 0 %s %s" % (dt, name))
191 for name, size in sorted(paths):
192 output("-r--r--r-- 1 user group %d %s %s" % (size, dt, name))
195 def mctorrent_copyout():
196 """Extract a file from the VFS"""
198 torrent_filename = sys.argv[3]
199 real_filename = sys.argv[4]
202 for name in 'announce', 'announce-list', 'codepage', 'comment', \
203 'created by', 'creation date', 'encoding', \
204 'nodes', 'publisher', 'publisher-url':
205 if name == 'comment' and 'comment.utf-8' in torrent:
206 data = torrent['comment.utf-8']
207 elif torrent_filename == '.META/' + name:
209 if name == 'announce-list':
210 data = decode_announce_list(torrent[name])
211 elif name == 'codepage':
212 data = str(torrent[name])
213 elif name == 'creation date':
214 data = decode_datetime_asc(torrent[name])
215 elif name == 'nodes':
216 data = ['%s:%s' % (host, port)
217 for host, port in torrent[name]]
218 data = '\n'.join(data)
220 data = str(torrent[name])
222 torrent_error('Unknown ' + name)
225 if torrent_filename in ('.META/private', '.META/piece length'):
226 info = torrent['info']
227 if torrent_filename == '.META/private':
228 if 'private' not in info:
229 torrent_error('Private absent')
230 if torrent_filename == '.META/piece length':
231 if 'piece length' not in info:
232 torrent_error('Piece length absent')
233 data = str(info[torrent_filename[len('.META/'):]])
235 if not torrent_filename.startswith('.META/'):
239 torrent_error('Unknown file name')
241 outfile = open(real_filename, 'wt')
246 def mctorrent_copyin():
247 """Put a file to the VFS"""
248 sys.exit("Torrent VFS doesn't support adding/overwriting files "
249 "(read-only filesystem)")
253 """Remove a file from the VFS"""
254 sys.exit("Torrent VFS doesn't support removing files/directories "
255 "(read-only filesystem)")
258 mctorrent_rmdir = mctorrent_rm
261 def mctorrent_mkdir():
262 """Create a directory in the VFS"""
263 sys.exit("Torrent VFS doesn't support creating directories "
264 "(read-only filesystem)")
267 def torrent_error(error_str):
268 logger.critical("Error parsing the torrent metafile: %s", error_str)
272 def decode_dict(d, encoding):
276 k = k.decode(encoding)
277 if isinstance(v, dict):
278 v = decode_dict(v, encoding)
279 elif isinstance(v, list):
280 v = decode_list(v, encoding)
281 elif isinstance(v, bytes):
282 v = v.decode(encoding)
287 def decode_list(l, encoding):
290 if isinstance(v, dict):
291 v = decode_dict(v, encoding)
292 elif isinstance(v, list):
293 v = decode_list(v, encoding)
294 elif isinstance(v, bytes):
295 v = v.decode(encoding)
300 def decode_torrent():
302 torrent_file = open(sys.argv[2], 'rb')
303 data = torrent_file.read()
305 torrent = decode(data)
306 except IOError as error_str:
307 torrent_error(error_str)
309 del torrent[b'info'][b'pieces']
310 if b'info' not in torrent:
311 torrent_error('Info absent')
314 codepage = torrent.get(b'codepage', None)
315 encoding = torrent.get(b'encoding', None)
317 encoding = encoding.decode('ascii')
319 encoding = codepage.decode('ascii')
322 return decode_dict(torrent, encoding)
327 def decode_datetime_asc(dt):
329 return asctime(localtime(float(dt)))
331 return datetime.max.ctime()
334 def decode_datetime(dt):
336 Y, m, d, H, M = localtime(float(dt))[0:5]
338 return datetime.max.ctime()
340 return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
343 def decode_announce_list(announce):
344 return '\n'.join(a[0] for a in announce if a)
347 command = sys.argv[1]
348 procname = "mctorrent_" + command
351 if procname not in g:
352 logger.critical("Unknown command %s", command)
355 torrent = decode_torrent()
362 logger.exception("Error during run")