|
<?php |
|
declare(strict_types=1); |
|
|
|
// Base directory for all uploads |
|
define('UPLOAD_DIR', '/var/www/uploads'); |
|
|
|
/** |
|
* Main request handler |
|
*/ |
|
function handleRequest(): void { |
|
$method = $_SERVER['REQUEST_METHOD']; |
|
$path = $_SERVER['PATH_INFO'] ?? '/'; |
|
|
|
// Normalize path to prevent directory traversal |
|
$path = normalizePath($path); |
|
|
|
try { |
|
switch ($method) { |
|
case 'GET': |
|
handleFileDownload($path); |
|
break; |
|
case 'POST': |
|
// For form uploads (-F parameter in curl) |
|
handleFormUpload($path); |
|
break; |
|
case 'PUT': |
|
// For direct file uploads (-T parameter in curl) |
|
handleFileUpload($path); |
|
break; |
|
case 'DELETE': |
|
handleFileDeletion($path); |
|
break; |
|
default: |
|
throw new Exception('Method not allowed', 405); |
|
} |
|
} catch (Exception $e) { |
|
header('Content-Type: application/json'); |
|
http_response_code($e->getCode() ?: 500); |
|
echo json_encode([ |
|
'error' => $e->getMessage(), |
|
'code' => $e->getCode() |
|
]); |
|
} |
|
} |
|
|
|
/** |
|
* Handle file download (GET method) |
|
*/ |
|
function handleFileDownload(string $path): void { |
|
// Remove leading slash and trim |
|
$path = trim($path, '/'); |
|
|
|
if (empty($path)) { |
|
throw new Exception('No file specified', 400); |
|
} |
|
|
|
$targetPath = UPLOAD_DIR . '/' . $path; |
|
|
|
// Check if file exists |
|
if (!file_exists($targetPath)) { |
|
throw new Exception('File not found', 404); |
|
} |
|
|
|
// Check if path is a directory |
|
if (is_dir($targetPath)) { |
|
throw new Exception('Cannot download directory', 400); |
|
} |
|
|
|
// Get file info |
|
$filename = basename($targetPath); |
|
$filesize = filesize($targetPath); |
|
$mimeType = mime_content_type($targetPath) ?: 'application/octet-stream'; |
|
|
|
// Set headers for download |
|
header('Content-Type: ' . $mimeType); |
|
header('Content-Disposition: inline; filename="' . $filename . '"'); |
|
header('Content-Length: ' . $filesize); |
|
header('Cache-Control: public, max-age=86400'); |
|
|
|
// Output file contents |
|
readfile($targetPath); |
|
exit; |
|
} |
|
|
|
/** |
|
* Handle form-based file upload (POST method) |
|
* This supports curl -F parameter |
|
*/ |
|
function handleFormUpload(string $path): void { |
|
if (empty($_FILES)) { |
|
throw new Exception('No file uploaded', 400); |
|
} |
|
|
|
$results = []; |
|
|
|
foreach ($_FILES as $fieldName => $fileInfo) { |
|
// Handle multiple files (array structure) |
|
if (is_array($fileInfo['name'])) { |
|
for ($i = 0; $i < count($fileInfo['name']); $i++) { |
|
if ($fileInfo['error'][$i] !== UPLOAD_ERR_OK) { |
|
continue; // Skip failed uploads |
|
} |
|
|
|
$uploadedFilename = $fileInfo['name'][$i]; |
|
$tempPath = $fileInfo['tmp_name'][$i]; |
|
$fileResult = processUploadedFile($path, $tempPath, $uploadedFilename); |
|
if ($fileResult) { |
|
$results[] = $fileResult; |
|
} |
|
} |
|
} else { |
|
// Handle single file |
|
if ($fileInfo['error'] === UPLOAD_ERR_OK) { |
|
$uploadedFilename = $fileInfo['name']; |
|
$tempPath = $fileInfo['tmp_name']; |
|
$fileResult = processUploadedFile($path, $tempPath, $uploadedFilename); |
|
if ($fileResult) { |
|
$results[] = $fileResult; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (empty($results)) { |
|
throw new Exception('Failed to process uploaded files', 400); |
|
} |
|
|
|
// Return JSON response |
|
header('Content-Type: application/json'); |
|
http_response_code(201); // Created |
|
echo json_encode($results); |
|
} |
|
|
|
/** |
|
* Process an uploaded file (used by form upload handler) |
|
* |
|
* @param string $path URL path |
|
* @param string $tempPath Temporary file path |
|
* @param string $uploadedFilename Original filename |
|
* @return array|null File information or null on failure |
|
*/ |
|
function processUploadedFile(string $path, string $tempPath, string $uploadedFilename): ?array { |
|
// Determine the target path based on URL path |
|
list($targetDir, $targetFilename, $relativePath) = getTargetPath($path, $uploadedFilename); |
|
|
|
// Create directory if it doesn't exist |
|
if (!is_dir($targetDir)) { |
|
if (!mkdir($targetDir, 0755, true)) { |
|
return null; // Failed to create directory |
|
} |
|
} |
|
|
|
$targetPath = $targetDir . '/' . $targetFilename; |
|
|
|
// Move uploaded file |
|
if (!move_uploaded_file($tempPath, $targetPath)) { |
|
return null; // Failed to move file |
|
} |
|
|
|
// Get file stats |
|
$filesize = filesize($targetPath); |
|
$md5 = md5_file($targetPath); |
|
|
|
// Return file information with path |
|
return [ |
|
'path' => $relativePath . '/' . $targetFilename, |
|
'filename' => $targetFilename, |
|
'size' => $filesize, |
|
'md5' => $md5 |
|
]; |
|
} |
|
|
|
/** |
|
* Handle direct file upload (PUT method) |
|
*/ |
|
function handleFileUpload(string $path): void { |
|
// Read the PUT data |
|
$putData = file_get_contents('php://input'); |
|
if ($putData === false) { |
|
throw new Exception('Failed to read input data', 400); |
|
} |
|
|
|
// Determine the target path based solely on the URL path |
|
list($targetDir, $targetFilename, $relativePath) = getTargetPath($path); |
|
|
|
// Create directory if it doesn't exist |
|
if (!is_dir($targetDir)) { |
|
if (!mkdir($targetDir, 0755, true)) { |
|
throw new Exception('Failed to create directory', 500); |
|
} |
|
} |
|
|
|
$targetPath = $targetDir . '/' . $targetFilename; |
|
|
|
// Write file |
|
if (file_put_contents($targetPath, $putData) === false) { |
|
throw new Exception('Failed to write file', 500); |
|
} |
|
|
|
// Get file stats |
|
$filesize = filesize($targetPath); |
|
$md5 = md5_file($targetPath); |
|
|
|
// Return JSON response |
|
header('Content-Type: application/json'); |
|
http_response_code(201); // Created |
|
echo json_encode([ |
|
'path' => $relativePath . '/' . $targetFilename, |
|
'filename' => $targetFilename, |
|
'size' => $filesize, |
|
'md5' => $md5 |
|
]); |
|
} |
|
|
|
/** |
|
* Handle file deletion (DELETE method) |
|
*/ |
|
function handleFileDeletion(string $path): void { |
|
// Determine if this is a directory deletion (ends with slash) |
|
$isDirectory = substr($path, -1) === '/'; |
|
|
|
// Normalize path for processing |
|
$path = trim($path, '/'); |
|
$targetPath = UPLOAD_DIR . ($path ? '/' . $path : ''); |
|
|
|
if ($isDirectory) { |
|
// Directory deletion - delete all files recursively |
|
if (!is_dir($targetPath)) { |
|
throw new Exception('Directory does not exist', 404); |
|
} |
|
|
|
// Get relative path for output |
|
$relativePath = $path; |
|
|
|
// Delete directory and all its contents recursively |
|
deleteDirectoryRecursively($targetPath); |
|
|
|
// Return JSON response |
|
header('Content-Type: application/json'); |
|
http_response_code(200); // OK |
|
echo json_encode([ |
|
'path' => $relativePath, |
|
'status' => 'deleted' |
|
]); |
|
} else { |
|
// File deletion |
|
if (!file_exists($targetPath)) { |
|
throw new Exception('File not found', 404); |
|
} |
|
|
|
if (is_dir($targetPath)) { |
|
throw new Exception('Cannot delete directory without trailing slash', 400); |
|
} |
|
|
|
$filesize = filesize($targetPath); |
|
$md5 = md5_file($targetPath); |
|
$filename = basename($targetPath); |
|
$relativePath = $path; |
|
|
|
if (!unlink($targetPath)) { |
|
throw new Exception('Failed to delete file', 500); |
|
} |
|
|
|
// Return JSON response |
|
header('Content-Type: application/json'); |
|
http_response_code(200); // OK |
|
echo json_encode([ |
|
'path' => $relativePath, |
|
'filename' => $filename, |
|
'size' => $filesize, |
|
'md5' => $md5 |
|
]); |
|
} |
|
} |
|
|
|
/** |
|
* Recursively delete a directory and all its contents |
|
*/ |
|
function deleteDirectoryRecursively(string $dir): void { |
|
if (!is_dir($dir)) { |
|
return; |
|
} |
|
|
|
$files = new RecursiveIteratorIterator( |
|
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), |
|
RecursiveIteratorIterator::CHILD_FIRST |
|
); |
|
|
|
foreach ($files as $fileinfo) { |
|
$action = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); |
|
if (!$action($fileinfo->getRealPath())) { |
|
// Silently continue if deletion fails |
|
continue; |
|
} |
|
} |
|
|
|
// Finally remove the directory itself |
|
@rmdir($dir); |
|
} |
|
|
|
/** |
|
* Parse path to get target directory and filename |
|
*/ |
|
function getTargetPath(string $path, ?string $uploadedFilename = null): array { |
|
// Check if path ends with a slash - indicating it's a directory |
|
$isDirectory = substr($path, -1) === '/'; |
|
|
|
// Remove trailing slashes for processing |
|
$path = trim($path, '/'); |
|
|
|
if (empty($path)) { |
|
// Root path with no specific filename - generate a random one or use uploaded filename |
|
$targetDir = UPLOAD_DIR; |
|
$filename = $uploadedFilename ?? generateRandomFilename(); |
|
$relativePath = ''; |
|
} else if ($isDirectory) { |
|
// Path ends with slash, so it's definitely a directory |
|
$targetDir = UPLOAD_DIR . '/' . $path; |
|
$filename = $uploadedFilename ?? generateRandomFilename(); |
|
$relativePath = $path; |
|
} else { |
|
// No trailing slash, the last segment is a filename |
|
$pathParts = explode('/', $path); |
|
$filename = array_pop($pathParts); |
|
$dirPath = implode('/', $pathParts); |
|
$targetDir = UPLOAD_DIR . ($dirPath ? '/' . $dirPath : ''); |
|
$relativePath = $dirPath; |
|
} |
|
|
|
return [$targetDir, $filename, $relativePath]; |
|
} |
|
|
|
/** |
|
* Generate a random filename with timestamp |
|
*/ |
|
function generateRandomFilename(): string { |
|
return 'upload_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)); |
|
} |
|
|
|
/** |
|
* Normalize path to prevent directory traversal |
|
*/ |
|
function normalizePath(string $path): string { |
|
// Remember if path had a trailing slash |
|
$hadTrailingSlash = substr($path, -1) === '/'; |
|
|
|
// Replace multiple slashes with a single slash |
|
$path = preg_replace('#/+#', '/', $path); |
|
|
|
// Remove any "." segments |
|
$path = str_replace('/./', '/', $path); |
|
|
|
// Remove any ".." segments and the directory above them |
|
while (strpos($path, '..') !== false) { |
|
$path = preg_replace('#/[^/]+/\.\./#', '/', $path); |
|
} |
|
|
|
// Restore trailing slash if it was there |
|
if ($hadTrailingSlash && substr($path, -1) !== '/') { |
|
$path .= '/'; |
|
} |
|
|
|
return $path; |
|
} |
|
|
|
// Run the main handler |
|
handleRequest(); |