(config)
| 153 | |
| 154 | |
| 155 | def write_config(config): |
| 156 | config = validate_config(config) |
| 157 | directory = os.path.dirname(CONFIG_PATH) or "." |
| 158 | os.makedirs(directory, exist_ok=True) |
| 159 | timestamp = time.strftime("%Y%m%d-%H%M%S") |
| 160 | if os.path.exists(CONFIG_PATH): |
| 161 | shutil.copy2(CONFIG_PATH, f"{CONFIG_PATH}.bak-{timestamp}") |
| 162 | mode = os.stat(CONFIG_PATH).st_mode |
| 163 | else: |
| 164 | mode = 0o644 |
| 165 | data = json.dumps(config, ensure_ascii=False, indent="\t") + "\n" |
| 166 | fd, tmp_path = tempfile.mkstemp(prefix=".config.", suffix=".tmp", dir=directory) |
| 167 | try: |
| 168 | with os.fdopen(fd, "w", encoding="utf-8") as f: |
| 169 | f.write(data) |
| 170 | f.flush() |
| 171 | os.fsync(f.fileno()) |
| 172 | os.chmod(tmp_path, mode) |
| 173 | try: |
| 174 | os.replace(tmp_path, CONFIG_PATH) |
| 175 | except OSError as exc: |
| 176 | if exc.errno != errno.EBUSY: |
| 177 | raise |
| 178 | # A single-file Docker bind mount cannot be atomically replaced. |
| 179 | # Keep the backup above, then rewrite the mounted file in place. |
| 180 | with open(CONFIG_PATH, "w", encoding="utf-8") as f: |
| 181 | f.write(data) |
| 182 | f.flush() |
| 183 | os.fsync(f.fileno()) |
| 184 | finally: |
| 185 | if os.path.exists(tmp_path): |
| 186 | os.unlink(tmp_path) |
| 187 | return config |
| 188 | |
| 189 | |
| 190 | def write_and_reload(config): |
no test coverage detected