diff --git a/.dockerignore b/.dockerignore index 07488a0..8f50285 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,10 @@ .git -LICENSE +.gitignore +.idea +.drone.yml README.md -logo.jpg \ No newline at end of file +CLAUDE.md +LICENSE +*.jpg +docker-compose.yml +.env* diff --git a/Dockerfile b/Dockerfile index 9448256..cdab1ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,59 +5,69 @@ LABEL maintainer="Struchkov Mark " LABEL org.opencontainers.image.source="https://github.com/upagge/samba" 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 && \ apk --no-cache --no-progress add bash samba shadow tini tzdata && \ addgroup -S smb && \ - 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 && \ - sed -i 's|^;* *\(load printers = \).*| \1no|' $file && \ - sed -i 's|^;* *\(printcap name = \).*| \1/dev/null|' $file && \ - sed -i 's|^;* *\(printing = \).*| \1bsd|' $file && \ - sed -i 's|^;* *\(unix password sync = \).*| \1no|' $file && \ - sed -i 's|^;* *\(preserve case = \).*| \1yes|' $file && \ - sed -i 's|^;* *\(short preserve case = \).*| \1yes|' $file && \ - sed -i 's|^;* *\(default case = \).*| \1lower|' $file && \ - sed -i '/Share Definitions/,$d' $file && \ - echo ' pam password change = yes' >>$file && \ - echo ' map to guest = bad user' >>$file && \ - echo ' usershare allow guests = yes' >>$file && \ - echo ' create mask = 0664' >>$file && \ - echo ' force create mode = 0664' >>$file && \ - echo ' directory mask = 0775' >>$file && \ - echo ' force directory mode = 0775' >>$file && \ - echo ' force user = smbuser' >>$file && \ - echo ' force group = smb' >>$file && \ - echo ' follow symlinks = yes' >>$file && \ - echo ' load printers = no' >>$file && \ - echo ' printing = bsd' >>$file && \ - echo ' printcap name = /dev/null' >>$file && \ - echo ' disable spoolss = yes' >>$file && \ - echo ' strict locking = no' >>$file && \ - echo ' aio read size = 0' >>$file && \ - echo ' aio write size = 0' >>$file && \ - echo ' vfs objects = catia fruit recycle streams_xattr' >>$file && \ - echo ' recycle:keeptree = yes' >>$file && \ - echo ' recycle:maxsize = 0' >>$file && \ - echo ' recycle:repository = .deleted' >>$file && \ - echo ' recycle:versions = yes' >>$file && \ - echo '' >>$file && \ - echo ' # Security' >>$file && \ - echo ' client ipc max protocol = SMB3' >>$file && \ - echo ' client ipc min protocol = SMB2_10' >>$file && \ - echo ' client max protocol = SMB3' >>$file && \ - echo ' client min protocol = SMB2_10' >>$file && \ - echo ' server max protocol = SMB3' >>$file && \ - echo ' server min protocol = SMB2_10' >>$file && \ - echo '' >>$file && \ - echo ' # Time Machine' >>$file && \ - echo ' fruit:delete_empty_adfiles = yes' >>$file && \ - echo ' fruit:time machine = yes' >>$file && \ - echo ' fruit:veto_appledouble = no' >>$file && \ - echo ' fruit:wipe_intentionally_left_blank_rfork = yes' >>$file && \ - echo '' >>$file && \ - rm -rf /tmp/* + adduser -S -D -H -h /tmp -s /sbin/nologin -G smb -g 'Samba User' smbuser + +# Configure smb.conf +RUN file="/etc/samba/smb.conf" && \ + # Modify existing options + sed -i 's|^;* *\(log file = \).*| \1/dev/stdout|' "$file" && \ + sed -i 's|^;* *\(load printers = \).*| \1no|' "$file" && \ + sed -i 's|^;* *\(printcap name = \).*| \1/dev/null|' "$file" && \ + sed -i 's|^;* *\(printing = \).*| \1bsd|' "$file" && \ + sed -i 's|^;* *\(unix password sync = \).*| \1no|' "$file" && \ + sed -i 's|^;* *\(preserve case = \).*| \1yes|' "$file" && \ + sed -i 's|^;* *\(short preserve case = \).*| \1yes|' "$file" && \ + sed -i 's|^;* *\(default case = \).*| \1lower|' "$file" && \ + sed -i '/Share Definitions/,$d' "$file" && \ + # Append additional configuration + cat >> "$file" <<'EOF' + pam password change = yes + map to guest = bad user + usershare allow guests = yes + create mask = 0664 + force create mode = 0664 + directory mask = 0775 + force directory mode = 0775 + force user = smbuser + force group = smb + follow symlinks = yes + load printers = no + printing = bsd + printcap name = /dev/null + disable spoolss = yes + strict locking = no + aio read size = 0 + aio write size = 0 + vfs objects = catia fruit recycle streams_xattr + + # Recycle bin + recycle:keeptree = yes + recycle:maxsize = 0 + recycle:repository = .deleted + recycle:versions = yes + + # Security + client ipc max protocol = SMB3 + client ipc min protocol = SMB2_10 + client max protocol = SMB3 + client min protocol = SMB2_10 + server max protocol = SMB3 + server min protocol = SMB2_10 + + # 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/ @@ -66,7 +76,6 @@ EXPOSE 137/udp 138/udp 139 445 HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=3 \ CMD smbclient -L \\localhost -U % -m SMB3 || exit 1 -VOLUME ["/etc", "/var/cache/samba", "/var/lib/samba", "/var/log/samba",\ - "/run/samba"] +VOLUME ["/etc", "/var/cache/samba", "/var/lib/samba", "/var/log/samba", "/run/samba"] -ENTRYPOINT ["/sbin/tini", "--", "/usr/bin/samba.sh"] \ No newline at end of file +ENTRYPOINT ["/sbin/tini", "--", "/usr/bin/samba.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index 7ebadce..944e4a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: samba: image: docker.struchkov.dev/samba + restart: unless-stopped + environment: TZ: 'EST5EDT' # Use environment variables for shares and users instead of command line @@ -8,30 +10,41 @@ services: # SHARE2: "Bobs Volume;/mnt2;yes;no;no;bob" # USER: "bob;${SAMBA_BOB_PASSWORD}" # PERMISSIONS: "true" + env_file: - .env # Put sensitive data like passwords here - networks: - - default + ports: - "137:137/udp" - "138:138/udp" - "139:139/tcp" - "445:445/tcp" - read_only: true - tmpfs: - - /tmp - restart: unless-stopped - stdin_open: true - tty: true + volumes: - /mnt:/mnt: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: resources: limits: memory: 512M reservations: memory: 128M + command: '-s "Mount;/mnt" -s "Bobs Volume;/mnt2;yes;no;no;bob" -u "bob;${SAMBA_BOB_PASSWORD:-changeme}" -p' networks: diff --git a/samba.sh b/samba.sh index 60317ba..c48837c 100755 --- a/samba.sh +++ b/samba.sh @@ -18,6 +18,9 @@ set -o nounset # Treat unset variables as an error +# Constants +readonly SMB_CONF="/etc/samba/smb.conf" + ### parse_args: safely parse semicolon-separated arguments # Arguments: # input) semicolon-separated string @@ -44,15 +47,34 @@ parse_args() { # Arguments: # chars) from:to character mappings separated by ',' # Return: configured character mapings -charmap() { local chars="$1" file=/etc/samba/smb.conf - grep -q catia $file || sed -i '/TCP_NODELAY/a \ +charmap() { local chars="$1" + grep -q catia "$SMB_CONF" || sed -i '/TCP_NODELAY/a \ \ vfs objects = catia\ 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 @@ -60,36 +82,24 @@ charmap() { local chars="$1" file=/etc/samba/smb.conf # section) section of config file # option) raw option # Return: line added to smb.conf (replaces existing line with same key) -generic() { local section="$1" key="$(sed 's| *=.*||' <<< $2)" \ - value="$(sed 's|[^=]*= *||' <<< $2)" file=/etc/samba/smb.conf - 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 +generic() { + set_config_option "$1" "$2" } ### global: set a global config option # Arguments: # option) raw option # Return: line added to smb.conf (replaces existing line with same key) -global() { local key="$(sed 's| *=.*||' <<< $1)" \ - value="$(sed 's|[^=]*= *||' <<< $1)" file=/etc/samba/smb.conf - 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 +global() { + set_config_option "global" "$1" } ### include: add a samba config file include # Arguments: # file) file to import -include() { local includefile="$1" file=/etc/samba/smb.conf - sed -i "\\|include = $includefile|d" "$file" - echo "include = $includefile" >> "$file" +include() { local includefile="$1" + sed -i "\\|include = $includefile|d" "$SMB_CONF" + echo "include = $includefile" >> "$SMB_CONF" } ### import: import a smbpasswd file @@ -107,11 +117,11 @@ import() { local file="$1" name id # Arguments: # none) # Return: result -perms() { local i file=/etc/samba/smb.conf - for i in $(awk -F ' = ' '/ path = / {print $2}' $file); do - chown -Rh smbuser. $i - find $i -type d ! -perm 775 -exec chmod 775 {} \; - find $i -type f ! -perm 0664 -exec chmod 0664 {} \; +perms() { local i + for i in $(awk -F ' = ' '/ path = / {print $2}' "$SMB_CONF"); do + chown -Rh smbuser. "$i" + find "$i" -type d ! -perm 775 -exec chmod 775 {} \; + find "$i" -type f ! -perm 0664 -exec chmod 0664 {} \; done } export -f perms @@ -120,8 +130,8 @@ export -f perms # Arguments: # none) # Return: result -recycle() { local file=/etc/samba/smb.conf - sed -i '/recycle:/d; /vfs objects/s/ recycle / /' $file +recycle() { + sed -i '/recycle:/d; /vfs objects/s/ recycle / /' "$SMB_CONF" } ### 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 # comment) description of share # Return: result -share() { local share="$1" path="$2" browsable="${3:-yes}" ro="${4:-yes}" \ - guest="${5:-yes}" users="${6:-""}" admins="${7:-""}" \ - writelist="${8:-""}" comment="${9:-""}" file=/etc/samba/smb.conf - sed -i "/\\[$share\\]/,/^\$/d" $file - echo "[$share]" >>$file - echo " path = $path" >>$file - echo " browsable = $browsable" >>$file - echo " read only = $ro" >>$file - echo " guest ok = $guest" >>$file +share() { + local share="$1" path="$2" browsable="${3:-yes}" ro="${4:-yes}" \ + guest="${5:-yes}" users="${6:-""}" admins="${7:-""}" \ + writelist="${8:-""}" comment="${9:-""}" + + sed -i "/\\[$share\\]/,/^\$/d" "$SMB_CONF" + echo "[$share]" >> "$SMB_CONF" + echo " path = $path" >> "$SMB_CONF" + echo " browsable = $browsable" >> "$SMB_CONF" + echo " read only = $ro" >> "$SMB_CONF" + echo " guest ok = $guest" >> "$SMB_CONF" [[ ${VETO:-yes} == no ]] || { - echo -n " veto files = /.apdisk/.DS_Store/.TemporaryItems/" >>$file - echo -n ".Trashes/desktop.ini/ehthumbs.db/Network Trash Folder/" >>$file - echo "Temporary Items/Thumbs.db/" >>$file - echo " delete veto files = yes" >>$file + echo -n " veto files = /.apdisk/.DS_Store/.TemporaryItems/" >> "$SMB_CONF" + echo -n ".Trashes/desktop.ini/ehthumbs.db/Network Trash Folder/" >> "$SMB_CONF" + echo "Temporary Items/Thumbs.db/" >> "$SMB_CONF" + echo " delete veto files = yes" >> "$SMB_CONF" } [[ ${users:-""} && ! ${users:-""} == all ]] && - echo " valid users = $(tr ',' ' ' <<< $users)" >>$file + echo " valid users = $(tr ',' ' ' <<< "$users")" >> "$SMB_CONF" [[ ${admins:-""} && ! ${admins:-""} =~ none ]] && - echo " admin users = $(tr ',' ' ' <<< $admins)" >>$file + echo " admin users = $(tr ',' ' ' <<< "$admins")" >> "$SMB_CONF" [[ ${writelist:-""} && ! ${writelist:-""} =~ none ]] && - echo " write list = $(tr ',' ' ' <<< $writelist)" >>$file + echo " write list = $(tr ',' ' ' <<< "$writelist")" >> "$SMB_CONF" [[ ${comment:-""} && ! ${comment:-""} =~ none ]] && - echo " comment = $(tr ',' ' ' <<< $comment)" >>$file - echo "" >>$file - [[ -d $path ]] || mkdir -p $path + echo " comment = $(tr ',' ' ' <<< "$comment")" >> "$SMB_CONF" + echo "" >> "$SMB_CONF" + [[ -d "$path" ]] || mkdir -p "$path" } ### smb: disable SMB2 minimum # Arguments: # none) # Return: result -smb() { local file=/etc/samba/smb.conf - sed -i 's/\([^#]*min protocol *=\).*/\1 LANMAN1/' $file +smb() { + sed -i 's/\([^#]*min protocol *=\).*/\1 LANMAN1/' "$SMB_CONF" } ### user: add a user @@ -192,17 +204,17 @@ user() { local name="$1" passwd="$2" id="${3:-""}" group="${4:-""}" \ # Arguments: # workgroup) the name to set # Return: configure the correct workgroup -workgroup() { local workgroup="$1" file=/etc/samba/smb.conf - sed -i 's|^\( *workgroup = \).*|\1'"$workgroup"'|' $file +workgroup() { local workgroup="$1" + sed -i 's|^\( *workgroup = \).*|\1'"$workgroup"'|' "$SMB_CONF" } ### widelinks: allow access wide symbolic links # Arguments: # none) # Return: result -widelinks() { local file=/etc/samba/smb.conf \ - replace='\1\n wide links = yes\n unix extensions = no' - sed -i 's/\(follow symlinks = yes\)/'"$replace"'/' $file +widelinks() { + local replace='\1\n wide links = yes\n unix extensions = no' + sed -i 's/\(follow symlinks = yes\)/'"$replace"'/' "$SMB_CONF" } ### usage: Help @@ -311,7 +323,7 @@ if [[ $# -ge 1 && -x $(which $1 2>&-) ]]; then elif [[ $# -ge 1 ]]; then echo "ERROR: command not found: $1" 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" else [[ ${NMBD:-""} ]] && ionice -c 3 nmbd -D