วัน: 16 กันยายน 2012

PHP: การใช้ Chunked Upload สำหรับการอัปโหลดไฟล์ขนาดใหญ่PHP: การใช้ Chunked Upload สำหรับการอัปโหลดไฟล์ขนาดใหญ่

ในโลกของการพัฒนาเว็บสมัยใหม่ ผู้ใช้งานจำนวนมากต้องมีการอัปโหลดไฟล์ที่มีขนาดใหญ่ เช่น วิดีโอ, ชุดข้อมูล (Datasets), หรือเอกสาร PDF ขนาดมหาศาล อย่างไรก็ตาม PHP และ HTTP มีข้อจำกัดที่สำคัญในการจัดการการอัปโหลดไฟล์ข้ามครั้งหรือไฟล์ที่มีขนาดเกินกว่าหน่วยความจำ (memory limit) หรือเวลาประมวลผล (execution time limit) ของเซิร์ฟเวอร์ การพยายามส่งไฟล์เหล่านี้ด้วยวิธีปกติจึงมักล้มเหลว

Chunked Upload คืออะไร?

Chunked Upload คือเทคนิคการแบ่งไฟล์ขนาดใหญ่ออกเป็นส่วนย่อย ๆ (Chunks) ที่มีขนาดคงที่ ก่อนที่จะส่งข้อมูลเหล่านี้ไปยังเซิร์ฟเวอร์ผ่าน HTTP อย่างต่อเนื่อง เมื่อทุก Chunk ถูกส่งมาถึงฝั่ง Server แล้ว PHP จะทำหน้าที่รวบรวมและประกอบชิ้นส่วนเหล่านั้นให้กลับมาเป็นไฟล์ต้นฉบับที่สมบูรณ์

หลักการทำงานโดยย่อ

  • Client-Side (JavaScript): ฝั่งไคลเอนต์จะรับผิดชอบในการแบ่งไฟล์ออกเป็นส่วน ๆ และส่งแต่ละส่วนไปยัง API endpoint ของ PHP
  • Server-Side (PHP): Script ใน PHP จะต้องทำหน้าที่พิเศษ คือการรับและบันทึก Chunk เข้าไปยังไฟล์ชั่วคราว (Temporary Location) โดยต้องทราบว่า Chunk นี้คือลำดับที่เท่าไหร่ และมีขนาดเท่าใด เพื่อให้สามารถเรียงร้อยกลับมาได้
  • Completion: เมื่อได้รับ Chunk ลำดับสุดท้ายแล้ว PHP จะทำการยืนยันความสมบูรณ์ของไฟล์และรวมส่วนประกอบทั้งหมดเป็นไฟล์ปลายทางจริง พร้อมทั้งลบไฟล์ชั่วคราวก่อนหน้าออก

ทำไมจึงสำคัญ? การใช้ Chunked Upload ทำให้เราสามารถจัดการกับไฟล์ที่มีขนาดหลายร้อยเมกะไบต์หรือกิกะไบต์ได้ โดยไม่ติดขัดข้อจำกัดของ upload_max_filesize หรือ max_execution_time ของ PHP ธรรมดา


ตัวอย่างการใช้งานและรูปแบบโค้ด (Code Examples)

ในการทำ Chunked Upload จริง ๆ ส่วนใหญ่แล้ว จะต้องใช้ JavaScript ในการแบ่งไฟล์จากฝั่ง Client แต่เราจะเน้นที่ส่วนของ PHP Backend ซึ่งเป็นหัวใจของการรับและประกอบชิ้นส่วน

โครงสร้าง PHP Backend (upload_handler.php)

  • file_id: ID ของไฟล์ต้นฉบับ
  • chunk_index: ลำดับของ Chunk (เช่น 0, 1, 2…)
  • total_chunks: จำนวน Chunk ทั้งหมดที่คาดหวัง
  • chunk_data: ข้อมูลไบนารีของ Chunk นั้น ๆ (รับผ่าน Body Request)
 'error', 'message' => 'Missing required chunk data parameters.']);
    exit;
}

// 1. สร้างชื่อไฟล์ชั่วคราวที่ระบุ Chunk และ File ID
$tempFileName = $fileId . "_chunk_" . $chunkIndex . ".tmp";
$fullTempPath = TEMP_DIR . $tempFileName;

// ตรวจสอบและสร้าง Directory ชั่วคราวถ้ายังไม่มี
if (!is_dir(TEMP_DIR)) {
    mkdir(TEMP_DIR, 0777, true);
}

// 2. บันทึก Chunk เข้าสู่ไฟล์ชั่วคราว
if (file_put_contents($fullTempPath, $chunkData) === false) {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'message' => 'Failed to write chunk data.']);
    exit;
}

// 3. การตรวจสอบและประกอบไฟล์ (Logic นี้ควรทำเมื่อได้รับ Chunk ลำดับสุดท้ายเท่านั้น)
if ($chunkIndex == $totalChunks - 1) {
    $finalFilePath = 'uploads/' . basename($fileId); // ไฟล์ปลายทางจริง
    $tempFiles = glob(TEMP_DIR . $fileId . "_chunk_*.tmp");

    // ตรวจสอบความสมบูรณ์ของจำนวน Chunk
    if (count($tempFiles) !== $totalChunks) {
        http_response_code(422);
        echo json_encode(['status' => 'error', 'message' => 'Incomplete upload. Expected ' . $totalChunks . ' chunks, found ' . count($tempFiles) . '.']);
    } else {
        // เรียงร้อย Chunk ทั้งหมดเข้าด้วยกัน (The Core Logic)
        $sourcePaths = array_map(function($path, $index) { return $path; }, $tempFiles, range(0, $totalChunks - 1));

        $handle = fopen($finalFilePath, 'wb');
        foreach ($sourcePaths as $chunkPath) {
            $sourceHandle = fopen($chunkPath, 'rb');
            if ($sourceHandle) {
                while (!feof($sourceHandle)) {
                    fwrite($handle, fread($sourceHandle, 8192)); // อ่านและเขียนทีละบล็อกเพื่อประสิทธิภาพ
                }
                fclose($sourceHandle);
            }
        }
        fclose($handle);

        // 4. การล้างไฟล์ชั่วคราว (Crucial Step)
        foreach ($tempFiles as $path) {
            unlink($path);
        }

        http_response_code(200);
        echo json_encode(['status' => 'success', 'message' => 'File assembled successfully.', 'file_path' => $finalFilePath]);
    }
} else {
    // กรณีที่ยังไม่ถึง Chunk สุดท้าย ให้ตอบกลับว่ารับข้อมูลเรียบร้อยแล้ว
    http_response_code(202); // 202 Accepted: รับทราบและกำลังดำเนินการต่อ
    echo json_encode(['status' => 'processing', 'message' => 'Chunk received and saved. Waiting for final chunk...']);
}

?>

คำอธิบายโค้ดที่สำคัญ

  • file_get_contents('php://input'): เป็นส่วนสำคัญที่สุดในการรับข้อมูลไบนารี (Binary Data) จาก Body ของ HTTP Request เนื่องจากไฟล์ไม่ได้ถูกส่งผ่านตัวแปร Superglobal อย่าง $_FILES แต่ถูกส่งมาเป็น Stream ข้อมูลดิบ
  • Directory Structure temp_uploads/: ต้องสร้างพื้นที่จัดเก็บชั่วคราว แยกจากไฟล์จริง เพื่อหลีกเลี่ยงการเขียนทับและการเข้าถึงที่ผิดพลาด และทำให้กระบวนการ Cleanup ง่ายขึ้น
  • Assembly Logic (The Core): การใช้ fopen, fread และ fwrite ในลูป เป็นวิธีที่มีประสิทธิภาพสูงในการจัดการกับข้อมูลไบนารีขนาดใหญ่ โดยหลีกเลี่ยงการสร้าง String ขนาดมหึมาในหน่วยความจำ
  • Security: การล้างไฟล์ unlink(): หลังจากประกอบไฟล์เสร็จสิ้น สิ่งสำคัญที่สุดคือต้องทำความสะอาด (Clean up) โดยการลบ Chunk ชิ้นส่วนที่อยู่ในไดเร็กทอรี่ชั่วคราวทั้งหมด เพื่อป้องกันข้อมูลส่วนตัวตกค้างและปัญหาพื้นที่จัดเก็บ

ข้อควรระวัง Security และ Best Practices

  • ⚠️ การจัดการทรัพยากร (Resource Management): หาก Client หลุดก่อนส่ง Chunk สุดท้าย PHP จะมีไฟล์ชั่วคราวจำนวนมากหลงเหลืออยู่ แนวทางแก้ไขที่ดีที่สุดคือการใช้ Cron Job หรือ Cleanup Script ที่ทำงานเป็นระยะเพื่อลบ Directory temp_uploads ที่ไม่มีการเข้าใช้งานเกินกว่าช่วงเวลาที่กำหนด
  • 🔒 การป้องกัน Path Traversal และ ID Spoofing: ห้ามเชื่อถือค่า fileId ที่มาจาก Client โดยเด็ดขาด ควรใช้ Unique Identifier (UUID) หรือการ Hash ค่าที่ได้รับมาเพื่อสร้างชื่อไฟล์ชั่วคราว เพื่อให้แน่ใจว่าชื่อไฟล์นั้นไม่สามารถนำไปชี้ไปยัง Directory อื่นในระบบได้
// แนวทางป้องกันที่ดีที่สุดคือการใช้ Hash หรือ UUID ที่สร้างจาก Server Side 
$safeFileId = hash('sha256', $fileId);
  • ⚙️ การประมวลผลแบบ Asynchronous (Queue System): ไม่ควรประกอบไฟล์ทั้งหมดใน Web Request เดียว หากการรวมไฟล์ใช้เวลานานเกินไป ควรส่งงาน “Assembly” ไปยังระบบ Background Job (เช่น Redis, RabbitMQ) เพื่อให้ PHP Web Process สามารถตอบกลับผู้ใช้ได้ทันที และป้องกัน Timeout
  • ✅ การตรวจสอบความสมบูรณ์ของข้อมูล (Integrity Check): ในขั้นตอนสุดท้าย ต้องมีการคำนวณค่า Hash (เช่น SHA-256) ของไฟล์ที่ประกอบเสร็จแล้ว และเปรียบเทียบกับค่า Hash ที่ Client ส่งมาตั้งแต่ต้น เพื่อยืนยันว่าข้อมูลไม่ถูกแก้ไขระหว่างทาง
  • 🚦 การใช้ HTTP Status Codes อย่างถูกต้อง: ควรใช้ 202 Accepted เมื่อรับ Chunk และยังไม่เสร็จสมบูรณ์, ใช้ 422 Unprocessable Entity เมื่อข้อมูลที่ได้รับมามีโครงสร้างผิดพลาด (เช่น ลำดับ Chunk ผิด)

  • สรุปและการนำไปประยุกต์ใช้งาน

    Chunked Upload เป็นเครื่องมือที่ทรงพลังและจำเป็นสำหรับการสร้างระบบอัปโหลดไฟล์ขนาดใหญ่ในระดับองค์กร (Enterprise-level applications) มันช่วยให้ Backend PHP ของเรามีความยืดหยุ่นในการจัดการกับข้อจำกัดของเซิร์ฟเวอร์และเครือข่ายได้อย่างมีประสิทธิภาพ

    • Client (JS): รับผิดชอบการแบ่งไฟล์และการจัดการสถานะการส่ง
    • Server (PHP): ทำหน้าที่เป็นตัวรับข้อมูลชั่วคราว (Temporary Receiver) อย่างปลอดภัย และมี Logic ในการประกอบและล้างข้อมูลอย่างสมบูรณ์เมื่อได้รับ Chunk ลำดับสุดท้าย

    การทำความเข้าใจหลักการเหล่านี้ ไม่เพียงแต่จะแก้ปัญหาทางเทคนิคด้านขนาดไฟล์เท่านั้น แต่ยังเป็นการยกระดับความเสถียร ความปลอดภัย และสถาปัตยกรรมของ Backend System ของคุณให้ทัดเทียมกับระบบระดับโลกอีกด้วย


    อ่านเพิ่มเติม