#!/bin/bash set -euo pipefail # Configuration SCRIPT_DIR="$HOME/Documents/Photon" CONFIG_FILE="${SCRIPT_DIR}/photon.conf" LOCKFILE="${SCRIPT_DIR}/.photon-upload.lock" LOGFILE="${SCRIPT_DIR}/photon-upload.log" # When launched as a GUI app there is no TTY — redirect stdout/stderr to the # log file immediately so nothing tries to write to a closed fd. mkdir -p "$SCRIPT_DIR" exec >>"$LOGFILE" 2>&1 # Ensure Homebrew binaries are in PATH when launched as a GUI app export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" # Debug: Log script invocation echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Script invoked with args: $*" >> /tmp/photon-upload-debug.log echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') PID: $$, PPID: $PPID" >> /tmp/photon-upload-debug.log # Load configuration (mandatory) if [[ ! -f "$CONFIG_FILE" ]]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] Config file not found: $CONFIG_FILE" >> "$LOGFILE" osascript -e "display alert \"Photon Uploader\" message \"Config file not found:\\n$CONFIG_FILE\" as critical buttons {\"OK\"} default button \"OK\"" exit 1 fi source "$CONFIG_FILE" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] Config loaded: $CONFIG_FILE" >> "$LOGFILE" # Logging function log() { local level="$1" shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" >> "$LOGFILE" } # Check dependencies check_dependencies() { local missing_deps=() log "INFO" "Checking dependencies..." for cmd in fswatch openssl ssh scp; do if ! command -v "$cmd" >/dev/null 2>&1; then missing_deps+=("$cmd") fi done if [[ ${#missing_deps[@]} -gt 0 ]]; then log "ERROR" "Missing dependencies: ${missing_deps[*]}" log "INFO" "Install with: brew install ${missing_deps[*]}" exit 1 fi log "INFO" "All dependencies found" } # Create necessary directories setup_directories() { log "INFO" "Setting up directories..." for dir in "$DIR_TO_MONITOR" "$DIR_TO_MOVE" "$DIR_ARCHIVE"; do if [[ ! -d "$dir" ]]; then log "INFO" "Creating directory: $dir" mkdir -p "$dir" || { log "ERROR" "Failed to create directory: $dir" exit 1 } fi done log "INFO" "Directory setup complete" } generate_random_filename() { local extension="$1" local random_part=$(openssl rand -base64 6 | tr -dc 'A-Za-z' | head -c 8) echo "${random_part}.${extension}" } # Send system notification send_notification() { if [[ "$ENABLE_NOTIFICATIONS" != "true" ]]; then return; fi local NOTIFY_HELPER NOTIFY_HELPER="$(dirname "$(dirname "$0")")/Helpers/PhotonNotify.app" if [[ -d "$NOTIFY_HELPER" ]]; then open -n -jg "$NOTIFY_HELPER" --args "$1" "$2" elif command -v osascript >/dev/null; then osascript -e "display notification \"$2\" with title \"Photon Uploader\" subtitle \"$1\"" fi } # Check if file extension is supported is_supported_file() { local file="$1" local extension="${file##*.}" # Convert to lowercase (bash 3.2 compatible) extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]') [[ ",$SUPPORTED_EXTENSIONS," == *",$extension,"* ]] } # Validate SSH key authentication setup validate_ssh_key() { if [[ ! -f "$SSH_KEY_PATH" ]]; then log "ERROR" "SSH key not found at: $SSH_KEY_PATH" log "INFO" "Generate SSH key with: ssh-keygen -t rsa -b 4096 -f $SSH_KEY_PATH -N ''" log "INFO" "Then copy public key to server: ssh-copy-id -i ${SSH_KEY_PATH}.pub -p $PORT $USER@$SERVER" return 1 fi # Check key permissions local key_perms=$(stat -f "%Lp" "$SSH_KEY_PATH" 2>/dev/null || echo "000") if [[ "$key_perms" != "600" ]]; then log "WARN" "Fixing SSH key permissions: $SSH_KEY_PATH" chmod 600 "$SSH_KEY_PATH" fi return 0 } # Function to process uploaded files process_file() { local file_path="$1" local file_name=$(basename "$file_path") # Skip hidden files and directories if [[ "$file_name" == .* ]] || [[ -d "$file_path" ]]; then return 0 fi # Check if file exists and is readable if [[ ! -f "$file_path" ]] || [[ ! -r "$file_path" ]]; then log "WARN" "File not accessible: $file_path" return 1 fi # Check if file is already being processed if is_file_being_processed "$file_path"; then log "INFO" "File already being processed: $file_path" return 0 fi # Check if file type is supported if ! is_supported_file "$file_name"; then log "INFO" "Unsupported file type: $file_path" mark_processing_complete "$file_path" return 0 fi # Wait for file to be completely written (common with screenshots) local file_size=0 local new_size=1 while [[ $file_size -ne $new_size ]]; do file_size=$new_size sleep 0.5 new_size=$(stat -f%z "$file_path" 2>/dev/null || echo 0) done log "INFO" "Processing file: $file_path" local extension="${file_name##*.}" local new_filename=$(generate_random_filename "$extension") local staged_path="$DIR_TO_MOVE/$new_filename" # Move file to staging area if ! cp "$file_path" "$staged_path"; then log "ERROR" "Failed to stage file: $file_path" mark_processing_complete "$file_path" return 1 fi # Upload file if upload_file "$staged_path" "$new_filename"; then mark_processing_complete "$file_path" return 0 else mark_processing_complete "$file_path" return 1 fi } # Upload file with improved error handling upload_file() { local file_path="$1" local filename="$2" local url="$BASE_URL/$filename" log "INFO" "Uploading: $filename" # Validate SSH key before attempting upload if ! validate_ssh_key; then log "ERROR" "SSH key validation failed" return 1 fi # Upload using SSH key authentication if scp -P "$PORT" -i "$SSH_KEY_PATH" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$file_path" "$USER@$SERVER:$REMOTE_DIR"; then upload_success=true else upload_success=false fi if [[ "$upload_success" == "true" ]]; then log "INFO" "Upload successful: $filename" # Copy URL to clipboard echo "$url" | pbcopy log "INFO" "URL copied to clipboard: $url" # Send notification send_notification "" "$filename uploaded successfully" generate_thumbnails # Archive or cleanup if [[ "$CLEANUP_AFTER_UPLOAD" == "true" ]]; then if [[ -d "$DIR_ARCHIVE" ]]; then mv "$file_path" "$DIR_ARCHIVE/" log "INFO" "File archived: $filename" else rm "$file_path" log "INFO" "File removed: $filename" fi fi return 0 else log "ERROR" "Upload failed: $filename" send_notification "Failed" "Upload failed for $filename" return 1 fi } generate_thumbnails() { log "INFO" "Requesting thumbnail generation..." if [[ -z "${PHOTON_DOMAIN:-}" ]]; then log "ERROR" "PHOTON_DOMAIN not configured" return 1 fi local response if response=$(curl -s "https://${PHOTON_DOMAIN}/job/generate_thumbnails" 2>&1); then # Parse JSON response to check status if echo "$response" | grep -q '"status":"success"'; then log "INFO" "Thumbnail generation completed successfully" return 0 else log "WARN" "Thumbnail generation may have failed: $response" return 1 fi else log "ERROR" "Failed to connect to thumbnail service: $response" return 1 fi } # Handle concurrent uploads (bash 3.2 compatible) ACTIVE_UPLOADS_DIR="${SCRIPT_DIR}/.active_uploads" PROCESSING_DIR="${SCRIPT_DIR}/.processing" # Function to check if file is already being processed is_file_being_processed() { local file_path="$1" local file_hash=$(echo "$file_path" | shasum -a 256 | cut -d' ' -f1) local processing_file="$PROCESSING_DIR/$file_hash" # Check if processing file exists and is recent (less than 60 seconds old) if [[ -f "$processing_file" ]]; then local processing_time=$(stat -f%m "$processing_file" 2>/dev/null || echo 0) local current_time=$(date +%s) if (( current_time - processing_time < 60 )); then return 0 # Still being processed else rm -f "$processing_file" 2>/dev/null || true # Stale processing file fi fi # Create processing marker atomically to prevent race conditions mkdir -p "$PROCESSING_DIR" if (set -C; echo $$ > "$processing_file") 2>/dev/null; then return 1 # Not being processed, marker created else return 0 # Already being processed by another instance fi } # Function to mark file processing as complete mark_processing_complete() { local file_path="$1" local file_hash=$(echo "$file_path" | shasum -a 256 | cut -d' ' -f1) local processing_file="$PROCESSING_DIR/$file_hash" rm -f "$processing_file" 2>/dev/null || true } # Signal handlers cleanup() { log "INFO" "Shutting down..." [[ -f "$LOCKFILE" ]] && rm -f "$LOCKFILE" # Kill any fswatch processes monitoring our directory if command -v pkill >/dev/null 2>&1; then pkill -f "fswatch.*$DIR_TO_MONITOR" 2>/dev/null || true fi # Wait for active uploads to complete if [[ -d "$ACTIVE_UPLOADS_DIR" ]]; then for upload_file in "$ACTIVE_UPLOADS_DIR"/*; do [[ -f "$upload_file" ]] || continue local pid=$(basename "$upload_file") if kill -0 "$pid" 2>/dev/null; then local filename="" if [[ -r "$upload_file" ]]; then filename=$(cat "$upload_file" 2>/dev/null || echo "unknown") else filename="unknown" fi log "INFO" "Waiting for upload to complete: $filename" wait "$pid" 2>/dev/null || true fi rm -f "$upload_file" 2>/dev/null || true done rmdir "$ACTIVE_UPLOADS_DIR" 2>/dev/null || true fi # Clean up processing directory if [[ -d "$PROCESSING_DIR" ]]; then rm -rf "$PROCESSING_DIR" 2>/dev/null || true fi exit 0 } # Main function main() { echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Entering main function" >> /tmp/photon-upload-debug.log log "INFO" "Starting screenshot upload service" echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Checking for existing lockfile" >> /tmp/photon-upload-debug.log # Enhanced lock creation with better race condition protection local max_attempts=5 local attempt=1 local lock_acquired=false while [[ $attempt -le $max_attempts ]] && [[ "$lock_acquired" == "false" ]]; do echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Lock attempt $attempt" >> /tmp/photon-upload-debug.log if (set -C; echo $$ > "$LOCKFILE") 2>/dev/null; then lock_acquired=true echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Lock acquired on attempt $attempt" >> /tmp/photon-upload-debug.log else existing_pid=$(cat "$LOCKFILE" 2>/dev/null || echo "") echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Lock exists, checking PID: $existing_pid" >> /tmp/photon-upload-debug.log if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then log "ERROR" "Another instance is already running (PID: $existing_pid)" echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Existing process $existing_pid is alive, exiting" >> /tmp/photon-upload-debug.log exit 1 else echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Stale lock file, removing" >> /tmp/photon-upload-debug.log rm -f "$LOCKFILE" 2>/dev/null || true sleep 0.1 fi fi ((attempt++)) done if [[ "$lock_acquired" == "false" ]]; then log "ERROR" "Unable to acquire lockfile after $max_attempts attempts" exit 1 fi echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Created lockfile with PID: $$" >> /tmp/photon-upload-debug.log log "INFO" "Created lockfile with PID: $$" # Set up signal handlers for this monitoring process only trap cleanup SIGINT SIGTERM EXIT echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Calling check_dependencies" >> /tmp/photon-upload-debug.log # Check dependencies and setup check_dependencies setup_directories # Validate SSH key authentication log "INFO" "Validating SSH key authentication..." if ! validate_ssh_key; then log "ERROR" "SSH key authentication setup failed" exit 1 fi log "INFO" "SSH key authentication validated successfully" # Create active uploads tracking directory mkdir -p "$ACTIVE_UPLOADS_DIR" log "INFO" "Created tracking directory: $ACTIVE_UPLOADS_DIR" log "INFO" "Monitoring directory: $DIR_TO_MONITOR" log "INFO" "Supported file types: $SUPPORTED_EXTENSIONS" # Test fswatch before starting the main loop log "INFO" "Testing fswatch compatibility..." if ! fswatch --version >/dev/null 2>&1; then log "ERROR" "fswatch not working properly" exit 1 fi log "INFO" "fswatch test passed" echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') About to start fswatch monitoring" >> /tmp/photon-upload-debug.log log "INFO" "Starting file monitoring with fswatch..." # Create named pipe for fswatch to avoid subshell issues local fswatch_pipe="/tmp/fswatch-$$" mkfifo "$fswatch_pipe" # Start fswatch in background writing to pipe fswatch -0 "$DIR_TO_MONITOR" > "$fswatch_pipe" & local fswatch_pid=$! echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Started fswatch PID: $fswatch_pid" >> /tmp/photon-upload-debug.log # Cleanup function for pipe cleanup_pipe() { kill -TERM "$fswatch_pid" 2>/dev/null || true rm -f "$fswatch_pipe" } trap cleanup_pipe EXIT # Read events from pipe without subshell while read -d "" event; do echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') fswatch event: $event" >> /tmp/photon-upload-debug.log # Skip directories and hidden files immediately if [[ -d "$event" ]] || [[ "$(basename "$event")" == .* ]]; then echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Skipping directory/hidden file: $event" >> /tmp/photon-upload-debug.log continue fi # Add small delay to allow multiple rapid events to settle sleep 0.2 # Limit concurrent uploads while true; do local active_count=0 if [[ -d "$ACTIVE_UPLOADS_DIR" ]]; then for upload_file in "$ACTIVE_UPLOADS_DIR"/*; do [[ -f "$upload_file" ]] || continue local pid=$(basename "$upload_file") if kill -0 "$pid" 2>/dev/null; then ((active_count++)) else rm -f "$upload_file" fi done fi [[ $active_count -lt $MAX_CONCURRENT_UPLOADS ]] && break sleep 0.1 done echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Starting background process for: $event" >> /tmp/photon-upload-debug.log # Process file in background ( echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Background worker PID $$ processing: $event" >> /tmp/photon-upload-debug.log process_file "$event" echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Background worker PID $$ finished: $event" >> /tmp/photon-upload-debug.log rm -f "$ACTIVE_UPLOADS_DIR/$$" 2>/dev/null || true ) & local upload_pid=$! echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Spawned upload worker: $upload_pid for $(basename "$event")" >> /tmp/photon-upload-debug.log echo "$(basename "$event")" > "$ACTIVE_UPLOADS_DIR/$upload_pid" done < "$fswatch_pipe" } # Help function show_help() { cat << EOF Screenshot Upload Service Usage: $0 [OPTIONS] Options: -h, --help Show this help message -c, --config Specify config file path -d, --daemon Run in background -s, --stop Stop running daemon -t, --test Test upload with a sample file --status Show service status and related processes Configuration: Edit $CONFIG_FILE to customize settings EOF } # Parse command line arguments BACKGROUND_MODE=false while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; -c|--config) CONFIG_FILE="$2" shift 2 ;; -d|--daemon) # Ensure log directory exists mkdir -p "$(dirname "$LOGFILE")" echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Starting daemon mode" >> /tmp/photon-upload-debug.log # Start daemon with logging nohup "$0" --background >> "$LOGFILE" 2>&1 & daemon_pid=$! echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Spawned background process: $daemon_pid" >> /tmp/photon-upload-debug.log echo "Started in background with PID: $daemon_pid" echo "Monitor with: tail -f $LOGFILE" exit 0 ;; --background) # Internal flag used by daemon mode - don't document in help # This prevents infinite daemon loops echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') Background mode activated, breaking from arg parse" >> /tmp/photon-upload-debug.log BACKGROUND_MODE=true shift break ;; -s|--stop) if [[ -f "$LOCKFILE" ]]; then pid=$(cat "$LOCKFILE") if kill -0 "$pid" 2>/dev/null; then echo "Stopping service with PID: $pid" kill -TERM "$pid" # Also kill any related fswatch processes if command -v pkill >/dev/null 2>&1; then pkill -f "fswatch.*$DIR_TO_MONITOR" 2>/dev/null || true echo "Stopped related fswatch processes" fi # Wait a moment for graceful shutdown sleep 2 # Force kill if still running if kill -0 "$pid" 2>/dev/null; then kill -KILL "$pid" 2>/dev/null || true echo "Force killed process $pid" fi echo "Service stopped" else echo "Service not running (PID $pid not found)" rm -f "$LOCKFILE" fi else echo "No lockfile found, checking for orphaned processes..." # Look for orphaned upload or fswatch processes if command -v pgrep >/dev/null 2>&1; then orphaned_pids=$(pgrep -f "upload.*background|fswatch.*$DIR_TO_MONITOR" 2>/dev/null || true) if [[ -n "$orphaned_pids" ]]; then echo "Found orphaned processes: $orphaned_pids" kill -TERM $orphaned_pids 2>/dev/null || true sleep 2 kill -KILL $orphaned_pids 2>/dev/null || true echo "Cleaned up orphaned processes" else echo "No running processes found" fi else echo "Service may not be running" fi fi exit 0 ;; -t|--test) echo "Testing upload functionality..." check_dependencies # Create a test file test_file="/tmp/test_$(date +%s).txt" echo "Test upload $(date)" > "$test_file" process_file "$test_file" rm -f "$test_file" exit 0 ;; --status) echo "Screenshot Upload Service Status" echo "================================" if [[ -f "$LOCKFILE" ]]; then lock_pid=$(cat "$LOCKFILE") echo "Lock file: $LOCKFILE (PID: $lock_pid)" if kill -0 "$lock_pid" 2>/dev/null; then echo "Main process: RUNNING (PID: $lock_pid)" else echo "Main process: DEAD (stale lock file)" fi else echo "Lock file: NOT FOUND" fi echo "" echo "Related processes:" if ps aux | grep -E "(upload|fswatch.*$DIR_TO_MONITOR)" | grep -v grep | grep -v status; then true else echo "No related processes found" fi echo "" if [[ -d "$ACTIVE_UPLOADS_DIR" ]]; then upload_count=$(ls -1 "$ACTIVE_UPLOADS_DIR" 2>/dev/null | wc -l | tr -d ' ') echo "Active uploads: $upload_count" else echo "Active uploads: 0" fi exit 0 ;; *) echo "Unknown option: $1" show_help exit 1 ;; esac done # Run main function main