]> git.phdru.name Git - extfs.d.git/blob - torrent
Fix(locale): Do not call `locale.getdefaultlocale()` under Python 3.11+
[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                     _name_utf8 = name
130                     path = '/'.join([_name_utf8] + file['path.utf-8'])
131             else:
132                 if name_utf8:
133                     path = '/'.join([name_utf8] + path)
134                 else:
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']
142         if name_utf8:
143             name = name_utf8
144         paths = [(name, length)]
145
146     meta = []
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':
159                 dt = torrent[name]
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)
165             else:
166                 data = torrent[name]
167             meta.append(('.META/' + name, len(data)))
168
169     if 'private' in info:
170         meta.append(('.META/private', 1))
171
172     if 'piece length' in info:
173         meta.append(('.META/piece length', len(str(info['piece length']))))
174
175     paths += meta
176     dirs = set()
177     for name, size in paths:
178         if '/' in name:
179             dirs.add(dirname(name))
180
181     if not dt:
182         dt = decode_datetime(getmtime(sys.argv[2]))
183
184     for name in sorted(dirs):
185         print("dr-xr-xr-x 1 user group 0 %s %s" % (dt, name))
186
187     for name, size in sorted(paths):
188         print("-r--r--r-- 1 user group %d %s %s" % (size, dt, name))
189
190
191 def mctorrent_copyout():
192     """Extract a file from the VFS"""
193
194     torrent_filename = sys.argv[3]
195     real_filename = sys.argv[4]
196     data = None
197
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:
204             if name in torrent:
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)
215                 else:
216                     data = str(torrent[name])
217             else:
218                 torrent_error('Unknown ' + name)
219             break
220
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/'):]])
232
233     if not torrent_filename.startswith('.META/'):
234         data = ''
235
236     if data is None:
237         torrent_error('Unknown file name')
238     else:
239         outfile = open(real_filename, 'w')
240         outfile.write(data)
241         outfile.close()
242
243
244 def mctorrent_copyin():
245     """Put a file to the VFS"""
246     sys.exit("Torrent VFS doesn't support adding files (read-only filesystem)")
247
248
249 def mctorrent_rm():
250     """Remove a file from the VFS"""
251     sys.exit("Torrent VFS doesn't support removing files/directories "
252              "(read-only filesystem)")
253
254
255 mctorrent_rmdir = mctorrent_rm
256
257
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)")
262
263
264 def torrent_error(error_str):
265     logger.critical("Error parsing the torrent metafile: %s", error_str)
266     sys.exit(1)
267
268
269 def decode_torrent():
270     try:
271         torrent_file = open(sys.argv[2], 'rb')
272         data = torrent_file.read()
273         torrent_file.close()
274         return decode(data)
275     except IOError as error_str:
276         torrent_error(error_str)
277
278
279 def decode_datetime_asc(dt):
280     try:
281         return asctime(localtime(float(dt)))
282     except ValueError:
283         return datetime.max.ctime()
284
285
286 def decode_datetime(dt):
287     try:
288         Y, m, d, H, M = localtime(float(dt))[0:5]
289     except ValueError:
290         return datetime.max.ctime()
291     else:
292         return "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
293
294
295 def decode_announce_list(announce):
296     return '\n'.join(a[0] for a in announce if a)
297
298
299 command = sys.argv[1]
300 procname = "mctorrent_" + command
301
302 g = globals()
303 if procname not in g:
304     logger.critical("Unknown command %s", command)
305     sys.exit(1)
306
307 torrent = decode_torrent()
308
309 try:
310     g[procname]()
311 except SystemExit:
312     raise
313 except Exception:
314     logger.exception("Error during run")