init_file_system(); } /** * Generate a unique file ID. * * @since 1.0.0 * @return string A unique alphanumeric hash */ public function generate_fileid() { // Current timestamp + random string $timestamp = time(); $random = bin2hex(random_bytes(10)); // 20 chars $unique = uniqid('', true); // ~23 chars // Create hash from combined values $hash = md5($timestamp . $random . $unique); // Return first 40 chars of the hash + timestamp hex (should be under 50 chars total) return substr($hash, 0, 40) . dechex($timestamp); } /** * Initialize file system directories. * * @since 1.0.0 * @return bool True on success, false on failure. */ public function init_file_system() { // Create base upload directory if it doesn't exist if (!file_exists(QDR_TSS_UPLOADS_DIR)) { if (!wp_mkdir_p(QDR_TSS_UPLOADS_DIR)) { return false; } // Create .htaccess file to protect direct access $htaccess_content = "# Disable directory browsing\nOptions -Indexes\n\n# Deny direct access to all files\n\nOrder Allow,Deny\nDeny from all\n"; file_put_contents(QDR_TSS_UPLOADS_DIR . '.htaccess', $htaccess_content); } // Create temporary directory for chunked uploads if (!file_exists(QDR_TSS_UPLOADS_DIR . 'tmp/')) { if (!wp_mkdir_p(QDR_TSS_UPLOADS_DIR . 'tmp/')) { return false; } } return true; } /** * Create a dated directory for storing files. * * @since 1.0.0 * @param string $date Optional. Date string in Y-m-d format. Default is current date. * @return string|false Path to the directory or false on failure. */ public function create_dated_directory($date = '') { if (empty($date)) { $date = date('Y/m/d'); } $directory = QDR_TSS_UPLOADS_DIR . $date; if (!file_exists($directory)) { if (!wp_mkdir_p($directory)) { return false; } } return $directory; } /** * Initialize a chunked upload. * * @since 1.0.0 * @return string|false The upload ID or false on failure. */ public function init_chunked_upload() { $upload_id = uniqid(); $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id; if (!wp_mkdir_p($upload_dir)) { return false; } return $upload_id; } /** * Store a chunk of a file. * * @since 1.0.0 * @param string $upload_id The upload ID. * @param int $chunk_index The chunk index. * @param array $file The chunk file data from $_FILES. * @return bool True on success, false on failure. */ public function store_chunk($upload_id, $chunk_index, $file) { $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id; // Check if the upload directory exists if (!file_exists($upload_dir)) { return false; } // Check if file was uploaded successfully if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) { return false; } // Move uploaded chunk to temporary directory $chunk_path = $upload_dir . '/' . $chunk_index; return move_uploaded_file($file['tmp_name'], $chunk_path); } /** * Check if all chunks are present. * * @since 1.0.0 * @param string $upload_id The upload ID. * @param int $total_chunks The total number of chunks. * @return bool True if all chunks are present, false otherwise. */ public function check_all_chunks($upload_id, $total_chunks) { $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id; // Check if the upload directory exists if (!file_exists($upload_dir)) { return false; } // Check that all chunks are present for ($i = 0; $i < $total_chunks; $i++) { if (!file_exists($upload_dir . '/' . $i)) { return false; } } return true; } /** * Finalize a chunked upload. * * @since 1.0.0 * @param string $upload_id The upload ID. * @param int $total_chunks The total number of chunks. * @param string $original_filename The original file name. * @return array|false { * @type string $file_path The path to the combined file. * @type int $file_size The size of the combined file. * } or false on failure. */ public function finalize_chunked_upload($upload_id, $total_chunks, $original_filename) { $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id; // Check if the upload directory exists if (!file_exists($upload_dir)) { return false; } // Check that all chunks are present if (!$this->check_all_chunks($upload_id, $total_chunks)) { return false; } // Create final file directory $file_dir = $this->create_dated_directory(); if (!$file_dir) { return false; } // Sanitize file name $original_filename = sanitize_file_name($original_filename); $file_name = md5($original_filename . time()) . '-' . $original_filename; $file_path = $file_dir . '/' . $file_name; // Combine chunks if (!$this->combine_chunks($upload_dir, $total_chunks, $file_path)) { return false; } // Clean up temporary directory $this->cleanup_chunks($upload_dir); // Get file size $file_size = filesize($file_path); return array( 'file_path' => str_replace(QDR_TSS_UPLOADS_DIR, '', $file_path), 'file_size' => $file_size ); } /** * Combine chunks into a single file. * * @since 1.0.0 * @access private * @param string $upload_dir The upload directory. * @param int $total_chunks The total number of chunks. * @param string $file_path The path to the combined file. * @return bool True on success, false on failure. */ private function combine_chunks($upload_dir, $total_chunks, $file_path) { // Create final file $final_file = fopen($file_path, 'wb'); if (!$final_file) { return false; } // Combine chunks for ($i = 0; $i < $total_chunks; $i++) { $chunk_path = $upload_dir . '/' . $i; $chunk = fopen($chunk_path, 'rb'); if (!$chunk) { fclose($final_file); unlink($file_path); return false; } stream_copy_to_stream($chunk, $final_file); fclose($chunk); } fclose($final_file); return true; } /** * Clean up chunks after successful upload. * * @since 1.0.0 * @access private * @param string $upload_dir The upload directory. * @return bool True on success, false on failure. */ private function cleanup_chunks($upload_dir) { // Delete all chunk files $files = glob($upload_dir . '/*'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } // Delete upload directory return rmdir($upload_dir); } /** * Delete a file. * * @since 1.0.0 * @param string $file_path The file path relative to the upload directory. * @return bool True on success, false on failure. */ public function delete_file($file_path) { $full_path = QDR_TSS_UPLOADS_DIR . $file_path; if (file_exists($full_path)) { return unlink($full_path); } return false; } /** * Get file size. * * @since 1.0.0 * @param string $file_path The file path relative to the upload directory. * @return int|false The file size in bytes or false if file doesn't exist. */ public function get_file_size($file_path) { $full_path = QDR_TSS_UPLOADS_DIR . $file_path; if (file_exists($full_path)) { return filesize($full_path); } return false; } /** * Clean up empty directories. * * @since 1.0.0 * @param string $directory The directory to clean. * @return void */ public function cleanup_empty_directories($directory = '') { if (empty($directory)) { $directory = QDR_TSS_UPLOADS_DIR; } // Skip special directories if (basename($directory) === 'tmp') { return; } // Skip if not a directory if (!is_dir($directory)) { return; } // Get all files and directories $files = scandir($directory); $files = array_diff($files, array('.', '..', '.htaccess')); // Process subdirectories first foreach ($files as $file) { $path = $directory . '/' . $file; if (is_dir($path)) { $this->cleanup_empty_directories($path); } } // Check if directory is empty now $remaining_files = scandir($directory); $remaining_files = array_diff($remaining_files, array('.', '..', '.htaccess')); // Remove directory if empty if (empty($remaining_files)) { @rmdir($directory); } } }