feat: initial commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Photon Uploader (Apple Silicon)
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
```
|
||||
@@ -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"
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Photon Uploader</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Photon Uploader</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>dev.jrdn.PhotonUploader</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSArchitecturePriority</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>upload</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Photon Uploader</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Photon Uploader</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>dev.jrdn.PhotonUploaderHelper</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>PhotonNotify</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSBackgroundOnly</key>
|
||||
<true/>
|
||||
<key>NSUserNotificationAlertStyle</key>
|
||||
<string>banner</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Executable
+639
@@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
+33
@@ -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
|
||||
Reference in New Issue
Block a user