Force resumable chunk size & fix chunk cleanup

This commit is contained in:
Ryan
2025-04-14 16:58:12 -04:00
committed by GitHub
parent 844976ef89
commit 1d36d002c6
4 changed files with 104 additions and 124 deletions

View File

@@ -4,6 +4,7 @@
- Fix Gallery View: medium screen devices get 3 max columns and small screen devices 2 max columns. - 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. - Ensure gallery view toggle button displays after refresh page.
- Force resumable chunk size & fix chunk cleanup
### filePreview.js Enhancements ### filePreview.js Enhancements

View File

@@ -409,6 +409,7 @@ function initResumableUpload() {
query: { folder: window.currentFolder || "root", upload_token: window.csrfToken }, query: { folder: window.currentFolder || "root", upload_token: window.csrfToken },
chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks chunkSize: 1.5 * 1024 * 1024, // 1.5 MB chunks
simultaneousUploads: 3, simultaneousUploads: 3,
forceChunkSize: true,
testChunks: false, testChunks: false,
throttleProgressCallbacks: 1, throttleProgressCallbacks: 1,
headers: { "X-CSRF-Token": window.csrfToken } headers: { "X-CSRF-Token": window.csrfToken }

View File

@@ -2,63 +2,52 @@
require_once 'config.php'; require_once 'config.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
// Validate CSRF token from POST
$receivedToken = isset($_POST['csrf_token']) ? trim($_POST['csrf_token']) : ''; $receivedToken = isset($_POST['csrf_token']) ? trim($_POST['csrf_token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) { if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403); http_response_code(403);
echo json_encode(["error" => "Invalid CSRF token"]);
exit; exit;
} }
// Ensure a folder parameter is provided
if (!isset($_POST['folder'])) { if (!isset($_POST['folder'])) {
echo json_encode(["error" => "No folder specified"]);
http_response_code(400); http_response_code(400);
echo json_encode(["error" => "No folder specified"]);
exit; exit;
} }
$folder = urldecode($_POST['folder']); $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)) { if (!preg_match($regex, $folder)) {
echo json_encode(["error" => "Invalid folder name"]);
http_response_code(400); http_response_code(400);
echo json_encode(["error" => "Invalid folder name"]);
exit; exit;
} }
$tempDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder; $tempDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
// If the folder doesn't exist, simply return success.
if (!is_dir($tempDir)) { if (!is_dir($tempDir)) {
echo json_encode(["success" => true, "message" => "Temporary folder already removed."]); echo json_encode(["success" => true, "message" => "Temporary folder already removed."]);
exit; exit;
} }
// Recursively delete directory using RecursiveDirectoryIterator
function rrmdir($dir) { function rrmdir($dir) {
if (!is_dir($dir)) { if (!is_dir($dir)) return;
return;
}
$it = new RecursiveIteratorIterator( $it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST RecursiveIteratorIterator::CHILD_FIRST
); );
foreach ($it as $file) { foreach ($it as $file) {
if ($file->isDir()){ $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
rmdir($file->getRealPath());
} else {
unlink($file->getRealPath());
}
} }
rmdir($dir); rmdir($dir);
} }
rrmdir($tempDir); rrmdir($tempDir);
// Verify removal
if (!is_dir($tempDir)) { if (!is_dir($tempDir)) {
echo json_encode(["success" => true, "message" => "Temporary folder removed."]); echo json_encode(["success" => true, "message" => "Temporary folder removed."]);
} else { } else {
echo json_encode(["error" => "Failed to remove temporary folder."]);
http_response_code(500); http_response_code(500);
echo json_encode(["error" => "Failed to remove temporary folder."]);
} }
?> ?>

View File

@@ -7,48 +7,43 @@ $headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) { if ($receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403); http_response_code(403);
echo json_encode(["error" => "Invalid CSRF token"]);
exit; exit;
} }
// Ensure user is authenticated. // Ensure user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
echo json_encode(["error" => "Unauthorized"]);
http_response_code(401); http_response_code(401);
echo json_encode(["error" => "Unauthorized"]);
exit; exit;
} }
$username = $_SESSION['username'] ?? ''; $username = $_SESSION['username'] ?? '';
if ($username) { if ($username) {
$userPermissions = loadUserPermissions($username); $userPermissions = loadUserPermissions($username);
if (isset($userPermissions['disableUpload']) && $userPermissions['disableUpload'] === true) { if (!empty($userPermissions['disableUpload'])) {
http_response_code(403); // Return a 403 Forbidden status. http_response_code(403);
echo json_encode(["error" => "Disabled upload users are not allowed to upload."]); echo json_encode(["error" => "Upload disabled for this user."]);
exit; exit;
} }
} }
/* /*
* Handle test chunk requests. * 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'])) { if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['resumableTest'])) {
$chunkNumber = intval($_GET['resumableChunkNumber']); $chunkNumber = intval($_GET['resumableChunkNumber']);
$resumableIdentifier = $_GET['resumableIdentifier']; $resumableIdentifier = $_GET['resumableIdentifier'] ?? '';
$folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root';
// Determine the base upload directory.
$baseUploadDir = UPLOAD_DIR; $baseUploadDir = UPLOAD_DIR;
if ($folder !== 'root') { if ($folder !== 'root') {
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
} }
$tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR;
$chunkFile = $tempDir . $chunkNumber; $chunkFile = $tempDir . $chunkNumber;
if (file_exists($chunkFile)) { echo json_encode(["status" => file_exists($chunkFile) ? "found" : "not found"]);
http_response_code(200); http_response_code(file_exists($chunkFile) ? 200 : 404);
} else {
http_response_code(404);
}
exit; exit;
} }
@@ -56,73 +51,91 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['resumableTest'])) {
// Chunked upload handling (POST requests) // Chunked upload handling (POST requests)
// --------------------- // ---------------------
if (isset($_POST['resumableChunkNumber'])) { if (isset($_POST['resumableChunkNumber'])) {
// ------------- Chunked Upload Handling ------------- $chunkNumber = intval($_POST['resumableChunkNumber']);
$chunkNumber = intval($_POST['resumableChunkNumber']); // current chunk (1-indexed)
$totalChunks = intval($_POST['resumableTotalChunks']); $totalChunks = intval($_POST['resumableTotalChunks']);
$chunkSize = intval($_POST['resumableChunkSize']); $chunkSize = intval($_POST['resumableChunkSize']);
$totalSize = intval($_POST['resumableTotalSize']); $totalSize = intval($_POST['resumableTotalSize']);
$resumableIdentifier = $_POST['resumableIdentifier']; // unique file identifier $resumableIdentifier = $_POST['resumableIdentifier'] ?? '';
$resumableFilename = $_POST['resumableFilename']; $resumableFilename = urldecode(basename($_POST['resumableFilename']));
if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
// First, strip directory components. http_response_code(400);
$resumableFilename = urldecode(basename($_POST['resumableFilename'])); echo json_encode(["error" => "Invalid file name: $resumableFilename"]);
if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) { exit;
http_response_code(400); }
echo json_encode(["error" => "Invalid file name: " . $resumableFilename]);
exit;
}
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name"]); echo json_encode(["error" => "Invalid folder name"]);
exit; exit;
} }
// Determine the base upload directory. // Determine the base upload directory.
$baseUploadDir = UPLOAD_DIR; $baseUploadDir = UPLOAD_DIR;
if ($folder !== 'root') { if ($folder !== 'root') {
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($baseUploadDir)) { }
mkdir($baseUploadDir, 0775, true); if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) {
} http_response_code(500);
} else { echo json_encode(["error" => "Failed to create upload directory"]);
if (!is_dir($baseUploadDir)) { exit;
mkdir($baseUploadDir, 0775, true);
}
} }
// Use a temporary directory for the chunks. // Use a temporary directory for the chunks.
$tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR; $tempDir = $baseUploadDir . 'resumable_' . $resumableIdentifier . DIRECTORY_SEPARATOR;
if (!is_dir($tempDir)) { if (!is_dir($tempDir) && !mkdir($tempDir, 0775, true)) {
mkdir($tempDir, 0775, true); http_response_code(500);
} echo json_encode(["error" => "Failed to create temporary chunk directory"]);
// 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"]);
exit; exit;
} }
// Check if all chunks have been uploaded. // Ensure there is no PHP upload error.
$uploadedChunks = glob($tempDir . "*"); if (!isset($_FILES["file"]) || $_FILES["file"]["error"] !== UPLOAD_ERR_OK) {
if (count($uploadedChunks) < $totalChunks) { http_response_code(400);
// More chunks remain respond and let the client continue. 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"]); echo json_encode(["status" => "chunk uploaded"]);
exit; exit;
} }
// All chunks are present. Merge chunks. // All chunks are present. Merge the chunks.
$targetPath = $baseUploadDir . $resumableFilename; $targetPath = $baseUploadDir . $resumableFilename;
if (!$out = fopen($targetPath, "wb")) { if (!$out = fopen($targetPath, "wb")) {
http_response_code(500);
echo json_encode(["error" => "Failed to open target file for writing"]); echo json_encode(["error" => "Failed to open target file for writing"]);
exit; exit;
} }
// Concatenate each chunk in order.
for ($i = 1; $i <= $totalChunks; $i++) { for ($i = 1; $i <= $totalChunks; $i++) {
$chunkPath = $tempDir . $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")) { if (!$in = fopen($chunkPath, "rb")) {
fclose($out); fclose($out);
http_response_code(500);
echo json_encode(["error" => "Failed to open chunk $i"]); echo json_encode(["error" => "Failed to open chunk $i"]);
exit; exit;
} }
@@ -134,27 +147,17 @@ if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
fclose($out); fclose($out);
// --- Metadata Update for Chunked Upload --- // --- Metadata Update for Chunked Upload ---
// For chunked uploads, assume no relativePath; so folderPath is simply $folder.
$folderPath = $folder; $folderPath = $folder;
$metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath; $metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath;
// Generate a metadata file name based on the folder path.
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName; $metadataFile = META_DIR . $metadataFileName;
$uploadedDate = date(DATE_TIME_FORMAT); $uploadedDate = date(DATE_TIME_FORMAT);
$uploader = $_SESSION['username'] ?? "Unknown"; $uploader = $_SESSION['username'] ?? "Unknown";
// Load existing metadata, if any. $metadataCollection = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
if (file_exists($metadataFile)) { if (!is_array($metadataCollection)) {
$metadataCollection = json_decode(file_get_contents($metadataFile), true);
if (!is_array($metadataCollection)) {
$metadataCollection = [];
}
} else {
$metadataCollection = []; $metadataCollection = [];
} }
// Add metadata for this file if not already present.
if (!isset($metadataCollection[$resumableFilename])) { if (!isset($metadataCollection[$resumableFilename])) {
$metadataCollection[$resumableFilename] = [ $metadataCollection[$resumableFilename] = [
"uploaded" => $uploadedDate, "uploaded" => $uploadedDate,
@@ -164,97 +167,83 @@ if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
} }
// --- End Metadata Update --- // --- End Metadata Update ---
// Cleanup: remove the temporary directory and its chunks. // Cleanup: use a robust recursive function.
array_map('unlink', glob("$tempDir*")); function rrmdir($dir) {
rmdir($tempDir); 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"]); echo json_encode(["success" => "File uploaded successfully"]);
exit; exit;
} else { } else {
// ------------- Full Upload (Non-chunked) ------------- // ------------- Full Upload (Non-chunked) -------------
// Validate folder name input.
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root'; $folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { if ($folder !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name"]); echo json_encode(["error" => "Invalid folder name"]);
exit; exit;
} }
// Determine the base upload directory.
$baseUploadDir = UPLOAD_DIR; $baseUploadDir = UPLOAD_DIR;
if ($folder !== 'root') { if ($folder !== 'root') {
$baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR; $baseUploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($baseUploadDir)) { }
mkdir($baseUploadDir, 0775, true); if (!is_dir($baseUploadDir) && !mkdir($baseUploadDir, 0775, true)) {
} http_response_code(500);
} else { echo json_encode(["error" => "Failed to create upload directory"]);
if (!is_dir($baseUploadDir)) { exit;
mkdir($baseUploadDir, 0775, true);
}
} }
// Prepare a collection to hold metadata for each folder. $metadataCollection = [];
$metadataCollection = []; // key: folder path, value: metadata array $metadataChanged = [];
$metadataChanged = []; // key: folder path, value: boolean
// Use a Unicode-enabled pattern to allow special characters.
$safeFileNamePattern = REGEX_FILE_NAME; $safeFileNamePattern = REGEX_FILE_NAME;
foreach ($_FILES["file"]["name"] as $index => $fileName) { 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))); $safeFileName = trim(urldecode(basename($fileName)));
if (!preg_match($safeFileNamePattern, $safeFileName)) { if (!preg_match($safeFileNamePattern, $safeFileName)) {
http_response_code(400);
echo json_encode(["error" => "Invalid file name: " . $fileName]); echo json_encode(["error" => "Invalid file name: " . $fileName]);
exit; exit;
} }
// --- Minimal Folder/Subfolder Logic ---
$relativePath = ''; $relativePath = '';
if (isset($_POST['relativePath'])) { if (isset($_POST['relativePath'])) {
if (is_array($_POST['relativePath'])) { $relativePath = is_array($_POST['relativePath']) ? $_POST['relativePath'][$index] ?? '' : $_POST['relativePath'];
$relativePath = $_POST['relativePath'][$index] ?? '';
} else {
$relativePath = $_POST['relativePath'];
}
} }
$folderPath = $folder;
// Determine the complete folder path for upload and for metadata. $uploadDir = $baseUploadDir;
$folderPath = $folder; // Base folder as provided ("root" or a subfolder)
$uploadDir = $baseUploadDir; // Start with the base upload directory
if (!empty($relativePath)) { if (!empty($relativePath)) {
$subDir = dirname($relativePath); $subDir = dirname($relativePath);
if ($subDir !== '.' && $subDir !== '') { if ($subDir !== '.' && $subDir !== '') {
$folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir; $folderPath = ($folder === 'root') ? $subDir : $folder . "/" . $subDir;
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
. str_replace('/', DIRECTORY_SEPARATOR, $folderPath) . DIRECTORY_SEPARATOR;
} }
// Reapply basename to the relativePath to get the final safe file name.
$safeFileName = basename($relativePath); $safeFileName = basename($relativePath);
} }
// --- End Minimal Folder/Subfolder Logic --- if (!is_dir($uploadDir) && !mkdir($uploadDir, 0775, true)) {
http_response_code(500);
// Make sure the final upload directory exists. echo json_encode(["error" => "Failed to create subfolder"]);
if (!is_dir($uploadDir)) { exit;
mkdir($uploadDir, 0775, true);
} }
$targetPath = $uploadDir . $safeFileName; $targetPath = $uploadDir . $safeFileName;
if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) { 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; $metadataKey = ($folderPath === '' || $folderPath === 'root') ? "root" : $folderPath;
$metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json';
$metadataFile = META_DIR . $metadataFileName; $metadataFile = META_DIR . $metadataFileName;
if (!isset($metadataCollection[$metadataKey])) { if (!isset($metadataCollection[$metadataKey])) {
if (file_exists($metadataFile)) { $metadataCollection[$metadataKey] = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
$metadataCollection[$metadataKey] = json_decode(file_get_contents($metadataFile), true); if (!is_array($metadataCollection[$metadataKey])) {
} else {
$metadataCollection[$metadataKey] = []; $metadataCollection[$metadataKey] = [];
} }
$metadataChanged[$metadataKey] = false; $metadataChanged[$metadataKey] = false;
} }
if (!isset($metadataCollection[$metadataKey][$safeFileName])) { if (!isset($metadataCollection[$metadataKey][$safeFileName])) {
$uploadedDate = date(DATE_TIME_FORMAT); $uploadedDate = date(DATE_TIME_FORMAT);
$uploader = $_SESSION['username'] ?? "Unknown"; $uploader = $_SESSION['username'] ?? "Unknown";
@@ -265,12 +254,12 @@ if (!preg_match(REGEX_FILE_NAME, $resumableFilename)) {
$metadataChanged[$metadataKey] = true; $metadataChanged[$metadataKey] = true;
} }
} else { } else {
http_response_code(500);
echo json_encode(["error" => "Error uploading file"]); echo json_encode(["error" => "Error uploading file"]);
exit; exit;
} }
} }
// After processing all files, write out metadata files for folders that changed.
foreach ($metadataCollection as $folderKey => $data) { foreach ($metadataCollection as $folderKey => $data) {
if ($metadataChanged[$folderKey]) { if ($metadataChanged[$folderKey]) {
$metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json'; $metadataFileName = str_replace(['/', '\\', ' '], '-', $folderKey) . '_metadata.json';