]> git.phdru.name Git - extfs.d.git/blob - torrent
Refactor(torrent): Use `name` directly
[extfs.d.git] / torrent
1 #! /usr/bin/env python3
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 https://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.3.0"
39 __author__ = "Oleg Broytman <phd@phdru.name>"
40 __copyright__ = "Copyright (C) 2010-2023 PhiloSoft Design"
41 __license__ = "GPL"
42
43
44 from datetime import datetime
45 from os.path import dirname, getmtime
46 import sys
47 from time import localtime, asctime
48 from eff_bdecode import decode
49
50 try:
51     import locale
52     use_locale = True
53 except ImportError:
54     use_locale = False
55
56 if use_locale:
57     # Get the default charset.
58     try:
59         if sys.version_info[:2] < (3, 11):
60             lcAll = locale.getdefaultlocale()
61         else:
62             lcAll = []
63     except locale.Error as err:
64         #print("WARNING:", err, file=sys.stderr)
65         lcAll = []
66
67     if len(lcAll) == 2:
68         default_encoding = lcAll[1]
69     else:
70         try:
71             default_encoding = locale.getpreferredencoding()
72         except locale.Error as err:
73             #print("WARNING:", err, file=sys.stderr)
74             default_encoding = sys.getdefaultencoding()
75 else:
76     default_encoding = sys.getdefaultencoding()
77
78 import logging
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)
83
84 if len(sys.argv) < 3:
85     logger.critical("""\
86 Torrent Virtual FileSystem for Midnight Commander version %s
87 Author: %s
88 %s
89
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__)
93     sys.exit(1)
94
95 locale.setlocale(locale.LC_ALL, '')
96
97
98 def mctorrent_list():
99     """List the entire VFS"""
100
101     if 'info' not in torrent:
102         torrent_error('Info absent')
103
104     info = torrent['info']
105     if 'name' not in info and 'name.utf-8' not in info:
106         torrent_error('Unknown name')
107
108     codepage = torrent.get('codepage', None)
109     encoding = torrent.get('encoding', None)
110     if not encoding and codepage:
111         encoding = str(codepage)
112
113     name = info['name']
114     name_utf8 = info.get('name.utf-8', None)
115
116     dt = None
117     if 'files' in info:
118         files = info['files']
119         paths = []
120         for file in 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:
126                 if name_utf8:
127                     path = '/'.join([name_utf8] + file['path.utf-8'])
128                 else:
129                     path = '/'.join([name] + file['path.utf-8'])
130             else:
131                 if name_utf8:
132                     path = '/'.join([name_utf8] + path)
133                 else:
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']
141         if name_utf8:
142             name = name_utf8
143         paths = [(name, length)]
144
145     meta = []
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':
158                 dt = torrent[name]
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)
164             else:
165                 data = torrent[name]
166             meta.append(('.META/' + name, len(data)))
167
168     if 'private' in info:
169         meta.append(('.META/private', 1))
170
171     if 'piece length' in info:
172         meta.append(('.META/piece length', len(str(info['piece length']))))
173
174     paths += meta
175     dirs = set()
176     for name, size in paths:
177         if '/' in name:
178             dirs.add(dirname(name))
179
180     if not dt:
181         dt = decode_datetime(getmtime(sys.argv[2]))
182
183     for name in sorted(dirs):
184         print("dr-xr-xr-x 1 user group 0 %s %s" % (dt, name))
185
186     for name, size in sorted(paths):
187         print("-r--r--r-- 1 user group %d %s %s" % (size, dt, name))
188
189
190 def mctorrent_copyout():
191     """Extract a file from the VFS"""
192
193     torrent_filename = sys.argv[3]
194     real_filename = sys.argv[4]
195     data = None
196
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:
203             if name in torrent:
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)
214                 else:
215                     data = str(torrent[name])
216             else:
217                 torrent_error('Unknown ' + name)
218             break
219
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/'):]])
231
232     if not torrent_filename.startswith('.META/'):
233         data = ''
234
235     if data is None:
236         torrent_error('Unknown file name')
237     else:
238         outfile = open(real_filename, 'w')
239         outfile.write(data)
240         outfile.close()
241
242
243 def mctorrent_copyin():
244     """Put a file to the VFS"""
245     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
246
247
248 def mctorrent_rm():
249     """Remove a file from the VFS"""
250     sys.exit("Torrent VFS doesn't support removing files/directories "
251              "(read-only filesystem)")
252
253
254 mctorrent_rmdir = mctorrent_rm
255
256
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)")
261
262
263 def torrent_error(error_str):
264     logger.critical("Error parsing the torrent metafile: %s", error_str)
265     sys.exit(1)
266
267
268 def decode_torrent():
269     try:
270         torrent_file = open(sys.argv[2], 'rb')
271         data = torrent_file.read()
272         torrent_file.close()
273         return decode(data)
274     except IOError as error_str:
275         torrent_error(error_str)
276
277
278 def decode_datetime_asc(dt):
279     try:
280         return asctime(localtime(float(dt)))
281     except ValueError:
282         return datetime.max.ctime()
283
284
285 def decode_datetime(dt):
286     try:
287         Y, m, d, H, M = localtime(float(dt))[0:5]
288     except ValueError:
289         return datetime.max.ctime()
290     else:
291         return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
292
293
294 def decode_announce_list(announce):
295     return '\n'.join(a[0] for a in announce if a)
296
297
298 command = sys.argv[1]
299 procname = "mctorrent_" + command
300
301 g = globals()
302 if procname not in g:
303     logger.critical("Unknown command %s", command)
304     sys.exit(1)
305
306 torrent = decode_torrent()
307
308 try:
309     g[procname]()
310 except SystemExit:
311     raise
312 except Exception:
313     logger.exception("Error during run")