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 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 15:27:05 -05:00
parent bc6a5cecf8
commit 591fad52cc
4 changed files with 217 additions and 1 deletions

5
.gitignore vendored
View File

@@ -2,3 +2,8 @@
debug.log debug.log
.claude .claude
settings.local.json settings.local.json
# Upload files (keep folder structure, ignore actual uploads)
uploads/*
!uploads/.gitkeep
!uploads/.htaccess

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env php
<?php
/**
* Cleanup Orphan Uploads
*
* Removes uploaded files that are no longer associated with any ticket.
* Run periodically via cron: 0 2 * * * php /path/to/cleanup_orphan_uploads.php
*/
require_once dirname(__DIR__) . '/config/config.php';
$conn = new mysqli(
$GLOBALS['config']['DB_HOST'],
$GLOBALS['config']['DB_USER'],
$GLOBALS['config']['DB_PASS'],
$GLOBALS['config']['DB_NAME']
);
if ($conn->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);
}

63
scripts/deploy.sh Executable file
View File

@@ -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!"

0
uploads/.gitkeep Normal file
View File