How to concatenate chunked file uploads from Dropz

2019-08-19 23:19发布

问题:

I'm using Dropzone.js to take files of various types (including images and non-images, like a PDF), and upload them in 1mb chunks to our server. I'm then attempting to concatenate the files with PHP and later upload them to our company's FileMaker database.

So far I've been able to get the files to upload in the chunks, as they should. I store them all in a temporary "uploads" folder with the same "codename," with "-INDEX#" appended to the end of each name (INDEX# being the chunk # being uploaded, as passed by Dropzone).

I've isolated the failure to the portion of the PHP that loops through the already uploaded chunks to grab the content. Specifically, when I go to fetch the content, I attempt to save the file path of the chunk to a variable by using PHP's "realpath" to both get the absolute path, and also serve as a true/false check of the file's existence. Without fail, PHP is unable to "see" the file.

I'm not an expert at folder permissions, so there's a strong chance it could be related to that and that I just don't know how to deal with it. You'll see that I did attempt a chmod at the top of the PHP on the uploads/ directory.

I've included only the PHP code below, and none of the javascript. I'm not sure that the JS is relevant since it's apparently working just fine with the actual chunk uploads.

The PHP file below is in the same directory as the uploads/ folder where the chunks are to end up.

<?php

header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");

/* ========================================
  VARIABLES
======================================== */

// chunk variables
$fileId = $_POST['dzuuid'];
$chunkIndex = $_POST['dzchunkindex'] + 1;
$chunkTotal = $_POST['dztotalchunkcount'];

// file path variables
$ds = DIRECTORY_SEPARATOR;
$targetPath = dirname( __FILE__ ) . "{$ds}uploads{$ds}";
$fileType = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
$fileSize = $_FILES["file"]["size"];
$filename = "{$fileId}-{$chunkIndex}.{$fileType}";
$targetFile = $targetPath . $filename;

// change directory permissions
chmod(realpath($targetPath), 0777);

/* ========================================
  DEPENDENCY FUNCTIONS
======================================== */

$returnResponse = function ($info = null, $filelink = null, $status = "error") {
  die (json_encode( array(
    "status" => $status,
    "info" => $info,
    "file_link" => $filelink
  )));
};

/* ========================================
  VALIDATION CHECKS
======================================== */

// I removed all the validation code here. They just prevent upload, so assume the upload is going through.

/* ========================================
  CHUNK UPLOAD
======================================== */

move_uploaded_file($_FILES['file']['tmp_name'], $targetFile);

// Be sure that the file has been uploaded
if ( !file_exists($targetFile) ) $returnResponse("An error occurred and we couldn't upload the requested file.");

/* ========================================
  FINAL UPLOAD CONDITIONAL
======================================== */

if ( $chunkIndex == $chunkTotal ) {

  // ===== concatenate uploaded files =====
  // set emtpy string for file content concatonation
  $file_content = "";
  // loop through temp files and grab the content
  for ($i = 1; $i <= $chunkTotal; $i++) {

    // target temp file
    $temp_file_path = realpath("{$targetPath}{$fileId}-{$i}.{$fileType}") or $returnResponse("Your chunk was lost mid-upload.");
    // ^^^^^^^ this is where the failure is occurring, $i = 1, so first iteration

    // copy chunk...you'll see a bunch of methods included below that I've tried, but the simplest one is method 3, so I've tested mostly there
    // method 1
    /*$temp_file = fopen($temp_file_path, "rb") or $returnResponse("The server cannot open your chunks");
    $chunk = base64_encode(fread($temp_file, $fileSize));
    fclose($temp_file);
    // method 2
    $chunk = base64_encode(stream_get_contents($temp_file_path, $fileSize));*/
    // method 3
    $chunk = base64_encode(file_get_contents($temp_file_path));

    // check chunk content
    if ( empty($chunk) ) $returnResponse("Chunks are uploading as empty strings.");

    // add chunk to main file
    $file_content .= $chunk;

    // delete chunk
    unlink($temp_file_path);
    if ( file_exists($temp_file_path) ) $returnResponse("Your temp files could not be deleted.");

    continue;

  }
  // create and write concatonated chunk to the main file
  file_put_contents("{$targetPath}{$fileId}.{$fileType}", base64_decode($file_content));
// other method of adding contents to new file below, but the one above seemed simpler
  //$final = fopen("{$target_file}.{$fileType}", 'ab');
  //fwrite($final, base64_decode($file_content));
  //fclose($final);

  // create new FileMaker code removed here, irrelevant
// run FileMaker script to populate container field with concatenated file code removed here, irrelevant
// somewhere in the code above, if everything succeeds, I unlink the concatenated file so that it's not cluttering my "uploads" folder, but I never get this far

} else {
  $returnResponse(null, null, "success");
}

回答1:

I figured it out! The problem is that I was trying to call the concatenation loop when $chunkIndex == $chunkTotal. If you look in the browser network monitor, you'll see that often the chunks upload in the wrong order, which would make the concatenation fail on the realpath step since the file really didn't exist yet (it just looked like it did when I visited the actual folder a few second later). To prove it, just try sleep(5) to give the rest time to upload and see it succeed (of course this is a bad solution, but a quick tester).

The solution is to separate the upload script from the concatenation script. If you're using Dropzone.js you can trigger the concatenation script from "chunksUploaded," as described at this link:

Dropzone JS - Chunking

You can see below how the final scripts ended up looking:

script.js

var myDropzone = new Dropzone(target, {
  url: ($(target).attr("action")) ? $(target).attr("action") : "../../chunk-upload.php", // Check that our form has an action attr and if not, set one here
  maxFilesize: 25, // megabytes
  chunking: true,
  parallelUploads: 1,
  parallelChunkUploads: true,
  retryChunks: true,
  retryChunksLimit: 3,
  forceChunking: true,
  chunkSize: 1000000,
  acceptedFiles: "image/*,application/pdf,.doc,.docx,.xls,.xlsx,.csv,.tsv,.ppt,.pptx,.pages,.odt,.rtf,.heif,.hevc",
  previewTemplate: previewTemplate,
  previewsContainer: "#previews",
  clickable: true,
  autoProcessQueue: false,
  chunksUploaded: function(file, done) {
    // All chunks have been uploaded. Perform any other actions
    let currentFile = file;

    // This calls server-side code to merge all chunks for the currentFile
    $.ajax({
        url: "chunk-concat.php?dzuuid=" + currentFile.upload.uuid + "&dztotalchunkcount=" + currentFile.upload.totalChunkCount + "&fileName=" + currentFile.name.substr( (currentFile.name.lastIndexOf('.') +1) ),
        success: function (data) {
            done();
        },
        error: function (msg) {
            currentFile.accepted = false;
            myDropzone._errorProcessing([currentFile], msg.responseText);
        }
     });
  },

});

chunk-upload.php

<?php

/* ========================================
  VARIABLES
======================================== */

// chunk variables
$fileId = $_POST['dzuuid'];
$chunkIndex = $_POST['dzchunkindex'] + 1;
$chunkTotal = $_POST['dztotalchunkcount'];

// file path variables
$ds = DIRECTORY_SEPARATOR;
$targetPath = dirname( __FILE__ ) . "{$ds}uploads{$ds}";
$fileType = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
$fileSize = $_FILES["file"]["size"];
$filename = "{$fileId}-{$chunkIndex}.{$fileType}";
$targetFile = $targetPath . $filename;

// change directory permissions
chmod(realpath($targetPath), 0777) or die("Could not modify directory permissions.");

/* ========================================
  DEPENDENCY FUNCTIONS
======================================== */

$returnResponse = function ($info = null, $filelink = null, $status = "error") {
  die (json_encode( array(
    "status" => $status,
    "info" => $info,
    "file_link" => $filelink
  )));
};

/* ========================================
  VALIDATION CHECKS
======================================== */

// blah, blah, blah validation stuff goes here

/* ========================================
  CHUNK UPLOAD
======================================== */

move_uploaded_file($_FILES['file']['tmp_name'], $targetFile);

// Be sure that the file has been uploaded
if ( !file_exists($targetFile) ) $returnResponse("An error occurred and we couldn't upload the requested file.");
chmod($targetFile, 0777) or $returnResponse("Could not reset permissions on uploaded chunk.");

$returnResponse(null, null, "success");

chunk-concat.php

<?php

// get variables
$fileId = $_GET['dzuuid'];
$chunkTotal = $_GET['dztotalchunkcount'];

// file path variables
$ds = DIRECTORY_SEPARATOR;
$targetPath = dirname( __FILE__ ) . "{$ds}uploads{$ds}";
$fileType = $_GET['fileName'];

/* ========================================
  DEPENDENCY FUNCTIONS
======================================== */

$returnResponse = function ($info = null, $filelink = null, $status = "error") {
  die (json_encode( array(
    "status" => $status,
    "info" => $info,
    "file_link" => $filelink
  )));
};

/* ========================================
  CONCATENATE UPLOADED FILES
======================================== */

// loop through temp files and grab the content
for ($i = 1; $i <= $chunkTotal; $i++) {

  // target temp file
  $temp_file_path = realpath("{$targetPath}{$fileId}-{$i}.{$fileType}") or $returnResponse("Your chunk was lost mid-upload.");

  // copy chunk
  $chunk = file_get_contents($temp_file_path);
  if ( empty($chunk) ) $returnResponse("Chunks are uploading as empty strings.");

  // add chunk to main file
  file_put_contents("{$targetPath}{$fileId}.{$fileType}", $chunk, FILE_APPEND | LOCK_EX);

  // delete chunk
  unlink($temp_file_path);
  if ( file_exists($temp_file_path) ) $returnResponse("Your temp files could not be deleted.");

}

/* ========== a bunch of steps I removed below here because they're irrelevant, but I described them anyway ========== */
// create FileMaker record
// run FileMaker script to populate container field with newly-created file
// unlink newly created file
// return success