#ATWORK# ACL START #ATWORK# # File: ./acl/0_vars.sh export ACL_URL_FILES="https://cli.atwork.software" export ACL_DELIMITER="#ATWORK#" export ACL_START="${ACL_DELIMITER} ACL START ${ACL_DELIMITER}" export ACL_END="${ACL_DELIMITER} ACL END ${ACL_DELIMITER}" # configurar locale export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 # File: ./acl/1_echo.sh echo_info() { echo -e "\033[38;2;56;125;252m INFO \033[0m $1" } echo_success() { echo -e "\033[0;32m SUCCESS \033[0m $1" } echo_warning() { echo -e "\033[0;33m WARNING \033[0m $1" } echo_danger() { echo -e "\033[0;31m DANGER \033[0m $1" } echo_error() { echo -e "\033[0;31m ERROR \033[0m $1" } echo_title() { local lolcat_path=$(command -v lolcat 2>/dev/null) local title_text="${1^^}" local text_length=${#title_text} local line_length=$((text_length + 2)) echo if [ -n "$lolcat_path" ]; then echo " ${title_text} " | "$lolcat_path" -F 0.2 else echo " ${title_text} " fi printf "%${line_length}s\n" | sed 's/ /─/g' echo } echo_readme() { local readme_file="" # Buscar archivo README en diferentes variantes (case insensitive) for file in README.md readme.md Readme.md README.MD readme.MD Readme.MD; do if [[ -f "$file" ]]; then readme_file="$file" break fi done # Si no se encuentra README.md, mostrar mensaje if [[ -z "$readme_file" ]]; then return 1 fi # Verificar si tiene contenido if [[ ! -s "$readme_file" ]]; then echo "⚠️ El archivo $readme_file existe pero está vacío" return 1 fi echo "════════════════════════════════════════════════════════════════" echo cat "$readme_file" | glow - echo echo "════════════════════════════════════════════════════════════════" # Le preguntamos al usario si entendio las instrucciones, debe solo dar enter (En ingles) echo_warning "Please read the instructions before proceeding. Press Enter to continue..." read } export -f echo_info export -f echo_success export -f echo_warning export -f echo_danger export -f echo_error export -f echo_title export -f echo_readme # File: ./acl/acl_clean_files .sh acl_clean_files() { # Buscar archivos de texto y procesarlos while IFS= read -r -d '' file; do if file "$file" | grep -q "text"; then dos2unix "$file" 2>/dev/null if [ ! $? -eq 0 ]; then echo_error "Failed dos2unix: $file" fi fi done < <(find . -type f -print0) } export -f acl_clean_files # File: ./acl/acl_download_file.sh # Uso: download_file [directorio_destino] [nombre_archivo_local] [permisos_octal] acl_download_file() { local url="$1" local dest_dir="${2:-.}" # Por defecto, el directorio actual local local_filename="${3:-}" # Por defecto, se extrae de la URL local permissions="${4:-}" # Permisos por defecto (se calcularán si no se dan) if [[ -z "$url" ]]; then echo_error "URL de descarga no proporcionada." return fi # Extraer el nombre del archivo de la URL si no se proporciona if [[ -z "$local_filename" ]]; then local_filename=$(basename "$url") # Eliminar posibles parámetros de consulta de la URL si el nombre del archivo contiene '?' local_filename="${local_filename%%\?*}" fi local full_path="$dest_dir/$local_filename" # Asegurarse de que el directorio de destino exista if [[ ! -d "$dest_dir" ]]; then mkdir -p "$dest_dir" || { echo_error "No se pudo crear el directorio '$dest_dir'"; return 1; } fi # Detectar si es archivo binario por extensión local is_binary=false if [[ "$local_filename" =~ \.(zip|tar|gz|bz2|xz|7z|rar|exe|bin|iso|img|pdf|jpg|jpeg|png|gif|bmp|tiff|mp3|mp4|avi|mkv|mov|doc|docx|xls|xlsx|ppt|pptx|dmg|deb|rpm|AppImage)$ ]]; then is_binary=true fi # Descargar el archivo if [[ "$is_binary" == true ]]; then # Descarga directa para archivos binarios (sin modificaciones) if ! curl -sSL "$url" -o "$full_path"; then echo_error "No se pudo descargar '$url'." return 1 fi else # Descarga con tr -d '\r' solo para archivos de texto if ! curl -sSL "$url" | tr -d '\r' > "$full_path"; then echo_error "No se pudo descargar '$url'." return 1 fi fi # Verificar si el directorio termina con _XXXXXX (6 caracteres alfanuméricos) local dir_name=$(basename "$(realpath "$dest_dir")") if [[ "$dir_name" =~ _([a-zA-Z0-9]{6})$ ]]; then local app_code="${BASH_REMATCH[1]}" # Reemplazar a4k con el código real en el archivo (solo en archivos de texto) if [[ "$is_binary" == false ]]; then sed -i "s|a4k|${app_code}|g" "$full_path" fi fi # Asignar permisos if [[ -z "$permissions" ]]; then # Determinar permisos por defecto si no se especifican # Si el archivo parece un script (ej. termina en .sh), usar 755 # Para otros (ej. .env), usar 644 if [[ "$local_filename" =~ \.(sh|py|pl|rb)$ ]]; then # Puedes añadir más extensiones de script aquí permissions="755" else permissions="644" fi fi chmod "$permissions" "$full_path" || { echo_error "No se pudieron establecer los permisos '$permissions' en '$full_path'"; return 1; } return 0 } export -f acl_download_file # File: ./acl/acl_get_env_value.sh acl_get_env_value() { local var_name="$1" if [ -f ".env" ]; then grep "^${var_name}=" .env | cut -d'=' -f2 | tr -d '"' | tr -d "'" fi } export -f acl_get_env_value # File: ./acl/acl_get_sha.sh acl_get_sha() { local bits=${1:-256} local input="$(date +%s%N)$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32)" case $bits in 256) echo -n "$input" | sha256sum | cut -d' ' -f1 ;; 512) echo -n "$input" | sha512sum | cut -d' ' -f1 ;; *) echo "Error: Solo se soporta 256 o 512" >&2; return 1 ;; esac } export -f acl_get_sha # File: ./acl/acl_get_string.sh acl_get_string() { local length=${1:-64} cat /dev/urandom | tr -dc 'a-z0-9' | head -c "$length" } export -f acl_get_string # File: ./acl/acl_get_traefik_basic_auth.sh acl_get_traefik_basic_auth() { local user="${1:-admin}" local password=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32) local hash=$(echo "$password" | htpasswd -ni "$user" | tail -1) echo "## Traefik Basic Auth" >> info.md echo "User: $user" >> info.md echo "Pass: $password" >> info.md echo "" >> info.md echo "$hash" } export -f acl_get_traefik_basic_auth # File: ./acl/acl_get_username.sh acl_get_username() { local adj=$(grep -E '^[a-z]{3,8}ing$|^[a-z]{3,8}ed$|^[a-z]{3,8}y$' /usr/share/dict/words | shuf -n1 | tr '[:upper:]' '[:lower:]') local noun=$(grep -E '^[a-z]{4,8}$' /usr/share/dict/words | grep -v '[aeiouy]ing$' | shuf -n1 | tr '[:upper:]' '[:lower:]') echo "${adj}_${noun}" } export -f acl_get_username # File: ./acl/acl_get_uuid.sh acl_get_uuid() { cat /proc/sys/kernel/random/uuid } export -f acl_get_uuid # File: ./acl/acl_load_env.sh acl_load_env() { if [ -f ".env" ]; then # Primero cargar variables simples while IFS= read -r line; do if [[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*=.*$ ]] && [[ ! "$line" =~ \$\{ ]]; then export "$line" fi done < .env # Luego cargar variables que contienen referencias a otras variables while IFS= read -r line; do if [[ "$line" =~ ^[A-Za-z_][A-Za-z0-9_]*=.*\$\{.*\}.*$ ]]; then var_name=$(echo "$line" | cut -d'=' -f1) var_value=$(echo "$line" | cut -d'=' -f2 | tr -d '"' | tr -d "'") eval "export $var_name=\"$var_value\"" fi done < .env fi } export -f acl_load_env # File: ./acl/acl_root_verification.sh acl_root_verification() { if [[ $EUID -ne 0 ]]; then echo_error "Root user is required" exit 1 fi } export -f acl_root_verification # File: ./acl/acl_run.sh run() { chmod +x *.sh 2>/dev/null if [ -f "run.sh" ]; then ./run.sh return fi local scripts=( *.sh ) if [ ${#scripts[@]} -eq 0 ] || [ "${scripts[0]}" = "*.sh" ]; then if type -t echo_error >/dev/null 2>&1; then echo_error "No hay scripts .sh disponibles." else printf "No hay scripts .sh disponibles.\n" >&2 fi return 1 fi # agregar opción Cancelar al final scripts+=( "Cancelar" ) echo "Available scripts:" PS3="Elige el número del script (o Cancelar): " select script in "${scripts[@]}"; do if [ -n "$script" ]; then if [ "$script" = "Cancelar" ]; then # usuario eligió cancelar return 0 fi ./"$script" break else if type -t echo_error >/dev/null 2>&1; then echo_error "Selección inválida." else printf "Selección inválida.\n" >&2 fi fi done } export -f run # File: ./cli/0_vars.sh export DOCKER_BASE_DIR="/srv/docker" export DOCKER_APPS_DIR="$DOCKER_BASE_DIR/apps" export DOCKER_BACKUPS_DIR="$DOCKER_BASE_DIR/backups" export DOCKER_MIGRATIONS_DIR="$DOCKER_BASE_DIR/migrations" export DOCKER_TRANSFERS_DIR="$DOCKER_BASE_DIR/transfers" # File: ./cli/atwork.sh atwork() { root_verification atwork_register local command="$1" shift # Removemos el primer argumento (el comando) if ! declare -f "atwork_${command}" > /dev/null 2>&1; then echo_error "Command '${command}' not found." atwork_help return fi # INSTALL if [ "$command" = "install" ]; then atwork_install return fi local apps_list=($(atwork_list apps)) local apps_list_to_show=("${apps_list[@]}") local apps_commands=("uninstall" "update" "delete" "start" "stop" "restart") if [[ " ${apps_commands[@]} " =~ " ${command} " ]]; then # Si apps_list está vacío, y es un comando que requiere aplicaciones instaladas if [ ${#apps_list[@]} -eq 0 ] && ! [[ "$command" =~ ^(uninstall|update)$ ]]; then echo_info "No applications installed" return fi # si el comando es de core, se añade al princio de apps_list local apps_commands_core=("uninstall" "update") if [[ " ${apps_commands_core[@]} " =~ " ${command} " ]]; then apps_list_to_show=("core" "${apps_list_to_show[@]}") fi # si el comando es de all, se añade al princio de apps_list local apps_commands_all=("uninstall" "delete" "start" "stop" "restart") if [[ " ${apps_commands_all[@]} " =~ " ${command} " ]] && [ ${#apps_list[@]} -gt 1 ]; then apps_list_to_show=("all" "${apps_list_to_show[@]}") fi echo echo "0) To cancel" # Mostrar las aplicaciones instaladas for i in "${!apps_list_to_show[@]}"; do echo printf "%d) %s " $((i+1)) "${apps_list_to_show[i]}" done echo read -rp $'\nChoose an option to '"${command}"': ' choice # Validar que sea un número dentro del rango if [[ ! $choice =~ ^[0-9]+$ ]] \ || (( choice < 1 )) \ || (( choice > ${#apps_list_to_show[@]} )); then echo "Operation cancelled." return fi # Guardar la app escogida local selection="${apps_list_to_show[choice-1]}" # Si la selección es "all", se ejecuta el comando para todas las aplicaciones if [ "$selection" = "all" ]; then for app in "${apps_list[@]}"; do "atwork_${command}" "$app" "force" done return fi # ejecutar el comando correspondiente "atwork_${command}" "$selection" "force" return fi # ejecucion de funcion generica "atwork_${command}" "$@" } export -f atwork alias a4k=atwork alias aw=atwork # File: ./cli/atwork_apps.sh atwork_apps() { echo_title "APPS" apps_list=($(atwork_list apps)) # Si no se encontraron apps if [ ${#apps_list[@]} -eq 0 ]; then echo "No applications found." return fi # Definir anchos de columna local col_app=20 local col_id=27 local col_domain=35 local col_ports=20 local col_status=12 local col_services=10 local col_size=8 local col_created=12 # Función para truncar texto truncate_text() { local text="$1" local max_length="$2" if [ ${#text} -gt $max_length ]; then echo "${text:0:$((max_length-3))}..." else echo "$text" fi } # Imprimir encabezado de tabla printf "┌─%-${col_app}s─┬─%-${col_id}s─┬─%-${col_domain}s─┬─%-${col_ports}s─┬─%-${col_status}s─┬─%-${col_services}s─┬─%-${col_size}s─┬─%-${col_created}s─┐\n" \ "$(printf '─%.0s' {1..20})" \ "$(printf '─%.0s' {1..27})" \ "$(printf '─%.0s' {1..35})" \ "$(printf '─%.0s' {1..20})" \ "$(printf '─%.0s' {1..12})" \ "$(printf '─%.0s' {1..10})" \ "$(printf '─%.0s' {1..8})" \ "$(printf '─%.0s' {1..12})" printf "│ %-${col_app}s │ %-${col_id}s │ %-${col_domain}s │ %-${col_ports}s │ %-${col_status}s │ %-${col_services}s │ %-${col_size}s │ %-${col_created}s │\n" \ "APP NAME" "APP ID" "DOMAIN" "PORTS" "STATUS" "SERVICES" "SIZE" "CREATED" printf "├─%-${col_app}s─┼─%-${col_id}s─┼─%-${col_domain}s─┼─%-${col_ports}s─┼─%-${col_status}s─┼─%-${col_services}s─┼─%-${col_size}s─┼─%-${col_created}s─┤\n" \ "$(printf '─%.0s' {1..20})" \ "$(printf '─%.0s' {1..27})" \ "$(printf '─%.0s' {1..35})" \ "$(printf '─%.0s' {1..20})" \ "$(printf '─%.0s' {1..12})" \ "$(printf '─%.0s' {1..10})" \ "$(printf '─%.0s' {1..8})" \ "$(printf '─%.0s' {1..12})" cd "${DOCKER_APPS_DIR}" || return 1 local row_count=0 for app in "${apps_list[@]}"; do # Leer APP_NAME y APP_ID desde config.sh source "$app/config.sh" 2>/dev/null # Valores por defecto si no se encuentran APP_NAME=${APP_NAME:-"N/A"} APP_ID=${APP_ID:-"N/A"} # Leer dominio del .env (buscar variables terminadas en _DOMAIN) domain="" if [ -f "$app/.env" ]; then # Buscar cualquier variable que termine en _DOMAIN o sea DOMAIN domain_value=$(grep -E '^[A-Z_]*DOMAIN=' "$app/.env" | head -1 | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$domain_value" ]; then domain="https://$domain_value" fi fi domain=${domain:-"N/A"} # Leer puertos desde config.sh (ya sourced arriba) ports="" if [ -n "${ATWORK_PORTS[*]}" ]; then # Convertir array a string separado por comas ports=$(IFS=', '; echo "${ATWORK_PORTS[*]}") fi # Si no hay puertos en config.sh, intentar con EXTERNAL_PORT del .env if [ -z "$ports" ] && [ -f "$app/.env" ]; then external_port=$(grep '^EXTERNAL_PORT=' "$app/.env" | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$external_port" ]; then ports="${external_port}/tcp" fi fi ports=${ports:-"N/A"} # Obtener estado de docker compose services_info="N/A" if [ -f "$app/docker-compose.yaml" ] || [ -f "$app/docker-compose.yml" ]; then cd "$app" # Contar contenedores en diferentes estados running=$(docker compose ps --filter "status=running" -q 2>/dev/null | wc -l) total=$(docker compose ps -aq 2>/dev/null | wc -l) if [ "$total" -eq 0 ]; then status="\033[31mstopped\033[0m" services_info="00/00" elif [ "$running" -eq "$total" ] && [ "$running" -gt 0 ]; then status="\033[32mrunning\033[0m" services_info=$(printf "%02d/%02d" "$running" "$total") elif [ "$running" -gt 0 ]; then status="\033[33mpartial\033[0m" services_info=$(printf "%02d/%02d" "$running" "$total") else status="\033[31mstopped\033[0m" services_info=$(printf "%02d/%02d" "$running" "$total") fi cd - > /dev/null else status="\033[90mno-compose\033[0m" services_info="N/A" fi # Obtener tamaño size=$(du -sh "$app" 2>/dev/null | awk '{print $1}') size=${size:-"N/A"} # Obtener fecha de creación created=$(stat -c "%y" "$app" 2>/dev/null | cut -d' ' -f1) created=${created:-"N/A"} # Truncar valores largos para que quepan en las columnas app_name_display=$(truncate_text "$APP_NAME" $col_app) app_id_display="$APP_ID" # No truncar APP_ID con 27 caracteres domain_display=$(truncate_text "$domain" $col_domain) ports_display=$(truncate_text "$ports" $col_ports) # Imprimir fila printf "│ %-${col_app}s │ %-${col_id}s │ %-${col_domain}s │ %-${col_ports}s │ %-${col_status}b │ %-${col_services}s │ %-${col_size}s │ %-${col_created}s │\n" \ "$app_name_display" \ "$app_id_display" \ "$domain_display" \ "$ports_display" \ "$status" \ "$services_info" \ "$size" \ "$created" # Limpiar variables para la siguiente iteración unset ATWORK_PORTS APP_NAME APP_ID ((row_count++)) done # Imprimir línea de cierre printf "└─%-${col_app}s─┴─%-${col_id}s─┴─%-${col_domain}s─┴─%-${col_ports}s─┴─%-${col_status}s─┴─%-${col_services}s─┴─%-${col_size}s─┴─%-${col_created}s─┘\n" \ "$(printf '─%.0s' {1..20})" \ "$(printf '─%.0s' {1..27})" \ "$(printf '─%.0s' {1..35})" \ "$(printf '─%.0s' {1..20})" \ "$(printf '─%.0s' {1..12})" \ "$(printf '─%.0s' {1..10})" \ "$(printf '─%.0s' {1..8})" \ "$(printf '─%.0s' {1..12})" echo echo "Total apps: $row_count" } # File: ./cli/atwork_apps_cards.sh atwork_apps_cards() { echo_title "APPS" apps_list=($(atwork_list apps)) # Si no se encontraron apps if [ ${#apps_list[@]} -eq 0 ]; then echo "No applications found." return fi cd "${DOCKER_APPS_DIR}" || return 1 for app in "${apps_list[@]}"; do # Leer APP_NAME y APP_ID desde config.sh source "$app/config.sh" 2>/dev/null # Leer dominio del .env (buscar variables terminadas en _DOMAIN) domain="" if [ -f "$app/.env" ]; then # Buscar cualquier variable que termine en _DOMAIN o sea DOMAIN domain_value=$(grep -E '^[A-Z_]*DOMAIN=' "$app/.env" | head -1 | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$domain_value" ]; then domain="https://$domain_value" fi fi # Leer puertos desde config.sh (ya sourced arriba) ports="" if [ -n "${ATWORK_PORTS[*]}" ]; then # Convertir array a string separado por comas ports=$(IFS=', '; echo "${ATWORK_PORTS[*]}") fi # Si no hay puertos en config.sh, intentar con EXTERNAL_PORT del .env if [ -z "$ports" ] && [ -f "$app/.env" ]; then external_port=$(grep '^EXTERNAL_PORT=' "$app/.env" | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$external_port" ]; then ports="${external_port}/tcp" fi fi # Obtener estado de docker compose if [ -f "$app/docker-compose.yaml" ] || [ -f "$app/docker-compose.yml" ]; then cd "$app" # Contar contenedores en diferentes estados running=$(docker compose ps --filter "status=running" -q 2>/dev/null | wc -l) total=$(docker compose ps -aq 2>/dev/null | wc -l) if [ "$total" -eq 0 ]; then status="stopped" status_icon="🔴" status_color="\033[31m" elif [ "$running" -eq "$total" ] && [ "$running" -gt 0 ]; then status="running" status_icon="🟢" status_color="\033[32m" elif [ "$running" -gt 0 ]; then status="partial ($running/$total)" status_icon="🟡" status_color="\033[33m" else status="stopped" status_icon="🔴" status_color="\033[31m" fi cd - > /dev/null else status="no-compose" status_icon="⚪" status_color="\033[90m" fi # Obtener tamaño size=$(du -sh "$app" 2>/dev/null | awk '{print $1}') # Obtener fecha de creación created=$(stat -c "%y" "$app" 2>/dev/null | cut -d' ' -f1) # Imprimir tarjeta mejorada echo "╭─ 📦 $APP_NAME / APP ID: $APP_ID" echo "│" echo "│ 🌐 DOMAIN: ${domain}" echo "│ 🔌 PORTS: ${ports}" echo "│ $status_icon STATUS: ${status}" echo "│ 💾 SIZE: $size" echo "│ 📅 CREATED: $created" echo "│ 📁 PATH: $app" echo "│" echo "╰────────────────────────────────────────────────────────────────────────────" echo # Limpiar variables para la siguiente iteración unset ATWORK_PORTS APP_NAME APP_ID done } # File: ./cli/atwork_backup.sh atwork_backup() { echo_title "BACKUPS OPERATIONS" echo "What do you want to do?" echo echo "0) To cancel" echo echo "1) List backups" echo "2) Create backup" echo "3) Restore backup" echo "4) Delete backup" echo read -p "Enter your choice: " choice case $choice in 1) atwork_backups ;; 2) atwork_backup_create "$APP_ID" ;; 3) atwork_backup_restore "$APP_ID" ;; 4) atwork_backup_delete "$APP_ID" ;; *) return ;; esac } # File: ./cli/atwork_backup_create.sh atwork_backup_create() { local APP_ID="$1" if [ -z "$APP_ID" ]; then echo "Error: APP_ID is required" echo "Usage: atwork_backup_package APP_ID" return 1 fi # Variables local START_TIME=$(date '+%Y-%m-%d %H:%M:%S') local START_TIMESTAMP=$(date +%s) local BACKUP_TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) local BACKUP_FILENAME="${APP_ID}_backup_${BACKUP_TIMESTAMP}.tar.gz" local INFO_FILENAME="${APP_ID}_backup_${BACKUP_TIMESTAMP}.txt" # Go to app directory atwork_goto "$APP_ID" || return 1 # Create backup directory if it doesn't exist local BACKUP_PATH="${DOCKER_BACKUPS_DIR}/${APP_ID}" mkdir -p "$BACKUP_PATH" # Start info file local INFO_FILE="${BACKUP_PATH}/${INFO_FILENAME}" atwork_version >> "$INFO_FILE" 2>&1 atwork_system >> "$INFO_FILE" 2>&1 atwork_docker >> "$INFO_FILE" 2>&1 echo_title "BACKUP INFORMATION" >> "$INFO_FILE" echo "APP_ID: $APP_ID" >> "$INFO_FILE" echo "Directory: $APP_ID" >> "$INFO_FILE" echo "Full path: $PWD" >> "$INFO_FILE" echo "Start time: $START_TIME" >> "$INFO_FILE" # Stop the app atwork_stop "$APP_ID" # Calculate size before compression local SIZE_BEFORE_BYTES=$(du -sb . | cut -f1) local SIZE_BEFORE_HUMAN=$(du -sh . | cut -f1) echo_title "ORIGINAL SIZE" >> "$INFO_FILE" echo "Bytes: $SIZE_BEFORE_BYTES" >> "$INFO_FILE" echo "Human readable: $SIZE_BEFORE_HUMAN" >> "$INFO_FILE" # Create backup tar -czpf "${BACKUP_PATH}/${BACKUP_FILENAME}" . # Check if backup was created successfully if [ $? -eq 0 ]; then # Calculate compressed file size local SIZE_AFTER_BYTES=$(stat -c%s "${BACKUP_PATH}/${BACKUP_FILENAME}") local SIZE_AFTER_HUMAN=$(ls -lh "${BACKUP_PATH}/${BACKUP_FILENAME}" | awk '{print $5}') # Calculate compression ratio local COMPRESSION_RATIO=$(echo "scale=2; 100 - ($SIZE_AFTER_BYTES * 100 / $SIZE_BEFORE_BYTES)" | bc) echo_title "COMPRESSED SIZE" >> "$INFO_FILE" echo "Bytes: $SIZE_AFTER_BYTES" >> "$INFO_FILE" echo "Human readable: $SIZE_AFTER_HUMAN" >> "$INFO_FILE" echo "Compression ratio: ${COMPRESSION_RATIO}%" >> "$INFO_FILE" # File checksum echo_title "INTEGRITY" >> "$INFO_FILE" echo "MD5: $(md5sum "${BACKUP_PATH}/${BACKUP_FILENAME}" | cut -d' ' -f1)" >> "$INFO_FILE" echo "SHA256: $(sha256sum "${BACKUP_PATH}/${BACKUP_FILENAME}" | cut -d' ' -f1)" >> "$INFO_FILE" else echo_error "Backup failed" | tee -a "$INFO_FILE" fi # Record end time local END_TIME=$(date '+%Y-%m-%d %H:%M:%S') local END_TIMESTAMP=$(date +%s) local DURATION=$((END_TIMESTAMP - START_TIMESTAMP)) echo_title "SUMMARY" >> "$INFO_FILE" echo "End time: $END_TIME" >> "$INFO_FILE" echo "Duration: ${DURATION} seconds" >> "$INFO_FILE" echo "Backup file: ${BACKUP_PATH}/${BACKUP_FILENAME}" >> "$INFO_FILE" echo "Info file: ${BACKUP_PATH}/${INFO_FILENAME}" >> "$INFO_FILE" # Start the app again atwork_start "$APP_ID" echo_success "Backup completed" } # File: ./cli/atwork_backups.sh atwork_backups() { echo_title "BACKUPS" # Check if backups directory exists if [ ! -d "${DOCKER_BACKUPS_DIR}" ]; then echo "No backups directory found." return fi # Find all backup files local backup_count=0 local backup_files=() # Collect all backup files while IFS= read -r -d '' backup_file; do backup_files+=("$backup_file") done < <(find "${DOCKER_BACKUPS_DIR}" -name "*_backup_*.tar.gz" -type f -print0 2>/dev/null | sort -z) # If no backups found if [ ${#backup_files[@]} -eq 0 ]; then echo "No backups found." return fi # Define column widths local col_app_id=30 local col_filename=40 local col_size=15 local col_created=20 # Function to truncate text truncate_text() { local text="$1" local max_length="$2" if [ ${#text} -gt $max_length ]; then echo "${text:0:$((max_length-3))}..." else echo "$text" fi } # Print table header printf "┌─%-${col_app_id}s─┬─%-${col_filename}s─┬─%-${col_size}s─┬─%-${col_created}s─┐\n" \ "$(printf '─%.0s' {1..30})" \ "$(printf '─%.0s' {1..40})" \ "$(printf '─%.0s' {1..15})" \ "$(printf '─%.0s' {1..20})" printf "│ %-${col_app_id}s │ %-${col_filename}s │ %-${col_size}s │ %-${col_created}s │\n" \ "APP ID" "BACKUP FILE" "SIZE" "CREATED" printf "├─%-${col_app_id}s─┼─%-${col_filename}s─┼─%-${col_size}s─┼─%-${col_created}s─┤\n" \ "$(printf '─%.0s' {1..30})" \ "$(printf '─%.0s' {1..40})" \ "$(printf '─%.0s' {1..15})" \ "$(printf '─%.0s' {1..20})" # Process each backup file for backup_path in "${backup_files[@]}"; do # Extract APP_ID from directory structure local app_id=$(basename "$(dirname "$backup_path")") # Get filename local filename=$(basename "$backup_path") # Get file size local size=$(ls -lh "$backup_path" 2>/dev/null | awk '{print $5}') size=${size:-"N/A"} # Get creation date local created=$(stat -c "%y" "$backup_path" 2>/dev/null | cut -d' ' -f1-2 | cut -d'.' -f1) created=${created:-"N/A"} # Truncate values for display local app_id_display=$(truncate_text "$app_id" $col_app_id) local filename_display=$(truncate_text "$filename" $col_filename) # Print row printf "│ %-${col_app_id}s │ %-${col_filename}s │ %-${col_size}s │ %-${col_created}s │\n" \ "$app_id_display" \ "$filename_display" \ "$size" \ "$created" ((backup_count++)) done # Print closing line printf "└─%-${col_app_id}s─┴─%-${col_filename}s─┴─%-${col_size}s─┴─%-${col_created}s─┘\n" \ "$(printf '─%.0s' {1..30})" \ "$(printf '─%.0s' {1..40})" \ "$(printf '─%.0s' {1..15})" \ "$(printf '─%.0s' {1..20})" echo echo "Total backups: $backup_count" # Show total size of all backups if [ $backup_count -gt 0 ]; then local total_size=$(du -sh "${DOCKER_BACKUPS_DIR}" 2>/dev/null | awk '{print $1}') echo "Total size: ${total_size:-N/A}" fi echo } # File: ./cli/atwork_delete.sh atwork_delete() { local app="$1" # definimos la varible force si existe como true, sino como false local force="${2:-noforce}" atwork_goto "${app}" # Confirmación de seguridad dependiendo de la variable force if [ "$force" != "force" ]; then echo -e "DANGER: This will completely DELETE ${app} including ALL data" echo -n "Type the App ID '${app}' to confirm: " read -r confirmation if [ "$confirmation" != "$app" ]; then echo_info "Confirmation does not match. Aborted deletion." return fi fi atwork_uninstall "$app" # Eliminar toda la carpeta cd $DOCKER_APPS_DIR rm -rf "${app}" echo_success "${app} deleted" } # File: ./cli/atwork_docker.sh atwork_docker(){ echo_title "DOCKER" docker --version docker compose version } # File: ./cli/atwork_firewall.sh atwork_firewall() { # Capturar salida de ufw local ufw_output=$(ufw status verbose 2>/dev/null) # Verificar si el firewall está activo if ! echo "$ufw_output" | grep -q "Status: active"; then echo "🔴 Firewall is not active" return 1 fi echo_title "FIREWALL" # Arrays para almacenar reglas declare -a system_rules=() declare -a atwork_rules=() # Procesar líneas de reglas (después de "To") while IFS= read -r line; do # Saltar líneas que no son reglas if [[ ! "$line" =~ ^[0-9]+.*ALLOW ]]; then continue fi # Extraer componentes port_proto=$(echo "$line" | awk '{print $1}') comment=$(echo "$line" | sed -n 's/.*# \(.*\)/\1/p') # Determinar si es IPv6 if [[ "$line" =~ \(v6\) ]]; then ip_version="v4/v6" # Limpiar puerto de (v6) port_proto=$(echo "$port_proto" | sed 's/ (v6)//') else # Si hay una regla v6 correspondiente, será v4/v6, sino solo v4 ip_version="v4" fi # Separar puerto y protocolo port=$(echo "$port_proto" | cut -d'/' -f1) protocol=$(echo "$port_proto" | cut -d'/' -f2) # Crear entrada de regla rule_entry="$port|$protocol|$ip_version|$comment" # Clasificar según si contiene "AtWork" if [[ "$comment" =~ AtWork ]]; then # Limpiar "AtWork: " del comentario comment=$(echo "$comment" | sed 's/AtWork: //') rule_entry="$port|$protocol|$ip_version|$comment" atwork_rules+=("$rule_entry") else system_rules+=("$rule_entry") fi done <<< "$ufw_output" # Función para imprimir tabla print_firewall_table() { local title="$1" shift local rules=("$@") if [ ${#rules[@]} -eq 0 ]; then return fi echo "$title" echo echo "PORT PROTOCOL IP USAGE" echo "──── ──────── ───── ─────" # Procesar reglas únicas (combinar v4 y v6) declare -A processed_rules for rule in "${rules[@]}"; do IFS='|' read -r port protocol ip usage <<< "$rule" key="$port|$protocol|$usage" if [[ -n "${processed_rules[$key]}" ]]; then # Si ya existe, actualizar a v4/v6 processed_rules[$key]="$port|$protocol|v4/v6|$usage" else processed_rules[$key]="$rule" fi done # Imprimir reglas procesadas for key in "${!processed_rules[@]}"; do IFS='|' read -r port protocol ip usage <<< "${processed_rules[$key]}" printf "%-6s %-11s %-9s %s\n" "$port" "$protocol" "$ip" "$usage" done } # Imprimir reglas del sistema print_firewall_table "🖥️ SYSTEM PORTS" "${system_rules[@]}" echo # Imprimir reglas de AtWork print_firewall_table "📦 ATWORK APPS PORTS" "${atwork_rules[@]}" } alias firewall=atwork_firewall # File: ./cli/atwork_goto.sh atwork_goto() { local app="$1" if [ -z "$app" ]; then echo_error "No app specified" return 1 fi if [ -z "$DOCKER_APPS_DIR" ]; then echo_error "DOCKER_APPS_DIR is not set" return 1 fi if [ ! -d "$DOCKER_APPS_DIR" ]; then echo_error "DOCKER_APPS_DIR does not exist" return 1 fi if [ ! -d "$DOCKER_APPS_DIR/${app}" ]; then echo_info "The App ${app} does not exist" return 1 fi cd "$DOCKER_APPS_DIR/${app}" } alias goto=atwork_goto # File: ./cli/atwork_help.sh atwork_help() { echo_title "ATWORK HELP" echo "AtWork CLI - Command Line Interface for AtWork Apps" echo "Available Commands:" # Listar todas las funciones que empiezan con 'atwork_' local commands=($(compgen -A function | grep '^atwork_')) # Procesar y limpiar la lista de comandos local cleaned_commands=() for cmd_full in "${commands[@]}"; do # Quitar el prefijo 'atwork_' local cmd_name="${cmd_full//atwork_/}" # Quitar de la lista los comandos que aún contienen un guion bajo (e.g., atwork_list_apps) # Esto asume que los comandos internos/auxiliares tienen _ en su nombre después de atwork_ if [[ ! "$cmd_name" =~ "_" ]]; then cleaned_commands+=("$cmd_name") fi done # Ordenar alfabéticamente los comandos para una mejor presentación IFS=$'\n' cleaned_commands=($(sort <<<"${cleaned_commands[*]}")) unset IFS for cmd in "${cleaned_commands[@]}"; do echo " - ${cmd}" done } # File: ./cli/atwork_install.sh atwork_install() { local raw=$(curl -fsSL "${ACL_URL_FILES}/apps/") mapfile -t apps < <(printf '%s\n' "$raw" | sed '/^[[:space:]]*$/d') # Imprime todo en una sola línea, numerado for i in "${!apps[@]}"; do echo printf "%d) %s " $((i+1)) "${apps[i]}" done echo read -rp $'\nSelect an app to install: ' choice # Valida que sea un entero dentro del rango if [[ ! $choice =~ ^[0-9]+$ ]] \ || (( choice < 1 )) \ || (( choice > ${#apps[@]} )); then echo_error "Invalid choice. Please enter a number between 1 and ${#apps[@]}." return 1 fi local app="${apps[choice-1]}" local app_dir="${app}" local code=$(acl_random_string 6) # Validar si la app debe tener un codigo _XXXXXX local temp_compose=$(mktemp) if curl -sSL "$ACL_URL_FILES/apps/${app}/docker-compose.yaml" -o "$temp_compose" 2>/dev/null; then if grep -q "#ATWORK#" "$temp_compose"; then echo_warning "The app '${app}' does not exist" rm -f "$temp_compose" return 1 fi if grep -q "a4k" "$temp_compose"; then app_dir="${code}_${app}" fi else echo_warning "The app '${app}' does not exist" rm -f "$temp_compose" return 1 fi rm -f "$temp_compose" # Validar si la carpeta ya existe if [ -d "$DOCKER_APPS_DIR/${app_dir}" ]; then echo_info "The app ${app} already exist" return fi # Instalar si no está instalado mkdir -p $DOCKER_APPS_DIR/${app_dir} atwork_goto "${app_dir}" # guardamos las variables de configuracion echo "APP_NAME=\"${app}\"" >> config.sh echo "APP_ID=\"${app_dir}\"" >> config.sh acl_download_file "$ACL_URL_FILES/app?name=${app}" "./" "${app}.zip" if ! unzip -q "${app}.zip"; then echo_error "The intaller package for the app ${app} has failed" return fi rm -f "${app}.zip" acl_clean_files # reemplazamos a4k por el _codigo generado if [ -f ".env" ]; then sed -i "s/a4k/${code}/g" .env fi if [ -f "docker-compose.yaml" ]; then sed -i "s/a4k/${code}/g" docker-compose.yaml fi # da permisos 0644 a crontab si existe if [ -f "crontab" ]; then chmod 0644 crontab chown root:root crontab fi # Los puertos ahora se procesan en atwork_start.sh # atwork_install_prepare_ports atwork_install_prepare_volumes # Ejecutamos el script "bake.sh" si existe if [ -f "bake.sh" ]; then chmod +x bake.sh . bake.sh fi # Ejecutamos el script de instalacion si existe if [ -f "install.sh" ]; then # validamos si existe el archivo uninstall.sh if [ ! -f "uninstall.sh" ]; then # el usuario confima si quiere continuar con la instalacion echo_warning "This will install the app ${app} and may modify your system, the changes are not reversible" echo -n "Type 'yes' to continue or Type 'no' to cancel this action: " read -r confirmation < /dev/tty if [[ "$confirmation" != "yes" ]]; then echo_info "Installation cancelled" return fi fi chmod +x install.sh . install.sh fi # si existe el archivo Dockerfile, lo construimos if [ -f "Dockerfile" ]; then echo_info "Building Docker image for ${app}..." if ! docker build -t "${app_dir}:latest" .; then echo_error "Failed to build Docker image for ${app}" return fi echo_success "Docker image for ${app} built successfully" fi echo_readme atwork_start "${app_dir}" } # File: ./cli/atwork_install_prepare_ports.sh atwork_install_prepare_ports() { echo_info "Analyzing docker-compose.yaml for ports..." local config_file="config.sh" local ports_array=() # Verificar que existe docker-compose.yaml if [ ! -f "docker-compose.yaml" ]; then echo "Error: docker-compose.yaml no encontrado" return 1 fi # Cargar variables de entorno si existe .env local env_vars="" if [ -f ".env" ]; then # Leer .env y crear un array asociativo con las variables while IFS='=' read -r key value; do # Ignorar líneas vacías y comentarios if [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]]; then continue fi # Eliminar espacios en blanco alrededor key=$(echo "$key" | xargs) value=$(echo "$value" | xargs) # Guardar para uso posterior env_vars="${env_vars}${key}=${value}\n" done < .env fi # Función auxiliar para resolver variables de entorno resolve_env_var() { local var_name="$1" # Buscar en las variables cargadas del .env local value=$(echo -e "$env_vars" | grep "^${var_name}=" | cut -d'=' -f2-) echo "$value" } # Extraer la sección de puertos usando sed local ports_section=$(sed -n '/^[[:space:]]*ports:/,/^[[:space:]]*[a-zA-Z]/p' docker-compose.yaml | sed '$d') # Procesar cada línea de puerto while IFS= read -r line; do # Saltar la línea "ports:" if echo "$line" | grep -q "^[[:space:]]*ports:"; then continue fi # Ignorar líneas completamente comentadas if echo "$line" | grep -qE '^[[:space:]]*#'; then continue fi # Buscar líneas con formato de puerto if echo "$line" | grep -qE '^[[:space:]]*-[[:space:]]*"?(\$\{[A-Z_]+\}|[0-9]+):[0-9]+"?'; then # Eliminar comentarios inline, espacios, guiones y comillas externas port_line=$(echo "$line" | sed 's/#.*//' | sed 's/^[[:space:]]*-[[:space:]]*//' | sed 's/"//g' | sed 's/[[:space:]]*$//') # Extraer la parte del puerto externo (antes de :) external_port_raw=$(echo "$port_line" | cut -d':' -f1) # Verificar si es una variable de entorno if echo "$external_port_raw" | grep -qE '^\$\{[A-Z_]+\}$'; then # Extraer nombre de la variable (sin ${}) var_name=$(echo "$external_port_raw" | sed 's/\${\(.*\)}/\1/') # Resolver la variable external_port=$(resolve_env_var "$var_name") # Si no se pudo resolver, intentar con valor por defecto o saltar if [ -z "$external_port" ]; then echo "Advertencia: No se pudo resolver la variable \${$var_name}" continue fi else external_port="$external_port_raw" fi # Verificar que tenemos un puerto válido if ! echo "$external_port" | grep -qE '^[0-9]+$'; then echo "Advertencia: Puerto inválido: $external_port" continue fi # Verificar si tiene protocolo especificado if echo "$port_line" | grep -qE '/(tcp|udp)'; then proto=$(echo "$port_line" | sed -n 's/.*\/\(tcp\|udp\).*/\1/p') ports_array+=("$external_port/$proto") else # Por defecto usar tcp ports_array+=("$external_port/tcp") fi fi done <<< "$ports_section" # Construir la línea ATWORK_PORTS local ports_line="ATWORK_PORTS=(" for port in "${ports_array[@]}"; do ports_line+="\"$port\" " done ports_line="${ports_line% }" # Eliminar espacio final ports_line+=")" # Actualizar o crear la variable ATWORK_PORTS en config.sh if [ -f "$config_file" ] && grep -q "^ATWORK_PORTS=" "$config_file"; then sed -i "s/^ATWORK_PORTS=.*/$ports_line/" "$config_file" else echo "$ports_line" >> "$config_file" fi echo "Puertos detectados: ${ports_array[*]}" } # File: ./cli/atwork_install_prepare_volumes.sh atwork_install_prepare_volumes() { # Obtener configuración expandida y extraer rutas de device docker compose config 2>/dev/null | \ grep -E '^[[:space:]]*device:[[:space:]]*' | \ sed -E 's/^[[:space:]]*device:[[:space:]]*//' | \ sed -E 's/^["\047]|["\047]$//g' | \ xargs -I {} dirname {} | \ while IFS= read -r path; do if [ -n "$path" ] && [ "$path" != "." ] && [ "$path" != "/" ]; then mkdir -p "$path" fi done # También crear directamente las rutas de device (sin dirname) docker compose config 2>/dev/null | \ grep -E '^[[:space:]]*device:[[:space:]]*' | \ sed -E 's/^[[:space:]]*device:[[:space:]]*//' | \ sed -E 's/^["\047]|["\047]$//g' | \ while IFS= read -r path; do if [ -n "$path" ] && [ "$path" != "null" ]; then mkdir -p "$path" fi done } # File: ./cli/atwork_list.sh atwork_list() { local subdir="$1" local items=() if [ -z "$subdir" ]; then # Si no se especifica directorio, mostrar directorios principales for item in "$DOCKER_BASE_DIR"/*; do if [ -d "$item" ]; then items+=($(basename "$item")) fi done else # Si se especifica directorio, mostrar contenido for item in "$DOCKER_BASE_DIR/$subdir"/*; do if [ -d "$item" ]; then items+=($(basename "$item")) fi done fi echo "${items[@]}" } # File: ./cli/atwork_prune.sh atwork_prune() { docker container prune -f docker volume prune -f docker network prune -f docker image prune -a -f docker system prune -a -f --volumes echo_success "Containers, volumes, networks, images and system cleanned!" } # File: ./cli/atwork_register.sh atwork_register() { local logs_file="$DOCKER_BASE_DIR/ssh_logins" local ips_file="$DOCKER_BASE_DIR/ssh_ips" # Get client IP from SSH environment local client_ip="" if [[ -n "$SSH_CLIENT" ]]; then client_ip=$(echo $SSH_CLIENT | cut -d' ' -f1) elif [[ -n "$SSH_CONNECTION" ]]; then client_ip=$(echo $SSH_CONNECTION | cut -d' ' -f1) else return 1 fi # Generate timestamp in standard log format local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # Add to logs file (always append) with error handling echo "$timestamp $client_ip" >> "$logs_file" 2>/dev/null || { # If direct write fails, try with sudo echo "$timestamp $client_ip" | sudo tee -a "$logs_file" >/dev/null 2>&1 } # Add to IPs file only if IP doesn't exist if [[ ! -f "$ips_file" ]] || ! grep -q "^$client_ip$" "$ips_file" 2>/dev/null; then echo "$client_ip" >> "$ips_file" 2>/dev/null || { # If direct write fails, try with sudo echo "$client_ip" | sudo tee -a "$ips_file" >/dev/null 2>&1 } fi } # File: ./cli/atwork_repair.sh atwork_repair(){ if [[ $EUID -ne 0 ]]; then echo_error "Root user is required" return 1 fi bash -c "$(curl -sL https://cli.atwork.software/install)" } # File: ./cli/atwork_restart.sh atwork_restart() { local app="$1" atwork_goto "${app}" atwork_stop "$app" atwork_start "$app" } # File: ./cli/atwork_set_ports.sh atwork_set_ports() { # open|close local action="$1" # Cargar los puertos desde config.sh if [ -f "config.sh" ]; then source config.sh # Gestionar cada puerto con ufw if [ -n "${ATWORK_PORTS[*]}" ]; then for port_spec in "${ATWORK_PORTS[@]}"; do if [ "$action" == "close" ]; then # Cerrar puerto ufw delete allow "$port_spec" comment "AtWork: $app_dir" > /dev/null elif [ "$action" == "open" ]; then # Abrir puerto ufw allow "$port_spec" comment "AtWork: $app_dir" > /dev/null else echo_error "Invalid action: $action. Use 'open' or 'close'." return 1 fi done fi fi } # File: ./cli/atwork_start.sh atwork_start() { local app="$1" atwork_goto "${app}" if [ ! -f "docker-compose.yaml" ]; then echo_error "No docker-compose.yaml found in ${app}. Cannot start the application." return fi if [ ! -f ".env" ]; then echo_warning "No .env file found in ${app}. Cannot start the application." return fi atwork_start_process_env atwork_start_validate_domains atwork_install_prepare_ports atwork_set_ports "open" docker compose up -d echo_success "${app} started." } # File: ./cli/atwork_start_process_env.sh atwork_start_process_env() { # Procesar variables de entorno vacías if [ -f ".env" ]; then # Encontrar variables sin valor (mejorado para manejar espacios) empty_vars=$(grep -E '^\s*[A-Z_][A-Z0-9_]*\s*=\s*$|^\s*[A-Z_][A-Z0-9_]*\s*=\s*""\s*$|^\s*[A-Z_][A-Z0-9_]*\s*=\s*'"'"''"'"'\s*$' .env | sed 's/^\s*//' | cut -d'=' -f1) if [ -n "$empty_vars" ]; then echo_info "The following environment variables need to be configured (leave empty to skip):" # Usar array para manejar mejor las variables mapfile -t vars_array <<< "$empty_vars" for var in "${vars_array[@]}"; do # Limpiar espacios en blanco var=$(echo "$var" | tr -d '[:space:]') [ -n "$var" ] && echo " - $var" done echo # Crear archivo temporal temp_env=$(mktemp) cp .env "$temp_env" # Preguntar por cada variable vacía for var in "${vars_array[@]}"; do # Limpiar espacios en blanco var=$(echo "$var" | tr -d '[:space:]') [ -z "$var" ] && continue echo -n "Enter value for $var: " read -r value < /dev/tty if [ -n "$value" ]; then # Escapar caracteres especiales en el valor escaped_value=$(printf '%s\n' "$value" | sed 's/[[\.*^$()+?{|]/\\&/g') # Actualizar la variable en el archivo temporal sed -i "s|^\s*${var}\s*=.*|${var}=${value}|" "$temp_env" else echo_warning "$var is empty, skipping configuration" fi done # Reemplazar el archivo original mv "$temp_env" .env fi fi } # File: ./cli/atwork_start_validate_domains.sh atwork_start_validate_domains() { # Validar DNS si existen variables *_DOMAIN if [ -f ".env" ]; then # Buscar todas las variables que terminen en _DOMAIN domain_vars=$(grep -E '^[A-Z_]*_DOMAIN=' .env) if [ -n "$domain_vars" ]; then # Obtener IP del servidor una sola vez server_ip=$(curl -4 -s ifconfig.me || curl -4 -s icanhazip.com || hostname -I | awk '{print $1}') skip_all_dns=false # Convertir a array para mejor manejo while IFS= read -r line; do [ -z "$line" ] && continue # Extraer nombre de variable y valor var_name=$(echo "$line" | cut -d'=' -f1) domain=$(echo "$line" | cut -d'=' -f2 | tr -d '"' | tr -d "'") if [ -n "$domain" ] && [ "$skip_all_dns" = false ]; then while true; do # Verificar registro DNS A dns_ip=$(dig +short A "$domain" @8.8.8.8 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1) if [ "$dns_ip" = "$server_ip" ]; then break else if [ -z "$dns_ip" ]; then echo_error "No DNS A record found for $domain" else echo_error "DNS A record points to $dns_ip instead of $server_ip" fi echo_info "Please create an A record for $domain pointing to $server_ip" echo -n "Press Enter to retry, 'skip' for this domain, or 'skipall' to skip all remaining: " read -r response < /dev/tty if [ "$response" = "skip" ]; then echo_warning "Skipping validation for $var_name: $domain" break elif [ "$response" = "skipall" ]; then echo_warning "Skipping validation for all remaining domains" skip_all_dns=true break fi fi done elif [ "$skip_all_dns" = true ]; then echo_warning "Skipping validation for $var_name: $domain" fi done <<< "$domain_vars" fi fi } # File: ./cli/atwork_stop.sh atwork_stop() { local app="$1" atwork_goto "${app}" if [ ! -f "docker-compose.yaml" ]; then echo_error "No docker-compose.yaml found in ${app}. Cannot stop the application." return fi docker compose stop atwork_set_ports "close" echo_success "${app} stopped." } # File: ./cli/atwork_system.sh atwork_system(){ echo_title "SYSTEM INFORMATION" [ -r /etc/lsb-release ] && . /etc/lsb-release if [ -z "$DISTRIB_DESCRIPTION" ] && [ -x /usr/bin/lsb_release ]; then # Fall back to using the very slow lsb_release utility DISTRIB_DESCRIPTION=$(lsb_release -s -d) fi echo "OS: $DISTRIB_DESCRIPTION | Kernel: $(uname -o) $(uname -r) $(uname -m)" echo "Hostname: $(hostname)" echo "" echo "vCPU: $(nproc) | RAM: $(free -h | awk 'NR==2{print $2}')" echo "Storage: $(df -h / | awk 'NR==2{print $2}') | Free: $(df -h / | awk 'NR==2{print $4}')" echo "IPv4: $(hostname -I | awk '{print $1}') | IPv6: $(ip -6 addr show scope global | grep inet6 | awk '{print $2}' | cut -d'/' -f1 | head -1)" } alias system=atwork_system # File: ./cli/atwork_transfer.sh #!/bin/bash atwork_transfer() { local operation="" echo_title "atwork transfer" # Check if parameter provided if [[ $# -ge 1 ]]; then case "$1" in "publish"|"p") operation="publish" ;; "send"|"s") operation="send" shift # Remove operation from params ;; "upload"|"u") operation="upload" ;; "download"|"d") operation="download" ;; "get"|"g") operation="get" shift # Remove operation from params ;; "delete"|"del"|"rm") operation="delete" ;; *) echo_error "Invalid parameter. Use: publish|p, send|s, upload|u, download|d, get|g, delete|del|rm" return 1 ;; esac fi # If no parameter, show menu if [[ -z "$operation" ]]; then echo "1) Publish - Compress file/directory" echo "2) Send - Upload to remote server" echo "3) Upload - Web interface URL" echo "4) Download - Show download links" echo "5) Get - Download from remote" echo "6) Delete - Remove files" echo read -p "Select (1-6): " selection case "$selection" in 1) operation="publish" ;; 2) operation="send" ;; 3) operation="upload" ;; 4) operation="download" ;; 5) operation="get" ;; 6) operation="delete" ;; *) echo_error "Invalid selection" return 1 ;; esac fi # Execute operation case "$operation" in "publish") atwork_transfer_publish ;; "send") # Check if IP was provided as parameter if [[ -n "$1" ]]; then atwork_transfer_send "$1" else atwork_transfer_send fi ;; "upload") atwork_transfer_upload ;; "download") atwork_transfer_download ;; "get") # Check if IP was provided as parameter if [[ -n "$1" ]]; then atwork_transfer_get "$1" else atwork_transfer_get fi ;; "delete") atwork_transfer_delete ;; esac } # File: ./cli/atwork_transfer_delete.sh #!/bin/bash atwork_transfer_delete() { local files=() local count=1 # Change to transfers directory cd "$DOCKER_MIGRATIONS_DIR" || { echo_error "Cannot access transfers directory" return 1 } # Get list of files while IFS= read -r -d '' file; do [[ -f "$file" && ! "$(basename "$file")" =~ ^\. ]] && files+=("$(basename "$file")") done < <(find . -maxdepth 1 -type f -print0 2>/dev/null | sort -z) # Check if any files found if [[ ${#files[@]} -eq 0 ]]; then echo "No files to delete" return 0 fi # Display files echo echo "0) DELETE ALL (${#files[@]} files)" echo for filename in "${files[@]}"; do # Get file size local size=$(stat -c%s "$filename" 2>/dev/null || stat -f%z "$filename" 2>/dev/null || echo "0") if [[ "$size" -lt 1024 ]]; then size_str="${size}B" elif [[ "$size" -lt 1048576 ]]; then size_str="$((size / 1024))KB" else size_str="$((size / 1048576))MB" fi printf "%2d) %-50s %s\n" "$count" "$filename" "$size_str" ((count++)) done # Get selection echo read -p "Select (0-${#files[@]} or 'c' to cancel): " selection # Check for cancel if [[ "$selection" == "c" || "$selection" == "C" ]]; then return 0 fi # Validate selection if ! [[ "$selection" =~ ^[0-9]+$ ]] || [[ "$selection" -lt 0 ]] || [[ "$selection" -gt ${#files[@]} ]]; then echo_error "Invalid selection" return 1 fi # Handle deletion if [[ "$selection" -eq 0 ]]; then # Delete all files read -p "Type 'DELETE ALL' to confirm: " confirmation if [[ "$confirmation" == "DELETE ALL" ]]; then for filename in "${files[@]}"; do rm -f "$filename" 2>/dev/null done echo_success "Deleted ${#files[@]} files" fi else # Delete single file local selected_file="${files[$((selection - 1))]}" read -p "Delete $selected_file? (y/N): " confirmation if [[ "$confirmation" == "y" || "$confirmation" == "Y" ]]; then if rm -f "$selected_file" 2>/dev/null; then echo_success "Deleted: $selected_file" else echo_error "Failed to delete" return 1 fi fi fi return 0 } # File: ./cli/atwork_transfer_download.sh #!/bin/bash atwork_transfer_download() { # Change to transfers directory cd "$DOCKER_MIGRATIONS_DIR" || { echo_error "Cannot access transfers directory" return 1 } # Get list of files local files=() while IFS= read -r -d '' file; do [[ -f "$file" && ! "$(basename "$file")" =~ ^\. ]] && files+=("$(basename "$file")") done < <(find . -maxdepth 1 -type f -print0 2>/dev/null | sort -zr) # Check if any files found if [[ ${#files[@]} -eq 0 ]]; then echo_warning "No files available" return 1 fi # Get server IP (force IPv4) local server_ip=$(curl -4 -s ifconfig.me 2>/dev/null || curl -4 -s ipinfo.io/ip 2>/dev/null || curl -4 -s icanhazip.com 2>/dev/null) if [[ -z "$server_ip" ]]; then read -p "Enter server IP: " server_ip fi echo echo "Download URLs:" echo # Display files with clickable download URLs for filename in "${files[@]}"; do # Get file size local size=$(stat -c%s "$filename" 2>/dev/null || stat -f%z "$filename" 2>/dev/null || echo "0") # Format size if [[ "$size" -lt 1024 ]]; then size_str="${size}B" elif [[ "$size" -lt 1048576 ]]; then size_str="$((size / 1024))KB" else size_str="$((size / 1048576))MB" fi # Create download URL local download_url="https://$server_ip:10005/download?file=$filename" # Display printf "%-50s %8s\n" "$filename" "$size_str" printf "\e[4m%s\e[0m\n\n" "$download_url" done return 0 } # File: ./cli/atwork_transfer_get.sh #!/bin/bash atwork_transfer_get() { local dest_ip="$1" local DEFAULT_PORT=10005 echo_info "Get mode - Download to: $DOCKER_MIGRATIONS_DIR" # URLs list_url="https://${dest_ip}:${DEFAULT_PORT}/list" download_base_url="https://${dest_ip}:${DEFAULT_PORT}/download" # Get file list local temp_json=$(mktemp) echo_info "Fetching file list from $dest_ip:$DEFAULT_PORT..." if ! curl -k -s "$list_url" -o "$temp_json"; then echo_error "Failed to fetch file list" rm -f "$temp_json" return 1 fi # Validate JSON if ! jq . "$temp_json" >/dev/null 2>&1; then echo_error "Invalid server response" echo_warning "Server response:" cat "$temp_json" rm -f "$temp_json" return 1 fi # Check file count local file_count=$(jq 'length' "$temp_json" 2>/dev/null || echo "0") if [[ "$file_count" -eq 0 ]]; then echo_warning "No files available on server" rm -f "$temp_json" return 1 fi # Display files echo echo "Available files:" local count=1 while read -r file_info; do local name=$(echo "$file_info" | jq -r '.name') local size=$(echo "$file_info" | jq -r '.size') local created=$(echo "$file_info" | jq -r '.created') # Format size if [[ "$size" -lt 1024 ]]; then size_str="${size}B" elif [[ "$size" -lt 1048576 ]]; then size_str="$((size / 1024))KB" else size_str="$((size / 1048576))MB" fi local date_str=$(echo "$created" | cut -d'T' -f1) printf "%2d) %-40s (%s) - %s\n" "$count" "$name" "$size_str" "$date_str" ((count++)) done < <(jq -c '.[]' "$temp_json") # Get selection echo while true; do read -p "Select file (1-$file_count): " selection if [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le $file_count ]]; then local selected_file_info=$(jq ".[$((selection - 1))]" "$temp_json") local selected_filename=$(echo "$selected_file_info" | jq -r '.name') break else echo_warning "Invalid selection" fi done rm -f "$temp_json" # Change to work directory cd "$DOCKER_MIGRATIONS_DIR" || { echo_error "Cannot access directory" return 1 } # Download download_url="${download_base_url}?file=${selected_filename}" output_file="$DOCKER_MIGRATIONS_DIR/$selected_filename" echo_info "Downloading $selected_filename..." if curl -k -L -o "$output_file" "$download_url" -s; then if [[ -f "$output_file" ]] && [[ -s "$output_file" ]]; then echo_success "Downloaded: $(basename "$output_file")" return 0 else echo_error "Download failed - file is empty" rm -f "$output_file" return 1 fi else echo_error "Download failed" rm -f "$output_file" return 1 fi } # File: ./cli/atwork_transfer_publish.sh #!/bin/bash atwork_transfer_publish() { local items=() local count=1 # Ensure transfers directory exists if [[ ! -d "$DOCKER_MIGRATIONS_DIR" ]]; then echo_error "Transfers directory does not exist: $DOCKER_MIGRATIONS_DIR" return 1 fi # Collect all files and directories (including hidden) from CURRENT directory while IFS= read -r -d '' item; do # Skip . and .. local basename_item=$(basename "$item") if [[ "$basename_item" != "." && "$basename_item" != ".." ]]; then items+=("$item") fi done < <(find . -maxdepth 1 \( -type f -o -type d \) -print0 2>/dev/null | sort -z) # Check if any items found if [[ ${#items[@]} -eq 0 ]]; then echo_warning "No files or directories found" return 1 fi # Display items echo for item in "${items[@]}"; do local basename_item=$(basename "$item") if [[ -f "$item" ]]; then # File size local size=$(stat -c%s "$item" 2>/dev/null || stat -f%z "$item" 2>/dev/null || echo "0") if [[ "$size" -lt 1024 ]]; then size_str="${size}B" elif [[ "$size" -lt 1048576 ]]; then size_str="$((size / 1024))KB" else size_str="$((size / 1048576))MB" fi printf "%2d) %-40s [file] %s\n" "$count" "$basename_item" "$size_str" else # Directory - count files inside local file_count=$(find "$item" -type f 2>/dev/null | wc -l) printf "%2d) %-40s [dir] %d files\n" "$count" "$basename_item" "$file_count" fi ((count++)) done # Get selection echo read -p "Select item (1-${#items[@]}): " selection if ! [[ "$selection" =~ ^[0-9]+$ ]] || [[ "$selection" -lt 1 ]] || [[ "$selection" -gt ${#items[@]} ]]; then echo_error "Invalid selection" return 1 fi selected_item="${items[$((selection - 1))]}" # Prepare filename local original_name=$(basename "$selected_item") # Sanitize filename - keep only a-zA-Z0-9._- local sanitized_name=$(echo "$original_name" | sed 's/[^a-zA-Z0-9._-]/_/g') sanitized_name=$(echo "$sanitized_name" | sed 's/__*/_/g' | sed 's/\.\.*/./g') # Generate timestamp local timestamp=$(date +%Y%m%d%H%M%S) # Final archive name local archive_name="${timestamp}-${sanitized_name}.tar.gz" local archive_path="$DOCKER_MIGRATIONS_DIR/$archive_name" # Compress the item if tar -czf "$archive_path" -C "$(dirname "$selected_item")" "$(basename "$selected_item")" 2>/dev/null; then echo_success "Published: $archive_name" else echo_error "Failed to compress" return 1 fi # Get server IP and show download URL local server_ip=$(curl -4 -s ifconfig.me 2>/dev/null || curl -4 -s ipinfo.io/ip 2>/dev/null || curl -4 -s icanhazip.com 2>/dev/null || echo "unknown") if [[ "$server_ip" != "unknown" ]]; then echo -e "\e[4mhttps://$server_ip:10005/download?file=$archive_name\e[0m" fi return 0 } # File: ./cli/atwork_transfer_send.sh #!/bin/bash atwork_transfer_send() { local dest_ip="$1" local DEFAULT_PORT=10005 local files=() local count=1 # Change to transfers directory cd "$DOCKER_MIGRATIONS_DIR" || { echo_error "Cannot access transfers directory" return 1 } # Collect all files while IFS= read -r -d '' file; do [[ -f "$file" && ! "$(basename "$file")" =~ ^\. ]] && files+=("$(basename "$file")") done < <(find . -maxdepth 1 -type f -print0 2>/dev/null | sort -z) # Check if any files found if [[ ${#files[@]} -eq 0 ]]; then echo_warning "No files in transfers" return 1 fi # Display available files echo for filename in "${files[@]}"; do # Get file size local size=$(stat -c%s "$filename" 2>/dev/null || stat -f%z "$filename" 2>/dev/null || echo "0") if [[ "$size" -lt 1024 ]]; then size_str="${size}B" elif [[ "$size" -lt 1048576 ]]; then size_str="$((size / 1024))KB" else size_str="$((size / 1048576))MB" fi printf "%2d) %-50s %s\n" "$count" "$filename" "$size_str" ((count++)) done # Get file selection echo read -p "Select file (1-${#files[@]}): " selection if ! [[ "$selection" =~ ^[0-9]+$ ]] || [[ "$selection" -lt 1 ]] || [[ "$selection" -gt ${#files[@]} ]]; then echo_error "Invalid selection" return 1 fi selected_file="${files[$((selection - 1))]}" # Get destination IP if not provided if [[ -z "$dest_ip" ]]; then read -p "Destination IP: " dest_ip # Basic IP validation if ! [[ "$dest_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then echo_error "Invalid IP format" return 1 fi fi # Upload file upload_url="https://${dest_ip}:${DEFAULT_PORT}/upload" echo "Uploading to $dest_ip:$DEFAULT_PORT..." if curl -k -X POST -F "file=@$selected_file" "$upload_url" \ --progress-bar \ -w "\nTime: %{time_total}s"; then echo_success "Upload completed" return 0 else echo_error "Upload failed" return 1 fi } # File: ./cli/atwork_transfer_upload.sh atwork_transfer_upload() { local DEFAULT_PORT=10005 echo_info "Upload interface - Open in browser" # Get server IP (force IPv4) local server_ip=$(curl -4 -s ifconfig.me 2>/dev/null || curl -4 -s ipinfo.io/ip 2>/dev/null || curl -4 -s icanhazip.com 2>/dev/null) if [[ -z "$server_ip" ]]; then read -p "Enter server IP: " server_ip fi # Create upload interface URL local upload_url="https://$server_ip:$DEFAULT_PORT/upload" echo echo "Upload Interface URL:" echo printf "\e[4m%s\e[0m\n" "$upload_url" echo return 0 } # File: ./cli/atwork_unistall.sh atwork_uninstall() { local app="$1" # Desinstalar core if [[ "${app^^}" =~ ^(CORE|ATWORK)$ ]]; then atwork_uninstall_core return fi # Desinstalar una aplicacion atwork_goto "${app}" atwork_stop "${app}" if [ -f "docker-compose.yaml" ]; then docker compose down --remove-orphans fi # Ejecutar el script de desinstalación si existe if [ -f "uninstall.sh" ]; then chmod +x uninstall.sh . uninstall.sh fi # Validación inline if [[ -z "$DOCKER_APPS_DIR" ]] || [[ "$(realpath "$(pwd)")" != "$(realpath "$DOCKER_APPS_DIR")"/* ]] || [[ "$(realpath "$(pwd)")" == "$(realpath "$DOCKER_APPS_DIR")" ]]; then echo_warning "Safety check failed: not in a valid app subdirectory of DOCKER_APPS_DIR" echo_warning "Current: $(pwd)" echo_warning "Expected parent: $DOCKER_APPS_DIR" return 1 fi find . -mindepth 1 -maxdepth 1 ! -name 'volumes' -exec rm -rf {} + echo_success "${app} uninstalled" } # File: ./cli/atwork_unistall_core.sh atwork_uninstall_core() { # se pide una confirmacion al usuario echo_warning "This will completely uninstall ACL and AtWork CLI from the system" echo -n "Type 'Delete core' to confirm uninstall: " read confirmation if [ "$confirmation" != "Delete core" ]; then echo_info "Uninstall cancelled" return fi # CONFIG rm -f "$DOCKER_BASE_DIR/.conf" rm -f "$DOCKER_BASE_DIR/ssh_ips" rm -f "$DOCKER_BASE_DIR/ssh_logins" # Eliminar las carpetas backups y migrations y transfers con todo su contenido rm -rf "$DOCKER_BACKUPS_DIR" rm -rf "$DOCKER_MIGRATIONS_DIR" rm -rf "$DOCKER_TRANSFERS_DIR" # ACL UNINSTALL sed -i "/${ACL_START}/,/${ACL_END}/d" "$HOME/.bashrc" || echo "Error en sed" echo -n "" >> "$HOME/.bashrc" || echo "Error en echo bashrc" echo -n "" > "/usr/local/lib/acl.sh" || echo "Error en echo acl.sh" rm -f "/usr/local/lib/acl.sh" || echo "Error en rm acl.sh" # Confirmacion printf "ACL and AtWork CLI uninstalled\n" sync sleep 2 # CLOSE SSH CONECTION kill -HUP $PPID } # File: ./cli/atwork_update.sh atwork_update() { local app="$1" # Actualizar core if [[ "${app^^}" =~ ^(CORE|ATWORK)$ ]]; then echo_info "Updating core..." atwork_update_core return fi # Actualizar una aplicacion echo_info "Update for application not are implemented" } # File: ./cli/atwork_update_core.sh atwork_update_core() { bash -c "$(curl -sL $ACL_URL_FILES/install)" } # File: ./cli/atwork_version.sh atwork_version() { local figlet_path=$(command -v figlet 2>/dev/null) local lolcat_path=$(command -v lolcat 2>/dev/null) if [ -n "$figlet_path" ] && [ -n "$lolcat_path" ]; then echo " AtWork" | "$figlet_path" | "$lolcat_path" -F 0.2 && echo " v$(date -r "$DOCKER_BASE_DIR/.conf" +%F)" elif [ -n "$figlet_path" ]; then echo " AtWork" | "$figlet_path" && echo " v$(date -r "$DOCKER_BASE_DIR/.conf" +%F)" else echo " AtWork v$(date -r "$DOCKER_BASE_DIR/.conf" +%F)" fi echo } #ATWORK# ACL END #ATWORK#