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, '')
99 """List the entire VFS"""
101 if 'info' not in torrent:
102 torrent_error('Info absent')
104 info = torrent['info']
105 if 'name' not in info and 'name.utf-8' not in info:
106 torrent_error('Unknown name')
108 codepage = torrent.get('codepage', None)
109 encoding = torrent.get('encoding', None)
110 if not encoding and codepage:
111 encoding = str(codepage)
114 name_utf8 = info.get('name.utf-8', None)
118 files = info['files']
121 if 'path' not in file and 'path.utf-8' not in file:
122 torrent_error('Unknown path')
123 if 'length' not in file:
124 torrent_error('Unknown length')
125 if 'path.utf-8' in file:
127 path = '/'.join([name_utf8] + file['path.utf-8'])
129 path = '/'.join([name] + file['path.utf-8'])
132 path = '/'.join([name_utf8] + path)
134 path = '/'.join([name] + file['path'])
135 length = file['length']
136 paths.append((path, length))
137 else: # One-file torrent
138 if 'length' not in info:
139 torrent_error('Unknown length')
140 length = info['length']
143 paths = [(name, length)]
146 for name in 'announce', 'announce-list', 'codepage', 'comment', \
147 'created by', 'creation date', 'encoding', \
148 'nodes', 'publisher', 'publisher-url':
149 if name == 'comment' and 'comment.utf-8' in torrent:
150 data = torrent['comment.utf-8']
151 meta.append(('.META/' + name, len(data)))
152 elif name in torrent:
153 if name == 'announce-list':
154 data = decode_announce_list(torrent[name])
155 elif name == 'codepage':
156 data = str(torrent[name])
157 elif name == 'creation date':
159 data = decode_datetime_asc(dt)
160 dt = decode_datetime(dt)
161 elif name == 'nodes':
162 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
163 data = '\n'.join(data)
166 meta.append(('.META/' + name, len(data)))
168 if 'private' in info:
169 meta.append(('.META/private', 1))
171 if 'piece length' in info:
172 meta.append(('.META/piece length', len(str(info['piece length']))))
176 for name, size in paths:
178 dirs.add(dirname(name))
181 dt = decode_datetime(getmtime(sys.argv[2]))
183 for name in sorted(dirs):
184 print("dr-xr-xr-x 1 user group 0 %s %s" % (dt, name))
186 for name, size in sorted(paths):
187 print("-r--r--r-- 1 user group %d %s %s" % (size, dt, name))
190 def mctorrent_copyout():
191 """Extract a file from the VFS"""
193 torrent_filename = sys.argv[3]
194 real_filename = sys.argv[4]
197 for name in 'announce', 'announce-list', 'codepage', 'comment', \
198 'created by', 'creation date', 'encoding', \
199 'nodes', 'publisher', 'publisher-url':
200 if name == 'comment' and 'comment.utf-8' in torrent:
201 data = torrent['comment.utf-8']
202 elif torrent_filename == '.META/' + name:
204 if name == 'announce-list':
205 data = decode_announce_list(torrent[name])
206 elif name == 'codepage':
207 data = str(torrent[name])
208 elif name == 'creation date':
209 data = decode_datetime_asc(torrent[name])
210 elif name == 'nodes':
211 data = ['%s:%s' % (host, port)
212 for host, port in torrent[name]]
213 data = '\n'.join(data)
215 data = str(torrent[name])
217 torrent_error('Unknown ' + name)
220 if torrent_filename in ('.META/private', '.META/piece length'):
221 if 'info' not in torrent:
222 torrent_error('Info absent')
223 info = torrent['info']
224 if torrent_filename == '.META/private':
225 if 'private' not in info:
226 torrent_error('Info absent')
227 if torrent_filename == '.META/piece length':
228 if 'piece length' not in info:
229 torrent_error('Info absent')
230 data = str(info[torrent_filename[len('.META/'):]])
232 if not torrent_filename.startswith('.META/'):
236 torrent_error('Unknown file name')
238 outfile = open(real_filename, 'w')
243 def mctorrent_copyin():
244 """Put a file to the VFS"""
245 sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
249 """Remove a file from the VFS"""
250 sys.exit("Torrent VFS doesn't support removing files/directories "
251 "(read-only filesystem)")
254 mctorrent_rmdir = mctorrent_rm
257 def mctorrent_mkdir():
258 """Create a directory in the VFS"""
259 sys.exit("Torrent VFS doesn't support creating directories "
260 "(read-only filesystem)")
263 def torrent_error(error_str):
264 logger.critical("Error parsing the torrent metafile: %s", error_str)
268 def decode_torrent():
270 torrent_file = open(sys.argv[2], 'rb')
271 data = torrent_file.read()
274 except IOError as error_str:
275 torrent_error(error_str)
278 def decode_datetime_asc(dt):
280 return asctime(localtime(float(dt)))
282 return datetime.max.ctime()
285 def decode_datetime(dt):
287 Y, m, d, H, M = localtime(float(dt))[0:5]
289 return datetime.max.ctime()
291 return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
294 def decode_announce_list(announce):
295 return '\n'.join(a[0] for a in announce if a)
298 command = sys.argv[1]
299 procname = "mctorrent_" + command
302 if procname not in g:
303 logger.critical("Unknown command %s", command)
306 torrent = decode_torrent()
313 logger.exception("Error during run")