Skip to content

Commit 8a218a9

Browse files
committed
Strengthen permission safety checks and compose parsing
1 parent 38f9c28 commit 8a218a9

2 files changed

Lines changed: 122 additions & 26 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ bash fix-permissions.sh --fix
6969

7070
# Fix with custom path
7171
bash fix-permissions.sh --fix --path /Volumes/Media
72+
73+
# Allow fixes for compose mounts outside --path
74+
bash fix-permissions.sh --fix --allow-outside-media-dir
7275
```
7376

7477
## Example Output
@@ -98,6 +101,7 @@ Run with `--fix` to automatically resolve permission issues:
98101

99102
- Runs `chown -R` on directories with wrong ownership
100103
- Reports what was changed
104+
- Protects paths outside `--path` by default (use `--allow-outside-media-dir` to override)
101105

102106
The script never modifies docker-compose.yml or .env. It only fixes file ownership on disk.
103107

fix-permissions.sh

Lines changed: 118 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ NC='\033[0m'
1313
# Defaults
1414
MEDIA_DIR="$HOME/Media"
1515
FIX_MODE=false
16+
ALLOW_OUTSIDE_MEDIA_DIR=false
1617
PASS_COUNT=0
1718
WARN_COUNT=0
1819
FAIL_COUNT=0
@@ -30,13 +31,16 @@ usage() {
3031
echo "Options:"
3132
echo " --fix Fix permission issues (chown directories)"
3233
echo " --path DIR Path to media directory (default: ~/Media)"
34+
echo " --allow-outside-media-dir Allow --fix to chown compose mounts outside --path"
3335
echo " --help Show this help message"
3436
echo ""
3537
echo "Examples:"
3638
echo " bash fix-permissions.sh"
3739
echo " bash fix-permissions.sh --path /Volumes/Media"
3840
echo " bash fix-permissions.sh --fix"
3941
echo " bash fix-permissions.sh --fix --path /Volumes/Media"
42+
echo " bash fix-permissions.sh --fix --allow-outside-media-dir"
43+
exit "${1:-0}"
4044
}
4145

4246
pass() {
@@ -62,21 +66,49 @@ while [[ $# -gt 0 ]]; do
6266
shift
6367
;;
6468
--path)
69+
if [[ $# -lt 2 ]]; then
70+
echo "Missing value for --path"
71+
usage 1
72+
fi
6573
MEDIA_DIR="$2"
6674
shift 2
6775
;;
76+
--allow-outside-media-dir)
77+
ALLOW_OUTSIDE_MEDIA_DIR=true
78+
shift
79+
;;
6880
--help)
6981
usage
70-
exit 0
7182
;;
7283
*)
7384
echo "Unknown option: $1"
74-
usage
75-
exit 1
85+
usage 1
7686
;;
7787
esac
7888
done
7989

90+
MEDIA_DIR="${MEDIA_DIR/#\~/$HOME}"
91+
92+
canonical_dir() {
93+
local input="$1"
94+
if [[ -d "$input" ]]; then
95+
(cd "$input" 2>/dev/null && pwd -P)
96+
else
97+
return 1
98+
fi
99+
}
100+
101+
is_path_under() {
102+
local candidate="$1"
103+
local root="$2"
104+
[[ "$candidate" == "$root" || "$candidate" == "$root/"* ]]
105+
}
106+
107+
MEDIA_DIR_REAL=""
108+
if [[ -d "$MEDIA_DIR" ]]; then
109+
MEDIA_DIR_REAL=$(canonical_dir "$MEDIA_DIR" || true)
110+
fi
111+
80112
echo ""
81113
echo "=============================="
82114
echo " Runtime Detection"
@@ -123,6 +155,22 @@ else
123155
fail "No docker-compose.yml found in $MEDIA_DIR"
124156
fi
125157

158+
echo ""
159+
echo "=============================="
160+
echo " Compose Parse Check"
161+
echo "=============================="
162+
echo ""
163+
164+
if [[ -n "$COMPOSE_FILE" ]]; then
165+
if (cd "$MEDIA_DIR" && docker compose -f "$COMPOSE_FILE" config >/dev/null 2>&1); then
166+
pass "Compose file parses successfully"
167+
else
168+
warn "Compose file failed to parse with 'docker compose config'"
169+
fi
170+
else
171+
warn "Skipping compose parse check (no compose file)"
172+
fi
173+
126174
echo ""
127175
echo "=============================="
128176
echo " .env Validation"
@@ -172,48 +220,73 @@ echo ""
172220

173221
# Parse PUID/PGID from docker-compose.yml for each service
174222
if [[ -n "$COMPOSE_FILE" ]]; then
223+
resolve_compose_id() {
224+
local raw="$1"
225+
local key="$2"
226+
local env_value="$3"
227+
local cleaned
228+
cleaned=$(echo "$raw" | sed 's/#.*$//' | tr -d '[:space:]"'"'"'')
229+
if [[ "$cleaned" == "\${$key}" || "$cleaned" == "\$$key" ]]; then
230+
if [[ -n "$env_value" ]]; then
231+
echo "$env_value"
232+
else
233+
echo "unset"
234+
fi
235+
else
236+
echo "$cleaned"
237+
fi
238+
}
239+
175240
# Extract service names and their PUID/PGID values
176241
CURRENT_SERVICE=""
177242
PUID_MISMATCHES=()
178243
PGID_MISMATCHES=()
179-
SERVICES_CHECKED=0
244+
ENTRIES_CHECKED=0
180245

181246
while IFS= read -r line; do
182247
# Detect service name (top-level key under services, indented with 2 spaces)
183248
if echo "$line" | grep -qE '^\s{2}[a-zA-Z][a-zA-Z0-9_-]+:\s*$'; then
184249
CURRENT_SERVICE=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/://')
185250
fi
186251

187-
# Detect PUID
188-
if echo "$line" | grep -qE '^\s+- PUID='; then
189-
SVC_PUID=$(echo "$line" | sed 's/.*PUID=//' | tr -d '[:space:]"'"'"'')
190-
# Resolve variable references
191-
if [[ "$SVC_PUID" == '${PUID}' ]] || [[ "$SVC_PUID" == '$PUID' ]]; then
192-
SVC_PUID="${ENV_PUID:-unset}"
193-
fi
194-
SERVICES_CHECKED=$((SERVICES_CHECKED + 1))
252+
# Detect PUID (list syntax: - PUID=..., mapping syntax: PUID: ...)
253+
SVC_PUID=""
254+
if echo "$line" | grep -qE '^\s*-\s*PUID='; then
255+
RAW_PUID=$(echo "$line" | sed 's/.*PUID=//')
256+
SVC_PUID=$(resolve_compose_id "$RAW_PUID" "PUID" "$ENV_PUID")
257+
elif echo "$line" | grep -qE '^\s*PUID:\s*'; then
258+
RAW_PUID=$(echo "$line" | sed -E 's/^[[:space:]]*PUID:[[:space:]]*//')
259+
SVC_PUID=$(resolve_compose_id "$RAW_PUID" "PUID" "$ENV_PUID")
260+
fi
261+
if [[ -n "$SVC_PUID" ]]; then
262+
ENTRIES_CHECKED=$((ENTRIES_CHECKED + 1))
195263
if [[ "$SVC_PUID" != "$EXPECTED_PUID" ]]; then
196264
PUID_MISMATCHES+=("$CURRENT_SERVICE uses PUID=$SVC_PUID, expected $EXPECTED_PUID")
197265
fi
198266
fi
199267

200-
# Detect PGID
201-
if echo "$line" | grep -qE '^\s+- PGID='; then
202-
SVC_PGID=$(echo "$line" | sed 's/.*PGID=//' | tr -d '[:space:]"'"'"'')
203-
if [[ "$SVC_PGID" == '${PGID}' ]] || [[ "$SVC_PGID" == '$PGID' ]]; then
204-
SVC_PGID="${ENV_PGID:-unset}"
205-
fi
268+
# Detect PGID (list syntax: - PGID=..., mapping syntax: PGID: ...)
269+
SVC_PGID=""
270+
if echo "$line" | grep -qE '^\s*-\s*PGID='; then
271+
RAW_PGID=$(echo "$line" | sed 's/.*PGID=//')
272+
SVC_PGID=$(resolve_compose_id "$RAW_PGID" "PGID" "$ENV_PGID")
273+
elif echo "$line" | grep -qE '^\s*PGID:\s*'; then
274+
RAW_PGID=$(echo "$line" | sed -E 's/^[[:space:]]*PGID:[[:space:]]*//')
275+
SVC_PGID=$(resolve_compose_id "$RAW_PGID" "PGID" "$ENV_PGID")
276+
fi
277+
if [[ -n "$SVC_PGID" ]]; then
278+
ENTRIES_CHECKED=$((ENTRIES_CHECKED + 1))
206279
if [[ "$SVC_PGID" != "$EXPECTED_PGID" ]]; then
207280
PGID_MISMATCHES+=("$CURRENT_SERVICE uses PGID=$SVC_PGID, expected $EXPECTED_PGID")
208281
fi
209282
fi
210283
done < "$COMPOSE_FILE"
211284

212-
if [[ $SERVICES_CHECKED -eq 0 ]]; then
285+
if [[ $ENTRIES_CHECKED -eq 0 ]]; then
213286
warn "No PUID/PGID environment variables found in compose file"
214287
else
215288
if [[ ${#PUID_MISMATCHES[@]} -eq 0 ]] && [[ ${#PGID_MISMATCHES[@]} -eq 0 ]]; then
216-
pass "All $SERVICES_CHECKED services use consistent PUID/PGID ($EXPECTED_PUID:$EXPECTED_PGID)"
289+
pass "All $ENTRIES_CHECKED PUID/PGID entries match expected $EXPECTED_PUID:$EXPECTED_PGID"
217290
else
218291
for msg in "${PUID_MISMATCHES[@]+"${PUID_MISMATCHES[@]}"}"; do
219292
[[ -n "$msg" ]] && warn "PUID mismatch: $msg"
@@ -246,6 +319,7 @@ DIRS_TO_CHECK=(
246319
"usenet"
247320
"transcode"
248321
"backup"
322+
"backups"
249323
)
250324

251325
DIRS_FOUND=0
@@ -261,9 +335,18 @@ for dir in "${DIRS_TO_CHECK[@]}"; do
261335
else
262336
fail "$dir/ owned by $DIR_UID:$DIR_GID, expected $EXPECTED_PUID:$EXPECTED_PGID"
263337
if [[ "$FIX_MODE" == true ]]; then
264-
echo -e "${CYAN}FIX${NC} Running: chown -R $EXPECTED_PUID:$EXPECTED_PGID $FULL_PATH"
265-
sudo chown -R "$EXPECTED_PUID:$EXPECTED_PGID" "$FULL_PATH"
266-
pass "$dir/ ownership fixed to $EXPECTED_PUID:$EXPECTED_PGID"
338+
DIR_REAL=$(canonical_dir "$FULL_PATH" || true)
339+
ALLOW_FIX=true
340+
if [[ "$ALLOW_OUTSIDE_MEDIA_DIR" != true ]] && [[ -n "$MEDIA_DIR_REAL" ]] && [[ -n "$DIR_REAL" ]] && ! is_path_under "$DIR_REAL" "$MEDIA_DIR_REAL"; then
341+
ALLOW_FIX=false
342+
warn "Skipping fix for $dir/ because it resolves outside $MEDIA_DIR (use --allow-outside-media-dir to override)"
343+
fi
344+
345+
if [[ "$ALLOW_FIX" == true ]]; then
346+
echo -e "${CYAN}FIX${NC} Running: chown -R $EXPECTED_PUID:$EXPECTED_PGID $FULL_PATH"
347+
sudo chown -R "$EXPECTED_PUID:$EXPECTED_PGID" "$FULL_PATH"
348+
pass "$dir/ ownership fixed to $EXPECTED_PUID:$EXPECTED_PGID"
349+
fi
267350
fi
268351
fi
269352
fi
@@ -284,15 +367,24 @@ if [[ -n "$COMPOSE_FILE" ]]; then
284367
if [[ "$HOST_PATH" == /* ]] && [[ -d "$HOST_PATH" ]]; then
285368
DIR_UID=$(stat -f "%u" "$HOST_PATH")
286369
DIR_GID=$(stat -f "%g" "$HOST_PATH")
370+
HOST_PATH_REAL=$(canonical_dir "$HOST_PATH" || true)
287371
SHORT_PATH=$(echo "$HOST_PATH" | sed "s|$HOME|~|")
288372
if [[ "$DIR_UID" == "$EXPECTED_PUID" ]] && [[ "$DIR_GID" == "$EXPECTED_PGID" ]]; then
289373
pass "Volume $SHORT_PATH owned by you ($DIR_UID:$DIR_GID)"
290374
else
291375
fail "Volume $SHORT_PATH owned by $DIR_UID:$DIR_GID, expected $EXPECTED_PUID:$EXPECTED_PGID"
292376
if [[ "$FIX_MODE" == true ]]; then
293-
echo -e "${CYAN}FIX${NC} Running: chown -R $EXPECTED_PUID:$EXPECTED_PGID $HOST_PATH"
294-
sudo chown -R "$EXPECTED_PUID:$EXPECTED_PGID" "$HOST_PATH"
295-
pass "Volume $SHORT_PATH ownership fixed"
377+
ALLOW_FIX=true
378+
if [[ "$ALLOW_OUTSIDE_MEDIA_DIR" != true ]] && [[ -n "$MEDIA_DIR_REAL" ]] && [[ -n "$HOST_PATH_REAL" ]] && ! is_path_under "$HOST_PATH_REAL" "$MEDIA_DIR_REAL"; then
379+
ALLOW_FIX=false
380+
warn "Skipping fix for $SHORT_PATH (outside $MEDIA_DIR). Use --allow-outside-media-dir to override."
381+
fi
382+
383+
if [[ "$ALLOW_FIX" == true ]]; then
384+
echo -e "${CYAN}FIX${NC} Running: chown -R $EXPECTED_PUID:$EXPECTED_PGID $HOST_PATH"
385+
sudo chown -R "$EXPECTED_PUID:$EXPECTED_PGID" "$HOST_PATH"
386+
pass "Volume $SHORT_PATH ownership fixed"
387+
fi
296388
fi
297389
fi
298390
fi

0 commit comments

Comments
 (0)