commit abdd8e5530691d92a560a31759a1cc3ef4bc628a Author: @nicholasmag Date: Thu Jul 18 17:31:32 2024 -0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47f8b5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# Ignore config file +config.php + +# Ignore backup directory +db_backup/ + +# Ignore upload directory +upload/ + +# Ignore node_modules directory +node_modules/ + +# Ignore log files +*.log + +# Ignore OS generated files +.DS_Store +Thumbs.db + +# Ignore compiled source files +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Ignore packaged files +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Ignore IDE and editor-specific files +.vscode/ +.idea/ +*.swp +*.swo + +# Ignore environment files +.env +.env.local +.env.*.local + +# Ignore macOS specific files +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Icon must end with two \r +Icon\r\r + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.backup + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..db7cc4d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +### Explanation + +1. **Features**: Describes the main functionalities of the project. +2. **Configuration**: Explains the configuration variables in both `process.sh` and `upload_handler.php`. +3. **Usage**: Provides step-by-step instructions on how to set up and use the scripts. +4. **Security**: Offers tips on keeping the project secure. +5. **Contributing**: Encourages contributions from the community. +6. **License**: Mentions the licensing information. + +This README should help users understand how to set up and use your project effectively. Adjust the paths and other configuration details according to your specific setup. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df8f569 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Database Archive Management + +This project automates the backup and management of database archives for different EverQuest expansions. It ensures that database versions are tracked, and only the latest archives are kept while older ones are deleted. If a new database version is detected, the previous version is protected and not deleted. + +## Features + +- Dumps specific tables from multiple databases. +- Compresses and uploads the database dumps to a web server. +- Keeps track of the database version and marks new versions as protected. +- Automatically cleans up old archives, retaining only the latest 30 days of backups. +- Protects weekly (Sunday) backups and any backups with a version change. + +## Configuration + +### Process Script (`process.sh`) + +This script dumps tables from multiple databases, compresses them, and uploads them to the web server. + +#### Configuration Variables + +- `DB_USER`: Database username. +- `DB_PASSWORD`: Database password. +- `DB_HOST`: Database host address. +- `DB_PORT`: Database port (e.g., `4406`). +- `DB_IDENTIFIERS`: Array of database identifiers (e.g., `Classic`, `Kunark`). +- `BACKUP_DIR`: Directory where backups are temporarily stored. +- `WEB_SERVER`: Web server URL. +- `UPLOAD_URL`: Full URL for the upload handler on the web server. +- `UPLOAD_KEY`: Secure key for authenticating the upload request. +- `SCHEMA_URL`: URL to download the latest `database_schema.h` file. +- `SCHEMA_FILE`: Local path to store the downloaded `database_schema.h` file. + +### Upload Handler (`upload_handler.php`) + +This PHP script handles the uploaded database archives, marks versions as protected if needed, and cleans up old archives. + +#### Configuration Variables + +- `uploadDir`: Directory where uploaded files are stored. +- `maxAgeDays`: Maximum number of days to keep unprotected archives. +- `uploadKey`: Secure key for authenticating the upload request (defined in `config.php`). + +### Database Schema + +The `archive_logs` table should have the following structure: + +```sql +CREATE TABLE `archive_logs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `archive` varchar(255) NOT NULL, + `size` bigint(20) NOT NULL, + `created` datetime NOT NULL, + `db_identifier` varchar(255) NOT NULL, + `db_version` varchar(255) NOT NULL, + `protected` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +); diff --git a/config.php.template b/config.php.template new file mode 100644 index 0000000..e3053ea --- /dev/null +++ b/config.php.template @@ -0,0 +1,113 @@ +sub($maxAgeInterval)->format('Y-m-d H:i:s'); + +if ($main_db->connect_error) { + die("Connection failed: " . $main_db->connect_error); +} + +$fullNames = [ + 'Classic' => 'Classic EverQuest', + 'Kunark' => 'Ruins of Kunark', + 'Velious' => 'Scars of Velious', + 'Luclin' => 'Shadows of Luclin', + 'Planes' => 'Planes of Power', + // Add more expansions as needed +]; + +// Function to check if table exists and create it if it doesn't +function createArchiveLogsTable($db) { + $query = "CREATE TABLE IF NOT EXISTS archive_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + downloads INT DEFAULT 0, + archive VARCHAR(255), + size BIGINT, + created DATETIME, + db_identifier VARCHAR(50), + db_version varchar(255), + protected tinyint(1) NOT NULL DEFAULT 0 + )"; + if ($db->query($query) === FALSE) { + echo "Error creating table: " . $db->error . "
"; + } +} + +// Check and create the table +createArchiveLogsTable($main_db); + +function formatSize($bytes) { + return number_format($bytes / 1048576, 2) . ' MB'; +} + +function timeElapsedString($datetime, $full = false) { + $now = new DateTime; + $ago = new DateTime($datetime); + $diff = $now->diff($ago); + + $diff->w = floor($diff->d / 7); + $diff->d -= $diff->w * 7; + + $string = [ + 'y' => 'year', + 'm' => 'month', + 'w' => 'week', + 'd' => 'day', + 'h' => 'hour', + 'i' => 'minute', + 's' => 'second', + ]; + foreach ($string as $k => &$v) { + if ($diff->$k) { + $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : ''); + } else { + unset($string[$k]); + } + } + + if (!$full) $string = array_slice($string, 0, 1); + return $string ? implode(', ', $string) . ' ago' : 'just now'; +} + +function cleanOldArchives($db, $uploadDir, $maxAgeDate) { + // Get all non-protected archives older than the maximum age + $stmt = $db->prepare("SELECT id, archive, created FROM archive_logs WHERE created < ? AND protected = 0"); + $stmt->bind_param("s", $maxAgeDate); + $stmt->execute(); + $result = $stmt->get_result(); + + while ($row = $result->fetch_assoc()) { + $archiveDate = new DateTime($row['created']); + $dayOfWeek = $archiveDate->format('N'); // 1 (for Monday) through 7 (for Sunday) + + // Delete the archive if it's Monday-Saturday or if it's older than 30 days + if ($dayOfWeek != 7 || $archiveDate < $maxAgeDate) { + $archivePath = $uploadDir . $row['archive']; + + // Delete the file + if (file_exists($archivePath)) { + unlink($archivePath); + } + + // Delete the database entry + $deleteStmt = $db->prepare("DELETE FROM archive_logs WHERE id = ?"); + $deleteStmt->bind_param("i", $row['id']); + $deleteStmt->execute(); + $deleteStmt->close(); + } + } + + $stmt->close(); +} + +?> diff --git a/download.php b/download.php new file mode 100644 index 0000000..6818470 --- /dev/null +++ b/download.php @@ -0,0 +1,44 @@ +prepare("SELECT * FROM archive_logs WHERE archive = ?"); + $stmt->bind_param("s", $file); + $stmt->execute(); + $result = $stmt->get_result(); + $fileDetails = $result->fetch_assoc(); + $stmt->close(); + + if ($fileDetails) { + $filePath = 'uploads/' . $file; + + if (file_exists($filePath)) { + // Increment download count + $stmt = $main_db->prepare("UPDATE archive_logs SET downloads = downloads + 1 WHERE archive = ?"); + $stmt->bind_param("s", $file); + $stmt->execute(); + $stmt->close(); + + // Serve the file for download + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="'.basename($filePath).'"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: ' . filesize($filePath)); + readfile($filePath); + exit; + } else { + echo "File not found."; + } + } else { + echo "Invalid file."; + } +} else { + echo "No file specified."; +} +?> diff --git a/index.php b/index.php new file mode 100644 index 0000000..7350024 --- /dev/null +++ b/index.php @@ -0,0 +1,132 @@ + + + + + + + Database Archives + + + + +
+ +
+
+
+ $fullName) { + echo "
"; + + echo "
"; + echo "
"; + echo "
"; + echo "
"; + echo "

"; + echo "Database Archives"; + echo "

"; + echo "

"; + echo "$fullName"; + echo "

"; + echo "
"; + echo "
"; + + // Fetch the latest archive details + $stmt = $main_db->prepare("SELECT downloads, archive, size, created FROM archive_logs WHERE db_identifier = ? ORDER BY created DESC LIMIT 1"); + $stmt->bind_param("s", $identifier); + $stmt->execute(); + $result = $stmt->get_result(); + $latest = $result->fetch_assoc(); + + if ($latest) { + echo "Download Count: " . $latest['downloads'] . ""; + echo "Size: " . formatSize($latest['size']) . ""; + echo "
"; + echo "Created: " . date("l F jS Y", strtotime($latest['created'])) . ""; + } + echo "
"; + echo ""; + echo ""; + echo "Download Latest $identifier Database"; + echo ""; + $stmt->close(); + echo "
"; + echo "
"; + echo "
"; + echo "
"; + echo "
"; + echo "

"; + echo ""; + echo "Archives are generated nightly from our Live servers, retained for 30 days, and then kept monthly beyond that.

"; + echo "
"; + + echo ""; + echo ""; + echo ""; + + $stmt = $main_db->prepare("SELECT downloads, archive, size, created, db_version, protected FROM archive_logs WHERE db_identifier = ? ORDER BY created DESC"); + $stmt->bind_param("s", $identifier); + $stmt->execute(); + $result = $stmt->get_result(); + + while ($row = $result->fetch_assoc()) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + + $stmt->close(); + + echo ""; + echo "
DownloadsArchiveSizeCreated
" . $row['downloads'] . "" . $row['archive'] . "" . formatSize($row['size']) . "" . date("l F jS Y", strtotime($row['created'])) . " (" . timeElapsedString($row['created']) . ")"; + echo " " . $row['db_version'] . ""; + if ($row['protected'] == 1) { + echo "Protected"; + } + echo "
"; + echo "
"; + } + ?> +

Would you like to help keep our databases accurate? Click Here!

+
+
+
+
+ +
+
+ + diff --git a/process.sh b/process.sh new file mode 100644 index 0000000..96371a8 --- /dev/null +++ b/process.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# Configuration +DB_USER="" +DB_PASSWORD="" +DB_HOST="" +DB_PORT=3306 +WEB_SERVER="https://" +UPLOAD_KEY="your_secure_upload_key" +DB_IDENTIFIERS=("Classic" "Kunark" "Velious" "Luclin" "Planes") + +BACKUP_DIR="./db_backup" +UPLOAD_URL="${WEB_SERVER}/upload_handler.php" +SCHEMA_URL="https://raw.githubusercontent.com/EQEmu/Server/master/common/database_schema.h" +SCHEMA_FILE="./database_schema.h" + +# Create backup directory if it doesn't exist +mkdir -p ${BACKUP_DIR} + +# Download the latest database_schema.h file +curl -o ${SCHEMA_FILE} ${SCHEMA_URL} + +# Extract tables from the schema file +extract_tables() { + local section=$1 + sed -n "/Get${section}Tables()/,/}/p" ${SCHEMA_FILE} | grep -Eo '"[a-zA-Z0-9_]+" ?,' | tr -d '",' | sort | uniq +} + +# Function to dump tables from a list +dump_tables() { + local db_identifier=$1 + local tables=$2 + local group_name=$3 + local lower_db_identifier=$(echo ${db_identifier} | tr '[:upper:]' '[:lower:]') + local dump_file="${BACKUP_DIR}/${group_name}-${lower_db_identifier}_$(date +\%F).sql" + + for table in ${tables}; do + mysqldump -u${DB_USER} -p${DB_PASSWORD} -h${DB_HOST} --port=${DB_PORT} "content-${lower_db_identifier}" ${table} >> ${dump_file} + done +} + +# Function to get the database version +get_db_version() { + local db_identifier=$1 + local lower_db_identifier=$(echo ${db_identifier} | tr '[:upper:]' '[:lower:]') + local version_query="SELECT version FROM db_version LIMIT 1;" + local version=$(mysql -u${DB_USER} -p${DB_PASSWORD} -h${DB_HOST} --port=${DB_PORT} -D "content-${lower_db_identifier}" -se "${version_query}") + echo ${version} +} + +# Extract tables from the schema file +CONTENT_TABLES=$(extract_tables "Content") +VERSION_TABLES=$(extract_tables "Version") + +# Dump each group of tables from each database +for db_identifier in "${DB_IDENTIFIERS[@]}"; do + timestamp=$(date +\%Y\%m\%d\%H\%M) + dump_tables ${db_identifier} "${CONTENT_TABLES}" "content" + dump_tables ${db_identifier} "${VERSION_TABLES}" "version" + + # Get the database version + db_version=$(get_db_version ${db_identifier}) + + # Compress the dumps + TAR_FILE="${BACKUP_DIR}/pa-${db_identifier,,}-${timestamp}.tar.gz" + tar -czf ${TAR_FILE} -C ${BACKUP_DIR} "content-${db_identifier,,}_$(date +\%F).sql" "version-${db_identifier,,}_$(date +\%F).sql" + + # Upload the compressed file to the web server + curl -F "file=@${TAR_FILE}" -F "db_identifier=${db_identifier}" -F "db_version=${db_version}" -F "key=${UPLOAD_KEY}" ${UPLOAD_URL} + + # Clean up the backup files after upload + rm ${BACKUP_DIR}/*.sql + rm ${TAR_FILE} +done + +# Clean up old backups (optional) +find ${BACKUP_DIR} -type f -name "*.sql" -mtime +7 -exec rm {} \; +find ${BACKUP_DIR} -type f -name "*.tar.gz" -mtime +7 -exec rm {} \; \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..3be3d54 --- /dev/null +++ b/styles.css @@ -0,0 +1,449 @@ +html, body { + height: 100%; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +.page-container { + flex: 1; + display: flex; + flex-direction: column; +} + +.content-wrap { + flex: 1; +} + +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + background-color: #f9fafb; + color: #333; +} + +.container { + width: 70%; + margin: auto; + overflow: hidden; + padding: 20px 0; +} + +.site-header { + background: #1a1c21; + color: #fff; + padding: 10px 0; + border-bottom: #3182ce 3px solid; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 70%; + margin: auto; + padding: 0 20px; +} + +.header-content h1 { + margin: 0; + font-size: 1.5rem; +} + +.dropdown-container { + display: flex; + align-items: center; +} + +.dropdown-container label { + margin-right: 10px; + color: #fff; +} + +.dropdown-container select { + padding: 5px 10px; + font-size: 1rem; + border-radius: 5px; + border: 1px solid #ddd; + outline: none; + cursor: pointer; +} + +.dropdown-container select:hover { + border-color: #3182ce; +} + +.table-container { + margin-top: 20px; + background-color: #fff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.tab-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background-color: #f7fafc; + border: 1px solid #ddd; + border-radius: 5px; + margin-bottom: 10px; +} + +.tab-title { + font-size: 1.5rem; + font-weight: bold; +} + +.tab-status span { + display: inline-block; + margin-left: 10px; + padding: 5px 10px; + background-color: #e2e8f0; + border-radius: 5px; + font-size: 0.875rem; + color: #4a5568; +} + +.status-downloads { + background-color: #ebf8ff; + color: #3182ce; +} + +.status-size { + background-color: #edf2f7; + color: #718096; +} + +.status-created { + background-color: #f0fff4; + color: #38a169; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0 0 0; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border-radius: 5px; + overflow: hidden; +} + +table, th, td { + border: 1px solid #ddd; + font-size: .8125rem; +} + +th, td { + padding: 12px; + text-align: left; +} + +th { + background-color: #f7fafc; + color: #333; + font-weight: bold; +} + +tr:nth-child(even) { + background-color: #f7fafc; +} + +tr:hover { + background-color: #edf2f7; +} + +.download-button { + padding: .125rem .5rem; + font-size: .8125rem; + line-height: 1.75; + border-radius: .25rem; + display: inline-block; + background-color: #daa520; + color: #fff; + text-decoration: none; +} + +.download-button:hover { + background-color: #707885; +} + +.tab-content { + display: none; +} + +.upload-form { + margin: 20px 0; +} + +.upload-form form { + display: flex; + flex-direction: column; + align-items: center; +} + +.upload-form label { + margin: 10px 0 5px; +} + +.upload-form input, .upload-form select, .upload-form button { + padding: 10px; + margin: 5px 0; + font-size: 1rem; + width: 100%; + max-width: 300px; +} + +.upload-form button { + background: #3182ce; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.upload-form button:hover { + background: #2c5282; +} + +.site-footer { + background: #1a1c21; + text-align: center; + padding: 20px 0; +} + +.footer-content { + color: #707885; + max-width: 70%; + margin: auto; + padding: 0 20px; +} + +.site-footer p { + margin: 5px 0; + font-size: 0.875rem; +} + +.container p { + margin-top: 0px; +} + +.info-bar { + background-color: #cce5ff; + border-color: #3182ce; + color: #004085; + position: relative; + padding: .75rem 1.25rem; + margin-bottom: 1.5rem; + border: 1px solid transparent; + border-radius: .375rem; +} + +.info-bar p { + margin: 0; + font-size: 1rem; +} + +.info-bar a { + color: #fff; + text-decoration: underline; + font-weight: bold; +} + +.info-bar a:hover { + color: #d0e8ff; +} + +.sub-header-body { + padding: 0 0 20px 0; +} + +.sub-header-pretitle { + font-size: 0.875rem; + font-weight: 600; + color: #6c757d; + margin-bottom: 0.5rem; +} + +.sub-header-title { + font-size: 1.5rem; + font-weight: 700; + color: #343a40; + margin-bottom: 0; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.align-items-end { + align-items: flex-end; +} + +.col, .col-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col { + flex: 1 0 0%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.badge { + display: inline-block; + padding: 0.35em 0.65em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.375rem; +} + +.badge-soft-primary { + color: #004085; + background-color: #cce5ff; +} + +.badge-soft-secondary { + color: #383d41; + background-color: #e2e3e5; +} + +.badge-soft-success { + color: #155724; + background-color: #d4edda; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 1rem; +} + +.btn { + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.btn-sm { + font-size: 0.875rem; + line-height: 1.5; + padding: 0.25rem 0.5rem; + border-radius: 0.2rem; +} + +.btn-primary { + color: #fff; + background-color: #daa520; + border-color: #daa520; +} + +.btn-primary:hover { + color: #fff; + background-color: #0069d9; + border-color: #0062cc; +} + +.header-title { + margin-bottom: 10px; +} + +.header-pretitle { + margin-top: 0px; +} + +.badge { + display: inline-block; + padding: 0.35em 0.65em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.375rem; + margin-right: 5px; /* Add space between badges */ +} + +.badge-soft-primary { + color: #004085; + background-color: #cce5ff; +} + +.badge-soft-secondary { + color: #383d41; + background-color: #e2e3e5; +} + +.badge-soft-success { + color: #155724; + background-color: #d4edda; +} + +a.btn-primary { + text-decoration: none; /* Remove underline from the download button */ +} + +.icon { + width: 1em; + height: 1em; + vertical-align: -0.125em; + padding: 5px 7px 0 0; +} + +.db-version .icon { + padding: 0 7px 0 7px; +} + +.db-protected .icon { + padding: 0 7px 0 7px; + color: #FF0000; /* Light grey color */ +} + +.text-right { + text-align: right; +} + +.db-version { + float: right; + color: #888; /* Light grey color */ + font-size: 0.875rem; /* Adjust font size if needed */ +} + +.db-protected { + float: right; + color: #888; /* Light grey color */ + font-size: 0.875rem; /* Adjust font size if needed */ +} + +.table-container p.contribute { + text-align: center; + margin: 20px 0 0 0; +} \ No newline at end of file diff --git a/targz_upload.php b/targz_upload.php new file mode 100644 index 0000000..947dcc0 --- /dev/null +++ b/targz_upload.php @@ -0,0 +1,34 @@ + + + + + + + Upload Database Dump + + + +
+
+

Upload Database Dump

+
+
+
+ + + + + +
+
+
+ + diff --git a/upload_handler.php b/upload_handler.php new file mode 100644 index 0000000..579c9ce --- /dev/null +++ b/upload_handler.php @@ -0,0 +1,54 @@ +prepare("SELECT db_version FROM archive_logs WHERE db_identifier = ? ORDER BY created DESC LIMIT 1"); + $stmt->bind_param("s", $dbIdentifierLower); + $stmt->execute(); + $result = $stmt->get_result(); + $latest = $result->fetch_assoc(); + $latestVersion = $latest ? $latest['db_version'] : null; + $stmt->close(); + $db_changed = false; + + // If the version has changed, protect the latest entry + if ($latestVersion && $dbVersion !== $latestVersion) { + $stmt = $main_db->prepare("UPDATE archive_logs SET protected = 1 WHERE db_identifier = ? AND db_version = ?"); + $stmt->bind_param("ss", $dbIdentifierLower, $latestVersion); + $stmt->execute(); + $stmt->close(); + $db_changed = true; + } + + // Insert the new archive log entry + $protected = $db_changed ? 1 : 0; + $stmt = $main_db->prepare("INSERT INTO archive_logs (archive, size, created, db_identifier, db_version, protected) VALUES (?, ?, NOW(), ?, ?, $protected)"); + $stmt->bind_param("sisss", $fileName, $size, $dbIdentifierLower, $dbVersion); + $stmt->execute(); + $stmt->close(); + + echo 'File uploaded successfully.'; + + // Clean up old archives + cleanOldArchives($main_db, $uploadDir, $maxAgeDate); + } else { + echo 'File upload failed.'; + } +} else { + echo 'Invalid request or key.'; +} + +?>