From 7cb4f9fb0ec151dc2eda45bba9e6f30093e96849 Mon Sep 17 00:00:00 2001 From: Samuel ROZE Date: Tue, 30 Dec 2014 13:47:40 +0100 Subject: [PATCH] Fixes #14: Added documentation for version 2 --- README.md | 31 +++-- Resources/doc/advanced.md | 84 ++++++++++++ Resources/doc/installation.md | 63 +++++++++ Resources/doc/reference.md | 32 +++++ Resources/doc/upload-ways.md | 243 ++++++++++++++++++++++++++++++++++ Resources/doc/usage.md | 100 ++++++++++++++ 6 files changed, 544 insertions(+), 9 deletions(-) create mode 100644 Resources/doc/advanced.md create mode 100644 Resources/doc/installation.md create mode 100644 Resources/doc/reference.md create mode 100644 Resources/doc/upload-ways.md create mode 100644 Resources/doc/usage.md diff --git a/README.md b/README.md index 2c0306b..db67968 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,30 @@ [![Build Status](https://api.travis-ci.org/sroze/SRIORestUploadBundle.png)](https://travis-ci.org/sroze/SRIORestUploadBundle) -## Version 2 +This bundle provide a simple ways to handle uploads on the server side. -The new major version 2.0.0 introduce a lot of new features such as: +Currently, it supports the simple, form-data, multipart and resumable ways. -- Using Gaufrette as storage, which allow you to push files on local filesystem, Amazon S3 or any provider... -- Add a NamingStrategy that allow to have a custom naming convention -- Add a DirectoryStrategy that allow to vote and create a sub-directory ordering system -- ... +## Getting started -The code is in the current `master` branch but **documentation is coming soon...** +Using [Gaufrette](https://github.com/KnpLabs/Gaufrette) as storage layer, you can handle file uploads and store files on many places such as a local file system, an Amazon S3 bucket, ... -## Legacy +- [Installation](Resources/doc/installation.md) +- [Usage](Resources/doc/usage.md) +- [Advanced usage](Resources/doc/advanced.md) +- [Upload ways summary](Resources/doc/upload-ways.md) +- [Configuration reference](Resources/doc/reference.md) -* Read documentation of 1.1.1 +## Testing + +Tests are run with [PHPUnit](http://phpunit.de). Once you installed dependencies with composer, then: + +- Create a database, allow access to a user, and set configuration in `Tests/Fixtures/App/app/config/parameters.yml` file +- Create the database schema for the `test` environment + ```sh + php Tests/Fixtures/App/app/console doctrine:schema:update --force --env=test + ``` +- Run PHPUnit + ```sh + phpunit + ``` diff --git a/Resources/doc/advanced.md b/Resources/doc/advanced.md new file mode 100644 index 0000000..160bd7f --- /dev/null +++ b/Resources/doc/advanced.md @@ -0,0 +1,84 @@ +# Advanced usage + +## Strategies + +You can set naming and storage strategies for each defined storage. +```yml +srio_rest_upload: + storages: + default: + filesystem: gaufrette.default_filesystem + naming_strategy: your_naming_strategy_service + storage_strategy: your_storage_strategy_service +``` + +### Naming strategy + +The naming strategy is responsible to set the name that the stored file will have. The [default naming strategy](../../Strategy/DefaultNamingStrategy.php) create a random file name. + +To create your own strategy you just have to create a class that implements the `NamingStrategy` interface. Here's an example with a strategy that generate a random file name but with its extension or the default one as fallback. + +```php +namespace Acme\Storage\Strategy; + +use SRIO\RestUploadBundle\Upload\UploadContext; +use SRIO\RestUploadBundle\Strategy\NamingStrategy; + +class DefaultNamingStrategy implements NamingStrategy +{ + const DEFAULT_EXTENSION = 'png'; + + /** + * {@inheritdoc} + */ + public function getName(UploadContext $context) + { + $name = uniqid(); + $extension = self::DEFAULT_EXTENSION; + + if (($request = $context->getRequest()) !== null) { + $files = $request->files->all(); + + /** @var $file \Symfony\Component\HttpFoundation\File\UploadedFile */ + $file = array_pop($files); + + if ($file !== null) { + $parts = explode('.', $file->getClientOriginalName()); + $extension = array_pop($parts); + } + } + + return $name.'.'.$extension; + } +} +``` + +Then, define a service and change the `naming_strategy` of your storage configuration with the created service ID. + +### Storage strategy + +It defines the (sub)directory in which the file will be created in your filesystem. + +The storage strategy is working the same way than the naming strategy: create a service with a class that implements `StorageStrategy` and set the `storage_strategy` configuration of your storage with the created service. + +## Create a custom handler + +You can easily create your custom upload providers (and feel free to _PR_ them on GitHub) by creating a [tagged service](http://symfony.com/doc/current/components/dependency_injection/tags.html) with the `rest_upload.processor` tag + +```yml + + Acme\AcmeBundle\Processor\MyUploadProcessor + + + + + + + + +``` + +Note the `uploadType` attribute that define the unique name of the upload way, set in the `uploadType` query parameters. + +Your `MyUploadProcessor` class should then implements the [`ProcessorInterface`](../../Processor/ProcessorInterface.php) or extends the [`AbstractUploadProcessor`](../../Processor/AbstractUploadProcessor.php) + diff --git a/Resources/doc/installation.md b/Resources/doc/installation.md new file mode 100644 index 0000000..f1012d7 --- /dev/null +++ b/Resources/doc/installation.md @@ -0,0 +1,63 @@ +# Installation + +First, you need to install [KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle), a Symfony integration of Gaufrette which will handle the file storage on places your want. + +## Add SRIORestUploadBundle in your dependencies + +In your `composer.json` file, add `srio/rest-upload-bundle`: +```json +{ + "require": { + "srio/rest-upload-bundle": "~2.0.0" + } +} +``` + +Then, update your dependencies: +``` +composer update srio/rest-upload-bundle +``` + +## Enable the bundle in your kernel + +```php +// app/AppKernel.php +public function registerBundles() +{ + $bundles = array( + // ... + new SRIO\RestUploadBundle\SRIORestUploadBundle(), + ); +} +``` + +## Create the Gaufrette filesystem + +In your configuration file, create your Gaufrette filesystem. Let's start with a local filesystem storage in the `web/uploads` directory. + +```yml +# app/config/config.yml + +knp_gaufrette: + adapters: + local_uploads: + local: + directory: %kernel.root_dir%/../web/uploads + filesystems: + uploads: + adapter: local_uploads +``` + +## Configure the bundle + +Then, we just have to configure the bundle to use the Gaufrette storage: +``` +srio_rest_upload: + storages: + default: + filesystem: gaufrette.uploads_filesystem +``` + +If you want to use the resumable upload way, you have to [configure it](upload-ways.md#resumable-configuration). + +Then, [start using it](usage.md). diff --git a/Resources/doc/reference.md b/Resources/doc/reference.md new file mode 100644 index 0000000..e2699b2 --- /dev/null +++ b/Resources/doc/reference.md @@ -0,0 +1,32 @@ +# Configuration reference + +```yml +srio_rest_upload: + # Define the available storages + storages: + name: + # Filesystem service created by Gaufrette (or your own matching the Gaufrette's interface) + filesystem: fs_service_id + + # Naming strategy service + naming_strategy: srio_rest_upload.naming.default_strategy + + # Storage strategy service + storage_strategy: srio_rest_upload.storage.default_strategy + + # The storage voter, that chose between storage based on upload context + storage_voter: srio_rest_upload.storage_voter.default + + # The default storage name. With the default storage voter, it'll use + # the first defined storage if value is null + default_storage: ~ + + # If you want to use the resumable upload way, you must set + # the class name of your entity which store the upload sessions. + resumable_entity: ~ + + # Parameter the define the upload way, internally the provider selector + upload_type_parameter: uploadType +``` + +The [Advanced usage](advanced.md) section explain the naming and storage strategies. diff --git a/Resources/doc/upload-ways.md b/Resources/doc/upload-ways.md new file mode 100644 index 0000000..cd24286 --- /dev/null +++ b/Resources/doc/upload-ways.md @@ -0,0 +1,243 @@ +# Upload ways + +This is a summary of currently supported upload ways. + +- [Simple](#simple-upload-way): Send binary data to an URL and use query parameters to submit additional data. +- [Multipart](#multipart-upload-way): Send both JSON and binary data using the [multipart Content-Type](http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). +- [FormData](#formdata-upload-way): Matches the classic browser file upload +- [Resumable](#resumable-upload-way): start a resumable upload session by sending JSON data and then send file entirely or by chunks. It allow to restart a failed upload where it stops. + +## Simple upload way + +The most straightforward method for uploading a file is by making a simple upload request. This option is a good choice when: +- The file is small enough to upload again in its entirety if the connection fails +- There is no or a very small amount of metadata to send + +To use simple upload, make a `POST` or `PUT` request to the upload method's URI and add the query parameter `uploadType=simple`. +The HTTP headers to use when making a simple upload request include: +- `Content-Type`. Set the media content-type +- `Content-Length`. Set to the number of bytes you are uploading. + +### Example + +The following example shows the use of a simple photo upload request for an upload path that would be `/upload`: + +``` +POST /upload?uploadType=simple HTTP/1.1 +Host: www.example.com +Content-Type: image/jpeg +Content-Length: number_of_bytes_in_JPEG_file + +JPEG data +``` + +## Multipart upload way + +If you have metadata that you want to send along with the data to upload, you can make a single `multipart/related` request. This is a good choice if the data you are sending is small enough to upload again in its entirety if the connection fails. +To use multipart upload, make a `POST` or `PUT` request to the upload method's URI and add the query parameter `uploadType=multipart`. + +The top-level HTTP headers to use when making a multipart upload request include: +- `Content-Type`. Set to `multipart/related` and include the boundary string you're using to identify the parts of the request. +- `Content-Length`. Set to the total number of bytes in the request body. + +The body of the request is formatted as a `multipart/related` content type [RFC2387] and contains exactly two parts. The parts are identified by a boundary string, and the final boundary string is followed by two hyphens. + +Each part of the multipart request needs an additional `Content-Type` header: +- **Metadata part**: Must come first, and Content-Type must match one of the the accepted metadata formats. +- **Media part**: Must come second, and Content-Type must match one the method's accepted media MIME types. + +### Example + +The following example shows the use of a multipart upload request for an upload path that would be `/upload`: + +``` +POST /upload?uploadType=multipart HTTP/1.1 +Host: www.example.com +Content-Type: multipart/related; boundary="foo_bar_baz" +Content-Length: number_of_bytes_in_entire_request_body + +--foo_bar_baz +Content-Type: application/json; charset=UTF-8 + +{ + "name": "Some value" +} + +--foo_bar_baz +Content-Type: image/jpeg + +JPEG data + +--foo_bar_baz-- +``` + +### FormData upload way + +This may be the most used way to upload files: it matches with the classic form "file" upload. + +You just have to have a field of type `file` named `file` on your form and set the action path to `/upload?uploadType=formData`. +It can ether be used with any XHR upload method. + +## Resumable upload way + +To upload data files more reliably, you can use the resumable upload protocol. This protocol allows you to resume an upload operation after a communication failure has interrupted the flow of data. It is especially useful if you are transferring large files and the likelihood of a network interruption or some other transmission failure is high, for example, when uploading from a mobile client app. It can also reduce your bandwidth usage in the event of network failures because you don't have to restart large file uploads from the beginning. + +The steps for using resumable upload include: + +1. [Start a resumable session](#start-resumable). Make an initial request to the upload URI that includes the metadata, if any. +2. [Save the resumable session URI](#save-session-uri). Save the session URI returned in the response of the initial request; you'll use it for the remaining requests in this session. +3. [Upload the file](#upload-resumable). Send the media file to the resumable session URI. + +In addition, apps that use resumable upload need to have code to [resume an interrupted upload](#resume-upload). If an upload is interrupted, find out how much data was successfully received, and then resume the upload starting from that point. + +### Resumable Configuration + +First, you need to configure the bundle. + +#### Create your `ResumableUploadSession` entity + +The entity will contains the resumable upload sessions and is required if you want the resumable way of upload to work. + +```php +get('srio_rest_upload.upload_handler'); + $result = $uploadHandler->handleRequest($request); + + if (($response = $result->getResponse()) !== null) { + return $response; + } + + if (($file = $result->getFile()) !== null) { + // Store the file path in an entity, call an API, + // do whatever with the uploaded file here. + + return new Response(); + } + + throw new BadRequestHttpException('Unable to handle upload request'); + } +} +``` + +## With form + +Because most of the time you may want to link a form to the file upload, you're able to handle it too. +Depending on the [upload way](upload-ways.md) you're using, form data will be fetched from request body or HTTP parameters. + +Here's an example of a controller with a form (it comes directly from tests, [feel free to have a look](../../Tests/Fixtures/Controller/UploadController.php), you'll have all sources): +```php +class UploadController extends Controller +{ + public function uploadAction(Request $request) + { + $form = $this->createForm(new MediaFormType()); + + /** @var $uploadHandler UploadHandler */ + $uploadHandler = $this->get('srio_rest_upload.upload_handler'); + $result = $uploadHandler->handleRequest($request, $form); + + if (($response = $result->getResponse()) != null) { + return $response; + } + + if (!$form->isValid()) { + throw new BadRequestHttpException(); + } + + if (($file = $result->getFile()) !== null) { + /** @var $media Media */ + $media = $form->getData(); + $media->setFile($file); + + $em = $this->getDoctrine()->getManager(); + $em->persist($media); + $em->flush(); + + return new JsonResponse($media); + } + + throw new NotAcceptableHttpException(); + } +} +``` + +## On the client side + +Here's a simple example of an upload (which is using the form-data handler) using [AngularJS's `$upload` service](https://github.com/danialfarid/angular-file-upload): +```js +$upload.upload({ + url: '/path/to/upload?uploadType=formData', + method: 'POST', + file: file +}) +``` +