diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c2973..e0e085e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix Gallery View: medium screen devices get 3 max columns and small screen devices 2 max columns. - Ensure gallery view toggle button displays after refresh page. +- Force resumable chunk size & fix chunk cleanup ### filePreview.js Enhancements diff --git a/js/upload.js b/js/upload.js index fad8c1f..947a807 100644 --- a/js/upload.js +++ b/js/upload.js @@ -409,6 +409,7 @@ function initResumableUpload() { query: { folder: window.currentFolder || "root", upload_token: window.csrfToken }, chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks simultaneousUploads: 3, + forceChunkSize: true, testChunks: false, throttleProgressCallbacks: 1, headers: { "X-CSRF-Token": window.csrfToken } diff --git a/removeChunks.php b/removeChunks.php index d6caca1..44f608a 100644 --- a/removeChunks.php +++ b/removeChunks.php @@ -2,63 +2,52 @@ require_once 'config.php'; header('Content-Type: application/json'); -// Validate CSRF token from POST $receivedToken = isset($_POST['csrf_token']) ? trim($_POST['csrf_token']) : ''; if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(["error" => "Invalid CSRF token"]); http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); exit; } -// Ensure a folder parameter is provided if (!isset($_POST['folder'])) { - echo json_encode(["error" => "No folder specified"]); http_response_code(400); + echo json_encode(["error" => "No folder specified"]); exit; } $folder = urldecode($_POST['folder']); -$regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; // full regex pattern +// The folder name should match the "resumable_" pattern exactly. +$regex = "/^resumable_" . PATTERN_FOLDER_NAME . "$/u"; if (!preg_match($regex, $folder)) { - echo json_encode(["error" => "Invalid folder name"]); http_response_code(400); + echo json_encode(["error" => "Invalid folder name"]); exit; } $tempDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; - -// If the folder doesn't exist, simply return success. if (!is_dir($tempDir)) { echo json_encode(["success" => true, "message" => "Temporary folder already removed."]); exit; } -// Recursively delete directory using RecursiveDirectoryIterator function rrmdir($dir) { - if (!is_dir($dir)) { - return; - } + if (!is_dir($dir)) return; $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($it as $file) { - if ($file->isDir()){ - rmdir($file->getRealPath()); - } else { - unlink($file->getRealPath()); - } + $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); } rmdir($dir); } rrmdir($tempDir); -// Verify removal if (!is_dir($tempDir)) { echo json_encode(["success" => true, "message" => "Temporary folder removed."]); } else { - echo json_encode(["error" => "Failed to remove temporary folder."]); http_response_code(500); + echo json_encode(["error" => "Failed to remove temporary folder."]); } ?> \ No newline at end of file diff --git a/upload.php b/upload.php index e7f0429..1c970bf 100644 --- a/upload.php +++ b/upload.php @@ -7,48 +7,43 @@ $headers = array_change_key_case(getallheaders(), CASE_LOWER); $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; if ($receivedToken !== $_SESSION['csrf_token']) { - echo json_encode(["error" => "Invalid CSRF token"]); http_response_code(403); + echo json_encode(["error" => "Invalid CSRF token"]); exit; } // Ensure user is authenticated. if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { - echo json_encode(["error" => "Unauthorized"]); http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); exit; } $username = $_SESSION['username'] ?? ''; if ($username) { $userPermissions = loadUserPermissions($username); - if (isset($userPermissions['disableUpload']) && $userPermissions['disableUpload'] === true) { - http_response_code(403); // Return a 403 Forbidden status. - echo json_encode(["error" => "Disabled upload users are not allowed to upload."]); + if (!empty($userPermissions['disableUpload'])) { + http_response_code(403); + echo json_encode(["error" => "Upload disabled for this user."]); exit; } } /* * Handle test chunk requests. - * When testChunks is enabled in Resumable.js, the client sends GET requests with a "resumableTest" parameter. */ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['resumableTest'])) { $chunkNumber = intval($_GET['resumableChunkNumber']); - $resumableIdentifier = $_GET['resumableIdentifier']; + $resumableIdentifier = $_GET['resumableIdentifier'] ?? ''; $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; - // Determine the base upload directory. $baseUploadDir = UPLOAD_DIR; if ($folder !== 'root') { $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; } $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; $chunkFile = $tempDir . $chunkNumber; - if (file_exists($chunkFile)) { - http_response_code(200); - } else { - http_response_code(404); - } + echo json_encode(["status" => file_exists($chunkFile) ? "found" : "not found"]); + http_response_code(file_exists($chunkFile) ? 200 : 404); exit; } @@ -56,73 +51,91 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['resumableTest'])) { // Chunked upload handling (POST requests) // --------------------- if (isset($_POST['resumableChunkNumber'])) { - // ------------- Chunked Upload Handling ------------- - $chunkNumber = intval($_POST['resumableChunkNumber']); // current chunk (1-indexed) + $chunkNumber = intval($_POST['resumableChunkNumber']); $totalChunks = intval($_POST['resumableTotalChunks']); $chunkSize = intval($_POST['resumableChunkSize']); $totalSize = intval($_POST['resumableTotalSize']); - $resumableIdentifier = $_POST['resumableIdentifier']; // unique file identifier - $resumableFilename = $_POST['resumableFilename']; + $resumableIdentifier = $_POST['resumableIdentifier'] ?? ''; + $resumableFilename = urldecode(basename($_POST['resumableFilename'])); - -// First, strip directory components. -$resumableFilename = urldecode(basename($_POST['resumableFilename'])); -if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { - http_response_code(400); - echo json_encode(["error" => "Invalid file name: " . $resumableFilename]); - exit; -} + if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { + http_response_code(400); + echo json_encode(["error" => "Invalid file name: $resumableFilename"]); + exit; + } $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + http_response_code(400); echo json_encode(["error" => "Invalid folder name"]); exit; } + // Determine the base upload directory. $baseUploadDir = UPLOAD_DIR; if ($folder !== 'root') { $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; - if (!is_dir($baseUploadDir)) { - mkdir($baseUploadDir, 0775, true); - } - } else { - if (!is_dir($baseUploadDir)) { - mkdir($baseUploadDir, 0775, true); - } + } + if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) { + http_response_code(500); + echo json_encode(["error" => "Failed to create upload directory"]); + exit; } // Use a temporary directory for the chunks. $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; - if (!is_dir($tempDir)) { - mkdir($tempDir, 0775, true); - } - - // Save the current chunk. - $chunkFile = $tempDir . $chunkNumber; // store chunk using its number as filename - if (!move_uploaded_file($_FILES["file"]["tmp_name"], $chunkFile)) { - echo json_encode(["error" => "Failed to move uploaded chunk"]); + if (!is_dir($tempDir) && !mkdir($tempDir, 0775, true)) { + http_response_code(500); + echo json_encode(["error" => "Failed to create temporary chunk directory"]); exit; } - // Check if all chunks have been uploaded. - $uploadedChunks = glob($tempDir . "*"); - if (count($uploadedChunks) < $totalChunks) { - // More chunks remain – respond and let the client continue. + // Ensure there is no PHP upload error. + if (!isset($_FILES["file"]) || $_FILES["file"]["error"] !== UPLOAD_ERR_OK) { + http_response_code(400); + echo json_encode(["error" => "Upload error on chunk $chunkNumber"]); + exit; + } + + // Save the current chunk. + $chunkFile = $tempDir . $chunkNumber; + if (!move_uploaded_file($_FILES["file"]["tmp_name"], $chunkFile)) { + http_response_code(500); + echo json_encode(["error" => "Failed to move uploaded chunk $chunkNumber"]); + exit; + } + + // Check if all chunks have been uploaded by verifying each expected chunk. + $allChunksPresent = true; + for ($i = 1; $i <= $totalChunks; $i++) { + if (!file_exists($tempDir . $i)) { + $allChunksPresent = false; + break; + } + } + if (!$allChunksPresent) { echo json_encode(["status" => "chunk uploaded"]); exit; } - // All chunks are present. Merge chunks. + // All chunks are present. Merge the chunks. $targetPath = $baseUploadDir . $resumableFilename; if (!$out = fopen($targetPath, "wb")) { + http_response_code(500); echo json_encode(["error" => "Failed to open target file for writing"]); exit; } - // Concatenate each chunk in order. for ($i = 1; $i <= $totalChunks; $i++) { $chunkPath = $tempDir . $i; + if (!file_exists($chunkPath)) { + fclose($out); + http_response_code(500); + echo json_encode(["error" => "Chunk $i missing during merge"]); + exit; + } if (!$in = fopen($chunkPath, "rb")) { fclose($out); + http_response_code(500); echo json_encode(["error" => "Failed to open chunk $i"]); exit; } @@ -134,27 +147,17 @@ if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { fclose($out); // --- Metadata Update for Chunked Upload --- - // For chunked uploads, assume no relativePath; so folderPath is simply $folder. $folderPath = $folder; $metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath; - // Generate a metadata file name based on the folder path. $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; $metadataFile = META_DIR . $metadataFileName; - $uploadedDate = date(DATE_TIME_FORMAT); $uploader = $_SESSION['username'] ?? "Unknown"; - // Load existing metadata, if any. - if (file_exists($metadataFile)) { - $metadataCollection = json_decode(file_get_contents($metadataFile), true); - if (!is_array($metadataCollection)) { - $metadataCollection = []; - } - } else { + $metadataCollection = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; + if (!is_array($metadataCollection)) { $metadataCollection = []; } - - // Add metadata for this file if not already present. if (!isset($metadataCollection[$resumableFilename])) { $metadataCollection[$resumableFilename] = [ "uploaded" => $uploadedDate, @@ -164,97 +167,83 @@ if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { } // --- End Metadata Update --- - // Cleanup: remove the temporary directory and its chunks. - array_map('unlink', glob("$tempDir*")); - rmdir($tempDir); + // Cleanup: use a robust recursive function. + function rrmdir($dir) { + if (!is_dir($dir)) return; + $items = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($items as $item) { + $item->isDir() ? rmdir($item->getRealPath()) : unlink($item->getRealPath()); + } + rmdir($dir); + } + rrmdir($tempDir); echo json_encode(["success" => "File uploaded successfully"]); exit; - } else { // ------------- Full Upload (Non-chunked) ------------- - // Validate folder name input. $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { + http_response_code(400); echo json_encode(["error" => "Invalid folder name"]); exit; } - // Determine the base upload directory. $baseUploadDir = UPLOAD_DIR; if ($folder !== 'root') { $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; - if (!is_dir($baseUploadDir)) { - mkdir($baseUploadDir, 0775, true); - } - } else { - if (!is_dir($baseUploadDir)) { - mkdir($baseUploadDir, 0775, true); - } + } + if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) { + http_response_code(500); + echo json_encode(["error" => "Failed to create upload directory"]); + exit; } - // Prepare a collection to hold metadata for each folder. - $metadataCollection = []; // key: folder path, value: metadata array - $metadataChanged = []; // key: folder path, value: boolean - - // Use a Unicode-enabled pattern to allow special characters. + $metadataCollection = []; + $metadataChanged = []; $safeFileNamePattern = REGEX_FILE_NAME; foreach ($_FILES["file"]["name"] as $index => $fileName) { - // First, ensure we only work with the base filename to avoid traversal issues. $safeFileName = trim(urldecode(basename($fileName))); if (!preg_match($safeFileNamePattern, $safeFileName)) { + http_response_code(400); echo json_encode(["error" => "Invalid file name: " . $fileName]); exit; } - - // --- Minimal Folder/Subfolder Logic --- $relativePath = ''; if (isset($_POST['relativePath'])) { - if (is_array($_POST['relativePath'])) { - $relativePath = $_POST['relativePath'][$index] ?? ''; - } else { - $relativePath = $_POST['relativePath']; - } + $relativePath = is_array($_POST['relativePath']) ? $_POST['relativePath'][$index] ?? '' : $_POST['relativePath']; } - - // Determine the complete folder path for upload and for metadata. - $folderPath = $folder; // Base folder as provided ("root" or a subfolder) - $uploadDir = $baseUploadDir; // Start with the base upload directory + $folderPath = $folder; + $uploadDir = $baseUploadDir; if (!empty($relativePath)) { $subDir = dirname($relativePath); if ($subDir !== '.' && $subDir !== '') { $folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir; - $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR - . str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR; + $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR; } - // Reapply basename to the relativePath to get the final safe file name. $safeFileName = basename($relativePath); } - // --- End Minimal Folder/Subfolder Logic --- - - // Make sure the final upload directory exists. - if (!is_dir($uploadDir)) { - mkdir($uploadDir, 0775, true); + if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true)) { + http_response_code(500); + echo json_encode(["error" => "Failed to create subfolder"]); + exit; } - $targetPath = $uploadDir . $safeFileName; - if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) { - // Generate a unique metadata file name based on the folder path. $metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath; $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; $metadataFile = META_DIR . $metadataFileName; - if (!isset($metadataCollection[$metadataKey])) { - if (file_exists($metadataFile)) { - $metadataCollection[$metadataKey] = json_decode(file_get_contents($metadataFile), true); - } else { + $metadataCollection[$metadataKey] = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : []; + if (!is_array($metadataCollection[$metadataKey])) { $metadataCollection[$metadataKey] = []; } $metadataChanged[$metadataKey] = false; } - if (!isset($metadataCollection[$metadataKey][$safeFileName])) { $uploadedDate = date(DATE_TIME_FORMAT); $uploader = $_SESSION['username'] ?? "Unknown"; @@ -265,12 +254,12 @@ if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { $metadataChanged[$metadataKey] = true; } } else { + http_response_code(500); echo json_encode(["error" => "Error uploading file"]); exit; } } - // After processing all files, write out metadata files for folders that changed. foreach ($metadataCollection as $folderKey => $data) { if ($metadataChanged[$folderKey]) { $metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json';