Skip to content

Instantly share code, notes, and snippets.

@romaninsh
Last active February 19, 2016 13:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save romaninsh/099dc008a5898dc0bbd3 to your computer and use it in GitHub Desktop.
Save romaninsh/099dc008a5898dc0bbd3 to your computer and use it in GitHub Desktop.
FileStore for ATK (version 2.0) draft - You will need https://github.com/blueimp/jQuery-File-Upload and tpyo/amazon-s3-php-class
<?php
class Model_File extends LF_Model {
public $table = 'file';
public $storage = null;
public $local_copy = null;
function init(){
parent::init();
$this->addField('location'); // e.g. s3://bucket/file or http://host/file, or file://path/to/file
$this->addField('url'); // public access URL if file is available
$this->addField('storage'); // storage controller used
$this->addField('status')->enum(['draft','uploaded','verified']);
$this->containsOne(['file_metadata', 'json'=>true], [$this, 'initFileMetadata']);
$this->addHook('beforeDelete,afterUnload,afterLoad', $this);
}
function initFileMetadata($m){
$m->addField('extension');
$m->addField('size');
$m->addField('md5');
$m->addField('original_filename');
$m->addField('is_image')->type('boolean');
$m->addField('mime');
$m->addField('width');
$m->addField('height');
}
/**
* Call to associate with a file storage service (such as Amazon S3)
*/
function setStorage($controller){
$controller = $this->api->normalizeClassName($controller, 'Storage');
$this->storage = $this->setController($controller);
return $this;
}
function import($source, $original_filename=null){
if(!$this->storage)throw $this->exception('Set Storage Controller first');
$this->unload();
$this['status']='draft';
$this->save();
$m = $this->ref('file_metadata');
$m['original_filename'] = basename($original_filename?:$source);
$m['size'] = filesize($source);
$m['extension'] = pathinfo($original_filename?:$source, PATHINFO_EXTENSION);
// TODO: that's only for images
$is = getimagesize($source);
if($m['is_image'] = (boolean)$is){
$m['mime'] = $is['mime'];
$m['width'] = $is[0];
$m['height'] = $is[1];
$m['md5'] = md5_file($source);
//$m['extension'] = $is['mime'];
}
$m->save();
$this->storage->put($this, $source);
$this['status']='uploaded';
//$this->local_copy = $source;
$this->save();
}
/**
* Verify that the file is uploaded correctly
* @param [type] $source [description]
* @return [type] [description]
*/
function verify($source = null){
// make sure, that public URL is accessible
if(md5_file($this['url']) == $this->ref('file_metadata')['md5']){
$this['status'] = 'verified';
$this->save();
return true;
}
return false;
}
/**
* Retrieve file and store in local file
*/
function getFile(){
if($this->local_copy)return $this->local_copy;
return $this->local_copy = $this->storage->get($this);
}
function beforeDelete(){
try {
if($this->loaded() and $this->storage){
$this->storage->delete($this);
}
}catch(Exception $e){
// ignore
}
}
function __destruct(){
if($this->local_copy)@unlink($this->local_copy);
}
function afterLoad(){
if($this['storage'] == 's3' && !$this->storage)$this->setStorage('S3');
}
function afterUnload(){
if($this->local_copy)@unlink($this->local_copy);
$this->local_copy=null;
}
}
<?php
/**
* Represents an uploaded file which is still being processed
*/
class Model_Image extends SQL_Model
{
public $table="image";
public $default_storage='S3';
public $title_field='thumb_url';
public $thumb_width = 600;
public $thumb_height = 400;
function init()
{
parent::init();
$this->addField('status')
->enum(['draft','uploaded','thumbok','normalok','ready','linked']);
$this->hasOne('File','original_file_id');
$this->addField('original_url');
$this->hasOne('File','thumb_file_id');
$this->addField('thumb_url');
$this->hasOne('File','normal_file_id');
$this->addField('url');
$this->addField('type')->enum(['testing','loan','gallery']);
$this->addHook('beforeDelete', $this);
}
function setStorage($st){
$this->default_storage = $st;
return $this;
}
function autoRotateImage($i){
$orientation = $i->getImageOrientation();
switch($orientation) {
case imagick::ORIENTATION_BOTTOMRIGHT:
$i->rotateimage("#000", 180); // rotate 180 degrees
break;
case imagick::ORIENTATION_RIGHTTOP:
$i->rotateimage("#000", 90); // rotate 90 degrees CW
break;
case imagick::ORIENTATION_LEFTBOTTOM:
$i->rotateimage("#000", -90); // rotate 90 degrees CCW
break;
}
// Now that it's auto-rotated, make sure the EXIF data is correct in case the EXIF gets saved with the image!
$i->setImageOrientation(imagick::ORIENTATION_TOPLEFT);
}
function import($original,$original_filename = null) {
$this->unload();
$f = $this->ref('original_file_id','model');
$f->setStorage(
$this->default_storage,
$this->app->getConfig('amazon/s3/image-bucket').'/original'
);
if($this->debug)$f->debug();
$f->import($original,$original_filename);
$f->verify();
$this['original_file_id'] = $f->id;
$this['original_url'] = $f['url'];
$orig = $f->getFile();
$i = new \Imagick($orig);
$this->autoRotateImage($i);
$i->cropThumbnailImage($this->thumb_width, $this->thumb_height);
$thumb = tempnam('/tmp',$f->ref('file_metadata')['original_filename'].'.thumb.');
$i->writeImage($thumb);
unset($i);
$f = $this->ref('thumb_file_id','model');
if($this->debug)$f->debug();
$f->setStorage(
$this->default_storage,
$this->app->getConfig('amazon/s3/image-bucket').'/thumb'
);
$f->import($thumb, $original_filename);
$f->verify();
$this['thumb_file_id'] = $f->id;
$this['thumb_url'] = $f['url'];
$i = new \Imagick($orig);
$this->autoRotateImage($i);
$i->cropThumbnailImage($this->thumb_width*2, $this->thumb_height*2);
$norm = tempnam('/tmp',$f->ref('file_metadata')['original_filename'].'.norm.');
$i->writeImage($norm);
$f = $this->ref('normal_file_id','model');
if($this->debug)$f->debug();
$f->setStorage(
$this->default_storage,
$this->app->getConfig('amazon/s3/image-bucket').'/normal'
);
$f->import($norm, $original_filename);
$f->verify();
$this['normal_file_id'] = $f->id;
$this['url'] = $f['url'];
$this->save();
}
function ref($x,$t = null){
$m = parent::ref($x,$t);
if($m instanceof Model_File){
$m->setStorage('S3');
}
return $m;
}
function beforeDelete(){
try {
if($this['original_file_id'])$this->ref('original_file_id')->delete();
if($this['thumb_file_id'])$this->ref('thumb_file_id')->delete();
if($this['normal_file_id'])$this->ref('normal_file_id')->delete();
}catch(Exception $e){
//
}
}
}
<?php
abstract class Controller_Storage extends AbstractController {
public $default_exception = 'Exception_Storage';
/**
* Some file storage systems support public URLs to the
* file (CDN)
*/
public $supportPublicURL = false;
function init(){
parent::init();
if(!$this->owner instanceof Model_File)throw $this->exception('Add only into File model');
}
/**
* Places currently loaded file into the store. Second argument
* must contain the file which is being imported and it must be
* readable. Model will be modified with some additional information
* which can then further be used to retrieve the file.
*/
abstract function put($model, $source_file);
/**
* Retrieve file from the store, store in a local file and
* return name of that file. Can also return URL to public
* file.
*/
abstract function get($model);
/**
* Deletes associtaed file from the store.
*/
abstract function delete($model);
}
<?php
class Controller_Storage_S3 extends Controller_Storage {
public $useSSL = true;
public $endpoint = 's3-eu-west-1.amazonaws.com';
function connect(){
return new S3(
$this->app->getConfig('amazon/s3/Access_Key_ID'),
$this->app->getConfig('amazon/s3/Secret_Access_Key'),
$this->useSSL,
$this->endpoint
);
}
function put($model, $source_file){
// genetare unique filename
$uniq = uniqid().'.'.$model->ref('file_metadata')['extension'];
$s3 = $this->connect();
$res = $s3 -> putObject(
S3::inputFile($source_file, false),
$bucket = $this->app->getConfig('amazon/s3/image-bucket').'/original',
$uniq,
S3::ACL_PUBLIC_READ,
[],
['Content-Type' => $model->ref('file_metadata')['mime']]
);
$model['storage']='s3';
$model['location']='s3://'.$this->endpoint.'/'.$bucket.'/'.$uniq;
list($b1, $b2) = explode('/', $bucket, 2);
$model['url']='https://'.$b1.'.s3.amazonaws.com/'.$b2.'/'.$uniq;
return $res;
}
/**
* Break location such as s3://s3-eu-west-1.amazonaws.com/lf-image/original/55dc62824072f.png
* into bucket and filename.
*
* @return array(bucket, uri);
*/
function splitLocation($location){
// s3://s3-eu-west-1.amazonaws.com/lf-image/original/55dc62824072f.png
list(,$location) = explode('//', $location, 2);
// s3-eu-west-1.amazonaws.com/lf-image/original/55dc62824072f.png
list(,$location) = explode('/', $location, 2);
// lf-image/original/55dc62824072f.png
return [dirname($location), basename($location)];
// lf-image/original | 55dc62824072f.png
}
/**
* Retrieve file from the store, store in a local file and
* return name of that file. Can also return URL to public
* file.
*/
function get($model){
$s3 = $this->connect();
list($bucket,$file) = $this->splitLocation($model['location']);
$s = tempnam('/tmp','atk-file-');
$s3->getObject($bucket, $file, $s);
return $s;
}
/**
* Deletes associtaed file from the store.
*/
function delete($model){
$s3 = $this->connect();
list($bucket,$file) = $this->splitLocation($model['location']);
$s = tempnam('/tmp','atk-file-');
return $s3->deleteObject($bucket, $file, $s);
}
}
<?php
class Form_Field_Upload2 extends Form_Field {
public $vp;
function init(){
parent::init();
$this->app->jquery->addStaticInclude('jquery.iframe-transport');
$this->app->jquery->addStaticInclude('jquery.fileupload');
}
function setModel($m){
parent::setModel($m);
$this->vp = $this->app->page_object->add('VirtualPage');
$this->vp->set(function($f){
try {
error_reporting(E_ALL | E_STRICT);
if(!$_FILES[$this->short_name]['tmp_name']){
throw $this->exception('File data empty. Possibly it is too large')
->addMoreInfo('content-length',$_SERVER['CONTENT_LENGTH'])
->addMoreInfo('post_max_size',ini_get('post_max_size'))
;
}
$this->model->import($_FILES[$this->short_name]['tmp_name'], $_FILES[$this->short_name]['name']);
$res = $this->model->get();
echo json_encode($res);
exit;
}catch(Exception $e){
$res = ['exception' => $e->getHTML()];
echo json_encode($res);
exit;
}
});
return $this->model;
}
function getInput(){
if($_GET['val']){
if(!$this->value)$this->js(true)->trigger('uploaded'); // we just changed our value.
$this->value=$_GET['val'];
}
if($_GET['del']){
$f = $this->model->tryLoad($_GET['del']);
$this->js(true)->trigger('removed'); // we just changed our value.
if($f->loaded())$f->delete() ;
$this->value=null;
}
if($this->value){
$f = $this->model->tryLoad($this->value);
if($f->loaded()){
if($f instanceof Model_Image)$f=$f->ref('thumb_file_id');
$this->on('click', '.do-link')->univ()->dialogOK('Image','<img src="'.$f['url'].'"/>',null,['width'=>'1000']);
$this->on('click', '.do-remove-but', $this->form->js()->atk4_form('reloadField',$this->short_name,null,null,null,['del'=>$this->value]));
return '<div class="atk-form-field" data-shortname="'.$this->short_name.'" id="'.$this->getJSID().'">'.
parent::getInput(array('type'=>'hidden')).
'<div class="atk-cells atk-cells-gutter-small"><div class="atk-cell"><button class=" atk-button do-remove-but"><span class=" icon-trash" style=""></span>&nbsp;Remove</button></div><div class="atk-cell atk-expand"><div class="atk-effect-info atk-box-small atk-shape-rounded atk-text-nowrap">'.
'File: <a href="javascript: void(0)" class="do-link"><span class=" icon-file-image"
style=""></span>&nbsp;'.$f->ref('file_metadata')['original_filename'].'</a></div>
</div></div></div>';
}
}
$this->js(true)
->_load('jquery.iframe-transport')
->_load('jquery.fileupload')
->_selector('#'.$this->getJSID().' .do-upload')->fileupload([
'url'=>$this->vp->getURL(),
'dataType'=>'json',
'progressall'=>$this->js(null,'function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
if(progress!=100){
$("#'.$this->getJSID().'").find(".do-pill").css("width",progress+"%").find("span:first").text(progress+"%");
}else{
$("#'.$this->getJSID().'").find(".do-pill").css("width","100%").find("span:first").text("Processing file..");
$("#'.$this->getJSID().'").find(".do-spinner").show();
}
}'),
'start'=>$this->js(null,'function (e, data) {
$("#'.$this->getJSID().' .do-upload-but").attr("disabled",true).hide();
$("#'.$this->getJSID().'").find(".do-info").remove();
$("#'.$this->getJSID().'").find(".do-pill").removeClass("atk-effect-danger").addClass("atk-effect-info").show().css("width","0%");
}'),
'done'=>$this->js(null,'function (e, data) { if(data.result.exception){
var but = $(document.createElement("button")).insertAfter("#'.$this->getJSID().' .do-upload-but").addClass("atk-button").text("OK");
but.click(function(e){ e.preventDefault(); but.closest(".atk4_form_widget").atk4_form("reloadField","'.$this->short_name.'"); });
$("#'.$this->getJSID().'").find(".do-pill").text("Upload file problem").removeClass("atk-effect-info").addClass("atk-effect-danger");
$.univ().dialogOK("Upload Problem",data.result.exception,null,{width:1200});
return;
}; console.log(data);
$("#'.$this->getJSID().'").find(".do-pill").closest(".atk4_form_widget").atk4_form("reloadField","'.$this->short_name.'",null,null,null,{val:data.result.id});
}'),
]);
;
//$this->js('click', $this->js()->_selector('#upload')->click())->_selector('#upload-but');
$this->on('click','.do-upload-but',$this->js()->find('.do-upload')->click());
//$this->js('click', $this->js()->_selector('#pill')->show()->css('width','100%'))->_selector('#info')->remove();
//->css('width','100%');
// $this->js('click')->_selector('#upload-but')->univ()->alert(1);
return
'<div class="atk-form-field" data-shortname="'.$this->short_name.'" id="'.$this->getJSID().'"><input class=" atk-button do-upload" type="file" name="'.$this->short_name.'" multiple style="display: none">'.
'<div class="atk-cells atk-cells-gutter-small"><div class="atk-cell"><button class=" atk-button do-upload-but"><span class=" icon-upload"
style=""></span>&nbsp;Upload</button></div><div class="atk-cell atk-expand"><div
style="display: none; -webkit-transition: width 0.3s; width: 0%; left: 0; padding-left: 10px; position: relative; overflow: none"
class="atk-effect-info atk-box-small atk-shape-rounded atk-text-nowrap do-pill" style="overflow: hidden">Progress: <span>0%</span><span class="do-spinner animate-spin atk-move-right icon-spin4"
style="display: none"></span></div><div class="do-info">Select file then wait until upload finishes before submitting form.</div>
</div></div></div>';
}
}
class MyUploadHandler extends UploadHandler {
protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
$index = null, $content_range = null) {
$file = new \stdClass();
$file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error,
$index, $content_range);
$file->size = $this->fix_integer_overflow((int)$size);
$file->type = $type;
if ($this->validate($uploaded_file, $file, $error, $index)) {
$this->handle_form_data($file, $index);
$file_path = $this->get_upload_path($file->name);
var_dump($file_path, $uploaded_file, $name, $size, $type, $error, $index, $content_range);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment