@@ -13,6 +13,7 @@ NC='\033[0m'
1313# Defaults
1414MEDIA_DIR=" $HOME /Media"
1515FIX_MODE=false
16+ ALLOW_OUTSIDE_MEDIA_DIR=false
1617PASS_COUNT=0
1718WARN_COUNT=0
1819FAIL_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
4246pass () {
@@ -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
7888done
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+
80112echo " "
81113echo " =============================="
82114echo " Runtime Detection"
@@ -123,6 +155,22 @@ else
123155 fail " No docker-compose.yml found in $MEDIA_DIR "
124156fi
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+
126174echo " "
127175echo " =============================="
128176echo " .env Validation"
@@ -172,48 +220,73 @@ echo ""
172220
173221# Parse PUID/PGID from docker-compose.yml for each service
174222if [[ -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
251325DIRS_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