# SPDX-License-Identifier: AGPL-3.0-or-later """ Utilities for editing configuration files of Deluge. """ import copy import json import os import pathlib import re import shutil import tempfile _JSON_FORMAT = {'indent': 4, 'sort_keys': True, 'ensure_ascii': False} class Config: """Read or edit a Deluge configuration file.""" def __init__(self, file_name): """Initialize the configuration object.""" self.file_name = file_name self.file = pathlib.Path(self.file_name) self._version = None self.content = None self._original_content = None def load(self): """Parse the configuration file into memory.""" text = self.file.read_text(encoding='utf-8') matches = re.match(r'^({[^}]*})(.*)$', text, re.DOTALL) if not matches: raise Exception('Unexpected file format.') try: self._version = json.loads(matches.group(1)) self.content = json.loads(matches.group(2)) except json.decoder.JSONDecodeError: raise Exception('Unable to parse JSON in file.') if self._version['format'] != 1: raise Exception('Version of the config file not understood') self._original_content = copy.deepcopy(self.content) def save(self): """Atomically save the modified configuration to file.""" if self.content == self._original_content: return with tempfile.NamedTemporaryFile(dir=self.file.parent, delete=False) as new_file: new_file.write(json.dumps(self._version, **_JSON_FORMAT).encode()) new_file.write(json.dumps(self.content, **_JSON_FORMAT).encode()) new_file.flush() os.fsync(new_file.fileno()) new_file_path = pathlib.Path(new_file.name) new_file_path.chmod(0o600) try: shutil.chown(str(new_file_path), 'debian-deluged', 'debian-deluged') except (PermissionError, LookupError): pass # Not running as root, or deluge is not installed new_file_path.rename(self.file) def __enter__(self): """Enter the context.""" self.load() return self def __exit__(self, exc_type, exc_value, traceback): """Exit the context.""" self.save()