]> git.phdru.name Git - extfs.d.git/blob - xml
Fix torrent VFS: announce sublist can be empty
[extfs.d.git] / xml
1 #! /usr/bin/env python
2 """XML 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
7 For mc 4.7+ just put the script in $HOME/[.local/share/].mc/extfs.d.
8 For older versions put it in /usr/[local/][lib|share]/mc/extfs
9 and add a line "xml" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini.
10 Make the script executable.
11
12 For mc 4.7+ run this "cd" command in the Midnight Commander (in the "bindings"
13 file the command is "%cd"): cd file/xml://; in older versions it is
14 cd file#xml, where "file" is the name of your XML file.
15
16 See detailed installation instructions at
17 http://phdru.name/Software/mc/xml_INSTALL.html.
18
19 The VFS represents tags as directories; the directories are numbered to
20 distinguish tags with the same name; numbering also helps to sort tags by their
21 order in XML instead of sorting them by name and prevents name clash when tag
22 names coincide with the names of special files used by XML VFS. Attributes,
23 text nodes and comments are represented as text files; attributes are shown in
24 a file named "attributes", attributes are listed in the file as name=value
25 lines (I deliberately ignore a small chance of newline characters in values);
26 names and values are reencoded to the console encoding. Text nodes and comments
27 are collected in a file named "text", stripped and reencoded. The filesystem is
28 read-only.
29
30 Date/time for all directories/files set to the last modification time of the
31 XML file.
32
33 Implementation based on minidom doesn't understand namespaces, it just shows
34 them among other attributes. ElementTree-based implementation doesn't show
35 namespaces at all. Implementation based on lxml.etree shows namespaces in a
36 separate file "namespaces".
37
38 It is useful to have a top-down view on an XML structure but it's especially
39 convenient to extract text values from tags. One can get, for example, a
40 base64-encoded image - just walk down the VFS to the tag's directory and copy
41 its text file to a real file.
42
43 The VFS was inspired by a FUSE xmlfs: https://github.com/halhen/xmlfs
44
45 """
46
47 __version__ = "1.1.5"
48 __author__ = "Oleg Broytman <phd@phdru.name>"
49 __copyright__ = "Copyright (C) 2013-2015 PhiloSoft Design"
50 __license__ = "GPL"
51
52 # Can be None for default choice, 'lxml', 'elementtree' or 'minidom'.
53 force_implementation = None
54
55 use_minidom = True
56 use_elementtree = False
57 use_lxml = False
58
59 import math
60 from os.path import getmtime
61 import sys
62 from time import localtime
63 import xml.dom.minidom
64
65 try:
66     import xml.etree.ElementTree as ET
67 except ImportError:
68     pass
69 else:
70     use_elementtree = True
71
72 try:
73     import lxml.etree as etree
74 except ImportError:
75     pass
76 else:
77     use_lxml = True
78
79 try:
80     import locale
81     use_locale = True
82 except ImportError:
83     use_locale = False
84
85 if use_locale:
86     # Get the default charset.
87     try:
88         lcAll = locale.getdefaultlocale()
89     except locale.Error, err:
90         print >>sys.stderr, "WARNING:", err
91         lcAll = []
92
93     if len(lcAll) == 2:
94         default_encoding = lcAll[1]
95     else:
96         try:
97             default_encoding = locale.getpreferredencoding()
98         except locale.Error, err:
99             print >>sys.stderr, "WARNING:", err
100             default_encoding = sys.getdefaultencoding()
101 else:
102     default_encoding = sys.getdefaultencoding()
103
104 import logging
105 logger = logging.getLogger('xml-mcextfs')
106 log_err_handler = logging.StreamHandler(sys.stderr)
107 logger.addHandler(log_err_handler)
108 logger.setLevel(logging.INFO)
109
110 if len(sys.argv) < 3:
111     logger.critical("""\
112 XML Virtual FileSystem for Midnight Commander version %s
113 Author: %s
114 %s
115
116 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
117 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
118                     __version__, __author__, __copyright__)
119     sys.exit(1)
120
121
122 locale.setlocale(locale.LC_ALL, '')
123
124
125 class XmlVfs(object):
126     """Abstract base class"""
127
128     supports_namespaces = False
129
130     def __init__(self):
131         self.xml_file = sys.argv[2]
132         self.parse()
133
134     def list(self):
135         Y, m, d, H, M = localtime(getmtime(self.xml_file))[0:5]
136         self.xml_file_dt = "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
137
138         root_comments = self.get_root_comments()
139         if root_comments:
140             print "-r--r--r-- 1 user group %d %s text" % (
141                 len(root_comments), self.xml_file_dt)
142
143         self._list(self.getroot())
144
145     def _list(self, node, path=''):
146         n = len(self.getchildren(node))
147         if n:
148             width = int(math.log10(n)) + 1
149             template = "%%0%dd" % width
150         else:
151             template = "%d"
152         n = 0
153         for element in self.getchildren(node):
154             if not self.istag(element):
155                 continue
156             n += 1
157             tag = self.getlocalname(self.gettag(element))
158             if path:
159                 subpath = '%s/%s %s' % (path, template % n, tag)
160             else:
161                 subpath = '%s %s' % (template % n, tag)
162             subpath_encoded = subpath.encode(default_encoding, "replace")
163             print "dr-xr-xr-x 1 user group 0 %s %s" % (
164                 self.xml_file_dt, subpath_encoded)
165             if self.getattrs(element):
166                 attr_text = self.attrs2text(element)
167                 print "-r--r--r-- 1 user group %d %s %s/attributes" % (
168                     len(attr_text), self.xml_file_dt, subpath_encoded)
169             if self.supports_namespaces and self.has_ns(element):
170                 ns_text = self.ns2text(element)
171                 print "-r--r--r-- 1 user group %d %s %s/namespaces" % (
172                     len(ns_text), self.xml_file_dt, subpath_encoded)
173             text = self.collect_text(element)
174             if text:
175                 print "-r--r--r-- 1 user group %d %s %s/text" % (
176                     len(text), self.xml_file_dt, subpath_encoded)
177             self._list(element, subpath)
178
179     def get_tag_node(self, node, i):
180         n = 0
181         for element in self.getchildren(node):
182             if self.istag(element):
183                 n += 1
184                 if n == i:
185                     return element
186         xml_error('There are less than %d nodes' % i)
187
188     def attrs2text(self, node):
189         attr_accumulator = []
190         for name, value in self.getattrs(node):
191             name = self.getlocalname(name).encode(default_encoding, "replace")
192             value = value.encode(default_encoding, "replace")
193             attr_accumulator.append("%s=%s" % (name, value))
194         return '\n'.join(attr_accumulator)
195
196     def has_ns(self, node):
197         return False
198
199
200 class MiniDOMXmlVfs(XmlVfs):
201     def parse(self):
202         self.document = xml.dom.minidom.parse(self.xml_file)
203
204     def getattrs(self, node):
205         attrs = node.attributes
206         attrs = [attrs.item(i) for i in range(attrs.length)]
207         return [(a.name, a.value) for a in attrs]
208
209     def collect_text(self, node):
210         text_accumulator = []
211         for element in node.childNodes:
212             if element.localName:
213                 continue
214             elif element.nodeType == element.COMMENT_NODE:
215                 text = u"<!--%s-->" % element.nodeValue
216             elif element.nodeType == element.TEXT_NODE:
217                 text = element.nodeValue.strip()
218             else:
219                 xml_error("Unknown node type %d" % element.nodeType)
220             if text:
221                 text_accumulator.append(text)
222         return '\n'.join(text_accumulator).encode(default_encoding, "replace")
223
224     def getroot(self):
225         return self.document
226
227     def get_root_comments(self):
228         return self.collect_text(self.document)
229
230     def getchildren(self, node):
231         return node.childNodes
232
233     def gettag(self, node):
234         return node.localName
235
236     def istag(self, node):
237         return bool(node.localName)
238
239     def getlocalname(self, name):
240         return name
241
242
243 if use_elementtree or use_lxml:
244     class CommonEtreeXmlVfs(XmlVfs):
245         def getattrs(self, node):
246             return node.attrib.items()
247
248         def collect_text(self, node):
249             text_accumulator = []
250             if node.text:
251                 text = node.text.strip()
252                 if text:
253                     text_accumulator.append(text)
254             for element in node:
255                 if not self.istag(element):
256                     text = u"<!--%s-->" % element.text
257                     text_accumulator.append(text)
258             if node.tail:
259                 text = node.tail.strip()
260                 if text:
261                     text_accumulator.append(text)
262             return '\n'.join(text_accumulator).encode(
263                 default_encoding, "replace")
264
265         def getchildren(self, node):
266             return list(node)
267
268         def gettag(self, node):
269             return node.tag
270
271         def istag(self, node):
272             return isinstance(node.tag, basestring)
273
274
275 if use_elementtree:
276     class ElementTreeXmlVfs(CommonEtreeXmlVfs):
277         def parse(self):
278             # Copied from http://effbot.org/zone/element-pi.htm
279
280             class PIParser(ET.XMLTreeBuilder):
281
282                 def __init__(self):
283                     ET.XMLTreeBuilder.__init__(self)
284                     # assumes ElementTree 1.2.X
285                     self._parser.CommentHandler = self.handle_comment
286                     self._parser.ProcessingInstructionHandler = self.handle_pi
287                     self._target.start("document", {})
288
289                 def close(self):
290                     self._target.end("document")
291                     return ET.XMLTreeBuilder.close(self)
292
293                 def handle_comment(self, data):
294                     self._target.start(ET.Comment, {})
295                     self._target.data(data)
296                     self._target.end(ET.Comment)
297
298                 def handle_pi(self, target, data):
299                     self._target.start(ET.PI, {})
300                     self._target.data(target + " " + data)
301                     self._target.end(ET.PI)
302
303             self.document = ET.parse(self.xml_file, PIParser())
304
305         def getroot(self):
306             return self.document.getroot()
307
308         def get_root_comments(self):
309             text_accumulator = []
310             for element in self.getroot():
311                 if not self.istag(element):
312                     text = u"<!--%s-->" % element.text
313                     text_accumulator.append(text)
314             return '\n'.join(text_accumulator).encode(
315                 default_encoding, "replace")
316
317         def getlocalname(self, name):
318             if name.startswith('{'):
319                 name = name.split('}', 1)[1]  # Remove XML namespace
320             return name
321
322
323 if use_lxml:
324     class LxmlEtreeXmlVfs(CommonEtreeXmlVfs):
325         supports_namespaces = True
326
327         def parse(self):
328             self.document = etree.parse(self.xml_file)
329
330         def getroot(self):
331             return [self.document.getroot()]
332
333         def get_root_comments(self):
334             text_accumulator = []
335             for element in self.document.getroot().itersiblings(
336                     tag=etree.Comment, preceding=True):
337                 text = u"<!--%s-->" % element.text
338                 text_accumulator.append(text)
339             return '\n'.join(text_accumulator).encode(
340                 default_encoding, "replace")
341
342         def getlocalname(self, name):
343             return etree.QName(name).localname
344
345         def _get_local_ns(self, node):
346             this_nsmap = node.nsmap
347             parent = node.getparent()
348             if parent is not None:
349                 parent_nsmap = parent.nsmap
350                 for key in parent_nsmap:
351                     if this_nsmap[key] == parent_nsmap[key]:
352                         del this_nsmap[key]
353             return this_nsmap
354
355         def has_ns(self, node):
356             return bool(self._get_local_ns(node))
357
358         def ns2text(self, node):
359             ns_accumulator = []
360             for name, value in self._get_local_ns(node).items():
361                 if name:
362                     name = name.encode(default_encoding, "replace")
363                 else:
364                     name = 'xmlns'
365                 value = value.encode(default_encoding, "replace")
366                 ns_accumulator.append("%s=%s" % (name, value))
367             return '\n'.join(ns_accumulator)
368
369
370 def build_xmlvfs():
371     if force_implementation is None:
372         if use_lxml:
373             return LxmlEtreeXmlVfs()
374         elif use_elementtree:
375             return ElementTreeXmlVfs()
376         else:
377             return MiniDOMXmlVfs()
378     elif force_implementation == 'minidom':
379         return MiniDOMXmlVfs()
380     elif force_implementation == 'elementtree':
381         return ElementTreeXmlVfs()
382     elif force_implementation == 'lxml':
383         return LxmlEtreeXmlVfs()
384     else:
385         raise ValueError('Unknown implementation "%s", expected "minidom", '
386                          '"elementtree" or "lxml"' % force_implementation)
387
388
389 def mcxml_list():
390     """List the entire VFS"""
391
392     xmlvfs = build_xmlvfs()
393     xmlvfs.list()
394
395
396 def mcxml_copyout():
397     """Extract a file from the VFS"""
398
399     xmlvfs = build_xmlvfs()
400     xml_filename = sys.argv[3]
401     real_filename = sys.argv[4]
402
403     node = xmlvfs.getroot()
404     for path_comp in xml_filename.split('/'):
405         if ' ' in path_comp:
406             i = int(path_comp.split(' ', 1)[0])
407             node = xmlvfs.get_tag_node(node, i)
408         elif path_comp in ('attributes', 'namespaces', 'text'):
409             break
410         else:
411             xml_error('Unknown file')
412
413     if path_comp == 'attributes':
414         if xmlvfs.getattrs(node):
415             text = xmlvfs.attrs2text(node)
416         else:
417             xml_error('There are no attributes')
418
419     elif path_comp == 'namespaces':
420         if xmlvfs.supports_namespaces and xmlvfs.has_ns(node):
421             text = xmlvfs.ns2text(node)
422         else:
423             xml_error('There are no namespaces')
424
425     elif path_comp == 'text':
426         if '/' in xml_filename:
427             text = xmlvfs.collect_text(node)
428         else:
429             text = xmlvfs.get_root_comments()
430
431     else:
432         xml_error('Unknown file')
433
434     outfile = open(real_filename, 'w')
435     outfile.write(text)
436     outfile.close()
437
438
439 def mcxml_copyin():
440     """Put a file to the VFS"""
441     sys.exit("XML VFS doesn't support adding files (read-only filesystem)")
442
443
444 def mcxml_rm():
445     """Remove a file from the VFS"""
446     sys.exit("XML VFS doesn't support removing files/directories "
447              "(read-only filesystem)")
448
449 mcxml_rmdir = mcxml_rm
450
451
452 def mcxml_mkdir():
453     """Create a directory in the VFS"""
454     sys.exit("XML VFS doesn't support creating directories "
455              "(read-only filesystem)")
456
457
458 def xml_error(error_str):
459     logger.critical("Error walking XML file: %s", error_str)
460     sys.exit(1)
461
462 command = sys.argv[1]
463 procname = "mcxml_" + command
464
465 g = globals()
466 if procname not in g:
467     logger.critical("Unknown command %s", command)
468     sys.exit(1)
469
470 try:
471     g[procname]()
472 except SystemExit:
473     raise
474 except:
475     logger.exception("Error during run")