commit 6bea18e29597ec8eae3b10770e8a629793bc9f16 Author: Jordan Walster Date: Sat May 2 20:38:51 2026 +0100 feat: initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2bd34c --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Photon Uploader (Apple Silicon) + +![Photon Logo](/logo.png) + +A lightweight macOS menu bar app that watches a folder for new image files and automatically uploads them to your Photon SSH Container via SCP. After a successful upload, the URL is copied to your clipboard and a system notification is sent. + +## Requirements +- [Homebrew](https://brew.sh) +- `fswatch` — `brew install fswatch` +- `openssl` — `brew install openssl` + +## Configuration + +Copy `photon.conf` to `~/Documents/Photon/photon.conf` and fill in your values: + +```bash +# Directories +DIR_TO_MONITOR="/Users/Jordan/Documents/Photon/tmp" # Folder to watch for new files +DIR_TO_MOVE="/Users/Jordan/Documents/Photon/upload" # Staging area during upload +DIR_ARCHIVE="/Users/Jordan/Documents/Photon/archive" # Where files go after upload (if archiving) + +# Server +SERVER="photon.example.com" +USER="photon" +PORT="2222" +REMOTE_DIR="/path/on/server/" +PHOTON_DOMAIN="photon.example.com" +BASE_URL="https://photon.example.com/share" + +# SSH key +SSH_KEY_PATH="$HOME/.ssh/id_rsa" + +# File handling +SUPPORTED_EXTENSIONS="jpg,jpeg,png,gif,webp,bmp,tiff" +CLEANUP_AFTER_UPLOAD=true +ENABLE_NOTIFICATIONS=true + +# Performance +MAX_CONCURRENT_UPLOADS=3 +UPLOAD_TIMEOUT=30 +``` + +## Building + +```bash +./build.sh +``` + +This compiles both Swift binaries, assembles the app bundle, and installs the result to `~/Applications/Photon Uploader.app`. + +### Regenerating the app icon + +```bash +./create_icon.sh +``` + +Requires `magick` (`brew install imagemagick`). Generates `Resources/AppIcon.icns` from the source artwork and then runs `build.sh`. + +## Usage + +Launch **Photon Uploader** from `~/Applications`. It will start automatically monitoring `DIR_TO_MONITOR`. Drop any supported image file into that folder and it will be: + +1. Renamed to a random filename +2. Uploaded to Photon via SCP +3. URL copied to your clipboard +4. Thumbnail generation triggered on the server +5. Archived or removed locally (depending on `CLEANUP_AFTER_UPLOAD`) + +Use the menu bar icon to **Open Log** (`~/Documents/Photon/photon-upload.log`) or **Quit**. + +## Project structure + +``` +photon-uploader/ +├── build/ +│ ├── Info.plist App bundle metadata. +│ ├── PhotonUploader/ +│ │ └── main.swift Menu bar app source. +│ │ +│ ├── PhotonNotify/ +│ │ ├── main.swift Notification helper source. +│ │ └── info.plist Helper bundle metadata. +│ │ +│ ├── Resources/ +│ │ ├── AppIcon.icns App icon. +│ │ ├── MenuBarIcon.png Menu bar icon (1x). +│ │ └── MenuBarIcon@2x.png Menu bar icon (2x). +│ └── upload.sh Upload daemon script. +│ +├── build.sh Build and install script. +└── photon.conf Example configuration + +``` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..f755141 --- /dev/null +++ b/build.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +REPO="$(cd "$(dirname "$0")" && pwd)/build" +APP="$HOME/Applications/Photon Uploader.app" +CONTENTS="$APP/Contents" + +echo "==> Building PhotonUploader (menu bar binary)..." +swiftc -framework AppKit \ + "$REPO/PhotonUploader/main.swift" \ + -o "$REPO/PhotonUploader/upload" + +echo "==> Building PhotonNotify (notification helper)..." +swiftc -framework Foundation -framework UserNotifications -framework AppKit \ + "$REPO/PhotonNotify/main.swift" \ + -o "$REPO/PhotonNotify/PhotonNotify" + +echo "==> Assembling app bundle..." +mkdir -p "$CONTENTS/MacOS" +mkdir -p "$CONTENTS/Resources" +mkdir -p "$CONTENTS/Helpers/PhotonNotify.app/Contents/MacOS" +mkdir -p "$CONTENTS/Helpers/PhotonNotify.app/Contents/Resources" + +cp "$REPO/PhotonUploader/upload" "$CONTENTS/MacOS/upload" +cp "$REPO/upload.sh" "$CONTENTS/MacOS/upload.sh" +cp "$REPO/Info.plist" "$CONTENTS/Info.plist" + +cp "$REPO/PhotonNotify/PhotonNotify" "$CONTENTS/Helpers/PhotonNotify.app/Contents/MacOS/PhotonNotify" +cp "$REPO/PhotonNotify/Info.plist" "$CONTENTS/Helpers/PhotonNotify.app/Contents/Info.plist" +cp "$REPO/Resources/AppIcon.icns" "$CONTENTS/Helpers/PhotonNotify.app/Contents/Resources/AppIcon.icns" + +cp "$REPO/Resources/AppIcon.icns" "$CONTENTS/Resources/AppIcon.icns" +cp "$REPO/Resources/MenuBarIcon.png" "$CONTENTS/Resources/MenuBarIcon.png" +cp "$REPO/Resources/MenuBarIcon@2x.png" "$CONTENTS/Resources/MenuBarIcon@2x.png" + +chmod +x "$CONTENTS/MacOS/upload" +chmod +x "$CONTENTS/MacOS/upload.sh" +chmod +x "$CONTENTS/Helpers/PhotonNotify.app/Contents/MacOS/PhotonNotify" + +echo "==> Signing..." +codesign --force --sign - "$CONTENTS/Helpers/PhotonNotify.app" +codesign --force --sign - "$APP" + +echo "==> Done. App installed at: $APP" diff --git a/build/Info.plist b/build/Info.plist new file mode 100644 index 0000000..707d1f7 --- /dev/null +++ b/build/Info.plist @@ -0,0 +1,27 @@ + + + + + CFBundleName + Photon Uploader + CFBundleDisplayName + Photon Uploader + CFBundleIdentifier + dev.jrdn.PhotonUploader + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + LSArchitecturePriority + + arm64 + + CFBundleExecutable + upload + CFBundleIconFile + AppIcon + CFBundlePackageType + APPL + + diff --git a/build/PhotonNotify/Info.plist b/build/PhotonNotify/Info.plist new file mode 100644 index 0000000..0e6bf79 --- /dev/null +++ b/build/PhotonNotify/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleName + Photon Uploader + CFBundleDisplayName + Photon Uploader + CFBundleIdentifier + dev.jrdn.PhotonUploaderHelper + CFBundleExecutable + PhotonNotify + CFBundleIconFile + AppIcon + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + CFBundlePackageType + APPL + LSUIElement + + LSBackgroundOnly + + NSUserNotificationAlertStyle + banner + + diff --git a/build/PhotonNotify/main.swift b/build/PhotonNotify/main.swift new file mode 100644 index 0000000..31e92df --- /dev/null +++ b/build/PhotonNotify/main.swift @@ -0,0 +1,43 @@ +import AppKit +import UserNotifications + +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ aNotification: Notification) { + let args = ProcessInfo.processInfo.arguments + let subtitle = args.count > 1 ? args[1] : "" + let body = args.count > 2 ? args[2] : "" + + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound]) { granted, _ in + guard granted else { + DispatchQueue.main.async { NSApp.terminate(nil) } + return + } + + let content = UNMutableNotificationContent() + content.title = "Photon Uploader" + content.subtitle = subtitle + content.body = body + content.sound = .default + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + let request = UNNotificationRequest( + identifier: UUID().uuidString, + content: content, + trigger: trigger + ) + + center.add(request) { _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + NSApp.terminate(nil) + } + } + } + } +} + +let app = NSApplication.shared +app.setActivationPolicy(.prohibited) +let delegate = AppDelegate() +app.delegate = delegate +app.run() diff --git a/build/PhotonUploader/main.swift b/build/PhotonUploader/main.swift new file mode 100644 index 0000000..e61a7d7 --- /dev/null +++ b/build/PhotonUploader/main.swift @@ -0,0 +1,71 @@ +import AppKit + +class AppDelegate: NSObject, NSApplicationDelegate { + var statusItem: NSStatusItem? + var scriptProcess: Process? + + func applicationDidFinishLaunching(_ notification: Notification) { + app.setActivationPolicy(.prohibited) + + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + + if let button = statusItem?.button { + let bundle = Bundle.main + var icon: NSImage? + if let url = bundle.url(forResource: "MenuBarIcon@2x", withExtension: "png"), + let img = NSImage(contentsOf: url) { + img.isTemplate = true + icon = img + } else if let url = bundle.url(forResource: "MenuBarIcon", withExtension: "png"), + let img = NSImage(contentsOf: url) { + img.isTemplate = true + icon = img + } + if let icon = icon { + button.image = icon + } else { + button.title = "☁" + } + } + + let menu = NSMenu() + let openLog = NSMenuItem(title: "Open Log", action: #selector(openLog), keyEquivalent: "") + openLog.target = self + menu.addItem(openLog) + menu.addItem(NSMenuItem.separator()) + let quit = NSMenuItem(title: "Quit Photon Uploader", action: #selector(quitApp), keyEquivalent: "q") + quit.target = self + menu.addItem(quit) + statusItem?.menu = menu + + launchScript() + } + + func launchScript() { + guard let resourcePath = Bundle.main.resourcePath else { return } + let scriptDir = (resourcePath as NSString).deletingLastPathComponent + let scriptPath = "\(scriptDir)/MacOS/upload.sh" + + let proc = Process() + proc.executableURL = URL(fileURLWithPath: "/bin/bash") + proc.arguments = [scriptPath, "--background"] + try? proc.run() + scriptProcess = proc + } + + @objc func openLog() { + let logPath = (NSHomeDirectory() as NSString) + .appendingPathComponent("Documents/Photon/.photon-upload.log") + NSWorkspace.shared.open(URL(fileURLWithPath: logPath)) + } + + @objc func quitApp() { + scriptProcess?.terminate() + app.terminate(nil) + } +} + +let app = NSApplication.shared +let delegate = AppDelegate() +app.delegate = delegate +app.run() diff --git a/build/Resources/AppIcon.icns b/build/Resources/AppIcon.icns new file mode 100644 index 0000000..78068af Binary files /dev/null and b/build/Resources/AppIcon.icns differ diff --git a/build/Resources/MenuBarIcon.png b/build/Resources/MenuBarIcon.png new file mode 100644 index 0000000..bb707fb Binary files /dev/null and b/build/Resources/MenuBarIcon.png differ diff --git a/build/Resources/MenuBarIcon@2x.png b/build/Resources/MenuBarIcon@2x.png new file mode 100644 index 0000000..06cf282 Binary files /dev/null and b/build/Resources/MenuBarIcon@2x.png differ diff --git a/build/upload.sh b/build/upload.sh new file mode 100755 index 0000000..bcd938c --- /dev/null +++ b/build/upload.sh @@ -0,0 +1,639 @@ +#!/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" +} + +# Function to generate a random filename +# generate_random_filename() { +# local extension="$1" +# local timestamp=$(date +%s) +# local random_part=$(openssl rand -hex 4) +# echo "${timestamp}_${random_part}.${extension}" +# } + +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 + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] Using SSH key authentication: $SSH_KEY_PATH" >> "$LOGFILE" + 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 + log "INFO" "Thumbnail generation response: $response" + + # 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 diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..a724d0d Binary files /dev/null and b/logo.png differ diff --git a/photon-curved-bg.png b/photon-curved-bg.png new file mode 100644 index 0000000..f6bafbf Binary files /dev/null and b/photon-curved-bg.png differ diff --git a/photon-curved.png b/photon-curved.png new file mode 100644 index 0000000..5be314f Binary files /dev/null and b/photon-curved.png differ diff --git a/photon.conf b/photon.conf new file mode 100644 index 0000000..da1b665 --- /dev/null +++ b/photon.conf @@ -0,0 +1,33 @@ +# Photon Uploader Configuration + +# Directories +DIR_TO_MONITOR="/Users/jordan.walster/Documents/Photon/tmp" +DIR_TO_MOVE="/Users/jordan.walster/Documents/Photon/upload" +DIR_ARCHIVE="/Users/jordan.walster/Documents/Photon/archive" + +# Server settings +SERVER="10.1.1.3" +USER="photon" +PORT="2282" +REMOTE_DIR="/data/Mac Screenshots/" +PHOTON_DOMAIN="photon.jrdn.dev" +BASE_URL="https://m.jrdn.dev/Mac+Screenshots" + +# SSH Key Authentication +# Path to your SSH private key for server authentication +SSH_KEY_PATH="$HOME/.ssh/id_rsa" + +# File handling +SUPPORTED_EXTENSIONS="jpg,jpeg,png,gif,webp,bmp,tiff" +CLEANUP_AFTER_UPLOAD=true +ENABLE_NOTIFICATIONS=true + +# Performance settings +MAX_CONCURRENT_UPLOADS=3 +UPLOAD_TIMEOUT=30 + +# Advanced settings +# Retry failed uploads +RETRY_FAILED_UPLOADS=false +MAX_RETRIES=3 +RETRY_DELAY=5