Refactor codebase for better maintainability

- samba.sh: Add SMB_CONF constant, create set_config_option() to reduce
  duplication, replace deprecated egrep with grep -E, improve quoting
- Dockerfile: Replace multiple echo commands with heredoc for readability,
  separate logical build stages
- docker-compose.yml: Add healthcheck, improve formatting
- .dockerignore: Extend exclusion list

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Struchkov Mark
2026-01-07 16:29:18 +03:00
parent 1ee7f1ae6e
commit ff90440e32
4 changed files with 160 additions and 120 deletions

View File

@@ -1,4 +1,10 @@
.git .git
LICENSE .gitignore
.idea
.drone.yml
README.md README.md
logo.jpg CLAUDE.md
LICENSE
*.jpg
docker-compose.yml
.env*

View File

@@ -5,59 +5,69 @@ LABEL maintainer="Struchkov Mark <mark@struchkov.dev>"
LABEL org.opencontainers.image.source="https://github.com/upagge/samba" LABEL org.opencontainers.image.source="https://github.com/upagge/samba"
LABEL org.opencontainers.image.description="Samba file server with Time Machine support" LABEL org.opencontainers.image.description="Samba file server with Time Machine support"
# Install samba # Install packages and create samba user
RUN apk --no-cache --no-progress upgrade && \ RUN apk --no-cache --no-progress upgrade && \
apk --no-cache --no-progress add bash samba shadow tini tzdata && \ apk --no-cache --no-progress add bash samba shadow tini tzdata && \
addgroup -S smb && \ addgroup -S smb && \
adduser -S -D -H -h /tmp -s /sbin/nologin -G smb -g 'Samba User' smbuser &&\ adduser -S -D -H -h /tmp -s /sbin/nologin -G smb -g 'Samba User' smbuser
file="/etc/samba/smb.conf" && \
sed -i 's|^;* *\(log file = \).*| \1/dev/stdout|' $file && \ # Configure smb.conf
sed -i 's|^;* *\(load printers = \).*| \1no|' $file && \ RUN file="/etc/samba/smb.conf" && \
sed -i 's|^;* *\(printcap name = \).*| \1/dev/null|' $file && \ # Modify existing options
sed -i 's|^;* *\(printing = \).*| \1bsd|' $file && \ sed -i 's|^;* *\(log file = \).*| \1/dev/stdout|' "$file" && \
sed -i 's|^;* *\(unix password sync = \).*| \1no|' $file && \ sed -i 's|^;* *\(load printers = \).*| \1no|' "$file" && \
sed -i 's|^;* *\(preserve case = \).*| \1yes|' $file && \ sed -i 's|^;* *\(printcap name = \).*| \1/dev/null|' "$file" && \
sed -i 's|^;* *\(short preserve case = \).*| \1yes|' $file && \ sed -i 's|^;* *\(printing = \).*| \1bsd|' "$file" && \
sed -i 's|^;* *\(default case = \).*| \1lower|' $file && \ sed -i 's|^;* *\(unix password sync = \).*| \1no|' "$file" && \
sed -i '/Share Definitions/,$d' $file && \ sed -i 's|^;* *\(preserve case = \).*| \1yes|' "$file" && \
echo ' pam password change = yes' >>$file && \ sed -i 's|^;* *\(short preserve case = \).*| \1yes|' "$file" && \
echo ' map to guest = bad user' >>$file && \ sed -i 's|^;* *\(default case = \).*| \1lower|' "$file" && \
echo ' usershare allow guests = yes' >>$file && \ sed -i '/Share Definitions/,$d' "$file" && \
echo ' create mask = 0664' >>$file && \ # Append additional configuration
echo ' force create mode = 0664' >>$file && \ cat >> "$file" <<'EOF'
echo ' directory mask = 0775' >>$file && \ pam password change = yes
echo ' force directory mode = 0775' >>$file && \ map to guest = bad user
echo ' force user = smbuser' >>$file && \ usershare allow guests = yes
echo ' force group = smb' >>$file && \ create mask = 0664
echo ' follow symlinks = yes' >>$file && \ force create mode = 0664
echo ' load printers = no' >>$file && \ directory mask = 0775
echo ' printing = bsd' >>$file && \ force directory mode = 0775
echo ' printcap name = /dev/null' >>$file && \ force user = smbuser
echo ' disable spoolss = yes' >>$file && \ force group = smb
echo ' strict locking = no' >>$file && \ follow symlinks = yes
echo ' aio read size = 0' >>$file && \ load printers = no
echo ' aio write size = 0' >>$file && \ printing = bsd
echo ' vfs objects = catia fruit recycle streams_xattr' >>$file && \ printcap name = /dev/null
echo ' recycle:keeptree = yes' >>$file && \ disable spoolss = yes
echo ' recycle:maxsize = 0' >>$file && \ strict locking = no
echo ' recycle:repository = .deleted' >>$file && \ aio read size = 0
echo ' recycle:versions = yes' >>$file && \ aio write size = 0
echo '' >>$file && \ vfs objects = catia fruit recycle streams_xattr
echo ' # Security' >>$file && \
echo ' client ipc max protocol = SMB3' >>$file && \ # Recycle bin
echo ' client ipc min protocol = SMB2_10' >>$file && \ recycle:keeptree = yes
echo ' client max protocol = SMB3' >>$file && \ recycle:maxsize = 0
echo ' client min protocol = SMB2_10' >>$file && \ recycle:repository = .deleted
echo ' server max protocol = SMB3' >>$file && \ recycle:versions = yes
echo ' server min protocol = SMB2_10' >>$file && \
echo '' >>$file && \ # Security
echo ' # Time Machine' >>$file && \ client ipc max protocol = SMB3
echo ' fruit:delete_empty_adfiles = yes' >>$file && \ client ipc min protocol = SMB2_10
echo ' fruit:time machine = yes' >>$file && \ client max protocol = SMB3
echo ' fruit:veto_appledouble = no' >>$file && \ client min protocol = SMB2_10
echo ' fruit:wipe_intentionally_left_blank_rfork = yes' >>$file && \ server max protocol = SMB3
echo '' >>$file && \ server min protocol = SMB2_10
rm -rf /tmp/*
# Time Machine
fruit:delete_empty_adfiles = yes
fruit:time machine = yes
fruit:veto_appledouble = no
fruit:wipe_intentionally_left_blank_rfork = yes
EOF
# Cleanup
RUN rm -rf /tmp/*
COPY samba.sh /usr/bin/ COPY samba.sh /usr/bin/
@@ -66,7 +76,6 @@ EXPOSE 137/udp 138/udp 139 445
HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=3 \ HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=3 \
CMD smbclient -L \\localhost -U % -m SMB3 || exit 1 CMD smbclient -L \\localhost -U % -m SMB3 || exit 1
VOLUME ["/etc", "/var/cache/samba", "/var/lib/samba", "/var/log/samba",\ VOLUME ["/etc", "/var/cache/samba", "/var/lib/samba", "/var/log/samba", "/run/samba"]
"/run/samba"]
ENTRYPOINT ["/sbin/tini", "--", "/usr/bin/samba.sh"] ENTRYPOINT ["/sbin/tini", "--", "/usr/bin/samba.sh"]

View File

@@ -1,6 +1,8 @@
services: services:
samba: samba:
image: docker.struchkov.dev/samba image: docker.struchkov.dev/samba
restart: unless-stopped
environment: environment:
TZ: 'EST5EDT' TZ: 'EST5EDT'
# Use environment variables for shares and users instead of command line # Use environment variables for shares and users instead of command line
@@ -8,30 +10,41 @@ services:
# SHARE2: "Bobs Volume;/mnt2;yes;no;no;bob" # SHARE2: "Bobs Volume;/mnt2;yes;no;no;bob"
# USER: "bob;${SAMBA_BOB_PASSWORD}" # USER: "bob;${SAMBA_BOB_PASSWORD}"
# PERMISSIONS: "true" # PERMISSIONS: "true"
env_file: env_file:
- .env # Put sensitive data like passwords here - .env # Put sensitive data like passwords here
networks:
- default
ports: ports:
- "137:137/udp" - "137:137/udp"
- "138:138/udp" - "138:138/udp"
- "139:139/tcp" - "139:139/tcp"
- "445:445/tcp" - "445:445/tcp"
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
stdin_open: true
tty: true
volumes: volumes:
- /mnt:/mnt:z - /mnt:/mnt:z
- /mnt2:/mnt2:z - /mnt2:/mnt2:z
read_only: true
tmpfs:
- /tmp
stdin_open: true
tty: true
healthcheck:
test: ["CMD", "smbclient", "-L", "\\\\localhost", "-U", "%", "-m", "SMB3"]
interval: 60s
timeout: 15s
start_period: 10s
retries: 3
deploy: deploy:
resources: resources:
limits: limits:
memory: 512M memory: 512M
reservations: reservations:
memory: 128M memory: 128M
command: '-s "Mount;/mnt" -s "Bobs Volume;/mnt2;yes;no;no;bob" -u "bob;${SAMBA_BOB_PASSWORD:-changeme}" -p' command: '-s "Mount;/mnt" -s "Bobs Volume;/mnt2;yes;no;no;bob" -u "bob;${SAMBA_BOB_PASSWORD:-changeme}" -p'
networks: networks:

124
samba.sh
View File

@@ -18,6 +18,9 @@
set -o nounset # Treat unset variables as an error set -o nounset # Treat unset variables as an error
# Constants
readonly SMB_CONF="/etc/samba/smb.conf"
### parse_args: safely parse semicolon-separated arguments ### parse_args: safely parse semicolon-separated arguments
# Arguments: # Arguments:
# input) semicolon-separated string # input) semicolon-separated string
@@ -44,15 +47,34 @@ parse_args() {
# Arguments: # Arguments:
# chars) from:to character mappings separated by ',' # chars) from:to character mappings separated by ','
# Return: configured character mapings # Return: configured character mapings
charmap() { local chars="$1" file=/etc/samba/smb.conf charmap() { local chars="$1"
grep -q catia $file || sed -i '/TCP_NODELAY/a \ grep -q catia "$SMB_CONF" || sed -i '/TCP_NODELAY/a \
\ \
vfs objects = catia\ vfs objects = catia\
catia:mappings =\ catia:mappings =\
' $file ' "$SMB_CONF"
sed -i '/catia:mappings/s| =.*| = '"$chars"'|' $file sed -i '/catia:mappings/s| =.*| = '"$chars"'|' "$SMB_CONF"
}
### set_config_option: set a config option in a specific section
# Arguments:
# section) section name (e.g., "global", "share")
# option) raw option string (e.g., "log level = 2")
# Return: line added/updated in smb.conf
set_config_option() {
local section="$1"
local key value
key="$(sed 's| *=.*||' <<< "$2")"
value="$(sed 's|[^=]*= *||' <<< "$2")"
if sed -n '/^\['"$section"'\]/,/^\[/p' "$SMB_CONF" | grep -qE '^;*\s*'"$key"; then
sed -i '/^\['"$section"'\]/,/^\[/s|^;*\s*\('"$key"' = \).*| \1'"$value"'|' \
"$SMB_CONF"
else
sed -i '/\['"$section"'\]/a \ '"$key = $value" "$SMB_CONF"
fi
} }
### generic: set a generic config option in a section ### generic: set a generic config option in a section
@@ -60,36 +82,24 @@ charmap() { local chars="$1" file=/etc/samba/smb.conf
# section) section of config file # section) section of config file
# option) raw option # option) raw option
# Return: line added to smb.conf (replaces existing line with same key) # Return: line added to smb.conf (replaces existing line with same key)
generic() { local section="$1" key="$(sed 's| *=.*||' <<< $2)" \ generic() {
value="$(sed 's|[^=]*= *||' <<< $2)" file=/etc/samba/smb.conf set_config_option "$1" "$2"
if sed -n '/^\['"$section"'\]/,/^\[/p' $file | grep -qE '^;*\s*'"$key"; then
sed -i '/^\['"$1"'\]/,/^\[/s|^;*\s*\('"$key"' = \).*| \1'"$value"'|' \
"$file"
else
sed -i '/\['"$section"'\]/a \ '"$key = $value" "$file"
fi
} }
### global: set a global config option ### global: set a global config option
# Arguments: # Arguments:
# option) raw option # option) raw option
# Return: line added to smb.conf (replaces existing line with same key) # Return: line added to smb.conf (replaces existing line with same key)
global() { local key="$(sed 's| *=.*||' <<< $1)" \ global() {
value="$(sed 's|[^=]*= *||' <<< $1)" file=/etc/samba/smb.conf set_config_option "global" "$1"
if sed -n '/^\[global\]/,/^\[/p' $file | grep -qE '^;*\s*'"$key"; then
sed -i '/^\[global\]/,/^\[/s|^;*\s*\('"$key"' = \).*| \1'"$value"'|' \
"$file"
else
sed -i '/\[global\]/a \ '"$key = $value" "$file"
fi
} }
### include: add a samba config file include ### include: add a samba config file include
# Arguments: # Arguments:
# file) file to import # file) file to import
include() { local includefile="$1" file=/etc/samba/smb.conf include() { local includefile="$1"
sed -i "\\|include = $includefile|d" "$file" sed -i "\\|include = $includefile|d" "$SMB_CONF"
echo "include = $includefile" >> "$file" echo "include = $includefile" >> "$SMB_CONF"
} }
### import: import a smbpasswd file ### import: import a smbpasswd file
@@ -107,11 +117,11 @@ import() { local file="$1" name id
# Arguments: # Arguments:
# none) # none)
# Return: result # Return: result
perms() { local i file=/etc/samba/smb.conf perms() { local i
for i in $(awk -F ' = ' '/ path = / {print $2}' $file); do for i in $(awk -F ' = ' '/ path = / {print $2}' "$SMB_CONF"); do
chown -Rh smbuser. $i chown -Rh smbuser. "$i"
find $i -type d ! -perm 775 -exec chmod 775 {} \; find "$i" -type d ! -perm 775 -exec chmod 775 {} \;
find $i -type f ! -perm 0664 -exec chmod 0664 {} \; find "$i" -type f ! -perm 0664 -exec chmod 0664 {} \;
done done
} }
export -f perms export -f perms
@@ -120,8 +130,8 @@ export -f perms
# Arguments: # Arguments:
# none) # none)
# Return: result # Return: result
recycle() { local file=/etc/samba/smb.conf recycle() {
sed -i '/recycle:/d; /vfs objects/s/ recycle / /' $file sed -i '/recycle:/d; /vfs objects/s/ recycle / /' "$SMB_CONF"
} }
### share: Add share ### share: Add share
@@ -136,39 +146,41 @@ recycle() { local file=/etc/samba/smb.conf
# writelist) list of users that can write to a RO share # writelist) list of users that can write to a RO share
# comment) description of share # comment) description of share
# Return: result # Return: result
share() { local share="$1" path="$2" browsable="${3:-yes}" ro="${4:-yes}" \ share() {
local share="$1" path="$2" browsable="${3:-yes}" ro="${4:-yes}" \
guest="${5:-yes}" users="${6:-""}" admins="${7:-""}" \ guest="${5:-yes}" users="${6:-""}" admins="${7:-""}" \
writelist="${8:-""}" comment="${9:-""}" file=/etc/samba/smb.conf writelist="${8:-""}" comment="${9:-""}"
sed -i "/\\[$share\\]/,/^\$/d" $file
echo "[$share]" >>$file sed -i "/\\[$share\\]/,/^\$/d" "$SMB_CONF"
echo " path = $path" >>$file echo "[$share]" >> "$SMB_CONF"
echo " browsable = $browsable" >>$file echo " path = $path" >> "$SMB_CONF"
echo " read only = $ro" >>$file echo " browsable = $browsable" >> "$SMB_CONF"
echo " guest ok = $guest" >>$file echo " read only = $ro" >> "$SMB_CONF"
echo " guest ok = $guest" >> "$SMB_CONF"
[[ ${VETO:-yes} == no ]] || { [[ ${VETO:-yes} == no ]] || {
echo -n " veto files = /.apdisk/.DS_Store/.TemporaryItems/" >>$file echo -n " veto files = /.apdisk/.DS_Store/.TemporaryItems/" >> "$SMB_CONF"
echo -n ".Trashes/desktop.ini/ehthumbs.db/Network Trash Folder/" >>$file echo -n ".Trashes/desktop.ini/ehthumbs.db/Network Trash Folder/" >> "$SMB_CONF"
echo "Temporary Items/Thumbs.db/" >>$file echo "Temporary Items/Thumbs.db/" >> "$SMB_CONF"
echo " delete veto files = yes" >>$file echo " delete veto files = yes" >> "$SMB_CONF"
} }
[[ ${users:-""} && ! ${users:-""} == all ]] && [[ ${users:-""} && ! ${users:-""} == all ]] &&
echo " valid users = $(tr ',' ' ' <<< $users)" >>$file echo " valid users = $(tr ',' ' ' <<< "$users")" >> "$SMB_CONF"
[[ ${admins:-""} && ! ${admins:-""} =~ none ]] && [[ ${admins:-""} && ! ${admins:-""} =~ none ]] &&
echo " admin users = $(tr ',' ' ' <<< $admins)" >>$file echo " admin users = $(tr ',' ' ' <<< "$admins")" >> "$SMB_CONF"
[[ ${writelist:-""} && ! ${writelist:-""} =~ none ]] && [[ ${writelist:-""} && ! ${writelist:-""} =~ none ]] &&
echo " write list = $(tr ',' ' ' <<< $writelist)" >>$file echo " write list = $(tr ',' ' ' <<< "$writelist")" >> "$SMB_CONF"
[[ ${comment:-""} && ! ${comment:-""} =~ none ]] && [[ ${comment:-""} && ! ${comment:-""} =~ none ]] &&
echo " comment = $(tr ',' ' ' <<< $comment)" >>$file echo " comment = $(tr ',' ' ' <<< "$comment")" >> "$SMB_CONF"
echo "" >>$file echo "" >> "$SMB_CONF"
[[ -d $path ]] || mkdir -p $path [[ -d "$path" ]] || mkdir -p "$path"
} }
### smb: disable SMB2 minimum ### smb: disable SMB2 minimum
# Arguments: # Arguments:
# none) # none)
# Return: result # Return: result
smb() { local file=/etc/samba/smb.conf smb() {
sed -i 's/\([^#]*min protocol *=\).*/\1 LANMAN1/' $file sed -i 's/\([^#]*min protocol *=\).*/\1 LANMAN1/' "$SMB_CONF"
} }
### user: add a user ### user: add a user
@@ -192,17 +204,17 @@ user() { local name="$1" passwd="$2" id="${3:-""}" group="${4:-""}" \
# Arguments: # Arguments:
# workgroup) the name to set # workgroup) the name to set
# Return: configure the correct workgroup # Return: configure the correct workgroup
workgroup() { local workgroup="$1" file=/etc/samba/smb.conf workgroup() { local workgroup="$1"
sed -i 's|^\( *workgroup = \).*|\1'"$workgroup"'|' $file sed -i 's|^\( *workgroup = \).*|\1'"$workgroup"'|' "$SMB_CONF"
} }
### widelinks: allow access wide symbolic links ### widelinks: allow access wide symbolic links
# Arguments: # Arguments:
# none) # none)
# Return: result # Return: result
widelinks() { local file=/etc/samba/smb.conf \ widelinks() {
replace='\1\n wide links = yes\n unix extensions = no' local replace='\1\n wide links = yes\n unix extensions = no'
sed -i 's/\(follow symlinks = yes\)/'"$replace"'/' $file sed -i 's/\(follow symlinks = yes\)/'"$replace"'/' "$SMB_CONF"
} }
### usage: Help ### usage: Help
@@ -311,7 +323,7 @@ if [[ $# -ge 1 && -x $(which $1 2>&-) ]]; then
elif [[ $# -ge 1 ]]; then elif [[ $# -ge 1 ]]; then
echo "ERROR: command not found: $1" echo "ERROR: command not found: $1"
exit 13 exit 13
elif ps -ef | egrep -v grep | grep -q smbd; then elif ps -ef | grep -E -v grep | grep -q smbd; then
echo "Service already running, please restart container to apply changes" echo "Service already running, please restart container to apply changes"
else else
[[ ${NMBD:-""} ]] && ionice -c 3 nmbd -D [[ ${NMBD:-""} ]] && ionice -c 3 nmbd -D