From 591fad52cc9c28497a11fcf9a31a38c18398ce78 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 20 Jan 2026 15:27:05 -0500 Subject: [PATCH] Add deployment scripts and preserve uploads folder - Add scripts/deploy.sh for safe deployment with uploads preservation - Add scripts/cleanup_orphan_uploads.php to remove orphaned files - Add .gitkeep to uploads folder - Update .gitignore to exclude uploaded files but keep folder structure The deploy script now: - Backs up and restores .env file - Backs up and restores uploads folder contents - Runs database migrations automatically Co-Authored-By: Claude Opus 4.5 --- .gitignore | 7 +- scripts/cleanup_orphan_uploads.php | 148 +++++++++++++++++++++++++++++ scripts/deploy.sh | 63 ++++++++++++ uploads/.gitkeep | 0 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100755 scripts/cleanup_orphan_uploads.php create mode 100755 scripts/deploy.sh create mode 100644 uploads/.gitkeep diff --git a/.gitignore b/.gitignore index 5cb251f..3ca00a5 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ .env debug.log .claude -settings.local.json \ No newline at end of file +settings.local.json + +# Upload files (keep folder structure, ignore actual uploads) +uploads/* +!uploads/.gitkeep +!uploads/.htaccess \ No newline at end of file diff --git a/scripts/cleanup_orphan_uploads.php b/scripts/cleanup_orphan_uploads.php new file mode 100755 index 0000000..3f93144 --- /dev/null +++ b/scripts/cleanup_orphan_uploads.php @@ -0,0 +1,148 @@ +#!/usr/bin/env php +connect_error) { + die("Database connection failed: " . $conn->connect_error . "\n"); +} + +$uploadsDir = dirname(__DIR__) . '/uploads'; +$dryRun = in_array('--dry-run', $argv); + +if ($dryRun) { + echo "DRY RUN MODE - No files will be deleted\n"; +} + +echo "Scanning uploads directory: $uploadsDir\n"; + +// Get all valid ticket IDs from database +$ticketIds = []; +$result = $conn->query("SELECT ticket_id FROM tickets"); +while ($row = $result->fetch_assoc()) { + $ticketIds[] = $row['ticket_id']; +} +echo "Found " . count($ticketIds) . " tickets in database\n"; + +// Get all attachment records +$attachments = []; +$result = $conn->query("SELECT ticket_id, filename FROM ticket_attachments"); +if ($result) { + while ($row = $result->fetch_assoc()) { + $key = $row['ticket_id'] . '/' . $row['filename']; + $attachments[$key] = true; + } +} +echo "Found " . count($attachments) . " attachment records in database\n"; + +// Scan uploads directory +$orphanedFolders = []; +$orphanedFiles = []; +$totalSize = 0; + +$ticketDirs = glob($uploadsDir . '/*', GLOB_ONLYDIR); +foreach ($ticketDirs as $ticketDir) { + $ticketId = basename($ticketDir); + + // Skip non-ticket directories + if (!preg_match('/^\d{9}$/', $ticketId)) { + continue; + } + + // Check if ticket exists + if (!in_array($ticketId, $ticketIds)) { + // Ticket doesn't exist - entire folder is orphaned + $orphanedFolders[] = $ticketDir; + $folderSize = 0; + foreach (glob($ticketDir . '/*') as $file) { + if (is_file($file)) { + $folderSize += filesize($file); + } + } + $totalSize += $folderSize; + echo "Orphan folder (ticket deleted): $ticketDir (" . formatBytes($folderSize) . ")\n"; + continue; + } + + // Check individual files + $files = glob($ticketDir . '/*'); + foreach ($files as $file) { + if (is_file($file)) { + $filename = basename($file); + $key = $ticketId . '/' . $filename; + + if (!isset($attachments[$key])) { + $orphanedFiles[] = $file; + $fileSize = filesize($file); + $totalSize += $fileSize; + echo "Orphan file (no DB record): $file (" . formatBytes($fileSize) . ")\n"; + } + } + } +} + +echo "\n=== Summary ===\n"; +echo "Orphaned folders: " . count($orphanedFolders) . "\n"; +echo "Orphaned files: " . count($orphanedFiles) . "\n"; +echo "Total size to recover: " . formatBytes($totalSize) . "\n"; + +if (!$dryRun && ($orphanedFolders || $orphanedFiles)) { + echo "\nDeleting orphaned items...\n"; + + foreach ($orphanedFiles as $file) { + if (unlink($file)) { + echo "Deleted: $file\n"; + } else { + echo "Failed to delete: $file\n"; + } + } + + foreach ($orphanedFolders as $folder) { + deleteDirectory($folder); + echo "Deleted folder: $folder\n"; + } + + echo "Cleanup complete!\n"; +} elseif ($dryRun) { + echo "\nRun without --dry-run to delete these items.\n"; +} else { + echo "\nNo orphaned items found.\n"; +} + +$conn->close(); + +function formatBytes($bytes) { + if ($bytes >= 1073741824) { + return number_format($bytes / 1073741824, 2) . ' GB'; + } elseif ($bytes >= 1048576) { + return number_format($bytes / 1048576, 2) . ' MB'; + } elseif ($bytes >= 1024) { + return number_format($bytes / 1024, 2) . ' KB'; + } else { + return $bytes . ' bytes'; + } +} + +function deleteDirectory($dir) { + if (!is_dir($dir)) return; + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = "$dir/$file"; + is_dir($path) ? deleteDirectory($path) : unlink($path); + } + rmdir($dir); +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..6ed4c73 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# TinkerTickets Deployment Script +# This script safely deploys updates while preserving user data +set -e + +WEBROOT="/var/www/html/tinkertickets" +UPLOADS_BACKUP="/tmp/tinker_uploads_backup" + +echo "[TinkerTickets] Starting deployment..." + +# Backup .env if it exists +if [ -f "$WEBROOT/.env" ]; then + echo "[TinkerTickets] Backing up .env..." + cp "$WEBROOT/.env" /tmp/.env.backup +fi + +# Backup uploads folder if it exists and has files +if [ -d "$WEBROOT/uploads" ] && [ "$(ls -A $WEBROOT/uploads 2>/dev/null)" ]; then + echo "[TinkerTickets] Backing up uploads folder..." + rm -rf "$UPLOADS_BACKUP" + cp -r "$WEBROOT/uploads" "$UPLOADS_BACKUP" +fi + +if [ ! -d "$WEBROOT/.git" ]; then + echo "[TinkerTickets] Directory not a git repo — performing initial clone..." + rm -rf "$WEBROOT" + git clone https://code.lotusguild.org/LotusGuild/tinker_tickets.git "$WEBROOT" +else + echo "[TinkerTickets] Updating existing repo..." + cd "$WEBROOT" + git fetch --all + git reset --hard origin/main +fi + +# Restore .env if it was backed up +if [ -f /tmp/.env.backup ]; then + echo "[TinkerTickets] Restoring .env..." + mv /tmp/.env.backup "$WEBROOT/.env" +fi + +# Restore uploads folder if it was backed up +if [ -d "$UPLOADS_BACKUP" ]; then + echo "[TinkerTickets] Restoring uploads folder..." + # Don't overwrite .htaccess from repo + rsync -av --exclude='.htaccess' --exclude='.gitkeep' "$UPLOADS_BACKUP/" "$WEBROOT/uploads/" + rm -rf "$UPLOADS_BACKUP" +fi + +# Ensure uploads directory exists with proper permissions +mkdir -p "$WEBROOT/uploads" +chmod 755 "$WEBROOT/uploads" + +echo "[TinkerTickets] Setting permissions..." +chown -R www-data:www-data "$WEBROOT" + +# Run migrations if .env exists +if [ -f "$WEBROOT/.env" ]; then + echo "[TinkerTickets] Running database migrations..." + cd "$WEBROOT/migrations" + php run_migrations.php || echo "[TinkerTickets] Warning: Migration errors occurred" +fi + +echo "[TinkerTickets] Deployment complete!" diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 0000000..e69de29