Wednesday, April 26, 2017

No URL? No Problem. Adding local file support to 3shrink.com

3shrink does a great job of linking online resources with offline media. For example, shrinking a lengthy Google Mail URL into a 3 letter code that gets hand written on a TODO list. But what if the resource isn't yet online? Say, a photo I've just snapped with my cell phone, or a PDF I've downloaded to my local computer.

To support this use case, I'd need to add the ability to upload arbitrary files to 3shrink. So that's what I did.

The first step was detecting a file upload. Because I plan to use this functionality with Tasker, I based this functionality on its HTTP POST Action. Here's the code that powers the detection:

function is_upload_request() {
  return (g($_SERVER, 'REQUEST_METHOD') == 'POST' &&
          g($_SERVER, 'CONTENT_TYPE')   == 'application/octet-stream' && 
          geek_val()                    == GEEK_KEY);
}

Most of that's pretty standard, except for the geek related code. I setup 3shrink so that if the URL parameter geek is provided, then the system operates in a command line friendly way. In this latest version of 3shrink, I've extended this in two ways: first, geek can be provided as either a URL parameter or a cookie, the latter being more convenient for the HTTP POST Action. And second, I only allow uploads from requests that know the value of GEEK_KEY. So while anyone on the web can 3shrink a URL, only those who have requested the key from me (please do!), can perform file uploads.

Once I've detected and upload, storing the data in S3 and generating a URL to it, was surprisingly straightforward:

function do_upload() {
  $in_fd     = fopen("php://input", 'r');
  $buffer    = tempnam("/tmp", "upload");
  $buffer_fd = fopen($buffer, 'w');
  while($data = fgets($in_fd)) {
    fputs($buffer_fd, $data);
  }
  fclose($in_fd);
  fclose($buffer_fd);
  $info      = finfo_open();
  $mime_type = finfo_file($info, $buffer, FILEINFO_MIME_TYPE);
  $name      = md5_file($buffer);
  $s3 = upload_s3_client();
  if(!$s3->doesObjectExist(UPLOAD_BUCKET, $name)) {
    $s3->putObject(['Bucket'      => UPLOAD_BUCKET,
                    'Key'         => $name,
                    'SourceFile'  => $buffer,
                    'ContentType' => $mime_type]);
                    
  }
  return $s3->getObjectUrl(UPLOAD_BUCKET, $name);
}

I'm suing the finfo library offered by PHP to effortlessly determine the mime type, and the Amazon PHP library to push the data to S3. I'm basing the name of the file on its contents, which means that multiple attempts to shrink the same file always returns the same 3 letter code.

I know that PHP isn't a particularly sexy language, but I'm impressed at how effortlessly I was able to implement offline storage, including mime type detection. Much of what makes this code especially cool isn't visible. For example, the PHP library code is managed by composer, and authentication to Amazon is powered by IAM roles, and is therefore automatic. Library management and authentication aren't typically hard, but they're do contribute to the friction in getting work done. And in the above solution, much of that friction has been eliminated.

To integrate uploading into the shrinking process was almost too easy:

if(is_upload_request()) {
  $content = do_upload();
} else {
  $content = g($_GET, 'i');
}

That's because the result of do_upload is a URL that the rest of the 3shrink process can work with.

With the code in place, here's how it can be invoked using curl:

curl -s -H 'Content-Type: application/octet-stream' \
     -b geek=xxxxxxxxxxxx \
     --data-binary @foo.jpg \
      'http://projx.3shrink.com/shrink'

With this foundation in place, the next step is to enhance the AutoSharing capability on a mobile device to allow uploading of files. When that's done, I should be able to effortlessly snap a picture and moments later, have a 3 letter code that represents that picture.

No comments:

Post a Comment