Repository navigation
Expand file tree
/
Copy pathregister_plugin.py
More file actions
executable file
·144 lines (115 loc) · 5.04 KB
/
Copy pathregister_plugin.py
File metadata and controls
executable file
·144 lines (115 loc) · 5.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/usr/bin/env python3
"""Register the ADB WFX plugin in Double Commander's doublecmd.xml.
register_plugin.py <doublecmd.xml> <path-value>
<path-value> is what goes into <Path>, normally
%DC_CONFIG_PATH%/plugins/wfx/adb/fsplugin.wfx64
Idempotent: an entry this plugin already owns is updated in place rather than
duplicated, so re-running an install never grows the plugin list.
Ownership is decided by <Name> == "ADB" or by a <Path> ending in
plugins/wfx/adb/fsplugin.wfx64 -- never by the file name alone. Double
Commander's own MTP plugin is *also* called fsplugin.wfx64, and matching on
the basename would rewrite the user's MTP registration.
Double Commander rewrites doublecmd.xml when it exits, so it must not be
running while this script works. Checking that is install.sh's job.
The whole file is re-serialised, which normalises a few cosmetic things
ElementTree does differently from DC (self-closing tags, quoting in the XML
declaration). Those are undone below so the diff stays limited to the plugin
entry. Untouched elements keep their original whitespace, because ElementTree
parses indentation as text/tail nodes and writes it back unchanged.
"""
import datetime
import os
import shutil
import sys
import xml.etree.ElementTree as ET
NAME = "ADB"
PATH_SUFFIX = "plugins/wfx/adb/fsplugin.wfx64"
STEP = " " # DC indents with two spaces per level.
DEPTH_PLUGINS = 1 # <doublecmd> -> <Plugins>
DEPTH_WFXPLUGINS = 2 # -> <WfxPlugins>
DEPTH_ENTRY = 3 # -> <WfxPlugin>
def owns(entry, path_value):
"""True if this <WfxPlugin> is ours, by name or by installed location."""
name = entry.find("Name")
if name is not None and (name.text or "").strip() == NAME:
return True
path = entry.find("Path")
if path is not None and (path.text or "").strip():
text = path.text.strip().lower()
return text.endswith(PATH_SUFFIX) or text == path_value.lower()
return False
def append_child(parent, child, depth):
"""Append `child` to `parent`, indented as a level-`depth` element."""
own_indent = "\n" + STEP * depth
parent_indent = "\n" + STEP * (depth - 1)
if parent.text is None or "\n" not in parent.text:
parent.text = own_indent
kids = list(parent)
if kids:
# The previous last child is no longer last, so it gets sibling
# indentation instead of the closing tag's.
kids[-1].tail = own_indent
child.tail = parent_indent
parent.append(child)
def build_entry(path_value):
entry = ET.Element("WfxPlugin", {"Enabled": "True"})
inner = "\n" + STEP * (DEPTH_ENTRY + 1)
entry.text = inner
name = ET.SubElement(entry, "Name")
name.text = NAME
name.tail = inner
path = ET.SubElement(entry, "Path")
path.text = path_value
path.tail = "\n" + STEP * DEPTH_ENTRY
return entry
def find_or_create(parent, tag, depth):
found = parent.find(tag)
if found is not None:
return found
created = ET.Element(tag)
append_child(parent, created, depth)
return created
def serialise(root):
body = ET.tostring(root, encoding="unicode")
# ElementTree writes `<Tag />` and single-quoted declarations; DC writes
# `<Tag/>` and double quotes. Neither escapes a bare `>` into element
# text or attribute values, so this substring cannot occur in content.
body = body.replace(" />", "/>")
text = '<?xml version="1.0" encoding="UTF-8"?>\n' + body
if not text.endswith("\n"):
text += "\n"
return text
def main(config_path, path_value):
# Parse before touching anything: a config we cannot read is a config we
# must leave exactly as it is, without even leaving a backup behind.
try:
tree = ET.parse(config_path)
except (ET.ParseError, OSError) as exc:
sys.exit("register_plugin.py: cannot read %s: %s" % (config_path, exc))
root = tree.getroot()
plugins = find_or_create(root, "Plugins", DEPTH_PLUGINS)
wfx = find_or_create(plugins, "WfxPlugins", DEPTH_WFXPLUGINS)
existing = [el for el in list(wfx) if owns(el, path_value)]
action = "updated" if existing else "added"
for el in existing:
wfx.remove(el)
append_child(wfx, build_entry(path_value), DEPTH_ENTRY)
backup = "%s.bak-%s" % (config_path, datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
shutil.copy2(config_path, backup)
# Write through a temporary file in the same directory: a crash or a full
# disk halfway through must not leave the user with a truncated config.
temp = config_path + ".tmp-register"
try:
with open(temp, "w", encoding="utf-8") as handle:
handle.write(serialise(root))
os.replace(temp, config_path)
except OSError as exc:
if os.path.exists(temp):
os.unlink(temp)
sys.exit("register_plugin.py: cannot write %s: %s" % (config_path, exc))
print(" backup: %s" % backup)
print(" %s: %s -> %s" % (action.ljust(7), NAME, path_value))
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("usage: register_plugin.py <doublecmd.xml> <path-value>")
main(sys.argv[1], sys.argv[2])