Files
SODEW/sodew-bash-main/libs/file.sh
T
2026-08-18 09:40:08 +02:00

727 lines
21 KiB
Bash

# 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
}