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