Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding partially uploaded files (via FTP) when getting newest file with ajax and PHP

I'm aware the title of this is a little strange, but I'll get there.

I have a camera tethered to a laptop. Using Remote Shooting, when the photographer takes a photo it is saved into a folder on the laptop's hard drive. There is an Automator (Mac OS X) action set up on the folder that whenever a new file appears, it resizes it and pushes it up to an FTP using Transmit.

Here's where the code comes in.

I have a web page displaying the most recent photo taken. It uses ajax to repeatedly check whether or not a new file has been uploaded, and if it has, load the new photo and crossfade it with the old photo. Here is the Javascript running on the page.

(function() {
  var delay, refreshLoop;

  // Alias to setTimeout that reverses the parameters, making it much cleaner in code (for CoffeeScript)
  delay = function(time, callback) {
    return setTimeout(callback, time);
  };

  // Function that drives the loop of refreshing photos
  refreshLoop = function(currentFolderState, refreshRate) {
    // Get the new folder state
    $.get("ajax/getFolderState.php", function(newFolderState) {
      // If the new folder state is different
      if (newFolderState !== currentFolderState) {
        // Get the newest photo 
        $.get("ajax/getNewestPhoto.php", function(photoFilename) {
          var img;
          // Create the new image element
          img = $('<img class="new-photo"/>')
            // Append the src attribute to it after so it can BG load
            .attr('src', "/events/mindsmack/booth/cinco-de-mindsmack-2012/" + photoFilename)
            // When the image is loaded
            .load(function() {
              // Append the image to the photos container
              $('#photos').append(img);
              // Crossfade it with the old photo
              $('#photos .current-photo').fadeOut();
              $('#photos .new-photo').fadeIn().removeClass("new-photo").addClass("current-photo");
            });
        });
      }
      // Wait for the refresh rate and then repeat
      delay(refreshRate, function() {
        refreshLoop(newFolderState, refreshRate);
      });
    });
  };

  // Document Ready
  $(function() {
    var refreshRate;

    // Load the first photo 
    $.get("ajax/getNewestPhoto.php", function(photoFilename) {
      $('#photos').append("<img src='/events/mindsmack/booth/cinco-de-mindsmack-2012/" + photoFilename + "' class='current-photo' />");
    });
    refreshRate = 2000;
    // After the timeout
    delay(refreshRate, function() {
      // Get the initial folder state and kick off the loop
      $.get("ajax/getFolderState.php", function(initialFolderState) {
        refreshLoop(initialFolderState, refreshRate);
      });
    });
  });

}).call(this);

And here are the two PHP files that are called in that Javascript

getFolderState.php

<?php

    $path = $_SERVER['DOCUMENT_ROOT'] . "/events/mindsmack/booth/cinco-de-mindsmack-2012/";

    // Get a directory listing of the path where the photos are stored
    $dirListing = scandir( $path );

    // Echo an md5 hash of the directory listing
    echo md5(serialize($dirListing));

getNewestPhoto.php

<?php

    $path = $_SERVER['DOCUMENT_ROOT'] . "/events/mindsmack/booth/cinco-de-mindsmack-2012/";

    // Get a directory listing of the path where the photos are stored
    $listing = scandir($path);

    $modTime = 0;
    $mostRecent = "";

    // Find the most recent file
    foreach ( $listing as $file ) {
        if ( is_file($path.$file) && $file !== ".DS_Store" && filectime($path.$file) > $modTime) {
            $modTime = filectime($path.$file);
            $mostRecent = $file;
        }
    }

    // Echo the most recent filename
    echo $mostRecent;

All of this works mostly flawlessly. The problem, I believe, is when the loop is fired while a file is in the middle of being uploaded. Occasionally a photo will be taken and it will only show up on the page partway. An error isn't thrown at all, the script continues to run just fine, and the image file is actually cached in that state, leading me to believe that my code is catching a file upload in progress and only showing the part of the file that has been uploaded at that moment.

I don't mind changing my solution if I need to in order to overcome this issue, I'm just not sure exactly what to do.

EDIT

As per one of the suggestions below, I added code to my getNewestPhoto.php that checks the file size of the photo, waits a bit, and checks it again. If they're different, it goes back and checks again until the file sizes are the same. I was hoping this would catch the files that are mid-upload because the filesize would change between loops but even when photos are coming up partially rendered, the filesize check didn't catch it.

Here's the code I added

$currFilesize = filesize($path . $mostRecent);
$newFilesize;

while ( true ) {
    sleep(.5);
    $newFilesize = filesize($path . $mostRecent);

    if ( $newFilesize == $currFilesize) {
        break;
    }
    else {
        $currFilesize = $newFilesize;
    }
}

I'm thinking (via another suggestion) that I need to add some kind of lock file on upload that stops the code from refreshing the photo and is removed when the upload is done, but seeing that I'm not running any kind of web server on the computer tethered to the camera, I'm not sure how to accomplish that. I would love suggestions.

like image 849
Ryan Giglio Avatar asked Oct 08 '22 23:10

Ryan Giglio


2 Answers

There are many routes to solve this, most better than what I am about to suggest. I think the simplest and quickest solution, however, is to FTP to a tmp dir and when the transfer is completed trigger a move of the file to the production directory. Does your local automator job have such a mechanism in it's dealings with transmit?

like image 50
JAAulde Avatar answered Oct 12 '22 19:10

JAAulde


I would make the PHP script check the filesize in a loop with a (small) delay, and if it matches then output the file. Otherwise, loop until it does.

like image 40
gcochard Avatar answered Oct 12 '22 21:10

gcochard