2amigos / yii2-file-upload-widget

BlueImp File Upload Widget for Yii2
Other
251 stars 140 forks source link

UploadHandler #7

Closed andrewblake1 closed 10 years ago

andrewblake1 commented 10 years ago

have forked and developed implementation of this using the UploadHandler.php provided by Blueimp - working fine but coupled to my system for the moment.

It may be useful to others and if you think so then would love to develop it further (have used yii wheels in the past etc as well as many other awesome extension of course and would be good to offer something back) - with your input - hopefully to become part of your extension as opposed to a complete separate extension. I think a lot easier if only one blueimp file upload extension for yii2 rather than several to try and figure out which one suits them.

I don't have experience however creating useful extensions for other peoples use..

Use case: I have needed to allow the upload of files on a per model/ActiveRecord basis, e.g. a material may have 0-n drawings. Used XUpload in yii 1 - still had to customise it a lot though.

Basically want to fill in form data and select files for submission at the same time. Would expect this to be typical.

The trickiness of submitting other inputs in the form as well as the files seems to be that jquery-file-upload does a great job with files, and yii does a great a great job with the other form inputs.

Created FileUploadUIAR (for now), altered templates to hide the individual file submit inputs and only allow the button bar start submit (renamed as save). Because this doesn't allow submit unless files are selected, set click handler for the button to call yiiActiveForm click handler to submit form and deal with errors etc when no files selected. Gets a bit trickier when files are selected: submit thru jquery file upload (i.e. let its click handler submit) to actionUpload

actionUpload: merge model validation response with blueimps UploadHandler.php into a single json response. back on client: use callback to process the response and send the relevant json data to jquery-file-upload and the form validataion errors to yiiactiveform reset the file upload inputs to un-uploaded state (those that didn't error anyway) if any errors in uploading or validating/saving model.

The only problem struck was as described by http://www.yiiframework.com/forum/index.php/topic/57776-want-to-call-updateinputs-from-yiiactiveform/ which I haven't yet had any response on - i.e. it is working but hacked yii.ActiveForm.js

Next plan is to allow add files on a per attribute basis (0..n) instead of just a per model basis.

Incidentally, personally I store the files 'basically' under uploads/ModelName/primaryKeyValue/* as opposed to storing the path in the database which has been a maintenance problem in the past. Will store attribute related files under uploads/ModelName/primaryKeyValue/attribute/*. I say 'basically' asmy models form a tree structure which means rm -rf fired by exec in afterDelete event of the model will remove all child files (since can't execute system commands from after delete trigger in MySql) - but this is probably more complex than anyone else would be interested.

tonydspaniard commented 10 years ago

@andrewblake1 Thank you very much for your input. The reason why I didn't include validation and/or upload actions, is no other that the requirements of the application may vary quite a lot. Thats why we provided other tools such as https://github.com/2amigos/yii2-resource-manager-component to handle file upload whether is for AWS S3 or your file system...

andrewblake1 commented 10 years ago

Cool. Any demo or info about it?

tonydspaniard commented 10 years ago

I normally create a form where I save entity's basic info. Then I add media, or whatever extras. Only media directly related to the entity is uploaded with the main form, if any.

My controller's action (too many lines, but is more descriptive for you to understand what is going on):

 public function actionUpload($id)
{
    $tour = Tour::findOne($id);
    if (!$tour) {
        throw new NotFoundHttpException(Yii::t('app', 'Page not found'));
    }
    $picture = new TourPicture(['scenario' => 'upload']);
    $picture->tour_id = $id;
    $picture->image = UploadedFile::getInstance($picture, 'image');
    if ($picture->image !== null && $picture->validate(['image'])) {
        Yii::$app->response->getHeaders()->set('Vary', 'Accept');
        Yii::$app->response->format = Response::FORMAT_JSON;

        $response = [];

        if ($picture->save(false)) {
            $response['files'][] = [
                'name' => $picture->image->name,
                'type' => $picture->image->type,
                'size' => $picture->image->size,
                'url' => $picture->getImageUrl(),
                'thumbnailUrl' => $picture->getImageUrl(TourPicture::SMALL_IMAGE),
                'deleteUrl' => Url::to(['delete', 'id' => $picture->id]),
                'deleteType' => 'POST'
            ];
        } else {
            $response[] = ['error' => Yii::t('app', 'Unable to save picture')];
        }
        @unlink($picture->image->tempName);
    } else {
        if ($picture->hasErrors(['picture'])) {
            $response[] = ['error' => HtmlHelper::errors($picture)];
        } else {
            throw new HttpException(500, Yii::t('app', 'Could not upload file.'));
        }
    }
    return $response;
}

The beforeSave of the TourPicture model:

public function beforeSave($insert)
{
    if ($this->image && $this->image instanceof UploadedFile && !empty($this->image->tempName)) {
        $name = md5($this->image->name) . '.' . $this->image->getExtension();
        // I save two images and I have a helper that resizes them properly
        $large = ImageHelper::saveTemporaryImage($this->image->tempName, $name, self::LARGE_IMAGE);
        $small = ImageHelper::saveTemporaryImage($this->image->tempName, $name, self::SMALL_IMAGE);

       // pointer to the resourceManager (i have it configured to work with Amazon S3) 
        $manager = Yii::$app->resourceManager;

        $options = [
            'Bucket' => $manager->bucket,
            'Key' => self::LARGE_IMAGE.$name,
            'SourceFile' => $large,
            'ACL' => CannedAcl::PUBLIC_READ
        ];

        $manager->getClient()->putObject($options);

        $options['Key'] = self::SMALL_IMAGE.$name;
        $options['SourceFile'] = $small;

        $manager->getClient()->putObject($options);

        $this->name = $name;

        @unlink($large);
        @unlink($small);
    }

    return parent::beforeSave($insert);
}

The ImageHelper class :

class Image
{
    public static function saveTemporaryImage($filename, $name, $size)
    {
        list($width, $height) = explode('x', $size);

        $name = Yii::$app->getRuntimePath() . '/' . $size . $name;

        $file = Imagine::getImagine()->open($filename);

        if ($file->getSize()->getWidth() < $width) {
            $file->resize($file->getSize()->widen($width));
        }

        if ($file->getSize()->getHeight() < $height) {
            $file->resize($file->getSize()->heighten($height));
        }

        $file->thumbnail(new Box($width, $height), ManipulatorInterface::THUMBNAIL_OUTBOUND)
            ->save($name);

        return $name;
    }
} 

The resource manager configuration:

'components' => [
        // ...
        'resourceManager' => [
            'class' => 'dosamigos\resourcemanager\AmazonS3ResourceManager',
            'key' => '{YOURAWSKEY}',
            'secret' => '{YOURAWSSECRET}',
            'bucket' => '{YOURBUCKETNAME}'
        ],
      // ...

The resource manager can also be configured to work on the local system: https://github.com/2amigos/yii2-resource-manager-component/blob/master/FileSystemResourceManager.php

andrewblake1 commented 10 years ago

I like it. Will definitely use this. Very tidy approach. Had wondered how I was going to deal with s3 so this approach is great! Replaces a lot of the UploadHandler stuff from Blueimp - probably all of it though still need correctly formatted response to update the display of files - no major.

Still suspect the need for a more versatile widget on the client side that accepts other form inputs and handles submission and validation of those inputs and files at the same time.

Thanks for your help

Cheers Andrew

On 1/10/2014, at 12:37 am, Antonio Ramirez notifications@github.com wrote:

I normally create a form where I save entity's basic info. Then I add media, or whatever extras. Only media directly related to the entity is uploaded with the main form, if any.

My controller's action (too many lines, but is more descriptive for you to understand what is going on):

public function actionUpload($id) { $tour = Tour::findOne($id); if (!$tour) { throw new NotFoundHttpException(Yii::t('app', 'Page not found')); } $picture = new TourPicture(['scenario' => 'upload']); $picture->tour_id = $id; $picture->image = UploadedFile::getInstance($picture, 'image'); if ($picture->image !== null && $picture->validate(['image'])) { Yii::$app->response->getHeaders()->set('Vary', 'Accept'); Yii::$app->response->format = Response::FORMAT_JSON;

    $response = [];

    if ($picture->save(false)) {
        $response['files'][] = [
            'name' => $picture->image->name,
            'type' => $picture->image->type,
            'size' => $picture->image->size,
            'url' => $picture->getImageUrl(),
            'thumbnailUrl' => $picture->getImageUrl(TourPicture::SMALL_IMAGE),
            'deleteUrl' => Url::to(['delete', 'id' => $picture->id]),
            'deleteType' => 'POST'
        ];
    } else {
        $response[] = ['error' => Yii::t('app', 'Unable to save picture')];
    }
    @unlink($picture->image->tempName);
} else {
    if ($picture->hasErrors(['picture'])) {
        $response[] = ['error' => HtmlHelper::errors($picture)];
    } else {
        throw new HttpException(500, Yii::t('app', 'Could not upload file.'));
    }
}
return $response;

} The beforeSave of the TourPicture model:

public function beforeSave($insert) { if ($this->image && $this->image instanceof UploadedFile && !empty($this->image->tempName)) { $name = md5($this->image->name) . '.' . $this->image->getExtension(); // I save two images and I have a helper that resizes them properly $large = ImageHelper::saveTemporaryImage($this->image->tempName, $name, self::LARGE_IMAGE); $small = ImageHelper::saveTemporaryImage($this->image->tempName, $name, self::SMALL_IMAGE);

   // pointer to the resourceManager (i have it configured to work with Amazon S3) 
    $manager = Yii::$app->resourceManager;

    $options = [
        'Bucket' => $manager->bucket,
        'Key' => self::LARGE_IMAGE.$name,
        'SourceFile' => $large,
        'ACL' => CannedAcl::PUBLIC_READ
    ];

    $manager->getClient()->putObject($options);

    $options['Key'] = self::SMALL_IMAGE.$name;
    $options['SourceFile'] = $small;

    $manager->getClient()->putObject($options);

    $this->name = $name;

    @unlink($large);
    @unlink($small);
}

return parent::beforeSave($insert);

} The ImageHelper class :

class Image { public static function saveTemporaryImage($filename, $name, $size) { list($width, $height) = explode('x', $size);

    $name = Yii::$app->getRuntimePath() . '/' . $size . $name;

    $file = Imagine::getImagine()->open($filename);

    if ($file->getSize()->getWidth() < $width) {
        $file->resize($file->getSize()->widen($width));
    }

    if ($file->getSize()->getHeight() < $height) {
        $file->resize($file->getSize()->heighten($height));
    }

    $file->thumbnail(new Box($width, $height), ManipulatorInterface::THUMBNAIL_OUTBOUND)
        ->save($name);

    return $name;
}

} The resource manager configuration:

'components' => [ // ... 'resourceManager' => [ 'class' => 'dosamigos\resourcemanager\AmazonS3ResourceManager', 'key' => '{YOURAWSKEY}', 'secret' => '{YOURAWSSECRET}', 'bucket' => '{YOURBUCKETNAME}' ], // ... The resource manager can also be configured to work on the local system: https://github.com/2amigos/yii2-resource-manager-component/blob/master/FileSystemResourceManager.php

— Reply to this email directly or view it on GitHub.

tonydspaniard commented 10 years ago

probably all of it though still need correctly formatted response to update the display of files - no major.

Is done here:

        if ($picture->save(false)) {
            $response['files'][] = [
                'name' => $picture->image->name,
                'type' => $picture->image->type,
                'size' => $picture->image->size,
                'url' => $picture->getImageUrl(),
                'thumbnailUrl' => $picture->getImageUrl(TourPicture::SMALL_IMAGE),
                'deleteUrl' => Url::to(['delete', 'id' => $picture->id]),
                'deleteType' => 'POST'
            ];
        } else {
            $response[] = ['error' => Yii::t('app', 'Unable to save picture')];
        }
andrewblake1 commented 10 years ago

got it

Cheers

On 1/10/2014, at 12:55 am, Antonio Ramirez notifications@github.com wrote:

probably all of it though still need correctly formatted response to update the display of files - no major.

Is done here:

    if ($picture->save(false)) {
        $response['files'][] = [
            'name' => $picture->image->name,
            'type' => $picture->image->type,
            'size' => $picture->image->size,
            'url' => $picture->getImageUrl(),
            'thumbnailUrl' => $picture->getImageUrl(TourPicture::SMALL_IMAGE),
            'deleteUrl' => Url::to(['delete', 'id' => $picture->id]),
            'deleteType' => 'POST'
        ];
    } else {
        $response[] = ['error' => Yii::t('app', 'Unable to save picture')];
    }

— Reply to this email directly or view it on GitHub.

andrewblake1 commented 10 years ago

Hi Antonio

Have finished the widget I was working on - using your classes etc above and derived from your yii2-file-upload-widget. For now I have kept it under the dosamigos\fileupload namespace in case you might at sometime want to use it - its currently not quite ready for that - not heavily tested but no known issues.

It allows many attributes of active record to have 0 .. n files associated where the paths are not stored in a database but associated by model name and primary key in storage.

E.g an Account profile model might want, a logo-image, a banner-image, and several outside-photos, the logo-image could be mandatory and the out-side photos property could have up to 10 images. This could be achieved within multiple forms. This widget does it in 1 form, and show thumbnail of each etc as per jqueryfileupload

It sends all files and form data in a single request (as well as delete list), and validates all form fields and files individually by your example above and overall per attribute in the case of maxFiles. If any part of validation fails the, files arn't stored nor database updated. Basically like typical actionCreate and actionUpdate. It does take into account existing files with maxFiles validation on update. Errors are reported as would be expected i.e. for each file, for the attribute as a whole(maxFiles) and for the other form data - though that did require a minor hack of yiiActiveForm as documented and raised on github and forum.

I have stripped out the non applicable files for this widget from my fork (for simplicity) and done my best for now to document it within the code and readme in case at some stage you can use it.

Am not expecting anyone else to use this so havn't completely de-coupled - though minor and documented in the example traits.

If you think it will be of no use then I will change the namespace to save confusion. On the other hand, happy to simplify it further or alter it in any way to make it more re-usable if you have suggestions. I know that applications will vary a lot, but storing files against AR is obviously common requirement and doesn't seem to be too many good examples of an approach - fileuploading sure, but what to do with the files once they hit the temporary directory seems under documented currently re AR as does how to deal with multiple files, and multiple file attributes per AR.

Cheers Andrew

tonydspaniard commented 10 years ago

How can I see it? El 13/10/2014 02:21, "Andrew Blake" notifications@github.com escribió:

Hi Antonio

Have finished the widget I was working on - using your classes etc above and derived from your yii2-file-upload-widget. For now I have kept it under the dosamigos\fileupload namespace in case you might at sometime want to use it - its currently not quite ready for that - not heavily tested but no known issues.

It allows many attributes of active record to have 0 .. n files associated where the paths are not stored in a database but associated by model name and primary key in storage.

E.g an Account profile model might want, a logo-image, a banner-image, and several outside-photos, the logo-image could be mandatory and the out-side photos property could have up to 10 images. This could be achieved within multiple forms. This widget does it in 1 form, and show thumbnail of each etc as per jqueryfileupload

It sends all files and form data in a single request (as well as delete list), and validates all form fields and files individually by your example above and overall per attribute in the case of maxFiles. If any part of validation fails the, files arn't stored nor database updated. Basically like typical actionCreate and actionUpdate. It does take into account existing files with maxFiles validation on update. Errors are reported as would be expected i.e. for each file, for the attribute as a whole(maxFiles) and for the other form data - though that did require a minor hack of yiiActiveForm as documented and raised on github and forum.

I have stripped out the non applicable files for this widget from my fork (for simplicity) and done my best for now to document it within the code and readme in case at some stage you can use it.

Am not expecting anyone else to use this so havn't completely de-coupled - though minor and documented in the example traits.

If you think it will be of no use then I will change the namespace to save confusion. On the other hand, happy to simplify it further or alter it in any way to make it more re-usable if you have suggestions. I know that applications will vary a lot, but storing files against AR is obviously common requirement and doesn't seem to be too many good examples of an approach - fileuploading sure, but what to do with the files once they hit the temporary directory seems under documented currently re AR as does how to deal with multiple files, and multiple file attributes per AR.

Cheers Andrew

— Reply to this email directly or view it on GitHub https://github.com/2amigos/yii2-file-upload-widget/issues/7#issuecomment-58829961 .

andrewblake1 commented 10 years ago

Code is in my fork https://github.com/andrewblake1/yii2-file-upload-widget

Demo may take me a few days to get to, but happy to set one up if this is what you mean - probably within a week?

On 13/10/2014, at 2:25 pm, Antonio Ramirez notifications@github.com wrote:

How can I see it? El 13/10/2014 02:21, "Andrew Blake" notifications@github.com escribió:

Hi Antonio

Have finished the widget I was working on - using your classes etc above and derived from your yii2-file-upload-widget. For now I have kept it under the dosamigos\fileupload namespace in case you might at sometime want to use it - its currently not quite ready for that - not heavily tested but no known issues.

It allows many attributes of active record to have 0 .. n files associated where the paths are not stored in a database but associated by model name and primary key in storage.

E.g an Account profile model might want, a logo-image, a banner-image, and several outside-photos, the logo-image could be mandatory and the out-side photos property could have up to 10 images. This could be achieved within multiple forms. This widget does it in 1 form, and show thumbnail of each etc as per jqueryfileupload

It sends all files and form data in a single request (as well as delete list), and validates all form fields and files individually by your example above and overall per attribute in the case of maxFiles. If any part of validation fails the, files arn't stored nor database updated. Basically like typical actionCreate and actionUpdate. It does take into account existing files with maxFiles validation on update. Errors are reported as would be expected i.e. for each file, for the attribute as a whole(maxFiles) and for the other form data - though that did require a minor hack of yiiActiveForm as documented and raised on github and forum.

I have stripped out the non applicable files for this widget from my fork (for simplicity) and done my best for now to document it within the code and readme in case at some stage you can use it.

Am not expecting anyone else to use this so havn't completely de-coupled - though minor and documented in the example traits.

If you think it will be of no use then I will change the namespace to save confusion. On the other hand, happy to simplify it further or alter it in any way to make it more re-usable if you have suggestions. I know that applications will vary a lot, but storing files against AR is obviously common requirement and doesn't seem to be too many good examples of an approach - fileuploading sure, but what to do with the files once they hit the temporary directory seems under documented currently re AR as does how to deal with multiple files, and multiple file attributes per AR.

Cheers Andrew

— Reply to this email directly or view it on GitHub https://github.com/2amigos/yii2-file-upload-widget/issues/7#issuecomment-58829961 .

— Reply to this email directly or view it on GitHub.