Skip to content

Instantly share code, notes, and snippets.

@flashlab
Created March 16, 2025 03:19
Simple curl file(s) uploading backend with php and nginx

Features

Upload file(s) with curl using the PUT or POST method

Support for specific path: https://curl.abc.com/subfolder/filename Auto-create folders if they don't exist Returns JSON with path, filename, size, and MD5

Delete file or folder with curl using the DELETE method

Delete specific file: https://curl.abc.com/subfolder/filename Clean entire folder: https://curl.abc.com/subfolder/

Download file with curl using the GET method

Get specific file: https://curl.abc.com/subfolder/filename

Security features

Path normalization to prevent directory traversal Proper error handling and status codes File size limits configurable in Nginx

Usage Examples

Upload file(s) with curl -T

# Upload to root with auto-generated filename
curl -T myfile.txt https://curl.abc.com/

# Upload to root with specific filename
curl -T myfile.txt https://curl.abc.com/desired-filename.txt

# Upload to a subfolder with auto-generated filename
curl -T myfile.txt https://curl.abc.com/subfolder/

# Upload multiple files
curl -T "{file1,file2}" https://curl.abc.com/

Upload file(s) with curl -F

# Upload with form data, keeping original filename
curl -F "file=@myfile.txt;filename=newname.txt" https://curl.abc.com/subfolder/

# Upload with form data, force using specific filename from URL
curl -F "file=@myfile.txt" https://curl.abc.com/subfolder/newname.txt

# Upload multiple files
curl -F file=@file1 -F file=@file2 https://curl.abc.com/subfolder/

Delete files

# Delete a specific file
curl -X DELETE https://curl.abc.com/subfolder/myfile.txt

# Clean an entire folder
curl -X DELETE https://curl.abc.com/subfolder/

Notice

  • Ensure PHP has write permittion on uploads folder.
  • If upload multiple files, do not specify filename in the url.
  • you can use --progress-bar and cat to show the uploading process, do not use under multiple files mode.
server {
listen 80;
server_name curl.abc.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name curl.abc.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# Base uploads directory
root /var/www/uploads;
client_max_body_size 100M; # Adjust based on your max file size needs
# Error logs
# error_log /var/log/nginx/error.log;
# PHP processing setup
location / {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/handler.php; # php handler location
fastcgi_param PATH_INFO $uri;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param QUERY_STRING $query_string;
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # maybe different
fastcgi_read_timeout 300;
}
}
<?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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment