djaploy 1.2.9__tar.gz → 1.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {djaploy-1.2.9 → djaploy-1.3.1}/PKG-INFO +2 -2
- {djaploy-1.2.9 → djaploy-1.3.1}/README.md +1 -1
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/rclone/infra/djaploy_hooks.py +48 -23
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/config.py +1 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/infra/djaploy_hooks.py +5 -0
- djaploy-1.3.1/djaploy/version.py +1 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy.egg-info/PKG-INFO +2 -2
- {djaploy-1.2.9 → djaploy-1.3.1}/pyproject.toml +1 -1
- djaploy-1.2.9/djaploy/version.py +0 -1
- {djaploy-1.2.9 → djaploy-1.3.1}/LICENSE +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/app.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/borg/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/borg/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/borg/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/janitor/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/janitor/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/janitor/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/nginx/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/nginx/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/nginx/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/nginx/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/rclone/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/rclone/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/rclone/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/sync_certs/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/sync_certs/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/systemd/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/systemd/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/systemd/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/systemd/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/tailscale/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/tailscale/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/tailscale/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/versioning/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/versioning/apps.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/versioning/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/artifact.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/bin/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/bin/django_pyinfra.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/bin/gunicornherder.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/builtin_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/certificates.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/changelog.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/_utils.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/activate.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/configure.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/deploy.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/restore.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/rollback.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/status.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/commands/sync_certs.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/deploy.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/discovery.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/infra/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/infra/bluegreen.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/infra/templates.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/infra/utils.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/commands/__init__.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/commands/djaploy.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/commands/restore_backup.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/commands/sync_certs.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/commands/update_certs.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/commands/verify.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/management/utils.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/notifications.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/utils.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy/versioning.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy.egg-info/SOURCES.txt +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy.egg-info/dependency_links.txt +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy.egg-info/entry_points.txt +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy.egg-info/requires.txt +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/djaploy.egg-info/top_level.txt +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/setup.cfg +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_bluegreen.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_borg.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_config.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_deploy_scripts.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_discovery.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_gunicornherder.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_hooks.py +0 -0
- {djaploy-1.2.9 → djaploy-1.3.1}/tests/test_versioning.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: djaploy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Modular Django deployment system based on pyinfra
|
|
5
5
|
Author-email: Johanna Mae Dimayuga <johanna@techco.fi>
|
|
6
6
|
License: MIT License
|
|
@@ -204,7 +204,7 @@ All deployment configuration lives on `djaploy.config.HostConfig`. Commonly used
|
|
|
204
204
|
| `shared_resources` | — | Extra paths symlinked from `shared/` |
|
|
205
205
|
| `db_dir` | — | External database directory template |
|
|
206
206
|
| `gunicorn_conf` | — | `workers`, `timeout`, `umask`, `wsgi_module`, `health_check_*` |
|
|
207
|
-
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `custom` |
|
|
207
|
+
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `static_path`, `media_path`, `custom` |
|
|
208
208
|
| `core_conf` | — | `poetry_no_root`, `exclude_groups`, `poetry_lock`, `databases` |
|
|
209
209
|
| `versioning_conf` / `notifications_conf` | — | See [Release Notifications & Versioning](#release-notifications--versioning) |
|
|
210
210
|
| `backup` / `borg_backup` | — | `BackupConfig` / `BorgBackupConfig` |
|
|
@@ -142,7 +142,7 @@ All deployment configuration lives on `djaploy.config.HostConfig`. Commonly used
|
|
|
142
142
|
| `shared_resources` | — | Extra paths symlinked from `shared/` |
|
|
143
143
|
| `db_dir` | — | External database directory template |
|
|
144
144
|
| `gunicorn_conf` | — | `workers`, `timeout`, `umask`, `wsgi_module`, `health_check_*` |
|
|
145
|
-
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `custom` |
|
|
145
|
+
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `static_path`, `media_path`, `custom` |
|
|
146
146
|
| `core_conf` | — | `poetry_no_root`, `exclude_groups`, `poetry_lock`, `databases` |
|
|
147
147
|
| `versioning_conf` / `notifications_conf` | — | See [Release Notifications & Versioning](#release-notifications--versioning) |
|
|
148
148
|
| `backup` / `borg_backup` | — | `BackupConfig` / `BorgBackupConfig` |
|
|
@@ -162,9 +162,17 @@ cleanup() {{
|
|
|
162
162
|
# Set trap to cleanup on exit
|
|
163
163
|
trap cleanup EXIT
|
|
164
164
|
|
|
165
|
+
# Retention enforcement (defined here, run before the dump so it still
|
|
166
|
+
# executes even if a later backup step fails)
|
|
167
|
+
{_generate_retention_function(retention_days, host_name)}
|
|
168
|
+
|
|
165
169
|
# Start backup
|
|
166
170
|
log_message "Starting backup for {host_name}"
|
|
167
171
|
|
|
172
|
+
# Enforce retention up front so stale backups are pruned on every run,
|
|
173
|
+
# independent of whether the dump/upload below succeeds.
|
|
174
|
+
enforce_retention || true
|
|
175
|
+
|
|
168
176
|
# Create temporary backup directory
|
|
169
177
|
mkdir -p "$TEMP_BACKUP_DIR"
|
|
170
178
|
|
|
@@ -174,6 +182,23 @@ log_message "Creating consistent database backups"
|
|
|
174
182
|
# List of databases to backup
|
|
175
183
|
DATABASES=({db_array})
|
|
176
184
|
|
|
185
|
+
# Fail fast (with a clear message) if there isn't enough scratch space. VACUUM
|
|
186
|
+
# INTO writes a full copy of every DB, then those are tarred — so we need room
|
|
187
|
+
# for roughly the total DB size plus the archive. The threshold scales from the
|
|
188
|
+
# live DB sizes (1.5x), so it is correct on any host without a hard-coded value.
|
|
189
|
+
REQUIRED_KB=0
|
|
190
|
+
for DB in "${{DATABASES[@]}}"; do
|
|
191
|
+
if [ -f "$DB_DIR/$DB" ]; then
|
|
192
|
+
REQUIRED_KB=$((REQUIRED_KB + $(du -k "$DB_DIR/$DB" | cut -f1)))
|
|
193
|
+
fi
|
|
194
|
+
done
|
|
195
|
+
REQUIRED_KB=$((REQUIRED_KB * 3 / 2))
|
|
196
|
+
AVAIL_KB=$(df --output=avail "$(dirname "$TEMP_BACKUP_DIR")" | tail -1)
|
|
197
|
+
if [ "${{AVAIL_KB:-0}}" -lt "$REQUIRED_KB" ]; then
|
|
198
|
+
log_message "ERROR: only $((AVAIL_KB/1024))MB free, need ~$((REQUIRED_KB/1024))MB for backup scratch space — aborting DB dump"
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
177
202
|
for DB in "${{DATABASES[@]}}"; do
|
|
178
203
|
if [ -f "$DB_DIR/$DB" ]; then
|
|
179
204
|
log_message "Backing up database: $DB"
|
|
@@ -296,36 +321,36 @@ else
|
|
|
296
321
|
log_message "Media directory not found or empty at $MEDIA_DIR, skipping media backup"
|
|
297
322
|
fi
|
|
298
323
|
|
|
299
|
-
#
|
|
300
|
-
{_generate_retention_cleanup(retention_days, host_name)}
|
|
324
|
+
# (Retention was already enforced at the start of this run via enforce_retention.)
|
|
301
325
|
|
|
302
326
|
log_message "Backup completed successfully for {host_name}"
|
|
303
327
|
'''
|
|
304
328
|
|
|
305
329
|
|
|
306
|
-
def
|
|
307
|
-
"""Generate
|
|
330
|
+
def _generate_retention_function(retention_days: int, host_name: str) -> str:
|
|
331
|
+
"""Generate an ``enforce_retention()`` shell function for the backup script.
|
|
332
|
+
|
|
333
|
+
Defined near the top of the script and invoked *before* the database dump
|
|
334
|
+
so retention runs on every invocation even if a later step fails. Previously
|
|
335
|
+
the cleanup was the final step, guarded by ``set -e`` and multiple ``exit 1``
|
|
336
|
+
branches, so any dump/upload failure skipped it and old backups accumulated
|
|
337
|
+
without bound (this is what filled the linnahovi disk).
|
|
338
|
+
"""
|
|
308
339
|
if retention_days <= 0:
|
|
309
|
-
return ""
|
|
340
|
+
return "enforce_retention() { :; } # retention disabled (retention_days<=0)"
|
|
310
341
|
return (
|
|
311
|
-
'
|
|
312
|
-
'\n'
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'\n'
|
|
316
|
-
'
|
|
317
|
-
'
|
|
318
|
-
'
|
|
319
|
-
'
|
|
320
|
-
'\n'
|
|
321
|
-
'
|
|
322
|
-
'
|
|
323
|
-
' log_message "Deleting old backup folder: $folder_name"\n'
|
|
324
|
-
' rclone purge "${{REMOTE_NAME}}:{host}/$folder_name" --config "$RCLONE_CONFIG" --quiet\n'
|
|
325
|
-
' fi\n'
|
|
326
|
-
'done\n'
|
|
327
|
-
'\n'
|
|
328
|
-
'log_message "Cleanup completed"'
|
|
342
|
+
'enforce_retention() {{\n'
|
|
343
|
+
' log_message "Enforcing retention: removing remote folders older than {days} days"\n'
|
|
344
|
+
' local cutoff; cutoff=$(date -d "{days} days ago" +"%Y-%m-%d")\n'
|
|
345
|
+
' rclone lsf "${{REMOTE_NAME}}:{host}/" --dirs-only --config "$RCLONE_CONFIG" 2>>"$LOG_FILE" | while read -r folder; do\n'
|
|
346
|
+
' local name="${{folder%/}}"\n'
|
|
347
|
+
' if [[ "$name" =~ ^[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}$ ]] && [[ "$name" < "$cutoff" ]]; then\n'
|
|
348
|
+
' log_message "Deleting old backup folder: $name"\n'
|
|
349
|
+
' rclone purge "${{REMOTE_NAME}}:{host}/$name" --config "$RCLONE_CONFIG" --quiet 2>>"$LOG_FILE" \\\n'
|
|
350
|
+
' || log_message "WARN: purge failed for $name"\n'
|
|
351
|
+
' fi\n'
|
|
352
|
+
' done\n'
|
|
353
|
+
'}}'
|
|
329
354
|
).format(days=retention_days, host=host_name)
|
|
330
355
|
|
|
331
356
|
|
|
@@ -176,6 +176,7 @@ class HostConfig(tuple, metaclass=HostConfigMetaclass):
|
|
|
176
176
|
manage_py_path: str = "manage.py" # Relative path to manage.py in the artifact
|
|
177
177
|
db_dir: Optional[str] = None # External database directory template
|
|
178
178
|
generate_local_settings: bool = False # Generate local.py with DATABASES, ALLOWED_HOSTS, etc.
|
|
179
|
+
secret_key: Optional[str] = None # Django SECRET_KEY value (e.g. OpSecret) injected into generated local.py
|
|
179
180
|
shared_resources: Optional[List[str]] = None # Paths to symlink from shared/
|
|
180
181
|
|
|
181
182
|
# Per-module configuration (merged with defaults)
|
|
@@ -562,6 +562,11 @@ def generate_local_settings(host_data, artifact_path):
|
|
|
562
562
|
|
|
563
563
|
lines.append("DEBUG = False\n")
|
|
564
564
|
|
|
565
|
+
secret_key = getattr(host_data, "secret_key", None)
|
|
566
|
+
if secret_key:
|
|
567
|
+
# str() resolves an OpSecret to its value; repr safely quotes/escapes it.
|
|
568
|
+
lines.append(f"SECRET_KEY = {str(secret_key)!r}\n")
|
|
569
|
+
|
|
565
570
|
if is_zero_downtime(host_data) or is_bluegreen(host_data):
|
|
566
571
|
static_path, media_path = get_static_media_paths(host_data)
|
|
567
572
|
lines.append(f'STATIC_ROOT = "{static_path}"\n')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: djaploy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Modular Django deployment system based on pyinfra
|
|
5
5
|
Author-email: Johanna Mae Dimayuga <johanna@techco.fi>
|
|
6
6
|
License: MIT License
|
|
@@ -204,7 +204,7 @@ All deployment configuration lives on `djaploy.config.HostConfig`. Commonly used
|
|
|
204
204
|
| `shared_resources` | — | Extra paths symlinked from `shared/` |
|
|
205
205
|
| `db_dir` | — | External database directory template |
|
|
206
206
|
| `gunicorn_conf` | — | `workers`, `timeout`, `umask`, `wsgi_module`, `health_check_*` |
|
|
207
|
-
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `custom` |
|
|
207
|
+
| `nginx_conf` | — | `server_name`, `listen`, `client_max_body_size`, `static_path`, `media_path`, `custom` |
|
|
208
208
|
| `core_conf` | — | `poetry_no_root`, `exclude_groups`, `poetry_lock`, `databases` |
|
|
209
209
|
| `versioning_conf` / `notifications_conf` | — | See [Release Notifications & Versioning](#release-notifications--versioning) |
|
|
210
210
|
| `backup` / `borg_backup` | — | `BackupConfig` / `BorgBackupConfig` |
|
djaploy-1.2.9/djaploy/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|