File: configuration.py

package info (click to toggle)
syncthing-gtk 0.9.4.4%2Bds%2Bgit20201209%2Bc46fbd8-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 3,260 kB
  • sloc: python: 7,592; sh: 259; xml: 115; makefile: 2
file content (234 lines) | stat: -rw-r--r-- 7,041 bytes parent folder | download
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#!/usr/bin/env python3
"""
Syncthing-GTK - Configuration

Configuration object implementation; Uses JSON.
Config file is by default in ~/.config/syncthing-gtk/config.json
or other ~/.config equivalent
"""


from datetime import datetime
from syncthing_gtk.tools import IS_WINDOWS, get_config_dir, is_portable
import dateutil.parser
import os, sys, json, logging
log = logging.getLogger("Configuration")

LONG_AGO = datetime.fromtimestamp(1)

class _Configuration(object):
	"""
	Configuration object implementation.
	Use like dict to save / access values
	"""
	
	# Dict with keys that are required in configuration file
	# and default values for those keys.
	# Format: key : (type, default)
	REQUIRED_KEYS = {
		"autostart_daemon"			: (int, 2),	# 0 - wait for daemon, 1 - autostart, 2 - ask
		"autokill_daemon"			: (int, 2),	# 0 - never kill, 1 - always kill, 2 - ask
		"daemon_priority"			: (int, 0), # uses nice values
		"max_cpus"					: (int, 0), # 0 for all cpus
		"syncthing_binary"			: (str, "/usr/bin/syncthing"),
		"syncthing_arguments"		: (str, ""),
		"minimize_on_start"			: (bool, False),
		"folder_as_path"			: (bool, True),
		"use_old_header"			: (bool, False),
		"icons_in_menu"				: (bool, True),
		"animate_icon"				: (bool, True),
		"notification_for_update"	: (bool, True),
		"notification_for_folder"	: (bool, False),
		"notification_for_error"	: (bool, True),
		"st_autoupdate"				: (bool, False),
		"last_updatecheck"			: (datetime, LONG_AGO),
		"window_position"			: (tuple, None),
		"infobox_style"				: (str, 'font_weight="bold" font_size="large"'),
		"icon_theme"				: (str, 'syncthing'),
		"force_dark_theme"			: (bool, False),	# Windows-only
		"language"					: (str, ""),		# Windows-only
		"file_browser"				: (str, "explore"),	# Windows-only
	}
	
	# Overrides some default values on Windows
	WINDOWS_OVERRIDE = {
		"syncthing_binary"			: (str, "C:\\Program Files\\Syncthing\\syncthing.exe"),
		"autokill_daemon"			: (int, 1),
		"use_old_header"			: (bool, False),
		"st_autoupdate"				: (bool, True),
	}
	
	def __init__(self):
		try:
			self.load()
		except Exception as e:
			log.warning("Failed to load configuration; Creating new one.")
			log.warning("Reason: %s", (e,))
			self.create()
		
		# Convert objects serialized as string back to object
		self.convert_values()
		# Check if everything is in place, add default value
		# where value is missing
		if self.check_values():
			# check_values returns True if any default value is added
			log.info("Saving configuration...")
			self.save()
	
	def load(self):
		# Check & create directory
		if not os.path.exists(self.get_config_dir()):
			try:
				os.makedirs(self.get_config_dir())
			except Exception as e:
				log.error("Cannot create configuration directory")
				log.exception(e)
				sys.exit(1)
		# Load json
		with open(self.get_config_file(), "r") as conf:
			self.values = json.loads(conf.read())

	def get_default_value(self, key):
		if IS_WINDOWS:
			return self.WINDOWS_OVERRIDE.get(key, self.REQUIRED_KEYS[key])[-1]
		return self.REQUIRED_KEYS[key][-1]
	
	def get_config_dir(self):
		return os.path.join(get_config_dir(), "syncthing-gtk")
	
	def get_config_file(self):
		return os.path.join(self.get_config_dir(), "config.json")
	
	def create(self):
		""" Creates new, empty configuration """
		self.values = {}
		self.check_values()
		self.save()
	
	def check_values(self):
		"""
		Check if all required values are in place and fill by default
		whatever is missing.
		
		Returns True if anything gets changed.
		"""
		needs_to_save = False
		for key in Configuration.REQUIRED_KEYS:
			tp, default = Configuration.REQUIRED_KEYS[key]
			if not self.check_type(key, tp):
				log.verbose("Configuration key %s is missing. Using default", key)
				if IS_WINDOWS and key in Configuration.WINDOWS_OVERRIDE:
					tp, default = Configuration.WINDOWS_OVERRIDE[key]
				self.values[key] = default
				needs_to_save = True
		return needs_to_save
	
	def convert_values(self):
		"""
		Converts all objects serialized as string back to object
		"""
		for key in Configuration.REQUIRED_KEYS:
			if key in self.values:
				tp, trash = Configuration.REQUIRED_KEYS[key]
				try:
					if tp == datetime and type(self.values[key]) == str:
						# Parse datetime
						self.values[key] = dateutil.parser.parse(self.values[key])
					elif tp == tuple and type(self.values[key]) == list:
						# Convert list to tuple
						self.values[key] = tuple(self.values[key])
					elif tp == bool and type(self.values[key]) == int:
						# Convert bools
						self.values[key] = bool(self.values[key])
				except Exception as e:
					log.warning("Failed to parse configuration value '%s'. Using default.", key)
					log.warning(e)
					# Value will be re-created by check_values method
					del self.values[key]
	
	def check_type(self, key, tp):
		"""
		Returns True if value is set and type match.
		Auto-converts objects serialized as string back to objects
		"""
		if not key in self.values:
			return False
		# Handle special cases
		if type(self.values[key]) == str and tp == str:
			return True
		if tp in (tuple,) and self.values[key] == None:
			return True
		# Return value
		return type(self.values[key]) == tp
	
	def save(self):
		""" Saves configuration file """
		with open(self.get_config_file(), "w") as conf:
			conf.write(
				json.dumps(
					self.values,
					sort_keys=True,
					indent=4,
					separators=(',', ': '),
					default=serializer
				)
			)

	def __iter__(self):
		for k in self.values:
			yield k
	
	def get(self, key):
		return self.values[key]
	
	def set(self, key, value):
		self.values[key] = value
		self.save()
	
	__getitem__ = get
	__setitem__ = set
	
	def __delitem__(self, key):
		del self.values[key]
	
	def __contains__(self, key):
		""" Returns true if there is such value """
		return key in self.values

def serializer(obj):
	""" Handles serialization where json can't do it by itself """
	if hasattr(obj, "isoformat"):
		# datetime object
		return obj.isoformat()
	raise TypeError("Can't serialize object of type %s" % (type(obj),))

def Configuration(*a, **b):
	if IS_WINDOWS and not is_portable():
		from syncthing_gtk.windows import WinConfiguration
		return WinConfiguration(*a, **b)
	return _Configuration(*a, **b)
Configuration.REQUIRED_KEYS = _Configuration.REQUIRED_KEYS
Configuration.WINDOWS_OVERRIDE = _Configuration.WINDOWS_OVERRIDE


def migrate_fs_watch(stgtk_config, st_config):
	"""
	Migrates filesystem watch config from ST-GTK configuration
	to Syncthing configuration and posts it to daemon.
	
	Returns True if anything was changed.
	
	Called automatically if old fs watch setting is found in ST-GTK config.
	"""
	# TODO: This can be removed later
	if "use_inotify" not in stgtk_config:
		return False
	changed = False
	folder_by_id = { x["id"]: x for x in st_config['folders'] }
	for rid in stgtk_config["use_inotify"]:
		if rid in folder_by_id:
			folder_by_id[rid]["fsWatcherDelayS"] = 10
			folder_by_id[rid]["fsWatcherEnabled"] = True
			changed = True
	del stgtk_config["use_inotify"]
	return changed