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'])
130 path = '/'.join([_name_utf8] + file['path.utf-8'])
133 path = '/'.join([name_utf8] + path)
135 path = '/'.join([name] + file['path'])
136 length = file['length']
137 paths.append((path, length))
138 else: # One-file torrent
139 if 'length' not in info:
140 torrent_error('Unknown length')
141 length = info['length']
144 paths = [(name, length)]
147 for name in 'announce', 'announce-list', 'codepage', 'comment', \
148 'created by', 'creation date', 'encoding', \
149 'nodes', 'publisher', 'publisher-url':
150 if name == 'comment' and 'comment.utf-8' in torrent:
151 data = torrent['comment.utf-8']
152 meta.append(('.META/' + name, len(data)))
153 elif name in torrent:
154 if name == 'announce-list':
155 data = decode_announce_list(torrent[name])
156 elif name == 'codepage':
157 data = str(torrent[name])
158 elif name == 'creation date':
160 data = decode_datetime_asc(dt)
161 dt = decode_datetime(dt)
162 elif name == 'nodes':
163 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
164 data = '\n'.join(data)
167 meta.append(('.META/' + name, len(data)))
169 if 'private' in info:
170 meta.append(('.META/private', 1))
172 if 'piece length' in info:
173 meta.append(('.META/piece length', len(str(info['piece length']))))
177 for name, size in paths:
179 dirs.add(dirname(name))
182 dt = decode_datetime(getmtime(sys.argv[2]))
184 for name in sorted(dirs):
185 print("dr-xr-xr-x 1 user group 0 %s %s" % (dt, name))
187 for name, size in sorted(paths):
188 print("-r--r--r-- 1 user group %d %s %s" % (size, dt, name))
191 def mctorrent_copyout():
192 """Extract a file from the VFS"""
194 torrent_filename = sys.argv[3]
195 real_filename = sys.argv[4]
198 for name in 'announce', 'announce-list', 'codepage', 'comment', \
199 'created by', 'creation date', 'encoding', \
200 'nodes', 'publisher', 'publisher-url':
201 if name == 'comment' and 'comment.utf-8' in torrent:
202 data = torrent['comment.utf-8']
203 elif torrent_filename == '.META/' + name:
205 if name == 'announce-list':
206 data = decode_announce_list(torrent[name])
207 elif name == 'codepage':
208 data = str(torrent[name])
209 elif name == 'creation date':
210 data = decode_datetime_asc(torrent[name])
211 elif name == 'nodes':
212 data = ['%s:%s' % (host, port)
213 for host, port in torrent[name]]
214 data = '\n'.join(data)
216 data = str(torrent[name])
218 torrent_error('Unknown ' + name)
221 if torrent_filename in ('.META/private', '.META/piece length'):
222 if 'info' not in torrent:
223 torrent_error('Info absent')
224 info = torrent['info']
225 if torrent_filename == '.META/private':
226 if 'private' not in info:
227 torrent_error('Info absent')
228 if torrent_filename == '.META/piece length':
229 if 'piece length' not in info:
230 torrent_error('Info absent')
231 data = str(info[torrent_filename[len('.META/'):]])
233 if not torrent_filename.startswith('.META/'):
237 torrent_error('Unknown file name')
239 outfile = open(real_filename, 'w')
244 def mctorrent_copyin():
245 """Put a file to the VFS"""
246 sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
250 """Remove a file from the VFS"""
251 sys.exit("Torrent VFS doesn't support removing files/directories "
252 "(read-only filesystem)")
255 mctorrent_rmdir = mctorrent_rm
258 def mctorrent_mkdir():
259 """Create a directory in the VFS"""
260 sys.exit("Torrent VFS doesn't support creating directories "
261 "(read-only filesystem)")
264 def torrent_error(error_str):
265 logger.critical("Error parsing the torrent metafile: %s", error_str)
269 def decode_torrent():
271 torrent_file = open(sys.argv[2], 'rb')
272 data = torrent_file.read()
275 except IOError as error_str:
276 torrent_error(error_str)
279 def decode_datetime_asc(dt):
281 return asctime(localtime(float(dt)))
283 return datetime.max.ctime()
286 def decode_datetime(dt):
288 Y, m, d, H, M = localtime(float(dt))[0:5]
290 return datetime.max.ctime()
292 return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
295 def decode_announce_list(announce):
296 return '\n'.join(a[0] for a in announce if a)
299 command = sys.argv[1]
300 procname = "mctorrent_" + command
303 if procname not in g:
304 logger.critical("Unknown command %s", command)
307 torrent = decode_torrent()
314 logger.exception("Error during run")