Skip to content

Commit 90d681a

Browse files
authored
Merge pull request #707 from PAWECOGmbH/staging
Staging to Production
2 parents ded4614 + 75fedd8 commit 90d681a

13 files changed

Lines changed: 1029 additions & 288 deletions

.github/copilot-instructions.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copilot Instructions for Saaster
2+
3+
## Overview
4+
Saaster is a CFML-based SaaS framework using Lucee 6, NGINX, and MySQL 8.1, containerized via Docker Compose. The codebase is modular, with clear separation between API, frontend, backend, and setup logic. Configuration is flexible and environment-driven.
5+
6+
## Architecture
7+
- **Core directories:**
8+
- `www/`: Main web app (entry points, config, API, resources)
9+
- `api/`: API endpoints, JWT auth, Taffy REST framework
10+
- `frontend/`: UI, themes, mail templates
11+
- `backend/`: Admin, modules, business logic
12+
- `setup/`: Installation wizard, mock data loader
13+
- `config/`: Environment, NGINX, DB migrations, backups
14+
- **Database migrations:**
15+
- SQL files in `config/db/core/` and `config/db/dev/` for schema and test data
16+
- **Modularity:**
17+
- Features are added via modules in `backend/modules/`
18+
19+
## Developer Workflow
20+
- **Local setup:**
21+
- Use `compose-dev.yml` with Docker Compose
22+
- Copy and edit config files from `config/` as described in the README
23+
- Access Lucee admin at `/lucee/admin/server.cfm` for DB and SMTP setup
24+
- Run setup wizard at `/setup/index.cfm` to initialize app and create sysadmin
25+
- **Reload config:**
26+
- Visit `/index.cfm?reinit=1` after changing `config.cfm`
27+
- **Test data:**
28+
- Import SQL from `config/db/dev/` or use `/setup/mockdata/index.cfm`
29+
- **Custom Docker image:**
30+
- Commit configured Lucee container and update `.env` with new image name
31+
32+
## Conventions & Patterns
33+
- **CFML/CFM:**
34+
- Application logic in `.cfc` (components), views in `.cfm` (templates)
35+
- API follows Taffy REST conventions in `api/taffy/`
36+
- JWT auth in `api/jwt/`
37+
- **Config:**
38+
- Environment variables in `.env`, app config in `www/config.cfm`
39+
- NGINX config in `config/nginx/conf.d/`
40+
- **Modules:**
41+
- Extendable via `backend/modules/` and `api/jwt/models/`
42+
- **Testing:**
43+
- Manual via setup wizard and mock data; no automated test suite detected
44+
45+
## Integration Points
46+
- **External:**
47+
- SMTP (local via Inbucket)
48+
- MySQL DB
49+
- NGINX reverse proxy
50+
- **Internal:**
51+
- API endpoints communicate via REST (Taffy)
52+
- Frontend and backend share config and DB
53+
54+
## Examples
55+
- To add a new API endpoint: create a `.cfc` in `api/resources/` and register in Taffy
56+
- To add a module: place in `backend/modules/` and wire up in config
57+
- To update DB schema: add migration SQL to `config/db/core/`
58+
59+
## References
60+
- See `readme.md` for setup and workflow details
61+
- Key config: `config/example.env`, `config/example.base.conf`, `config/example.config_dev.cfm`, `www/config.cfm`
62+
- Setup logic: `setup/index.cfm`, `setup/mockdata/`
63+
64+
## Coding Instructions
65+
- Comments and documentation should be in english. Never use german in code comments.
66+
- Follow standard ColdFusion (Lucee 6) best practices script based.
67+
- Use 4 spaces for indentation, no tabs.
68+
- Instead of `var`, always use `local` to declare local variables, but only in functions.
69+
- Instead of 'for' loops, prefer the cf tag 'loop'.
70+
- In CF Script, SQL queries must always be built in this order: options, params, sql. The types should be like: {type: "string", value: arguments.importUUID}

config/backup/backup.sh

Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,17 @@
11
#!/bin/bash
22

3-
set -a
4-
5-
# --------------------------------------
6-
# CONFIGURATION
7-
# --------------------------------------
8-
9-
LOGFILE="/var/log/backup-cron.log"
10-
echo "[BACKUP START] $(date)" | tee -a $LOGFILE
11-
12-
# Load environment variables from .env
3+
# Load env
134
source "$(dirname "$0")/../../.env"
145

15-
# Setup volume names and timestamp
16-
DB_VOLUME="${COMPOSE_PROJECT_NAME}_db_volume"
17-
USERDATA_VOLUME="${COMPOSE_PROJECT_NAME}_userdata_volume"
18-
TIMESTAMP=$(date +"%Y%m%d_%H%M")
19-
20-
# Ensure local backup directory exists
21-
mkdir -p /backup
22-
23-
# Create remote directories if they don't exist
24-
ssh -i ${SSH_KEY_PATH} ${SERVER_USER}@${SERVER_IP} \
25-
"mkdir -p ${REMOTE_BACKUP_PATH}/{db,userdata,lucee}" >> $LOGFILE 2>&1
26-
27-
# --------------------------------------
28-
# DATABASE BACKUP
29-
# --------------------------------------
30-
31-
echo "[DB] Creating archive..." | tee -a $LOGFILE
32-
docker run --rm -v ${DB_VOLUME}:/volume -v /backup:/backup alpine sh -c \
33-
"tar -czf /backup/database_${TIMESTAMP}.tar.gz -C /volume ." >> $LOGFILE 2>&1
34-
35-
echo "[DB] Uploading to remote..." >> $LOGFILE
36-
scp -i ${SSH_KEY_PATH} /backup/database_${TIMESTAMP}.tar.gz \
37-
${SERVER_USER}@${SERVER_IP}:${REMOTE_BACKUP_PATH}/db/ >> $LOGFILE 2>&1
38-
39-
# Verify and delete local backup if transfer succeeded
40-
ssh -i ${SSH_KEY_PATH} ${SERVER_USER}@${SERVER_IP} \
41-
"[ -f ${REMOTE_BACKUP_PATH}/db/database_${TIMESTAMP}.tar.gz ]" \
42-
&& { echo "[DB] Remote verified, deleting local file" | tee -a $LOGFILE; rm /backup/database_${TIMESTAMP}.tar.gz; } \
43-
|| echo "[DB] Remote file missing – NOT deleted" | tee -a $LOGFILE
44-
45-
# --------------------------------------
46-
# USERDATA BACKUP
47-
# --------------------------------------
48-
49-
echo "[USERDATA] Creating archive..." | tee -a $LOGFILE
50-
docker run --rm -v ${USERDATA_VOLUME}:/volume -v /backup:/backup alpine sh -c \
51-
"tar -czf /backup/userdata_${TIMESTAMP}.tar.gz -C /volume ." >> $LOGFILE 2>&1
52-
53-
echo "[USERDATA] Uploading to remote..." | tee -a $LOGFILE
54-
scp -i ${SSH_KEY_PATH} /backup/userdata_${TIMESTAMP}.tar.gz \
55-
${SERVER_USER}@${SERVER_IP}:${REMOTE_BACKUP_PATH}/userdata/ >> $LOGFILE 2>&1
56-
57-
ssh -i ${SSH_KEY_PATH} ${SERVER_USER}@${SERVER_IP} \
58-
"[ -f ${REMOTE_BACKUP_PATH}/userdata/userdata_${TIMESTAMP}.tar.gz ]" \
59-
&& { echo "[USERDATA] Remote verified, deleting local file" | tee -a $LOGFILE; rm /backup/userdata_${TIMESTAMP}.tar.gz; } \
60-
|| echo "[USERDATA] Remote file missing – NOT deleted" | tee -a $LOGFILE
61-
62-
# --------------------------------------
63-
# LUCEE IMAGE BACKUP
64-
# --------------------------------------
65-
66-
echo "[LUCEE] Saving Docker image..." | tee -a $LOGFILE
67-
docker save -o /backup/image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar \
68-
${LUCEE_IMAGE}:${LUCEE_IMAGE_VERSION} >> $LOGFILE 2>&1
69-
70-
echo "[LUCEE] Uploading to remote..." | tee -a $LOGFILE
71-
scp -i ${SSH_KEY_PATH} /backup/image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar \
72-
${SERVER_USER}@${SERVER_IP}:${REMOTE_BACKUP_PATH}/lucee/ >> $LOGFILE 2>&1
73-
74-
ssh -i ${SSH_KEY_PATH} ${SERVER_USER}@${SERVER_IP} \
75-
"[ -f ${REMOTE_BACKUP_PATH}/lucee/image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar ]" \
76-
&& { echo "[LUCEE] Remote verified, deleting local file" | tee -a $LOGFILE; rm /backup/image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar; } \
77-
|| echo "[LUCEE] Remote file missing – NOT deleted" | tee -a $LOGFILE
78-
79-
# --------------------------------------
80-
# REMOTE ROTATION
81-
# --------------------------------------
82-
83-
echo "[ROTATE] Cleaning up old remote backups..." | tee -a $LOGFILE
84-
for folder in db userdata lucee; do
85-
ssh -i ${SSH_KEY_PATH} ${SERVER_USER}@${SERVER_IP} \
86-
"cd ${REMOTE_BACKUP_PATH}/$folder && ls -tp | grep -v '/$' | tail -n +31 | xargs -I {} rm -- {}" >> $LOGFILE 2>&1
87-
done
88-
89-
echo "[BACKUP DONE] $(date)" | tee -a $LOGFILE
90-
91-
set +a
6+
# ensure restore scripts are executable
7+
chmod +x "$(dirname "$0")"/backup_ssh.sh 2>/dev/null
8+
chmod +x "$(dirname "$0")"/backup_sftp.sh 2>/dev/null
9+
10+
if [[ "$BACKUP_MODE" == "ssh" ]]; then
11+
exec "$(dirname "$0")/backup_ssh.sh"
12+
elif [[ "$BACKUP_MODE" == "sftp" ]]; then
13+
exec "$(dirname "$0")/backup_sftp.sh"
14+
else
15+
echo "[ERROR] BACKUP_MODE is invalid → use \"ssh\" or \"sftp\"."
16+
exit 1
17+
fi

config/backup/backup_sftp.sh

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/bin/bash
2+
set -a
3+
4+
LOGFILE="/var/log/backup-cron.log"
5+
echo "[BACKUP SFTP MODE] $(date)" | tee -a $LOGFILE
6+
7+
source "$(dirname "$0")/../../.env"
8+
9+
SFTP_FLAGS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o CheckHostIP=no -o LogLevel=ERROR -4"
10+
11+
DB_VOLUME="${COMPOSE_PROJECT_NAME}_db_volume"
12+
USERDATA_VOLUME="${COMPOSE_PROJECT_NAME}_userdata_volume"
13+
TIMESTAMP=$(date +"%Y%m%d_%H%M")
14+
15+
mkdir -p /backup
16+
17+
# -----------------------------------------
18+
# CREATE REMOTE FOLDERS
19+
# -----------------------------------------
20+
echo "[SFTP] Ensuring remote folders exists..." | tee -a $LOGFILE
21+
22+
sftp -q -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} \
23+
${SERVER_USER}@${SERVER_HOST} >/dev/null 2>&1 <<EOF
24+
mkdir ${REMOTE_BACKUP_PATH}/db
25+
mkdir ${REMOTE_BACKUP_PATH}/userdata
26+
mkdir ${REMOTE_BACKUP_PATH}/lucee
27+
EOF
28+
29+
30+
# -----------------------------------------
31+
# DB BACKUP
32+
# -----------------------------------------
33+
echo "[DB] Creating archive..." | tee -a $LOGFILE
34+
docker run --rm -v ${DB_VOLUME}:/volume -v /backup:/backup alpine sh -c \
35+
"tar -czf /backup/database_${TIMESTAMP}.tar.gz -C /volume ." >> $LOGFILE 2>&1
36+
37+
echo "[DB] Uploading..." | tee -a $LOGFILE
38+
scp -q -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} \
39+
/backup/database_${TIMESTAMP}.tar.gz \
40+
${SERVER_USER}@${SERVER_HOST}:${REMOTE_BACKUP_PATH}/db/ >/dev/null 2>&1
41+
42+
# verify remove
43+
FILE="database_${TIMESTAMP}.tar.gz"
44+
sftp -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} ${SERVER_USER}@${SERVER_HOST} <<EOF | grep "$FILE" >/dev/null
45+
ls ${REMOTE_BACKUP_PATH}/db
46+
EOF
47+
[[ $? -eq 0 ]] && rm /backup/${FILE}
48+
49+
# -----------------------------------------
50+
# USERDATA BACKUP
51+
# -----------------------------------------
52+
echo "[USERDATA] Creating archive..." | tee -a $LOGFILE
53+
docker run --rm -v ${USERDATA_VOLUME}:/volume -v /backup:/backup alpine sh -c \
54+
"tar -czf /backup/userdata_${TIMESTAMP}.tar.gz -C /volume ." >> $LOGFILE 2>&1
55+
56+
echo "[USERDATA] Uploading..." | tee -a $LOGFILE
57+
scp -q -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} \
58+
/backup/userdata_${TIMESTAMP}.tar.gz \
59+
${SERVER_USER}@${SERVER_HOST}:${REMOTE_BACKUP_PATH}/userdata/ >/dev/null 2>&1
60+
61+
FILE="userdata_${TIMESTAMP}.tar.gz"
62+
sftp -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} ${SERVER_USER}@${SERVER_HOST} <<EOF | grep "$FILE" >/dev/null
63+
ls ${REMOTE_BACKUP_PATH}/userdata
64+
EOF
65+
[[ $? -eq 0 ]] && rm /backup/${FILE}
66+
67+
# -----------------------------------------
68+
# LUCEE IMAGE BACKUP
69+
# -----------------------------------------
70+
echo "[LUCEE] Saving Docker image..." | tee -a $LOGFILE
71+
docker save -o /backup/image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar \
72+
${LUCEE_IMAGE}:${LUCEE_IMAGE_VERSION} >> $LOGFILE 2>&1
73+
74+
echo "[LUCEE] Uploading..." | tee -a $LOGFILE
75+
scp -q -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} \
76+
/backup/image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar \
77+
${SERVER_USER}@${SERVER_HOST}:${REMOTE_BACKUP_PATH}/lucee/ >/dev/null 2>&1
78+
79+
FILE="image_${LUCEE_IMAGE}_${LUCEE_IMAGE_VERSION}_${TIMESTAMP}.tar"
80+
sftp -i ${SSH_KEY_PATH} ${SFTP_FLAGS} -P ${SERVER_PORT} ${SERVER_USER}@${SERVER_HOST} <<EOF | grep "$FILE" >/dev/null
81+
ls ${REMOTE_BACKUP_PATH}/lucee
82+
EOF
83+
[[ $? -eq 0 ]] && rm /backup/${FILE}
84+
85+
echo "[ROTATE] Cleaning up old remote backups..." | tee -a $LOGFILE
86+
87+
for folder in db userdata lucee; do
88+
echo "[ROTATE] Folder: $folder" >> $LOGFILE
89+
90+
RAW=$(
91+
sftp -q -i "${SSH_KEY_PATH}" ${SFTP_FLAGS} -P "${SERVER_PORT}" \
92+
-b - "${SERVER_USER}@${SERVER_HOST}" <<EOF 2>/dev/null
93+
ls -1 ${REMOTE_BACKUP_PATH}/${folder}
94+
EOF
95+
)
96+
97+
FILELIST=$(echo "$RAW" \
98+
| grep -v "^sftp>" \
99+
| grep -v "^ls" \
100+
| sed "s#${REMOTE_BACKUP_PATH}/${folder}/##"
101+
)
102+
103+
[ -z "$FILELIST" ] && continue
104+
105+
SORTED=$(echo "$FILELIST" | sort -r)
106+
107+
DELETE=$(echo "$SORTED" | tail -n +$((${MAX_BACKUPS}+1)))
108+
109+
DELETE_COUNT=$(echo "$DELETE" | grep -c .)
110+
111+
[ "$DELETE_COUNT" -eq 0 ] && continue
112+
113+
echo "[ROTATE] $DELETE_COUNT files will be deleted..." >> $LOGFILE
114+
115+
BATCHFILE=$(mktemp)
116+
117+
for file in $DELETE; do
118+
echo "rm \"${REMOTE_BACKUP_PATH}/${folder}/$file\"" >> "$BATCHFILE"
119+
done
120+
121+
sftp -q -i "${SSH_KEY_PATH}" ${SFTP_FLAGS} -P "${SERVER_PORT}" \
122+
-b "$BATCHFILE" "${SERVER_USER}@${SERVER_HOST}" >/dev/null 2>&1
123+
124+
rm "$BATCHFILE"
125+
126+
echo "[ROTATE] $DELETE_COUNT files from $folder were deleted." >> $LOGFILE
127+
done
128+
129+
130+
echo "[BACKUP DONE] $(date)" | tee -a $LOGFILE
131+
132+
set +a

0 commit comments

Comments
 (0)