# Searches for entries (files and directories) in the specified local directory. # # $1 (path): The path to the directory on the local machine where the search for entries will be performed. # $2 (type): Type entry (f: files, d: directories, ...). function fileList() { local path="${1-}" local type="${2-}" local args=(-mindepth 1 -maxdepth 1) [[ -n "$path" ]] || { appError "Path not specified"; return 1; } [[ -d "$path" ]] || { appError "Directory not found: $path"; return 1; } if [[ -n "$type" ]]; then case "$type" in f|d|l|p|s|b|c) args+=(-type "$type") ;; *) appError "Unsupported file type: $type"; return 1 ;; esac fi find "$path" "${args[@]}" -printf '%f\n' } # Searches for entries (files or directories) in the specified directory on an FTP server. # # $1 (path): The path to the directory on the FTP server where the search for entries will be performed. # $2 (type): Type entry (f: files, d: directories, ...). function ftpFileList() { local path="$1" local type="$2" local output error run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10 \ || { appError "$error"; return 1; } case "$type" in f) printf '%s\n' "$output" | grep -v '^d' | awk '{print $NF}' ;; d) printf '%s\n' "$output" | grep '^d' | awk '{print $NF}' ;; *) printf '%s\n' "$output" | awk '{print $NF}' ;; esac } # Searches for entries (files or directories) in the specified directory on a remote server via SSH. # # $1 (path): The path to the directory on the remote server where the search for entries will be performed. # $2 (type): Type entry (f: files, d: directories, ...). function sshFileList() { local path="$1" local type="$2" local typeArg="${type:+-type $type}" local output error run output error ssh -i "$sshKeyFile" "$sshUser@$sshHost" \ "find '${sshPath}${path}' -maxdepth 1 -mindepth 1 ${typeArg} -exec basename {} \\;" \ || { appError "$error"; return 1; } printf '%s\n' "$output" } # Searches for entries (files or directories) in the specified directory on external storage. function storageFileList() { local -A storageFunc=( [ftp]=ftpFileList [rsync]=ftpFileList [ssh]=sshFileList ) [[ -n "${storageFunc[$storageType]:-}" ]] || { appError "Unknown value storageType: $storageType"; return 1; } "${storageFunc[$storageType]}" "$@" } # Cleans a specified directory on a remote server using rsync. # # $1 (path): The path to the directory to be cleaned on the remote server. # # The function creates a temporary empty directory on the local machine and syncs it with the remote directory # using rsync with the `--delete` option, which removes any files on the remote side that are not present in # the local directory. After completion, the temporary directory is deleted. An error message is displayed in case of failure. function rsyncDirectoryClean() { local path="$1" [[ -n "$path" ]] || { appError "Path not specified"; return 1; } local emptyPath="$appDataPath/$(uuidgen)" local error runError error mkdir -p "$emptyPath" || { appError "Failed to create temp directory: $error"; return 1; } runError error rsync -r --delete --password-file="$rsyncPassFile" "$emptyPath/" rsync://"$rsyncUri$path/" \ || { rm -rf "$emptyPath"; appError "Failed to clean remote directory: $error"; return 1; } rm -rf "$emptyPath" } # Deletes a directory on an FTP server. # # $1 (path): The path to the directory on the FTP server to be deleted (in ftpPath). function ftpDirectoryDelete() { local path="$1" [[ -n "$path" ]] || { appError "Path not specified"; return 1; } local error runError error curl -u "$ftpUser:$ftpPass" -Q "-RMD $ftpPath$path" "ftp://$ftpHost:$ftpPort" --connect-timeout 10 \ || { appError "$error"; return 1; } } # Deletes a directory on a remote server via SSH (in sshPath). # # $1 (path): The path to the directory to be deleted. function sshDirectoryDelete() { local path="$1" [[ -n "$path" ]] || { appError "Path not specified"; return 1; } local error runError error ssh -i "$sshKeyFile" "$sshUser@$sshHost" "rm -rf '${sshPath}${path}'" \ || { appError "$error"; return 1; } } function fileAtomicWrite() { local file="$1" local content="$2" local path tmp path="$(dirname "$file")" mkdir -p "$path" tmp="$(mktemp "$path/.tmp.XXXXXX")" printf '%s\n' "$content" > "$tmp" mv -f "$tmp" "$file" } # Create a timestamped backup copy of a file if it exists. # $1 (file): path to the file to back up. # [$2] (suffix): custom suffix for the backup file (defaults to a timestamp .bak suffix). function fileBackup() { local file="$1" if [[ -z "$file" ]]; then appError "File not specified" return 1 fi if [[ ! -f "$file" ]]; then return 0 fi local suffix="${2:-.$(date +%F_%H-%M-%S).bak}" cp -p -- "$file" "$file$suffix" || { appError "Failed to backup file: $file" return 1 } return 0 } # Download remote file to a local path. function fileDownload() { local remoteFile="$1" local localFile="$2" local retries="${3:-5}" local delay="${4:-3}" [[ -n "$remoteFile" ]] || { appError "Remote file not specified"; return 1; } [[ -n "$localFile" ]] || { appError "Local file not specified"; return 1; } mkdir -p "$(dirname "$localFile")" || { appError "Failed to create folder"; return 1; } local tmpFile="${localFile}.part" local i rc curlError lastError for ((i = 1; i <= retries; i++)); do curlError=$(curl -kfsSL --http1.1 --max-redirs 10 --connect-timeout 30 --speed-time 60 --speed-limit 1024 -C - -o "$tmpFile" "$remoteFile" 2>&1) rc=$? if [[ $rc -eq 0 ]]; then mv -f "$tmpFile" "$localFile" || { appError "Failed to move downloaded file"; return 1; } return 0 fi lastError="$curlError" [[ $rc -ne 33 ]] || rm -f "$tmpFile" # rc=33: server doesn't support resume, restart from scratch sleep "$delay" done lastError="${lastError//$'\r'/ }" lastError="${lastError//$'\n'/ }" appError "Failed to download file after $retries attempts | $lastError" return 1 } # Creates an archive from a source file or directory. # # Supported archive formats are selected by the target file extension: # .zip, .tar.gz, or .tgz. # # $1 (source): Source file or directory to archive. # $2 (target): Target archive file path. # # Returns: # 0 on success, 1 on validation or archive creation failure. function archiveCreate() { local source="$1" if [[ -z "$source" ]]; then appError "Source path not specified" return 1 fi if [[ ! -e "$source" ]]; then appError "Source path not found: $source" return 1 fi local target="$2" if [[ -z "$target" ]]; then appError "Target file not specified" return 1 fi local compressProgram="${3:-}" local sourcePath sourceBase targetPath targetBase output rc sourcePath=$(dirname -- "$source") sourceBase=$(basename -- "$source") targetPath=$(dirname -- "$target") targetBase=$(basename -- "$target") case "$targetBase" in *.zip|*.tar.gz|*.tgz) ;; *) appError "Unknown type archive: $targetBase" return 1 ;; esac mkdir -p -- "$targetPath" || { appError "Failed to create directory: $targetPath" return 1 } case "$targetBase" in *.zip) if [[ -d "$source" ]]; then output=$(cd "$source" && zip -qr "$target" . 2>&1) else output=$(cd "$sourcePath" && zip -qr "$target" "$sourceBase" 2>&1) fi rc=$? [[ $rc -le 1 ]] || { appError "Error creating ZIP (rc=$rc)${output:+: $output}"; return 1; } [[ $rc -ne 1 ]] || [[ -z "$output" ]] || appError "Warning creating ZIP${output:+: $output}" ;; *.tar.gz|*.tgz) local -a tarCompressFlags if [[ -n "$compressProgram" ]]; then tarCompressFlags=("--use-compress-program=$compressProgram") else tarCompressFlags=("-z") fi if [[ -d "$source" ]]; then output=$(tar --warning=no-file-changed -c "${tarCompressFlags[@]}" -f "$target" -C "$source" . 2>&1) else output=$(tar --warning=no-file-changed -c "${tarCompressFlags[@]}" -f "$target" -C "$sourcePath" "$sourceBase" 2>&1) fi rc=$? [[ $rc -le 1 ]] || { appError "Error creating TAR (rc=$rc)${output:+: $output}"; return 1; } [[ $rc -ne 1 ]] || [[ -z "$output" ]] || appError "Warning creating TAR${output:+: $output}" ;; esac return 0 } # Extracts an archive into a target directory. # # Supported archive formats are selected by the source file extension: # .zip, .tar.gz, or .tgz. # # $1 (source): Source archive file path. # $2 (target): Target directory where archive contents should be extracted. # # Returns: # 0 on success, 1 on validation or extraction failure. function archiveExtract() { local source="$1" if [[ -z "$source" ]]; then appError "Source archive not specified" return 1 fi if [[ ! -f "$source" ]]; then appError "Source archive not found: $source" return 1 fi local target="$2" if [[ -z "$target" ]]; then appError "Target directory not specified" return 1 fi local sourceBase output rc sourceBase=$(basename -- "$source") case "$sourceBase" in *.zip|*.tar.gz|*.tgz) ;; *) appError "Unknown type archive: $sourceBase" return 1 ;; esac mkdir -p -- "$target" || { appError "Failed to create directory: $target" return 1 } case "$sourceBase" in *.zip) output=$(unzip -oq "$source" -d "$target" 2>&1) rc=$? if [[ $rc -ne 0 ]]; then appError "Error extracting ZIP: $output" return 1 fi ;; *.tar.gz|*.tgz) output=$(tar -xzf "$source" -C "$target" 2>&1) rc=$? if [[ $rc -ne 0 ]]; then appError "Error extracting TAR: $output" return 1 fi ;; esac return 0 } # Retrieves the size of the specified path (file or directory) in b/kb/mb/gb. # # $1 (path): The path to the file or directory whose size is to be retrieved. # $2 (unit): Unit. # $3 (precision): Precision. function pathSize() { local path="$1" local unit="${2:-}" local precision="${3:-0}" local divisor="1" case "${unit,,}" in kb) divisor="1024" ;; mb) divisor="1048576" ;; gb) divisor="1073741824" ;; esac [[ -n "$path" ]] || { appError "Path not specified"; return 1; } local output error bytes if [[ -d "$path" ]]; then run output error du -sb "$path" || { appError "$error"; return 1; } bytes=$(printf '%s\n' "$output" | cut -f1) elif [[ -f "$path" ]]; then run output error stat -c %s "$path" || { appError "$error"; return 1; } bytes="$output" else return 1 fi LC_NUMERIC=C awk -v bytes="$bytes" -v divisor="$divisor" -v precision="$precision" 'BEGIN { printf "%.*f\n", precision, bytes / divisor }' } function ftpPathSize() { local path="$1" local unit="${2:-}" local precision="${3:-0}" local divisor="1" case "${unit,,}" in kb) divisor="1024" ;; mb) divisor="1048576" ;; gb) divisor="1073741824" ;; esac local output error total if run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$path/" --connect-timeout 10; then total=0 local line file size while IFS= read -r line; do file=$(awk '{print $NF}' <<< "$line") [[ -n "$file" && "$file" != "." && "$file" != ".." ]] || continue if [[ "$line" =~ ^d ]]; then size=$(ftpPathSize "$path/$file") || return 1 else size=$(awk '{print $5}' <<< "$line") [[ "$size" =~ ^[0-9]+$ ]] || continue fi (( total += size )) done <<< "$output" else local parent name parent=$(dirname "$path") name=$(basename "$path") run output error curl -sS -u "$ftpUser:$ftpPass" "ftp://$ftpHost:$ftpPort$ftpPath$parent/" --connect-timeout 10 || { appError "$error"; return 1 } total=$(awk -v f="$name" '$NF == f {print $5}' <<< "$output") [[ "$total" =~ ^[0-9]+$ ]] || { appError "Cannot determine size of: $path"; return 1; } fi LC_NUMERIC=C awk -v b="$total" -v d="$divisor" -v p="$precision" 'BEGIN { printf "%.*f\n", p, b/d }' } # Return the contents of a file if it exists and is non-empty, otherwise write the given value to it and return it. # $1 (file): path to the file. # $2 (value): value to write when the file is missing or empty. function fileValueGetOrCreate() { local file="${1-}" local value="${2-}" if [[ -z "$file" ]]; then appError "File not specified" return 1 fi if [[ -s "$file" ]]; then cat "$file" return $? fi if [[ -z "$value" ]]; then appError "Value not specified" return 1 fi mkdir -p -- "$(dirname -- "$file")" || { appError "Failed to create folder for: $file" return 1 } install -o root -g root -m 600 /dev/null "$file" || { appError "Failed to create file: $file" return 1 } printf '%s\n' "$value" > "$file" || { appError "Failed to save value: $file" return 1 } printf '%s\n' "$value" } # Retrieves or generates a password and stores it in the specified file. # # $1: path to the file where the password should be stored. # [$2+]: additional parameters are passed to the stringRandom function for generating the password (optional). function filePasswordGetOrCreate() { local file="${1-}" if [[ -z "$file" ]]; then appError "File not specified" return 1 fi shift || true local value if [[ -s "$file" ]]; then cat "$file" return $? fi value="$(strRandom "$@")" || { appError "Failed to generate password" return 1 } fileValueGetOrCreate "$file" "$value" } # Get value by key from "KEY=VALUE" content or file (returns first match) function configGet() { local file="${1-}" local key="${2-}" local line value if [[ -z "$file" ]]; then appError "Config file not specified" return 1 fi if [[ -z "$key" ]]; then appError "Config key not specified" return 1 fi [[ -f "$file" ]] || { printf '\n' return 0 } line="$(awk -F= -v key="$key" '$1 == key { print; exit }' "$file")" || { printf '\n'; return 0; } [[ "$line" == "$key="* ]] || { printf '\n' return 0 } value="${line#*=}" value="$(sed -E 's/[[:space:]]+$//' <<<"$value")" printf '%s\n' "$value" } # Ensure key=value is present in file (remove previous key lines, then append). function configSet() { local file="${1-}" local key="${2-}" local value="${3-}" local tmp if [[ -z "$file" ]]; then appError "Config file not specified" return 1 fi if [[ -z "$key" ]]; then appError "Config key not specified" return 1 fi value="${value//$'\r'/ }" value="${value//$'\n'/ }" mkdir -p -- "$(dirname -- "$file")" || { appError "Failed to create folder for: $file" return 1 } [[ -f "$file" ]] || install -o root -g root -m 600 /dev/null "$file" || { appError "Failed to create file: $file" return 1 } tmp="$(mktemp)" || return 1 awk -F= -v key="$key" '$1 != key' "$file" > "$tmp" || true if [[ -n "$value" ]]; then printf '%s=%s\n' "$key" "$value" >> "$tmp" || { rm -f -- "$tmp" appError "Failed to write config value: $key" return 1 } fi cat "$tmp" > "$file" || { rm -f -- "$tmp" appError "Failed to update config file: $file" return 1 } rm -f -- "$tmp" } # Get param from config or set via configSet if missing function configGetOrSet() { local file="${1-}" local key="${2-}" local value="${3-}" local current current="$(configGet "$file" "$key")" || return 1 if [[ -n "$current" ]]; then printf '%s\n' "$current" return 0 fi if [[ -z "$value" ]]; then appError "Config value not specified: $key" return 1 fi configSet "$file" "$key" "$value" || return 1 printf '%s\n' "$value" } # Get param from config or create via configSet (+gen stringRandom if missing value) if missing function configGetOrCreate() { local file="${1-}" local key="${2-}" shift 2 || true local value current current="$(configGet "$file" "$key")" || return 1 if [[ -n "$current" ]]; then printf '%s\n' "$current" return 0 fi value="$(strRandom "$@")" || { appError "Failed to generate config value: $key" return 1 } configSet "$file" "$key" "$value" || return 1 printf '%s\n' "$value" } # Check that no line in a file exceeds the specified maximum length, printing offending lines to stderr. # $1 (file): path to the file to check. # $2 (max): maximum allowed line length in characters. function fileCheckLineLength() { local file="$1" local max="$2" local line len num=0 found=0 [[ -f "$file" ]] || { appError "File not found: $file"; return 1; } [[ "$max" =~ ^[0-9]+$ ]] || { appError "Invalid max line length: $max"; return 1; } while IFS= read -r line || [[ -n $line ]]; do ((num++)) len=${#line} if (( len > max )); then printf 'Line %d exceeds %d characters (%d)\n' "$num" "$max" "$len" >&2 found=1 fi done < "$file" return "$found" } function fileSignatureDiffList() { local path="$1" type="$2" mask="$3" [[ -n "$path" && -n "$type" && -n "$mask" ]] || { appError "Missing argument(s)"; return 1; } [[ -d "$path" ]] || { appError "Path not found: $path"; return 1; } find "$path" -type "$type" -printf '%m:%u:%g:%p\n' 2>/dev/null | grep -vE "$mask:" return 0 } function fileSignatureDiff() { local path="$1" mask="$2" sign [[ -n "$path" && -n "$mask" ]] || { appError "Missing argument(s)"; return 1; } [[ -d "$path" ]] || { appError "Path not found: $path"; return 1; } sign="$(stat -c '%a:%U:%G' "$path")" [[ "$sign" == "$mask" ]] || printf "%s\n" "$sign:$path"; } function fileSignatureAclDiff() { local path="$1" mask="$2" [[ -n "$path" && -n "$mask" ]] || { printDanger "Missing argument(s)"; return 1; } [[ -d "$path" ]] || { printDanger "Path not found: $path"; return 1; } local acl unexpected acl=$(getfacl -p "$path" 2>/dev/null) unexpected=$(grep -vE "^(#|\$|(default:)?((user|group|mask)::|other::---|$mask\$))" <<< "$acl" | paste -sd '|') grep -qxF "$mask" <<< "$acl" || unexpected+="${unexpected:+|}missing:$mask" grep -qxF "default:$mask" <<< "$acl" || unexpected+="${unexpected:+|}missing:default:$mask" [[ -z "$unexpected" ]] || printf "%s\n" "$unexpected:$path" } function fileSignatureCheck() { local output error run output error fileSignatureDiff "$@" || return 1 [[ -z "$output" ]] } function fileSignatureAclCheck() { local output error run output error fileSignatureAclDiff "$@" || return 1 [[ -z "$output" ]] } # Checking the availability of a file (URL) for download. # Check if a URL is accessible and print the final effective URL on success. # $1 (url): the URL to check. function urlAccessible() { local url="$1" code final if [[ -z "$url" ]]; then appError "URL not specified" return 1 fi final=$(curl -ksSL --max-redirs 10 --range 0-0 -o /dev/null -w "%{url_effective} %{http_code}" "$url") || return 1 code="${final##* }" # http code final="${final% *}" # final URL if [[ "$code" =~ ^[23][0-9]{2}$ ]]; then printf "%s" "$final" return 0 fi return 1 } # Fetch and print the HTTP status code for the given URL. # $1 (url): the URL to request. function urlCode() { local url="$1" code if [[ -z "$url" ]]; then appError "URL not specified" return 1 fi code=$(curl -ksSL --max-redirs 10 -o /dev/null -w "%{http_code}" "$url") || return 1 printf "%s" "$code" return 0 } # Derive a local filename from a URL, optionally using a custom base name. # $1 (url): the source URL to extract the filename from. # [$2] (localFileName): custom base name; if given, the URL extension is appended to it. function urlLocalFileGen() { local url="$1" code if [[ -z "$url" ]]; then appError "URL not specified" return 1 fi local localFileName="$2" name="${url##*/}" name="${name%%\?*}" base="${name%%.*}" ext="${name#*.}" if [[ -z "$localFileName" ]]; then printf '%s' "$name" else printf '%s' "$localFileName.$ext" fi }