Skip to content

Commit 7f5f399

Browse files
committed
Preparation to export/copy the latest backup for archival. Script will now enum and delete current backups before creating a new one.
1 parent 917427b commit 7f5f399

File tree

5 files changed

+170
-16
lines changed

5 files changed

+170
-16
lines changed

.devcontainer/Dockerfile

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Base Image for Python Development
2+
FROM mcr.microsoft.com/devcontainers/python:3.11
3+
4+
# Install dependencies
5+
RUN apt-get update && apt-get install -y \
6+
curl \
7+
iptables \
8+
sudo \
9+
docker.io \
10+
git
11+
12+
# Install Tailscale
13+
RUN curl -fsSL https://tailscale.com/install.sh | sh
14+
15+
# Create the vscode user if it doesn't exist
16+
RUN id -u vscode &>/dev/null || useradd -m -s /bin/bash vscode
17+
18+
# Add vscode user to sudoers
19+
RUN usermod -aG sudo vscode && \
20+
echo 'vscode ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/vscode
21+
22+
# Ensure docker.sock is accessible
23+
RUN chmod 666 /var/run/docker.sock || true
24+
25+
# Expose Tailscale port
26+
EXPOSE 41641/udp
27+
28+
# Copy dependencies
29+
COPY requirements.txt /tmp/requirements.txt
30+
RUN pip install --upgrade pip && pip install -r /tmp/requirements.txt
31+
32+
# Set permissions for vscode user
33+
RUN chown -R vscode:vscode /home/vscode
34+
35+
# Switch to vscode user
36+
USER vscode
37+
38+
# Default command
39+
CMD ["bash", "-c", "sudo service docker start && tailscaled & tailscale up --authkey=${TAILSCALE_AUTHKEY} --hostname=devcontainer && sleep infinity"]

.devcontainer/devcontainer.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2+
// README at: https://github.com/devcontainers/templates/tree/main/src/python
3+
{
4+
"name": "Python 3",
5+
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6+
"image": "python:3.13-slim",
7+
8+
// Features to add to the dev container. More info: https://containers.dev/features.
9+
"features": {
10+
"ghcr.io/devcontainers-extra/features/tailscale": {}
11+
}
12+
13+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
14+
// "forwardPorts": [],
15+
16+
// Use 'postCreateCommand' to run commands after the container is created.
17+
// "postCreateCommand": "pip3 install --user -r requirements.txt",
18+
19+
// Configure tool-specific properties.
20+
// "customizations": {},
21+
22+
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
23+
// "remoteUser": "root"
24+
}

.devcontainer/requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Project Dependencies
2+
requests

.devcontainer/sample-env

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TAILSCALE_AUTHKEY=tskey-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

app/mealie-backup.py

+104-16
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,21 @@
2828
level=logging.INFO
2929
)
3030

31-
3231
def health_check(url: str) -> bool:
3332
"""
34-
Perform a health check on the API.
35-
33+
Perform a health check on the API to verify if the server is reachable and functional.
34+
3635
Args:
37-
url (str): The health check URL.
38-
36+
url (str): The health check URL, typically an endpoint of the server
37+
(e.g., "/" or "/health") used to test availability.
38+
3939
Returns:
40-
bool: True if the health check passes, False otherwise.
40+
bool: True if the health check passes (server responds successfully), False otherwise.
41+
42+
Notes:
43+
- Sends a GET request to the provided URL.
44+
- Logs and prints the status of the health check.
45+
- A timeout of 5 seconds is used to ensure quick failure for unresponsive servers.
4146
"""
4247
try:
4348
response = requests.get(url, timeout=5)
@@ -51,29 +56,112 @@ def health_check(url: str) -> bool:
5156
return False
5257

5358

54-
def make_post_request():
59+
def get_backups():
5560
"""
56-
Make a POST request to the API endpoint.
61+
Fetch the list of existing backups from the server.
62+
63+
Returns:
64+
list: A list of dicts representing each backup containing details,
65+
including metadata like `name`, `date`, and `size`.
66+
67+
Notes:
68+
- Sends a GET request to the backups API endpoint.
69+
- Logs and prints the response, including backup details if successful.
70+
- Returns an empty list if the request fails.
71+
"""
72+
try:
73+
response = requests.get(URL, headers=HEADERS)
74+
response.raise_for_status()
75+
backups = response.json() # Returns a dictionary
76+
logging.info(f"Fetched backups: {backups}")
77+
print(f"{datetime.datetime.now()} - Fetched backups: {backups}")
78+
return backups
79+
except requests.RequestException as e:
80+
logging.error(f"Error fetching backups: {e}")
81+
print(f"{datetime.datetime.now()} - Error fetching backups: {e}")
82+
return {}
83+
84+
85+
def delete_backup(backup_name: str):
86+
"""
87+
Delete a specific backup by its name.
88+
89+
Args:
90+
backup_name (str): The name of the backup to delete, typically the filename
91+
(e.g., "mealie_YYYY.MM.DD.HH.MM.SS.zip").
92+
93+
Notes:
94+
- Sends a DELETE request to the backup API endpoint.
95+
- Logs and prints the result of the deletion.
96+
- Handles and logs errors if the request fails.
5797
"""
5898
try:
59-
response = requests.post(URL, json=DATA, headers=HEADERS)
99+
delete_url = f"{URL}/{backup_name}"
100+
response = requests.delete(delete_url, headers=HEADERS)
60101
response.raise_for_status()
61-
logging.info(f"POST successful: {response.status_code}")
62-
print(f"{datetime.datetime.now()} - POST successful: {response.status_code}")
102+
logging.info(f"Deleted backup: {backup_name}")
103+
print(f"{datetime.datetime.now()} - Deleted backup: {backup_name}")
63104
except requests.RequestException as e:
64-
logging.error(f"POST Error: {e}")
65-
print(f"{datetime.datetime.now()} - POST Error: {e}")
105+
logging.error(f"Error deleting backup {backup_name}: {e}")
106+
print(f"{datetime.datetime.now()} - Error deleting backup {backup_name}: {e}")
107+
108+
109+
def delete_all_backups():
110+
"""
111+
Delete all existing backups on the server.
112+
113+
Notes:
114+
- Fetches the list of backups via `get_backups`.
115+
- Iterates over the `imports` list in the response and deletes each backup by its name.
116+
- Logs and prints the status of each deletion.
117+
- Skips backups that do not have a valid `name` field.
118+
"""
119+
backups = get_backups() # Fetch the backups
120+
imports = backups.get('imports', []) # Extract the list of imports
121+
for backup in imports:
122+
backup_name = backup.get('name') # Extract the backup name
123+
if backup_name:
124+
delete_backup(backup_name) # Pass the name to delete_backup
125+
logging.info("All backups deleted.")
126+
print(f"{datetime.datetime.now()} - All backups deleted.")
127+
128+
129+
def create_backup():
130+
"""
131+
Create a new backup by sending a POST request to the backups endpoint.
132+
133+
Notes:
134+
- Logs and prints the response if the backup creation is successful.
135+
- Handles and logs errors if the request fails.
136+
"""
137+
try:
138+
response = requests.post(URL, headers=HEADERS)
139+
response.raise_for_status()
140+
logging.info(f"Backup created: {response.json()}")
141+
print(f"{datetime.datetime.now()} - Backup created: {response.json()}")
142+
except requests.RequestException as e:
143+
logging.error(f"Error creating backup: {e}")
144+
print(f"{datetime.datetime.now()} - Error creating backup: {e}")
66145

67146

68147
if __name__ == "__main__":
148+
"""
149+
Main entry point of the script.
150+
151+
- Validates if the AUTH_TOKEN environment variable is set.
152+
- Performs a health check on the server.
153+
- If the health check passes, deletes all existing backups and creates a new backup.
154+
- Logs and prints the status of each operation.
155+
"""
69156
if not AUTH_TOKEN:
70157
error_msg = "Authorization token is missing! Please set the AUTH_TOKEN environment variable."
71158
logging.error(error_msg)
72159
print(error_msg)
73160
else:
74161
if health_check(HEALTH_URL):
75-
make_post_request()
162+
delete_all_backups()
163+
create_backup()
76164
else:
77-
error_msg = "Health check failed. Aborting POST request."
165+
error_msg = "Health check failed. Aborting backup operations."
78166
logging.error(error_msg)
79-
print(error_msg)
167+
print(error_msg)

0 commit comments

Comments
 (0)