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 lcAll = locale.getdefaultlocale()
60 except locale.Error as err:
61 #print("WARNING:", err, file=sys.stderr)
65 default_encoding = lcAll[1]
68 default_encoding = locale.getpreferredencoding()
69 except locale.Error as err:
70 #print("WARNING:", err, file=sys.stderr)
71 default_encoding = sys.getdefaultencoding()
73 default_encoding = sys.getdefaultencoding()
76 logger = logging.getLogger('torrent-mcextfs')
77 log_err_handler = logging.StreamHandler(sys.stderr)
78 logger.addHandler(log_err_handler)
79 logger.setLevel(logging.INFO)
83 Torrent Virtual FileSystem for Midnight Commander version %s
87 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
88 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
89 __version__, __author__, __copyright__)
92 locale.setlocale(locale.LC_ALL, '')
96 """List the entire VFS"""
98 if 'info' not in torrent:
99 torrent_error('Info absent')
101 info = torrent['info']
102 if 'name' not in info and 'name.utf-8' not in info:
103 torrent_error('Unknown name')
105 codepage = torrent.get('codepage', None)
106 encoding = torrent.get('encoding', None)
107 if not encoding and codepage:
108 encoding = str(codepage)
111 name_utf8 = info.get('name.utf-8', None)
115 files = info['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:
124 path = '/'.join([name_utf8] + file['path.utf-8'])
127 path = '/'.join([_name_utf8] + file['path.utf-8'])
130 path = '/'.join([name_utf8] + path)
132 path = '/'.join([name] + file['path'])
133 length = file['length']
134 paths.append((path, length))
135 else: # One-file torrent
136 if 'length' not in info:
137 torrent_error('Unknown length')
138 length = info['length']
141 paths = [(name, length)]
144 for name in 'announce', 'announce-list', 'codepage', 'comment', \
145 'created by', 'creation date', 'encoding', \
146 'nodes', 'publisher', 'publisher-url':
147 if name == 'comment' and 'comment.utf-8' in torrent:
148 data = torrent['comment.utf-8']
149 meta.append(('.META/' + name, len(data)))
150 elif name in torrent:
151 if name == 'announce-list':
152 data = decode_announce_list(torrent[name])
153 elif name == 'codepage':
154 data = str(torrent[name])
155 elif name == 'creation date':
157 data = decode_datetime_asc(dt)
158 dt = decode_datetime(dt)
159 elif name == 'nodes':
160 data = ['%s:%s' % (host, port) for host, port in torrent[name]]
161 data = '\n'.join(data)
164 meta.append(('.META/' + name, len(data)))
166 if 'private' in info:
167 meta.append(('.META/private', 1))
169 if 'piece length' in info:
170 meta.append(('.META/piece length', len(str(info['piece length']))))
174 for name, size in paths:
176 dirs.add(dirname(name))
179 dt = decode_datetime(getmtime(sys.argv[2]))
181 for name in sorted(dirs):
182 print("dr-xr-xr-x 1 user group 0 %s %s" % (dt, name))
184 for name, size in sorted(paths):
185 print("-r--r--r-- 1 user group %d %s %s" % (size, dt, name))
188 def mctorrent_copyout():
189 """Extract a file from the VFS"""
191 torrent_filename = sys.argv[3]
192 real_filename = sys.argv[4]
195 for name in 'announce', 'announce-list', 'codepage', 'comment', \
196 'created by', 'creation date', 'encoding', \
197 'nodes', 'publisher', 'publisher-url':
198 if name == 'comment' and 'comment.utf-8' in torrent:
199 data = torrent['comment.utf-8']
200 elif torrent_filename == '.META/' + name:
202 if name == 'announce-list':
203 data = decode_announce_list(torrent[name])
204 elif name == 'codepage':
205 data = str(torrent[name])
206 elif name == 'creation date':
207 data = decode_datetime_asc(torrent[name])
208 elif name == 'nodes':
209 data = ['%s:%s' % (host, port)
210 for host, port in torrent[name]]
211 data = '\n'.join(data)
213 data = str(torrent[name])
215 torrent_error('Unknown ' + name)
218 if torrent_filename in ('.META/private', '.META/piece length'):
219 if 'info' not in torrent:
220 torrent_error('Info absent')
221 info = torrent['info']
222 if torrent_filename == '.META/private':
223 if 'private' not in info:
224 torrent_error('Info absent')
225 if torrent_filename == '.META/piece length':
226 if 'piece length' not in info:
227 torrent_error('Info absent')
228 data = str(info[torrent_filename[len('.META/'):]])
230 if not torrent_filename.startswith('.META/'):
234 torrent_error('Unknown file name')
236 outfile = open(real_filename, 'w')
241 def mctorrent_copyin():
242 """Put a file to the VFS"""
243 sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
247 """Remove a file from the VFS"""
248 sys.exit("Torrent VFS doesn't support removing files/directories "
249 "(read-only filesystem)")
252 mctorrent_rmdir = mctorrent_rm
255 def mctorrent_mkdir():
256 """Create a directory in the VFS"""
257 sys.exit("Torrent VFS doesn't support creating directories "
258 "(read-only filesystem)")
261 def torrent_error(error_str):
262 logger.critical("Error parsing the torrent metafile: %s", error_str)
266 def decode_torrent():
268 torrent_file = open(sys.argv[2], 'rb')
269 data = torrent_file.read()
272 except IOError as error_str:
273 torrent_error(error_str)
276 def decode_datetime_asc(dt):
278 return asctime(localtime(float(dt)))
280 return datetime.max.ctime()
283 def decode_datetime(dt):
285 Y, m, d, H, M = localtime(float(dt))[0:5]
287 return datetime.max.ctime()
289 return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
292 def decode_announce_list(announce):
293 return '\n'.join(a[0] for a in announce if a)
296 command = sys.argv[1]
297 procname = "mctorrent_" + command
300 if procname not in g:
301 logger.critical("Unknown command %s", command)
304 torrent = decode_torrent()
311 logger.exception("Error during run")