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