f7dc718b71f1e08f08a7c66c08012e3c09018066
[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 force_implementation = None  # Can be None for default choice,
50                              # 'lxml', 'elementtree' or 'minidom'
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 )
117     sys.exit(1)
118
119
120 locale.setlocale(locale.LC_ALL, '')
121
122
123 class XmlVfs(object):
124     """Abstract base class"""
125
126     supports_namespaces = False
127
128     def __init__(self):
129         self.xml_file = sys.argv[2]
130         self.parse()
131
132     def list(self):
133         Y, m, d, H, M = localtime(getmtime(self.xml_file))[0:5]
134         self.xml_file_dt = "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
135
136         root_comments = self.get_root_comments()
137         if root_comments:
138             print "-r--r--r-- 1 user group %d %s text" % (
139                 len(root_comments), self.xml_file_dt)
140
141         self._list(self.getroot())
142
143     def _list(self, node, path=''):
144         n = len(self.getchildren(node))
145         if n:
146             width = int(math.log10(n)) + 1
147             template = "%%0%dd" % width
148         else:
149             template = "%d"
150         n = 0
151         for element in self.getchildren(node):
152             if not self.istag(element):
153                 continue
154             n += 1
155             tag = self.getlocalname(self.gettag(element))
156             if path:
157                 subpath = '%s/%s %s' % (path, template % n, tag)
158             else:
159                 subpath = '%s %s' % (template % n, tag)
160             subpath_encoded = subpath.encode(default_encoding, "replace")
161             print "dr-xr-xr-x 1 user group 0 %s %s" % (
162                 self.xml_file_dt, subpath_encoded)
163             if self.getattrs(element):
164                 attr_text = self.attrs2text(element)
165                 print "-r--r--r-- 1 user group %d %s %s/attributes" % (
166                     len(attr_text), self.xml_file_dt, subpath_encoded)
167             if self.supports_namespaces and self.has_ns(element):
168                 ns_text = self.ns2text(element)
169                 print "-r--r--r-- 1 user group %d %s %s/namespaces" % (
170                     len(ns_text), self.xml_file_dt, subpath_encoded)
171             text = self.collect_text(element)
172             if text:
173                 print "-r--r--r-- 1 user group %d %s %s/text" % (
174                     len(text), self.xml_file_dt, subpath_encoded)
175             self._list(element, subpath)
176
177     def get_tag_node(self, node, i):
178         n = 0
179         for element in self.getchildren(node):
180             if self.istag(element):
181                 n += 1
182                 if n == i:
183                     return element
184         xml_error('There are less than %d nodes' % i)
185
186     def attrs2text(self, node):
187         attr_accumulator = []
188         for name, value in self.getattrs(node):
189             name = self.getlocalname(name).encode(default_encoding, "replace")
190             value = value.encode(default_encoding, "replace")
191             attr_accumulator.append("%s=%s" % (name, value))
192         return '\n'.join(attr_accumulator)
193
194     def has_ns(self, node):
195         return False
196
197
198 class MiniDOMXmlVfs(XmlVfs):
199     def parse(self):
200         self.document = xml.dom.minidom.parse(self.xml_file)
201
202     def getattrs(self, node):
203         attrs = node.attributes
204         attrs = [attrs.item(i) for i in range(attrs.length)]
205         return [(a.name, a.value) for a in attrs]
206
207     def collect_text(self, node):
208         text_accumulator = []
209         for element in node.childNodes:
210             if element.localName:
211                 continue
212             elif element.nodeType == element.COMMENT_NODE:
213                 text = u"<!--%s-->" % element.nodeValue
214             elif element.nodeType == element.TEXT_NODE:
215                 text = element.nodeValue.strip()
216             else:
217                 xml_error("Unknown node type %d" % element.nodeType)
218             if text: 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: text_accumulator.append(text)
250             for element in node:
251                 if not self.istag(element):
252                     text = u"<!--%s-->" % element.text
253                     text_accumulator.append(text)
254             if node.tail:
255                 text = node.tail.strip()
256                 if text: text_accumulator.append(text)
257             return '\n'.join(text_accumulator).encode(default_encoding, "replace")
258
259         def getchildren(self, node):
260             return list(node)
261
262         def gettag(self, node):
263             return node.tag
264
265         def istag(self, node):
266             return isinstance(node.tag, basestring)
267
268
269 if use_elementtree:
270     class ElementTreeXmlVfs(CommonEtreeXmlVfs):
271         def parse(self):
272             # Copied from http://effbot.org/zone/element-pi.htm
273
274             class PIParser(ET.XMLTreeBuilder):
275
276                 def __init__(self):
277                     ET.XMLTreeBuilder.__init__(self)
278                     # assumes ElementTree 1.2.X
279                     self._parser.CommentHandler = self.handle_comment
280                     self._parser.ProcessingInstructionHandler = self.handle_pi
281                     self._target.start("document", {})
282
283                 def close(self):
284                     self._target.end("document")
285                     return ET.XMLTreeBuilder.close(self)
286
287                 def handle_comment(self, data):
288                     self._target.start(ET.Comment, {})
289                     self._target.data(data)
290                     self._target.end(ET.Comment)
291
292                 def handle_pi(self, target, data):
293                     self._target.start(ET.PI, {})
294                     self._target.data(target + " " + data)
295                     self._target.end(ET.PI)
296
297             self.document = ET.parse(self.xml_file, PIParser())
298
299         def getroot(self):
300             return self.document.getroot()
301
302         def get_root_comments(self):
303             text_accumulator = []
304             for element in self.getroot():
305                 if not self.istag(element):
306                     text = u"<!--%s-->" % element.text
307                     text_accumulator.append(text)
308             return '\n'.join(text_accumulator).encode(default_encoding, "replace")
309
310         def getlocalname(self, name):
311             if name.startswith('{'):
312                 name = name.split('}', 1)[1]  # Remove XML namespace
313             return name
314
315
316 if use_lxml:
317     class LxmlEtreeXmlVfs(CommonEtreeXmlVfs):
318         supports_namespaces = True
319
320         def parse(self):
321             self.document = etree.parse(self.xml_file)
322
323         def getroot(self):
324             return [self.document.getroot()]
325
326         def get_root_comments(self):
327             text_accumulator = []
328             for element in self.document.getroot().itersiblings(tag=etree.Comment, preceding=True):
329                 text = u"<!--%s-->" % element.text
330                 text_accumulator.append(text)
331             return '\n'.join(text_accumulator).encode(default_encoding, "replace")
332
333         def getlocalname(self, name):
334             return etree.QName(name).localname
335
336         def _get_local_ns(self, node):
337             this_nsmap = node.nsmap
338             parent = node.getparent()
339             if parent is not None:
340                 parent_nsmap = parent.nsmap
341                 for key in parent_nsmap:
342                     if this_nsmap[key] == parent_nsmap[key]:
343                         del this_nsmap[key]
344             return this_nsmap
345
346         def has_ns(self, node):
347             return bool(self._get_local_ns(node))
348
349         def ns2text(self, node):
350             ns_accumulator = []
351             for name, value in self._get_local_ns(node).items():
352                 if name:
353                     name = name.encode(default_encoding, "replace")
354                 else:
355                     name = 'xmlns'
356                 value = value.encode(default_encoding, "replace")
357                 ns_accumulator.append("%s=%s" % (name, value))
358             return '\n'.join(ns_accumulator)
359
360
361 def build_xmlvfs():
362     if force_implementation is None:
363         if use_lxml:
364             return LxmlEtreeXmlVfs()
365         elif use_elementtree:
366             return ElementTreeXmlVfs()
367         else:
368             return MiniDOMXmlVfs()
369     elif force_implementation == 'minidom':
370         return MiniDOMXmlVfs()
371     elif force_implementation == 'elementtree':
372         return ElementTreeXmlVfs()
373     elif force_implementation == 'lxml':
374         return LxmlEtreeXmlVfs()
375     else:
376         raise ValueError('Unknown implementation "%s", expected "minidom", "elementtree" or "lxml"' % force_implementation)
377
378
379 def mcxml_list():
380     """List the entire VFS"""
381
382     xmlvfs = build_xmlvfs()
383     xmlvfs.list()
384
385
386 def mcxml_copyout():
387     """Extract a file from the VFS"""
388
389     xmlvfs = build_xmlvfs()
390     xml_filename = sys.argv[3]
391     real_filename = sys.argv[4]
392
393     node = xmlvfs.getroot()
394     for path_comp in xml_filename.split('/'):
395         if ' ' in path_comp:
396             i = int(path_comp.split(' ', 1)[0])
397             node = xmlvfs.get_tag_node(node, i)
398         elif path_comp in ('attributes', 'namespaces', 'text'):
399             break
400         else:
401             xml_error('Unknown file')
402
403     if path_comp == 'attributes':
404         if xmlvfs.getattrs(node):
405             text = xmlvfs.attrs2text(node)
406         else:
407             xml_error('There are no attributes')
408
409     elif path_comp == 'namespaces':
410         if xmlvfs.supports_namespaces and xmlvfs.has_ns(node):
411             text = xmlvfs.ns2text(node)
412         else:
413             xml_error('There are no namespaces')
414
415     elif path_comp == 'text':
416         if '/' in xml_filename:
417             text = xmlvfs.collect_text(node)
418         else:
419             text = xmlvfs.get_root_comments()
420
421     else:
422         xml_error('Unknown file')
423
424     outfile = open(real_filename, 'w')
425     outfile.write(text)
426     outfile.close()
427
428
429 def mcxml_copyin():
430     """Put a file to the VFS"""
431     sys.exit("XML VFS doesn't support adding files (read-only filesystem)")
432
433 def mcxml_rm():
434     """Remove a file from the VFS"""
435     sys.exit("XML VFS doesn't support removing files/directories (read-only filesystem)")
436
437 mcxml_rmdir = mcxml_rm
438
439 def mcxml_mkdir():
440     """Create a directory in the VFS"""
441     sys.exit("XML VFS doesn't support creating directories (read-only filesystem)")
442
443
444 def xml_error(error_str):
445     logger.critical("Error walking XML file: %s", error_str)
446     sys.exit(1)
447
448 command = sys.argv[1]
449 procname = "mcxml_" + command
450
451 g = globals()
452 if not g.has_key(procname):
453     logger.critical("Unknown command %s", command)
454     sys.exit(1)
455
456 try:
457     g[procname]()
458 except SystemExit:
459     raise
460 except:
461     logger.exception("Error during run")