├── .gitignore
├── .travis.yml
├── DependencyInjection
├── CompilerPass
│ └── ProcessorCompilerPass.php
├── Configuration.php
├── Factory
│ └── StorageFactory.php
└── SRIORestUploadExtension.php
├── Entity
└── ResumableUploadSession.php
├── Exception
├── FileExistsException.php
├── FileNotFoundException.php
├── InternalUploadProcessorException.php
├── UploadException.php
└── UploadProcessorException.php
├── Model
└── UploadableFileInterface.php
├── Processor
├── AbstractUploadProcessor.php
├── FormDataUploadProcessor.php
├── MultipartUploadProcessor.php
├── ProcessorInterface.php
├── ResumableUploadProcessor.php
└── SimpleUploadProcessor.php
├── README.md
├── Request
├── RequestContentHandler.php
└── RequestContentHandlerInterface.php
├── Resources
├── config
│ ├── doctrine
│ │ └── ResumableUploadSession.orm.xml
│ ├── handlers.xml
│ ├── processors.xml
│ ├── storage.xml
│ └── strategy.xml
└── doc
│ ├── advanced.md
│ ├── installation.md
│ ├── reference.md
│ ├── upload-ways.md
│ └── usage.md
├── SRIORestUploadBundle.php
├── Storage
├── FileAdapterInterface.php
├── FileStorage.php
├── FilesystemAdapterInterface.php
├── FlysystemFileAdapter.php
├── FlysystemFilesystemAdapter.php
├── GaufretteFileAdapter.php
├── GaufretteFilesystemAdapter.php
└── UploadedFile.php
├── Strategy
├── DefaultNamingStrategy.php
├── DefaultStorageStrategy.php
├── NamingStrategy.php
└── StorageStrategy.php
├── Tests
├── Fixtures
│ ├── App
│ │ ├── app
│ │ │ ├── AppKernel.php
│ │ │ ├── config
│ │ │ │ ├── config.yml
│ │ │ │ ├── config_flysystem.yml
│ │ │ │ ├── config_gaufrette.yml
│ │ │ │ ├── config_test.yml
│ │ │ │ ├── parameters.yml
│ │ │ │ ├── parameters.yml.dist
│ │ │ │ └── routing.yml
│ │ │ └── console
│ │ └── web
│ │ │ ├── .gitignore
│ │ │ └── uploads
│ │ │ └── .gitkeep
│ ├── Controller
│ │ └── UploadController.php
│ ├── Entity
│ │ ├── Media.php
│ │ └── ResumableUploadSession.php
│ ├── Form
│ │ └── Type
│ │ │ └── MediaFormType.php
│ └── Resources
│ │ ├── apple.gif
│ │ └── lorem.txt
├── Processor
│ ├── AbstractProcessorTestCase.php
│ ├── MultipartUploadProcessorTest.php
│ └── ResumableUploadProcessorTest.php
├── Request
│ └── RequestContentHandlerTest.php
├── Upload
│ ├── AbstractUploadTestCase.php
│ ├── MultipartUploadTest.php
│ ├── ResumableUploadTest.php
│ └── SimpleUploadTest.php
└── bootstrap.php
├── Upload
├── StorageHandler.php
├── UploadContext.php
├── UploadHandler.php
└── UploadResult.php
├── Voter
└── StorageVoter.php
├── composer.json
├── composer.lock
├── phpunit.xml.dist
└── test.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | *~
4 |
5 | /vendor
6 | /cov
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | env:
3 | - DB=mysql
4 |
5 | php:
6 | - 5.5
7 | - 5.4
8 |
9 | before_script:
10 | - composer install --dev --no-interaction
11 | - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS restuploadbundle;'; fi"
12 | - php Tests/Fixtures/App/app/console doctrine:schema:update --force --env=test
13 |
14 | script:
15 | - sh -c "export TEST_FILESYSTEM="Gaufrette"; phpunit"
16 | - sh -c "export TEST_FILESYSTEM="Flysystem"; phpunit"
17 |
18 | notifications:
19 | email:
20 | - travis+restuploadbundle@sroze.io
21 | - samuel.roze@gmail.com
--------------------------------------------------------------------------------
/DependencyInjection/CompilerPass/ProcessorCompilerPass.php:
--------------------------------------------------------------------------------
1 | hasDefinition('srio_rest_upload.upload_handler')) {
14 | return;
15 | }
16 |
17 | $uploadHandlerDefinition = $container->getDefinition('srio_rest_upload.upload_handler');
18 | $processorDefinitions = $container->findTaggedServiceIds('rest_upload.processor');
19 |
20 | foreach ($processorDefinitions as $id => $tagAttributes) {
21 | foreach ($tagAttributes as $attributes) {
22 | if (!array_key_exists('uploadType', $attributes)) {
23 | throw new \LogicException('A "rest_upload.processor" tag must have "uploadType" attribute');
24 | }
25 |
26 | $uploadHandlerDefinition->addMethodCall('addProcessor', array($attributes['uploadType'], new Reference($id)));
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | root('srio_rest_upload');
22 |
23 | $rootNode
24 | ->children()
25 | ->arrayNode('storages')
26 | ->useAttributeAsKey('name')
27 | ->prototype('array')
28 | ->children()
29 | ->enumNode('type')
30 | ->values(array('gaufrette', 'flysystem'))
31 | ->defaultValue('gaufrette')
32 | ->end()
33 | ->scalarNode('filesystem')->isRequired()->end()
34 | ->scalarNode('naming_strategy')
35 | ->defaultValue('srio_rest_upload.naming.default_strategy')
36 | ->end()
37 | ->scalarNode('storage_strategy')
38 | ->defaultValue('srio_rest_upload.storage.default_strategy')
39 | ->end()
40 | ->end()
41 | ->end()
42 | ->end()
43 | ->scalarNode('default_storage')->defaultNull()->end()
44 | ->scalarNode('storage_voter')
45 | ->cannotBeEmpty()
46 | ->defaultValue('srio_rest_upload.storage_voter.default')
47 | ->end()
48 | ->scalarNode('resumable_entity_class')->defaultNull()->end()
49 | ->scalarNode('upload_type_parameter')
50 | ->defaultValue('uploadType')
51 | ->end()
52 | ->end();
53 |
54 | return $treeBuilder;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/DependencyInjection/Factory/StorageFactory.php:
--------------------------------------------------------------------------------
1 | setPublic(false);
22 | $adapterDefinition->replaceArgument(0, new Reference($config['filesystem']));
23 |
24 | $container->setDefinition($adapterId, $adapterDefinition);
25 | } elseif ($config['type'] === 'flysystem') {
26 | $adapterDefinition = new DefinitionDecorator('srio_rest_upload.storage.flysystem_adapter');
27 | $adapterDefinition->setPublic(false);
28 | $adapterDefinition->replaceArgument(0, new Reference($config['filesystem']));
29 |
30 | $container->setDefinition($adapterId, $adapterDefinition);
31 | }
32 |
33 | $container
34 | ->setDefinition($id, new Definition('SRIO\RestUploadBundle\Storage\FileStorage'))
35 | ->addArgument($config['name'])
36 | ->addArgument(new Reference($adapterId))
37 | ->addArgument(new Reference($config['storage_strategy']))
38 | ->addArgument(new Reference($config['naming_strategy']))
39 | ;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/DependencyInjection/SRIORestUploadExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
27 |
28 | $container->setParameter('srio_rest_upload.upload_type_parameter', $config['upload_type_parameter']);
29 | $container->setParameter('srio_rest_upload.resumable_entity_class', $config['resumable_entity_class']);
30 | $container->setParameter('srio_rest_upload.default_storage', $config['default_storage']);
31 |
32 | $this->createStorageVoter($container, $config['storage_voter']);
33 |
34 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
35 | $loader->load('processors.xml');
36 | $loader->load('handlers.xml');
37 | $loader->load('strategy.xml');
38 | $loader->load('storage.xml');
39 |
40 | $this->createStorageServices($container, $config['storages']);
41 | }
42 |
43 | /**
44 | * Create storage services.
45 | *
46 | * @param ContainerBuilder $container
47 | * @param array $storageDefinitions
48 | */
49 | private function createStorageServices(ContainerBuilder $container, array $storageDefinitions)
50 | {
51 | $voterDefinition = $container->getDefinition('srio_rest_upload.storage_voter');
52 | $factory = new StorageFactory();
53 |
54 | foreach ($storageDefinitions as $name => $storage) {
55 | $id = $this->createStorage($factory, $container, $name, $storage);
56 | $voterDefinition->addMethodCall('addStorage', array(new Reference($id)));
57 | }
58 | }
59 |
60 | /**
61 | * Create a single storage service.
62 | *
63 | * @param StorageFactory $factory
64 | * @param ContainerBuilder $containerBuilder
65 | * @param $name
66 | * @param array $config
67 | *
68 | * @return string
69 | */
70 | private function createStorage(StorageFactory $factory, ContainerBuilder $containerBuilder, $name, array $config)
71 | {
72 | $id = sprintf('srio_rest_upload.storage.%s', $name);
73 |
74 | $config['name'] = $name;
75 | $factory->create($containerBuilder, $id, $config);
76 |
77 | return $id;
78 | }
79 |
80 | /**
81 | * Create the storage voter.
82 | *
83 | * @param ContainerBuilder $builder
84 | * @param $service
85 | */
86 | private function createStorageVoter(ContainerBuilder $builder, $service)
87 | {
88 | $definition = new DefinitionDecorator($service);
89 | $builder->setDefinition('srio_rest_upload.storage_voter', $definition);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Entity/ResumableUploadSession.php:
--------------------------------------------------------------------------------
1 | filePath = $filePath;
59 | }
60 |
61 | /**
62 | * @return string
63 | */
64 | public function getFilePath()
65 | {
66 | return $this->filePath;
67 | }
68 |
69 | /**
70 | * @param string $sessionId
71 | */
72 | public function setSessionId($sessionId)
73 | {
74 | $this->sessionId = $sessionId;
75 | }
76 |
77 | /**
78 | * @return string
79 | */
80 | public function getSessionId()
81 | {
82 | return $this->sessionId;
83 | }
84 |
85 | /**
86 | * @param string $data
87 | */
88 | public function setData($data)
89 | {
90 | $this->data = $data;
91 | }
92 |
93 | /**
94 | * @return string
95 | */
96 | public function getData()
97 | {
98 | return $this->data;
99 | }
100 |
101 | /**
102 | * @param int $contentLength
103 | */
104 | public function setContentLength($contentLength)
105 | {
106 | $this->contentLength = $contentLength;
107 | }
108 |
109 | /**
110 | * @return int
111 | */
112 | public function getContentLength()
113 | {
114 | return $this->contentLength;
115 | }
116 |
117 | /**
118 | * @param string $contentType
119 | */
120 | public function setContentType($contentType)
121 | {
122 | $this->contentType = $contentType;
123 | }
124 |
125 | /**
126 | * @return string
127 | */
128 | public function getContentType()
129 | {
130 | return $this->contentType;
131 | }
132 |
133 | /**
134 | * @param string $storageName
135 | */
136 | public function setStorageName($storageName)
137 | {
138 | $this->storageName = $storageName;
139 | }
140 |
141 | /**
142 | * @return string
143 | */
144 | public function getStorageName()
145 | {
146 | return $this->storageName;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Exception/FileExistsException.php:
--------------------------------------------------------------------------------
1 | storageHandler = $storageHandler;
45 | }
46 |
47 | /**
48 | * Constructor.
49 | *
50 | * @param Request $request
51 | * @param FormInterface $form
52 | * @param array $config
53 | *
54 | * @return bool
55 | */
56 | public function handleUpload(Request $request, FormInterface $form = null, array $config = array())
57 | {
58 | $this->form = $form;
59 | $this->config = $config;
60 |
61 | return $this->handleRequest($request);
62 | }
63 |
64 | /**
65 | * Handle an upload request.
66 | *
67 | * This method return a Response object that will be sent back
68 | * to the client or will be caught by controller.
69 | *
70 | * @param Request $request
71 | *
72 | * @return \SRIO\RestUploadBundle\Upload\UploadResult
73 | */
74 | abstract public function handleRequest(Request $request);
75 |
76 | /**
77 | * Create the form data that the form will be able to handle.
78 | *
79 | * It walk one the form and make an intersection between its keys and
80 | * provided data.
81 | *
82 | * @param array $data
83 | *
84 | * @return array
85 | */
86 | protected function createFormData(array $data)
87 | {
88 | $keys = $this->getFormKeys($this->form);
89 |
90 | return array_intersect_key($data, $keys);
91 | }
92 |
93 | /**
94 | * Get keys of the form.
95 | *
96 | * @param FormInterface $form
97 | *
98 | * @return array
99 | */
100 | protected function getFormKeys(FormInterface $form)
101 | {
102 | $keys = array();
103 | foreach ($form->all() as $child) {
104 | $keys[$child->getName()] = count($child->all()) > 0 ? $this->getFormKeys($child) : null;
105 | }
106 |
107 | return $keys;
108 | }
109 |
110 | /**
111 | * Get a request content handler.
112 | *
113 | * @param Request $request
114 | *
115 | * @return RequestContentHandlerInterface
116 | */
117 | protected function getRequestContentHandler(Request $request)
118 | {
119 | if ($this->contentHandler === null) {
120 | $this->contentHandler = new RequestContentHandler($request);
121 | }
122 |
123 | return $this->contentHandler;
124 | }
125 |
126 | /**
127 | * Check that needed headers are here.
128 | *
129 | * @param Request $request the request
130 | * @param array $headers the headers to check
131 | *
132 | * @throws \SRIO\RestUploadBundle\Exception\UploadException
133 | */
134 | protected function checkHeaders(Request $request, array $headers)
135 | {
136 | foreach ($headers as $header) {
137 | $value = $request->headers->get($header, null);
138 | if ($value === null) {
139 | throw new UploadException(sprintf('%s header is needed', $header));
140 | } elseif (!is_int($value) && empty($value) && $value !== '0') {
141 | throw new UploadException(sprintf('%s header must not be empty', $header));
142 | }
143 | }
144 | }
145 |
146 | /**
147 | * Set the uploaded file on the form data.
148 | *
149 | * @param UploadedFile $file
150 | *
151 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
152 | *
153 | * @deprecated
154 | */
155 | protected function setUploadedFile(UploadedFile $file)
156 | {
157 | $data = $this->form->getData();
158 | if ($data instanceof UploadableFileInterface) {
159 | $data->setFile($file);
160 | } else {
161 | throw new UploadProcessorException(sprintf(
162 | 'Unable to set file, %s do not implements %s',
163 | get_class($data),
164 | 'SRIO\RestUploadBundle\Model\UploadableFileInterface'
165 | ));
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/Processor/FormDataUploadProcessor.php:
--------------------------------------------------------------------------------
1 | 'file',
23 | self::KEY_FIELD_FORM => 'form',
24 | ), $config);
25 |
26 | return parent::handleUpload($request, $form, $config);
27 | }
28 |
29 | /**
30 | * @param Request $request
31 | *
32 | * @return \SRIO\RestUploadBundle\Upload\UploadResult
33 | *
34 | * @throws \Exception|\SRIO\RestUploadBundle\Exception\UploadException
35 | */
36 | public function handleRequest(Request $request)
37 | {
38 | // Check that needed headers exists
39 | $this->checkHeaders($request, array('Content-Length', 'Content-Type'));
40 | if (!$request->files->has($this->config[self::KEY_FIELD_FILE])) {
41 | throw new UploadException(sprintf('%s file not found', $this->config[self::KEY_FIELD_FILE]));
42 | }
43 |
44 | $response = new UploadResult();
45 | $response->setRequest($request);
46 | $response->setConfig($this->config);
47 |
48 | if ($this->form != null) {
49 | $response->setForm($this->form);
50 |
51 | if (!$request->request->has($this->config[self::KEY_FIELD_FORM])) {
52 | throw new UploadException(sprintf(
53 | '%s request field not found in (%s)',
54 | $this->config[self::KEY_FIELD_FORM],
55 | implode(', ', $request->request->keys())
56 | ));
57 | }
58 |
59 | $submittedValue = $request->request->get($this->config[self::KEY_FIELD_FORM]);
60 | if (is_string($submittedValue)) {
61 | $submittedValue = json_decode($submittedValue, true);
62 | if (!$submittedValue) {
63 | throw new UploadException('Unable to decode JSON');
64 | }
65 | } elseif (!is_array($submittedValue)) {
66 | throw new UploadException('Unable to parse form data');
67 | }
68 |
69 | // Submit form data
70 | $formData = $this->createFormData($submittedValue);
71 | $this->form->submit($formData);
72 | if (!$this->form->isValid()) {
73 | return $response;
74 | }
75 | }
76 |
77 | /** @var $uploadedFile \Symfony\Component\HttpFoundation\File\UploadedFile */
78 | $uploadedFile = $request->files->get($this->config[self::KEY_FIELD_FILE]);
79 | $contents = file_get_contents($uploadedFile->getPathname());
80 | $file = $this->storageHandler->store($response, $contents, array(
81 | 'metadata' => array(
82 | FileStorage::METADATA_CONTENT_TYPE => $uploadedFile->getMimeType(),
83 | ),
84 | ));
85 |
86 | $response->setFile($file);
87 |
88 | return $response;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Processor/MultipartUploadProcessor.php:
--------------------------------------------------------------------------------
1 | checkHeaders($request);
25 |
26 | // Create the response
27 | $result = new UploadResult();
28 | $result->setRequest($request);
29 | $result->setConfig($this->config);
30 | $result->setForm($this->form);
31 |
32 | // Submit form data
33 | if ($this->form != null) {
34 | // Get formData
35 | $formData = $this->getFormData($request);
36 | $formData = $this->createFormData($formData);
37 |
38 | $this->form->submit($formData);
39 | }
40 |
41 | if ($this->form === null || $this->form->isValid()) {
42 | list($contentType, $content) = $this->getContent($request);
43 |
44 | $file = $this->storageHandler->store($result, $content, array(
45 | 'metadata' => array(
46 | FileStorage::METADATA_CONTENT_TYPE => $contentType,
47 | ),
48 | ));
49 |
50 | $result->setFile($file);
51 | }
52 |
53 | return $result;
54 | }
55 |
56 | /**
57 | * Get the form data from the request.
58 | *
59 | * Note: MUST be called before getContent, and just one time.
60 | *
61 | * @param Request $request
62 | *
63 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
64 | *
65 | * @return array
66 | */
67 | protected function getFormData(Request $request)
68 | {
69 | list($boundaryContentType, $boundaryContent) = $this->getPart($request);
70 |
71 | $expectedContentType = 'application/json';
72 | if (substr($boundaryContentType, 0, strlen($expectedContentType)) != $expectedContentType) {
73 | throw new UploadProcessorException(sprintf(
74 | 'Expected content type of first part is %s. Found %s',
75 | $expectedContentType,
76 | $boundaryContentType
77 | ));
78 | }
79 |
80 | $jsonContent = json_decode($boundaryContent, true);
81 | if ($jsonContent === null) {
82 | throw new UploadProcessorException('Unable to parse JSON');
83 | }
84 |
85 | return $jsonContent;
86 | }
87 |
88 | /**
89 | * Get the content part of the request.
90 | *
91 | * Note: MUST be called after getFormData, and just one time.
92 | *
93 | * @param \Symfony\Component\HttpFoundation\Request $request
94 | * @param Request $request
95 | *
96 | * @return array
97 | */
98 | protected function getContent(Request $request)
99 | {
100 | return $this->getPart($request);
101 | }
102 |
103 | /**
104 | * Check multipart headers.
105 | *
106 | * @param Request $request
107 | * @param array $headers
108 | *
109 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
110 | */
111 | protected function checkHeaders(Request $request, array $headers = array())
112 | {
113 | list($contentType) = $this->parseContentTypeAndBoundary($request);
114 |
115 | $expectedContentType = 'multipart/related';
116 | if ($contentType != $expectedContentType) {
117 | throw new UploadProcessorException(sprintf(
118 | 'Content-Type must be %s',
119 | $expectedContentType
120 | ));
121 | }
122 |
123 | parent::checkHeaders($request, array('Content-Type', 'Content-Length'));
124 | }
125 |
126 | /**
127 | * Get a part of request.
128 | *
129 | * @param Request $request
130 | *
131 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
132 | *
133 | * @return array
134 | */
135 | protected function getPart(Request $request)
136 | {
137 | list($contentType, $boundary) = $this->parseContentTypeAndBoundary($request);
138 | $content = $this->getRequestPart($request, $boundary);
139 |
140 | if (empty($content)) {
141 | throw new UploadProcessorException(sprintf('An empty content found'));
142 | }
143 |
144 | $headerLimitation = strpos($content, "\r\n\r\n") + 1;
145 | if ($headerLimitation == -1) {
146 | throw new UploadProcessorException('Unable to determine headers limit');
147 | }
148 |
149 | $contentType = null;
150 | $headersContent = substr($content, 0, $headerLimitation);
151 | $headersContent = trim($headersContent);
152 | $body = substr($content, $headerLimitation);
153 | $body = trim($body);
154 |
155 | foreach (explode("\r\n", $headersContent) as $header) {
156 | $parts = explode(':', $header);
157 | if (count($parts) != 2) {
158 | continue;
159 | }
160 |
161 | $name = trim($parts[0]);
162 | if (strtolower($name) == 'content-type') {
163 | $contentType = trim($parts[1]);
164 | break;
165 | }
166 | }
167 |
168 | return array($contentType, $body);
169 | }
170 |
171 | /**
172 | * Get part of a resource.
173 | *
174 | * @param Request $request
175 | * @param $boundary
176 | *
177 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
178 | *
179 | * @return string
180 | */
181 | protected function getRequestPart(Request $request, $boundary)
182 | {
183 | $contentHandler = $this->getRequestContentHandler($request);
184 |
185 | $delimiter = '--'.$boundary."\r\n";
186 | $endDelimiter = '--'.$boundary.'--';
187 | $boundaryCount = 0;
188 | $content = '';
189 | while (!$contentHandler->eof()) {
190 | $line = $contentHandler->gets();
191 | if ($line === false) {
192 | throw new UploadProcessorException('An error appears while reading input');
193 | }
194 |
195 | if ($boundaryCount == 0) {
196 | if ($line != $delimiter) {
197 | if ($contentHandler->getCursor() == strlen($line)) {
198 | throw new UploadProcessorException('Expected boundary delimiter');
199 | }
200 | } else {
201 | continue;
202 | }
203 |
204 | ++$boundaryCount;
205 | } elseif ($line == $delimiter) {
206 | break;
207 | } elseif ($line == $endDelimiter || $line == $endDelimiter."\r\n") {
208 | break;
209 | }
210 |
211 | $content .= $line;
212 | }
213 |
214 | return trim($content);
215 | }
216 |
217 | /**
218 | * Parse the content type and boudary from Content-Type header.
219 | *
220 | * @param Request $request
221 | *
222 | * @return array
223 | *
224 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
225 | */
226 | protected function parseContentTypeAndBoundary(Request $request)
227 | {
228 | $contentParts = explode(';', $request->headers->get('Content-Type'));
229 | if (count($contentParts) != 2) {
230 | throw new UploadProcessorException('Boundary may be missing');
231 | }
232 |
233 | $contentType = trim($contentParts[0]);
234 | $boundaryPart = trim($contentParts[1]);
235 |
236 | $shouldStart = 'boundary=';
237 | if (substr($boundaryPart, 0, strlen($shouldStart)) != $shouldStart) {
238 | throw new UploadProcessorException('Boundary is not set');
239 | }
240 |
241 | $boundary = substr($boundaryPart, strlen($shouldStart));
242 | if (substr($boundary, 0, 1) == '"' && substr($boundary, -1) == '"') {
243 | $boundary = substr($boundary, 1, -1);
244 | }
245 |
246 | return array($contentType, $boundary);
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/Processor/ProcessorInterface.php:
--------------------------------------------------------------------------------
1 | em = $em;
46 | $this->resumableEntity = $resumableEntity;
47 | }
48 |
49 | /**
50 | * @param \Symfony\Component\HttpFoundation\Request $request
51 | *
52 | * @throws \Exception|\SRIO\RestUploadBundle\Exception\UploadException
53 | *
54 | * @return UploadResult
55 | */
56 | public function handleRequest(Request $request)
57 | {
58 | if (empty($this->resumableEntity)) {
59 | throw new UploadProcessorException(sprintf(
60 | 'You must configure the "%s" option',
61 | 'resumable_entity'
62 | ));
63 | }
64 |
65 | if ($request->query->has(self::PARAMETER_UPLOAD_ID)) {
66 | $this->checkHeaders($request, array('Content-Length'));
67 |
68 | $uploadId = $request->query->get(self::PARAMETER_UPLOAD_ID);
69 |
70 | $repository = $this->getRepository();
71 | $resumableUpload = $repository->findOneBy(array(
72 | 'sessionId' => $uploadId,
73 | ));
74 |
75 | if ($resumableUpload == null) {
76 | throw new UploadProcessorException('Unable to find upload session');
77 | }
78 |
79 | return $this->handleResume($request, $resumableUpload);
80 | }
81 |
82 | return $this->handleStartSession($request);
83 | }
84 |
85 | /**
86 | * Handle a start session.
87 | *
88 | * @param Request $request
89 | *
90 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
91 | *
92 | * @return UploadResult
93 | */
94 | protected function handleStartSession(Request $request)
95 | {
96 | // Check that needed headers exists
97 | $this->checkHeaders($request, array('Content-Type', 'X-Upload-Content-Type', 'X-Upload-Content-Length'));
98 | $expectedContentType = 'application/json';
99 | if (substr($request->headers->get('Content-Type'), 0, strlen($expectedContentType)) != $expectedContentType) {
100 | throw new UploadProcessorException(sprintf(
101 | 'Expected content type is %s. Found %s',
102 | $expectedContentType,
103 | $request->headers->get('Content-Type')
104 | ));
105 | }
106 |
107 | // Create the result object
108 | $result = new UploadResult();
109 | $result->setRequest($request);
110 | $result->setConfig($this->config);
111 | $result->setForm($this->form);
112 |
113 | $formData = array();
114 | if ($this->form != null) {
115 | // Submit form data
116 | $data = json_decode($request->getContent(), true);
117 | $formData = $this->createFormData($data);
118 | $this->form->submit($formData);
119 | }
120 |
121 | if ($this->form == null || $this->form->isValid()) {
122 | // Form is valid, store it
123 | $repository = $this->getRepository();
124 | $className = $repository->getClassName();
125 |
126 | // Create file from storage handler
127 | $file = $this->storageHandler->store($result, '', array(
128 | 'metadata' => array(
129 | FileStorage::METADATA_CONTENT_TYPE => $request->headers->get('X-Upload-Content-Type'),
130 | ),
131 | ));
132 |
133 | /** @var $resumableUpload ResumableUploadSession */
134 | $resumableUpload = new $className();
135 |
136 | $resumableUpload->setData(serialize($formData));
137 | $resumableUpload->setStorageName($file->getStorage()->getName());
138 | $resumableUpload->setFilePath($file->getFile()->getName());
139 | $resumableUpload->setSessionId($this->createSessionId());
140 | $resumableUpload->setContentType($request->headers->get('X-Upload-Content-Type'));
141 | $resumableUpload->setContentLength($request->headers->get('X-Upload-Content-Length'));
142 |
143 | // Store resumable session
144 | $this->em->persist($resumableUpload);
145 | $this->em->flush($resumableUpload);
146 |
147 | // Compute redirect location path
148 | $location = $request->getPathInfo().'?'.http_build_query(array_merge($request->query->all(), array(
149 | self::PARAMETER_UPLOAD_ID => $resumableUpload->getSessionId(),
150 | )));
151 |
152 | $response = new Response(null);
153 | $response->headers->set('Location', $location);
154 |
155 | $result->setResponse($response);
156 | }
157 |
158 | return $result;
159 | }
160 |
161 | /**
162 | * Handle an upload resume.
163 | *
164 | * @param Request $request
165 | * @param ResumableUploadSession $uploadSession
166 | *
167 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
168 | *
169 | * @return UploadResult
170 | */
171 | protected function handleResume(Request $request, ResumableUploadSession $uploadSession)
172 | {
173 | $filePath = $uploadSession->getFilePath();
174 |
175 | $context = new UploadContext();
176 | $context->setStorageName($uploadSession->getStorageName());
177 | $file = $this->storageHandler->getFilesystem($context)->get($filePath);
178 | $context->setFile(new UploadedFile(
179 | $this->storageHandler->getStorage($context),
180 | $file
181 | ));
182 |
183 | $contentLength = $request->headers->get('Content-Length');
184 | if ($request->headers->has('Content-Range')) {
185 | $range = $this->parseContentRange($request->headers->get('Content-Range'));
186 |
187 | if ($range['total'] != $uploadSession->getContentLength()) {
188 | throw new UploadProcessorException(sprintf(
189 | 'File size must be "%d", range total length is %d',
190 | $uploadSession->getContentLength(),
191 | $range['total']
192 | ));
193 | } elseif ($range['start'] === '*') {
194 | if ($contentLength == 0) {
195 | $file = $this->storageHandler->getFilesystem($context)->get($filePath);
196 |
197 | return $this->requestUploadStatus($context, $uploadSession, $file, $range);
198 | }
199 |
200 | throw new UploadProcessorException('Content-Length must be 0 if asking upload status');
201 | }
202 |
203 | $uploaded = $this->storageHandler->getFilesystem($context)->getSize($filePath);
204 | if ($range['start'] != $uploaded) {
205 | throw new UploadProcessorException(sprintf(
206 | 'Unable to start at %d while uploaded is %d',
207 | $range['start'],
208 | $uploaded
209 | ));
210 | }
211 | } else {
212 | $range = array(
213 | 'start' => 0,
214 | 'end' => $uploadSession->getContentLength() - 1,
215 | 'total' => $uploadSession->getContentLength() - 1,
216 | );
217 | }
218 |
219 | // Handle upload from
220 | $handler = $this->getRequestContentHandler($request);
221 | $stream = $this->storageHandler->getFilesystem($context)->getStreamCopy($filePath);
222 |
223 | fseek($stream, $range['start']);
224 | $wrote = 0;
225 | while (!$handler->eof()) {
226 | if (($bytes = fwrite($stream, $handler->gets())) !== false) {
227 | $wrote += $bytes;
228 | } else {
229 | throw new UploadProcessorException('Unable to write to file');
230 | }
231 | }
232 |
233 | // Get file in context and its size
234 | $uploadedFile = $this->storageHandler->storeStream($context, $stream, array(
235 | 'metadata' => array(
236 | FileStorage::METADATA_CONTENT_TYPE => $request->headers->get('X-Upload-Content-Type'),
237 | ),
238 | ), true);
239 | fclose($stream);
240 |
241 | $file = $uploadedFile->getFile();
242 | $size = $file->getSize();
243 |
244 | // If upload is completed, create the upload file, else
245 | // return like the request upload status
246 | if ($size < $uploadSession->getContentLength()) {
247 | return $this->requestUploadStatus($context, $uploadSession, $file, $range);
248 | } elseif ($size == $uploadSession->getContentLength()) {
249 | return $this->handleCompletedUpload($context, $uploadSession, $file);
250 | } else {
251 | throw new UploadProcessorException('Written file size is greater that expected Content-Length');
252 | }
253 | }
254 |
255 | /**
256 | * Handle a completed upload.
257 | *
258 | * @param \SRIO\RestUploadBundle\Upload\UploadContext $context
259 | * @param ResumableUploadSession $uploadSession
260 | * @param FileAdapterInterface $file
261 | *
262 | * @return UploadResult
263 | */
264 | protected function handleCompletedUpload(UploadContext $context, ResumableUploadSession $uploadSession, FileAdapterInterface $file)
265 | {
266 | $result = new UploadResult();
267 | $result->setForm($this->form);
268 |
269 | if ($this->form != null) {
270 | // Submit the form data
271 | $formData = unserialize($uploadSession->getData());
272 | $this->form->submit($formData);
273 | }
274 |
275 | if ($this->form == null || $this->form->isValid()) {
276 | // Create the uploaded file
277 | $uploadedFile = new UploadedFile(
278 | $this->storageHandler->getStorage($context),
279 | $file
280 | );
281 |
282 | $result->setFile($uploadedFile);
283 | }
284 |
285 | return $result;
286 | }
287 |
288 | /**
289 | * Return the upload status.
290 | *
291 | * @param \SRIO\RestUploadBundle\Upload\UploadContext $context
292 | * @param ResumableUploadSession $uploadSession
293 | * @param FileAdapterInterface $file
294 | * @param array $range
295 | *
296 | * @return UploadResult
297 | */
298 | protected function requestUploadStatus(UploadContext $context, ResumableUploadSession $uploadSession, FileAdapterInterface $file, array $range)
299 | {
300 | if (!$file->exists()) {
301 | $length = 0;
302 | } else {
303 | $length = $file->getSize();
304 | }
305 |
306 | $response = new Response(null, $length == $range['total'] ? 201 : 308);
307 |
308 | if ($length < 1) {
309 | $length = 1;
310 | }
311 |
312 | $response->headers->set('Range', '0-'.($length - 1));
313 |
314 | $result = new UploadResult();
315 | $result->setResponse($response);
316 |
317 | return $result;
318 | }
319 |
320 | /**
321 | * Parse the Content-Range header.
322 | *
323 | * It returns an array with these keys:
324 | * - `start` Start index of range
325 | * - `end` End index of range
326 | * - `total` Total number of bytes
327 | *
328 | * @param string $contentRange
329 | *
330 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
331 | *
332 | * @return array
333 | */
334 | protected function parseContentRange($contentRange)
335 | {
336 | $contentRange = trim($contentRange);
337 | if (!preg_match('#^bytes (\*|([0-9]+)-([0-9]+))/([0-9]+)$#', $contentRange, $matches)) {
338 | throw new UploadProcessorException('Invalid Content-Range header. Must start with "bytes ", range and total length');
339 | }
340 |
341 | $range = array(
342 | 'start' => $matches[1] === '*' ? '*' : ($matches[2] === '' ? null : (int) $matches[2]),
343 | 'end' => $matches[3] === '' ? null : (int) $matches[3],
344 | 'total' => (int) $matches[4],
345 | );
346 |
347 | if (empty($range['total'])) {
348 | throw new UploadProcessorException('Content-Range total length not found');
349 | }
350 | if ($range['start'] === '*') {
351 | if ($range['end'] !== null) {
352 | throw new UploadProcessorException('Content-Range end must not be present if start is "*"');
353 | }
354 | } elseif ($range['start'] === null || $range['end'] === null) {
355 | throw new UploadProcessorException('Content-Range end or start is empty');
356 | } elseif ($range['start'] > $range['end']) {
357 | throw new UploadProcessorException('Content-Range start must be lower than end');
358 | } elseif ($range['end'] > $range['total']) {
359 | throw new UploadProcessorException('Content-Range end must be lower or equal to total length');
360 | }
361 |
362 | return $range;
363 | }
364 |
365 | /**
366 | * Get resumable upload session entity repository.
367 | *
368 | * @return \Doctrine\ORM\EntityRepository
369 | */
370 | protected function getRepository()
371 | {
372 | return $this->em->getRepository($this->resumableEntity);
373 | }
374 |
375 | /**
376 | * Create a session ID.
377 | */
378 | protected function createSessionId()
379 | {
380 | return uniqid();
381 | }
382 | }
383 |
--------------------------------------------------------------------------------
/Processor/SimpleUploadProcessor.php:
--------------------------------------------------------------------------------
1 | checkHeaders($request, array('Content-Type'));
25 |
26 | $result = new UploadResult();
27 | $result->setForm($this->form);
28 | $result->setRequest($request);
29 | $result->setConfig($this->config);
30 |
31 | // Submit form data
32 | if ($this->form !== null) {
33 | $formData = $this->createFormData($request->query->all());
34 | $this->form->submit($formData);
35 | }
36 |
37 | if ($this->form == null || $this->form->isValid()) {
38 | $content = $request->getContent();
39 |
40 | // Nothing to store
41 | if (empty($content)) {
42 | throw new UploadException('There is no content to upload');
43 | }
44 |
45 | $file = $this->storageHandler->store($result, $content, array(
46 | 'metadata' => array(
47 | FileStorage::METADATA_CONTENT_TYPE => $request->headers->get('Content-Type'),
48 | ),
49 | ));
50 |
51 | $result->setFile($file);
52 | }
53 |
54 | return $result;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SRIORestUploadBundle
2 |
3 | [](https://travis-ci.org/sroze/SRIORestUploadBundle)
4 |
5 | This bundle provide a simple ways to handle uploads on the server side.
6 |
7 | Currently, it supports the simple, form-data, multipart and resumable ways.
8 |
9 | ## Getting started
10 |
11 | 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, ...
12 |
13 | - [Installation](Resources/doc/installation.md)
14 | - [Usage](Resources/doc/usage.md)
15 | - [Advanced usage](Resources/doc/advanced.md)
16 | - [Upload ways summary](Resources/doc/upload-ways.md)
17 | - [Configuration reference](Resources/doc/reference.md)
18 |
19 | ## Testing
20 |
21 | Tests are run with [PHPUnit](http://phpunit.de). Once you installed dependencies with composer, then:
22 |
23 | - Create a database, allow access to a user, and set configuration in `Tests/Fixtures/App/app/config/parameters.yml` file
24 | - Create the database schema for the `test` environment
25 | ```sh
26 | php Tests/Fixtures/App/app/console doctrine:schema:update --force --env=test
27 | ```
28 | - Run PHPUnit
29 | ```sh
30 | phpunit
31 | ```
32 |
--------------------------------------------------------------------------------
/Request/RequestContentHandler.php:
--------------------------------------------------------------------------------
1 | request = $request;
32 | $this->cursor = 0;
33 | }
34 |
35 | /**
36 | * Get a line.
37 | *
38 | * If false is return, it's the end of file.
39 | *
40 | * @return string|bool
41 | */
42 | public function gets()
43 | {
44 | $content = $this->getContent();
45 | if (is_resource($content)) {
46 | $line = fgets($content);
47 | $this->cursor = ftell($content);
48 |
49 | return $line;
50 | }
51 |
52 | $next = strpos($content, "\r\n", $this->cursor);
53 | $eof = $next < 0 || $next === false;
54 |
55 | if ($eof) {
56 | $line = substr($content, $this->cursor);
57 | } else {
58 | $length = $next - $this->cursor + strlen("\r\n");
59 | $line = substr($content, $this->cursor, $length);
60 | }
61 |
62 | $this->cursor = $eof ? -1 : $next + strlen("\r\n");
63 |
64 | return $line;
65 | }
66 |
67 | /**
68 | * @return int
69 | */
70 | public function getCursor()
71 | {
72 | return $this->cursor;
73 | }
74 |
75 | /**
76 | * Is end of file ?
77 | *
78 | * @return bool
79 | */
80 | public function eof()
81 | {
82 | return $this->cursor == -1 || (is_resource($this->getContent()) && feof($this->getContent()));
83 | }
84 |
85 | /**
86 | * Get request content.
87 | *
88 | * @return resource|string
89 | *
90 | * @throws \RuntimeException
91 | */
92 | public function getContent()
93 | {
94 | if ($this->content === null) {
95 | try {
96 | $this->content = $this->request->getContent(true);
97 | } catch (\LogicException $e) {
98 | $this->content = $this->request->getContent(false);
99 |
100 | if (!$this->content) {
101 | throw new \RuntimeException('Unable to get request content');
102 | }
103 | }
104 | }
105 |
106 | return $this->content;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Request/RequestContentHandlerInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Resources/config/handlers.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | SRIO\RestUploadBundle\Upload\UploadHandler
8 | SRIO\RestUploadBundle\Upload\StorageHandler
9 | SRIO\RestUploadBundle\Voter\StorageVoter
10 |
11 |
12 |
13 |
14 | %srio_rest_upload.upload_type_parameter%
15 |
16 |
17 |
18 |
19 |
20 |
21 | %srio_rest_upload.default_storage%
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Resources/config/processors.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | SRIO\RestUploadBundle\Processor\AbstractUploadProcessor
8 | SRIO\RestUploadBundle\Processor\SimpleUploadProcessor
9 | SRIO\RestUploadBundle\Processor\MultipartUploadProcessor
10 | SRIO\RestUploadBundle\Processor\ResumableUploadProcessor
11 | SRIO\RestUploadBundle\Processor\FormDataUploadProcessor
12 |
13 |
14 |
15 |
16 |
17 |
18 |
20 |
21 |
22 |
24 |
25 |
26 |
28 |
29 |
30 |
32 |
33 | %srio_rest_upload.resumable_entity_class%
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Resources/config/storage.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | SRIO\RestUploadBundle\Storage\FlysystemFilesystemAdapter
8 | SRIO\RestUploadBundle\Storage\GaufretteFilesystemAdapter
9 |
10 |
11 |
12 |
13 | null
14 |
15 |
16 | null
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Resources/config/strategy.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | SRIO\RestUploadBundle\Strategy\DefaultStorageStrategy
8 | SRIO\RestUploadBundle\Strategy\DefaultNamingStrategy
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Resources/doc/advanced.md:
--------------------------------------------------------------------------------
1 | # Advanced usage
2 |
3 | ## Strategies
4 |
5 | You can set naming and storage strategies for each defined storage.
6 | ```yml
7 | srio_rest_upload:
8 | storages:
9 | default:
10 | filesystem: gaufrette.default_filesystem
11 | naming_strategy: your_naming_strategy_service
12 | storage_strategy: your_storage_strategy_service
13 | ```
14 |
15 | ### Naming strategy
16 |
17 | 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.
18 |
19 | 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.
20 |
21 | ```php
22 | namespace Acme\Storage\Strategy;
23 |
24 | use SRIO\RestUploadBundle\Upload\UploadContext;
25 | use SRIO\RestUploadBundle\Strategy\NamingStrategy;
26 |
27 | class DefaultNamingStrategy implements NamingStrategy
28 | {
29 | const DEFAULT_EXTENSION = 'png';
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function getName(UploadContext $context)
35 | {
36 | $name = uniqid();
37 | $extension = self::DEFAULT_EXTENSION;
38 |
39 | if (($request = $context->getRequest()) !== null) {
40 | $files = $request->files->all();
41 |
42 | /** @var $file \Symfony\Component\HttpFoundation\File\UploadedFile */
43 | $file = array_pop($files);
44 |
45 | if ($file !== null) {
46 | $parts = explode('.', $file->getClientOriginalName());
47 | $extension = array_pop($parts);
48 | }
49 | }
50 |
51 | return $name.'.'.$extension;
52 | }
53 | }
54 | ```
55 |
56 | Then, define a service and change the `naming_strategy` of your storage configuration with the created service ID.
57 |
58 | ### Storage strategy
59 |
60 | It defines the (sub)directory in which the file will be created in your filesystem.
61 |
62 | 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.
63 |
64 | ## Create a custom handler
65 |
66 | 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
67 |
68 | ```yml
69 |
70 | Acme\AcmeBundle\Processor\MyUploadProcessor
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | ```
80 |
81 | Note the `uploadType` attribute that define the unique name of the upload way, set in the `uploadType` query parameters.
82 |
83 | Your `MyUploadProcessor` class should then implements the [`ProcessorInterface`](../../Processor/ProcessorInterface.php) or extends the [`AbstractUploadProcessor`](../../Processor/AbstractUploadProcessor.php)
84 |
85 |
--------------------------------------------------------------------------------
/Resources/doc/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | 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.
4 |
5 | ## Add SRIORestUploadBundle in your dependencies
6 |
7 | In your `composer.json` file, add `srio/rest-upload-bundle`:
8 | ```json
9 | {
10 | "require": {
11 | "srio/rest-upload-bundle": "~2.0.0"
12 | }
13 | }
14 | ```
15 |
16 | Then, update your dependencies:
17 | ```
18 | composer update srio/rest-upload-bundle
19 | ```
20 |
21 | ## Enable the bundle in your kernel
22 |
23 | ```php
24 | // app/AppKernel.php
25 | public function registerBundles()
26 | {
27 | $bundles = array(
28 | // ...
29 | new SRIO\RestUploadBundle\SRIORestUploadBundle(),
30 | );
31 | }
32 | ```
33 |
34 | ## Create the Gaufrette filesystem
35 |
36 | In your configuration file, create your Gaufrette filesystem. Let's start with a local filesystem storage in the `web/uploads` directory.
37 |
38 | ```yml
39 | # app/config/config.yml
40 |
41 | knp_gaufrette:
42 | adapters:
43 | local_uploads:
44 | local:
45 | directory: %kernel.root_dir%/../web/uploads
46 | filesystems:
47 | uploads:
48 | adapter: local_uploads
49 | ```
50 |
51 | ## Configure the bundle
52 |
53 | Then, we just have to configure the bundle to use the Gaufrette storage:
54 | ```
55 | srio_rest_upload:
56 | storages:
57 | default:
58 | filesystem: gaufrette.uploads_filesystem
59 | ```
60 |
61 | If you want to use the resumable upload way, you have to [configure it](upload-ways.md#resumable-configuration).
62 |
63 | Then, [start using it](usage.md).
64 |
--------------------------------------------------------------------------------
/Resources/doc/reference.md:
--------------------------------------------------------------------------------
1 | # Configuration reference
2 |
3 | ```yml
4 | srio_rest_upload:
5 | # Define the available storages
6 | storages:
7 | name:
8 | # Filesystem service created by Gaufrette (or your own matching the Gaufrette's interface)
9 | filesystem: fs_service_id
10 |
11 | # Naming strategy service
12 | naming_strategy: srio_rest_upload.naming.default_strategy
13 |
14 | # Storage strategy service
15 | storage_strategy: srio_rest_upload.storage.default_strategy
16 |
17 | # The storage voter, that chose between storage based on upload context
18 | storage_voter: srio_rest_upload.storage_voter.default
19 |
20 | # The default storage name. With the default storage voter, it'll use
21 | # the first defined storage if value is null
22 | default_storage: ~
23 |
24 | # If you want to use the resumable upload way, you must set
25 | # the class name of your entity which store the upload sessions.
26 | resumable_entity_class: ~
27 |
28 | # Parameter the define the upload way, internally the provider selector
29 | upload_type_parameter: uploadType
30 | ```
31 |
32 | The [Advanced usage](advanced.md) section explain the naming and storage strategies.
33 |
--------------------------------------------------------------------------------
/Resources/doc/upload-ways.md:
--------------------------------------------------------------------------------
1 | # Upload ways
2 |
3 | This is a summary of currently supported upload ways.
4 |
5 | - [Simple](#simple-upload-way): Send binary data to an URL and use query parameters to submit additional data.
6 | - [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).
7 | - [FormData](#formdata-upload-way): Matches the classic browser file upload
8 | - [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.
9 |
10 | ## Simple upload way
11 |
12 | The most straightforward method for uploading a file is by making a simple upload request. This option is a good choice when:
13 | - The file is small enough to upload again in its entirety if the connection fails
14 | - There is no or a very small amount of metadata to send
15 |
16 | To use simple upload, make a `POST` or `PUT` request to the upload method's URI and add the query parameter `uploadType=simple`.
17 | The HTTP headers to use when making a simple upload request include:
18 | - `Content-Type`. Set the media content-type
19 | - `Content-Length`. Set to the number of bytes you are uploading.
20 |
21 | ### Example
22 |
23 | The following example shows the use of a simple photo upload request for an upload path that would be `/upload`:
24 |
25 | ```
26 | POST /upload?uploadType=simple HTTP/1.1
27 | Host: www.example.com
28 | Content-Type: image/jpeg
29 | Content-Length: number_of_bytes_in_JPEG_file
30 |
31 | JPEG data
32 | ```
33 |
34 | ## Multipart upload way
35 |
36 | 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.
37 | To use multipart upload, make a `POST` or `PUT` request to the upload method's URI and add the query parameter `uploadType=multipart`.
38 |
39 | The top-level HTTP headers to use when making a multipart upload request include:
40 | - `Content-Type`. Set to `multipart/related` and include the boundary string you're using to identify the parts of the request.
41 | - `Content-Length`. Set to the total number of bytes in the request body.
42 |
43 | 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.
44 |
45 | Each part of the multipart request needs an additional `Content-Type` header:
46 | - **Metadata part**: Must come first, and Content-Type must match one of the the accepted metadata formats.
47 | - **Media part**: Must come second, and Content-Type must match one the method's accepted media MIME types.
48 |
49 | ### Example
50 |
51 | The following example shows the use of a multipart upload request for an upload path that would be `/upload`:
52 |
53 | ```
54 | POST /upload?uploadType=multipart HTTP/1.1
55 | Host: www.example.com
56 | Content-Type: multipart/related; boundary="foo_bar_baz"
57 | Content-Length: number_of_bytes_in_entire_request_body
58 |
59 | --foo_bar_baz
60 | Content-Type: application/json; charset=UTF-8
61 |
62 | {
63 | "name": "Some value"
64 | }
65 |
66 | --foo_bar_baz
67 | Content-Type: image/jpeg
68 |
69 | JPEG data
70 |
71 | --foo_bar_baz--
72 | ```
73 |
74 | ### FormData upload way
75 |
76 | This may be the most used way to upload files: it matches with the classic form "file" upload.
77 |
78 | You just have to have a field of type `file` named `file` on your form and set the action path to `/upload?uploadType=formData`.
79 | It can ether be used with any XHR upload method.
80 |
81 | ## Resumable upload way
82 |
83 | 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.
84 |
85 | The steps for using resumable upload include:
86 |
87 | 1. [Start a resumable session](#start-resumable). Make an initial request to the upload URI that includes the metadata, if any.
88 | 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.
89 | 3. [Upload the file](#upload-resumable). Send the media file to the resumable session URI.
90 |
91 | 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.
92 |
93 | ### Resumable Configuration
94 |
95 | First, you need to configure the bundle.
96 |
97 | #### Create your `ResumableUploadSession` entity
98 |
99 | The entity will contains the resumable upload sessions and is required if you want the resumable way of upload to work.
100 |
101 | ```php
102 | get('srio_rest_upload.upload_handler');
30 | $result = $uploadHandler->handleRequest($request);
31 |
32 | if (($response = $result->getResponse()) !== null) {
33 | return $response;
34 | }
35 |
36 | if (($file = $result->getFile()) !== null) {
37 | // Store the file path in an entity, call an API,
38 | // do whatever with the uploaded file here.
39 |
40 | return new Response();
41 | }
42 |
43 | throw new BadRequestHttpException('Unable to handle upload request');
44 | }
45 | }
46 | ```
47 |
48 | ## With form
49 |
50 | Because most of the time you may want to link a form to the file upload, you're able to handle it too.
51 | Depending on the [upload way](upload-ways.md) you're using, form data will be fetched from request body or HTTP parameters.
52 |
53 | 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):
54 | ```php
55 | class UploadController extends Controller
56 | {
57 | public function uploadAction(Request $request)
58 | {
59 | $form = $this->createForm(new MediaFormType());
60 |
61 | /** @var $uploadHandler UploadHandler */
62 | $uploadHandler = $this->get('srio_rest_upload.upload_handler');
63 | $result = $uploadHandler->handleRequest($request, $form);
64 |
65 | if (($response = $result->getResponse()) != null) {
66 | return $response;
67 | }
68 |
69 | if (!$form->isValid()) {
70 | throw new BadRequestHttpException();
71 | }
72 |
73 | if (($file = $result->getFile()) !== null) {
74 | /** @var $media Media */
75 | $media = $form->getData();
76 | $media->setFile($file);
77 |
78 | $em = $this->getDoctrine()->getManager();
79 | $em->persist($media);
80 | $em->flush();
81 |
82 | return new JsonResponse($media);
83 | }
84 |
85 | throw new NotAcceptableHttpException();
86 | }
87 | }
88 | ```
89 |
90 | ## On the client side
91 |
92 | 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):
93 | ```js
94 | $upload.upload({
95 | url: '/path/to/upload?uploadType=formData',
96 | method: 'POST',
97 | file: file
98 | })
99 | ```
100 |
101 |
--------------------------------------------------------------------------------
/SRIORestUploadBundle.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new ProcessorCompilerPass());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Storage/FileAdapterInterface.php:
--------------------------------------------------------------------------------
1 | name = $name;
44 | $this->filesystem = $filesystem;
45 | $this->storageStrategy = $storageStrategy;
46 | $this->namingStrategy = $namingStrategy;
47 | }
48 |
49 | /**
50 | * Store a file's content.
51 | *
52 | * @param UploadContext $context
53 | * @param string $content
54 | * @param array $config
55 | * @param bool $overwrite
56 | *
57 | * @return UploadedFile
58 | */
59 | public function store(UploadContext $context, $content, array $config = array(), $overwrite = false)
60 | {
61 | $path = $this->getFilePathFromContext($context);
62 | if ($overwrite === true) {
63 | $this->filesystem->put($path, $content, $config);
64 | } else {
65 | $this->filesystem->write($path, $content, $config);
66 | }
67 | $file = $this->filesystem->get($path);
68 |
69 | return new UploadedFile($this, $file);
70 | }
71 |
72 | /**
73 | * Store a file's content.
74 | *
75 | * @param UploadContext $context
76 | * @param resource $resource
77 | * @param array $config
78 | * @param bool $overwrite
79 | *
80 | * @return UploadedFile
81 | */
82 | public function storeStream(UploadContext $context, $resource, array $config = array(), $overwrite = false)
83 | {
84 | $path = $this->getFilePathFromContext($context);
85 | if ($overwrite === true) {
86 | $this->filesystem->putStream($path, $resource, $config);
87 | } else {
88 | $this->filesystem->writeStream($path, $resource, $config);
89 | }
90 | $file = $this->filesystem->get($path);
91 |
92 | return new UploadedFile($this, $file);
93 | }
94 |
95 | /**
96 | * Get or creates a file path from UploadContext.
97 | *
98 | * @param UploadContext $context
99 | *
100 | * @return string
101 | */
102 | protected function getFilePathFromContext(UploadContext $context)
103 | {
104 | if ($context->getFile() != null) {
105 | return $context->getFile()->getFile()->getName();
106 | }
107 |
108 | $name = $this->namingStrategy->getName($context);
109 | $directory = $this->storageStrategy->getDirectory($context, $name);
110 | $path = $directory.'/'.$name;
111 |
112 | return $path;
113 | }
114 |
115 | /**
116 | * @return FilesystemAdapterInterface
117 | */
118 | public function getFilesystem()
119 | {
120 | return $this->filesystem;
121 | }
122 |
123 | /**
124 | * @return string
125 | */
126 | public function getName()
127 | {
128 | return $this->name;
129 | }
130 |
131 | /**
132 | * @return NamingStrategy
133 | */
134 | public function getNamingStrategy()
135 | {
136 | return $this->namingStrategy;
137 | }
138 |
139 | /**
140 | * @return StorageStrategy
141 | */
142 | public function getStorageStrategy()
143 | {
144 | return $this->storageStrategy;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Storage/FilesystemAdapterInterface.php:
--------------------------------------------------------------------------------
1 | file = $file;
17 | }
18 |
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function exists()
23 | {
24 | return $this->file->exists();
25 | }
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | public function getSize()
31 | {
32 | return $this->file->getSize();
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function getName()
39 | {
40 | return $this->file->getPath();
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function getFile()
47 | {
48 | return $this->file;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Storage/FlysystemFilesystemAdapter.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function getFilesystem()
28 | {
29 | return $this->filesystem;
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function getAdapter()
36 | {
37 | return $this->filesystem->getAdapter();
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function has($path)
44 | {
45 | return $this->filesystem->has($path);
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function get($path)
52 | {
53 | return new FlysystemFileAdapter(new File($this->filesystem, $path));
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function write($path, $content, array $config = array())
60 | {
61 | try {
62 | return $this->filesystem->write($path, $content, $config);
63 | } catch (FileExistsException $ex) {
64 | throw $this->createFileExistsException($path, $ex);
65 | }
66 | }
67 |
68 | /**
69 | * {@inheritdoc}
70 | */
71 | public function writeStream($path, $resource, array $config = array())
72 | {
73 | try {
74 | return $this->filesystem->writeStream($path, $resource, $config);
75 | } catch (FileExistsException $ex) {
76 | throw $this->createFileExistsException($path, $ex);
77 | }
78 | }
79 |
80 | /**
81 | * {@inheritdoc}
82 | */
83 | public function put($path, $content, array $config = array())
84 | {
85 | return $this->filesystem->put($path, $content, $config);
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function putStream($path, $resource, array $config = array())
92 | {
93 | return $this->filesystem->putStream($path, $resource, $config);
94 | }
95 |
96 | /**
97 | * {@inheritdoc}
98 | */
99 | public function read($path)
100 | {
101 | try {
102 | return $this->filesystem->read($path);
103 | } catch (FileNotFoundException $ex) {
104 | throw $this->createFileNotFoundException($path, $ex);
105 | }
106 | }
107 |
108 | /**
109 | * {@inheritdoc}
110 | */
111 | public function readStream($path)
112 | {
113 | try {
114 | return $this->filesystem->readStream($path);
115 | } catch (FileNotFoundException $ex) {
116 | throw $this->createFileNotFoundException($path, $ex);
117 | }
118 | }
119 |
120 | /**
121 | * {@inheritdoc}
122 | */
123 | public function delete($path)
124 | {
125 | try {
126 | return $this->filesystem->delete($path);
127 | } catch (FileNotFoundException $ex) {
128 | throw $this->createFileNotFoundException($path, $ex);
129 | }
130 | }
131 |
132 | /**
133 | * {@inheritdoc}
134 | */
135 | public function getStreamCopy($path)
136 | {
137 | $stream = $this->readStream($path);
138 |
139 | // Neatly overflow into a file on disk after more than 10MBs.
140 | $mbLimit = 10 * 1024 * 1024;
141 | $streamCopy = fopen("php://temp/maxmemory:$mbLimit", 'w+b');
142 |
143 | stream_copy_to_stream($stream, $streamCopy);
144 | rewind($streamCopy);
145 |
146 | return $streamCopy;
147 | }
148 |
149 | /**
150 | * {@inheritdoc}
151 | */
152 | public function getModifiedTimestamp($path)
153 | {
154 | if (false === $timestamp = $this->filesystem->getTimestamp($path)) {
155 | throw $this->createFileNotFoundException($path);
156 | }
157 |
158 | return $timestamp;
159 | }
160 |
161 | /**
162 | * {@inheritdoc}
163 | */
164 | public function getSize($path)
165 | {
166 | if (false === $size = $this->filesystem->getSize($path)) {
167 | throw $this->createFileNotFoundException($path);
168 | }
169 |
170 | return $size;
171 | }
172 |
173 | /**
174 | * {@inheritdoc}
175 | */
176 | public function getMimeType($path)
177 | {
178 | if (false === $mimeType = $this->filesystem->getMimetype($path)) {
179 | throw $this->createFileNotFoundException($path);
180 | }
181 |
182 | return $mimeType;
183 | }
184 |
185 | protected function createFileNotFoundException($path, $previousEx = null)
186 | {
187 | if ($previousEx === null) {
188 | $previousEx = new FileNotFoundException($path);
189 | }
190 |
191 | return new WrappingFileNotFoundException($previousEx->getMessage(), $previousEx->getCode(), $previousEx);
192 | }
193 |
194 | protected function createFileExistsException($path, $previousEx = null)
195 | {
196 | if ($previousEx === null) {
197 | $previousEx = new FileExistsException($path);
198 | }
199 |
200 | return new WrappingFileExistsException($previousEx->getMessage(), $previousEx->getCode(), $previousEx);
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/Storage/GaufretteFileAdapter.php:
--------------------------------------------------------------------------------
1 | file = $file;
17 | }
18 |
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function exists()
23 | {
24 | return $this->file->exists();
25 | }
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | public function getSize()
31 | {
32 | return $this->file->getSize();
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function getName()
39 | {
40 | return $this->file->getKey();
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function getFile()
47 | {
48 | return $this->file;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Storage/GaufretteFilesystemAdapter.php:
--------------------------------------------------------------------------------
1 | filesystem = $filesystem;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public function getFilesystem()
29 | {
30 | return $this->filesystem;
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function getAdapter()
37 | {
38 | return $this->filesystem->getAdapter();
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function has($path)
45 | {
46 | return $this->filesystem->has($path);
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function get($path)
53 | {
54 | return new GaufretteFileAdapter($this->filesystem->get($path));
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function write($path, $content, array $config = array())
61 | {
62 | return $this->writeContents($path, $content, $config, false);
63 | }
64 |
65 | /**
66 | * {@inheritdoc}
67 | */
68 | public function writeStream($path, $resource, array $config = array())
69 | {
70 | // This is not ideal, stream_get_contents will read the full stream into memory before we can
71 | // flow it into the write function. Watch out with big files and Gaufrette!
72 | return $this->writeContents($path, stream_get_contents($resource, -1, 0), $config, false);
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function put($path, $content, array $config = array())
79 | {
80 | return $this->writeContents($path, $content, $config, true);
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | public function putStream($path, $resource, array $config = array())
87 | {
88 | // This is not ideal, stream_get_contents will read the full stream into memory before we can
89 | // flow it into the write function. Watch out with big files and Gaufrette!
90 | return $this->writeContents($path, stream_get_contents($resource, -1, 0), $config, true);
91 | }
92 |
93 | /**
94 | * General function for all writes.
95 | *
96 | * @param $path
97 | * @param $content
98 | * @param array $config
99 | * @param bool $overwrite
100 | *
101 | * @return bool
102 | */
103 | protected function writeContents($path, $content, array $config = array(), $overwrite = false)
104 | {
105 | if (!empty($config['metadata'])) {
106 | $adapter = $this->getAdapter();
107 | if ($adapter instanceof MetadataSupporter) {
108 | $allowed = empty($config['allowedMetadataKeys'])
109 | ? array(FileStorage::METADATA_CONTENT_TYPE)
110 | : array_merge($config['allowedMetadataKeys'], array(FileStorage::METADATA_CONTENT_TYPE));
111 |
112 | $adapter->setMetadata($path, $this->resolveMetadataMap($allowed, $config['metadata']));
113 | }
114 | }
115 |
116 | try {
117 | $this->filesystem->write($path, $content, $overwrite);
118 |
119 | return true;
120 | } catch (\RuntimeException $ex) {
121 | return false;
122 | }
123 | }
124 |
125 | /**
126 | * Resolve the metadata map.
127 | *
128 | * @param array $allowedMetadataKeys
129 | * @param array $metadataMap
130 | *
131 | * @return array
132 | */
133 | protected function resolveMetadataMap(array $allowedMetadataKeys, array $metadataMap)
134 | {
135 | $map = array();
136 |
137 | foreach ($allowedMetadataKeys as $key) {
138 | if (array_key_exists($key, $metadataMap)) {
139 | $map[$key] = $metadataMap[$key];
140 | }
141 | }
142 |
143 | return $map;
144 | }
145 |
146 | /**
147 | * {@inheritdoc}
148 | */
149 | public function read($path)
150 | {
151 | try {
152 | $this->filesystem->read($path);
153 |
154 | return true;
155 | } catch (FileNotFound $ex) {
156 | throw $this->createFileNotFoundException($path, $ex);
157 | } catch (\RuntimeException $ex) {
158 | return false;
159 | }
160 | }
161 |
162 | /**
163 | * {@inheritdoc}
164 | */
165 | public function readStream($path)
166 | {
167 | if (!$this->filesystem->has($path)) {
168 | throw $this->createFileNotFoundException($path);
169 | }
170 |
171 | // If castable to a real stream (local filesystem for instance) use that stream.
172 | $streamWrapper = $this->filesystem->createStream($path);
173 | $streamWrapper->open(new StreamMode('rb'));
174 | $stream = $streamWrapper->cast(0);
175 |
176 | if ($stream === false) {
177 | // This is not ideal, read will first read the full file into memory before we can
178 | // flow it into the temp stream. Watch out with big files and Gaufrette!
179 | $stream = fopen('php://temp', 'w+b');
180 | fwrite($stream, $this->read($path));
181 | rewind($stream);
182 | }
183 |
184 | return $stream;
185 | }
186 |
187 | /**
188 | * {@inheritdoc}
189 | */
190 | public function getStreamCopy($path)
191 | {
192 | if (!$this->filesystem->has($path)) {
193 | throw $this->createFileNotFoundException($path);
194 | }
195 |
196 | // If castable to a real stream (local filesystem for instance) use that stream.
197 | $streamWrapper = $this->filesystem->createStream($path);
198 | $streamWrapper->open(new StreamMode('rb'));
199 | $stream = $streamWrapper->cast(0);
200 |
201 | // Neatly overflow into a file on disk after more than 10MBs.
202 | $mbLimit = 10 * 1024 * 1024;
203 | $streamCopy = fopen("php://temp/maxmemory:$mbLimit", 'w+b');
204 | if ($stream === false) {
205 | // This is not ideal, read will first read the full file into memory before we can
206 | // flow it into the temp stream. Watch out with big files and Gaufrette!
207 | $data = $this->read($path);
208 | fwrite($streamCopy, $data);
209 | } else {
210 | stream_copy_to_stream($stream, $streamCopy);
211 | }
212 |
213 | rewind($streamCopy);
214 |
215 | return $streamCopy;
216 | }
217 |
218 | /**
219 | * {@inheritdoc}
220 | */
221 | public function delete($path)
222 | {
223 | try {
224 | return $this->filesystem->delete($path);
225 | } catch (FileNotFound $ex) {
226 | throw $this->createFileNotFoundException($path, $ex);
227 | }
228 | }
229 |
230 | /**
231 | * {@inheritdoc}
232 | */
233 | public function getModifiedTimestamp($path)
234 | {
235 | try {
236 | return $this->filesystem->mtime($path);
237 | } catch (FileNotFound $ex) {
238 | throw $this->createFileNotFoundException($path, $ex);
239 | }
240 | }
241 |
242 | /**
243 | * {@inheritdoc}
244 | */
245 | public function getSize($path)
246 | {
247 | try {
248 | return $this->filesystem->size($path);
249 | } catch (FileNotFound $ex) {
250 | throw $this->createFileNotFoundException($path, $ex);
251 | }
252 | }
253 |
254 | /**
255 | * {@inheritdoc}
256 | */
257 | public function getMimeType($path)
258 | {
259 | try {
260 | return $this->filesystem->mimeType($path);
261 | } catch (FileNotFound $ex) {
262 | throw $this->createFileNotFoundException($path, $ex);
263 | }
264 | }
265 |
266 | protected function createFileNotFoundException($path, $previousEx = null)
267 | {
268 | if ($previousEx === null) {
269 | $previousEx = new FileNotFound($path);
270 | }
271 |
272 | return new WrappingFileNotFoundException($previousEx->getMessage(), $previousEx->getCode(), $previousEx);
273 | }
274 |
275 | protected function createFileExistsException($path, $previousEx = null)
276 | {
277 | if ($previousEx === null) {
278 | $previousEx = new FileAlreadyExists($path);
279 | }
280 |
281 | return new WrappingFileExistsException($previousEx->getMessage(), $previousEx->getCode(), $previousEx);
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/Storage/UploadedFile.php:
--------------------------------------------------------------------------------
1 | storage = $storage;
24 | $this->file = $file;
25 | }
26 |
27 | /**
28 | * @return FileAdapterInterface
29 | */
30 | public function getFile()
31 | {
32 | return $this->file;
33 | }
34 |
35 | /**
36 | * @return \SRIO\RestUploadBundle\Storage\FileStorage
37 | */
38 | public function getStorage()
39 | {
40 | return $this->storage;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Strategy/DefaultNamingStrategy.php:
--------------------------------------------------------------------------------
1 | load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
27 | }
28 |
29 | /**
30 | * @return string
31 | */
32 | public function getCacheDir()
33 | {
34 | return sys_get_temp_dir().'/SRIORestUploadBundle/cache';
35 | }
36 |
37 | /**
38 | * @return string
39 | */
40 | public function getLogDir()
41 | {
42 | return sys_get_temp_dir().'/SRIORestUploadBundle/logs';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/config.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: parameters.yml }
3 |
4 | framework:
5 | secret: MySecret
6 | templating: { engines: ['twig'] }
7 | router: { resource: "%kernel.root_dir%/config/routing.yml" }
8 | form: true
9 | csrf_protection: true
10 | session: ~
11 | default_locale: en
12 | translator: { fallback: en }
13 | profiler: { only_exceptions: false }
14 |
15 | # Doctrine Configuration
16 | doctrine:
17 | dbal:
18 | driver: %database_driver%
19 | host: %database_host%
20 | port: %database_port%
21 | dbname: %database_name%
22 | user: %database_user%
23 | password: %database_password%
24 | charset: UTF8
25 |
26 | orm:
27 | auto_generate_proxy_classes: %kernel.debug%
28 | auto_mapping: true
29 | mappings:
30 | RestUploadBundleTest:
31 | type: annotation
32 | is_bundle: false
33 | dir: %kernel.root_dir%/../../Entity
34 | prefix: SRIO\RestUploadBundle\Tests\Fixtures\Entity
35 | alias: RestUploadBundleTest
36 |
37 | SRIORestUploadBundle:
38 | type: xml
39 | is_bundle: true
40 |
41 | # enable the web profiler
42 | web_profiler:
43 | toolbar: false
44 | intercept_redirects: false
45 |
46 | # Gaufrette configuration
47 | knp_gaufrette:
48 | adapters:
49 | test:
50 | local:
51 | directory: %kernel.root_dir%/../web/uploads
52 | filesystems:
53 | test:
54 | adapter: test
55 |
56 | oneup_flysystem:
57 | adapters:
58 | test:
59 | local:
60 | directory: %kernel.root_dir%/../web/uploads
61 | filesystems:
62 | test:
63 | adapter: test
64 |
65 | # RestUploadBundle configuration
66 | srio_rest_upload:
67 | upload_type_parameter: uploadType
68 | resumable_entity_class: SRIO\RestUploadBundle\Tests\Fixtures\Entity\ResumableUploadSession
69 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/config_flysystem.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: config.yml }
3 |
4 | framework:
5 | test: ~
6 | session:
7 | storage_id: session.storage.filesystem
8 |
9 | srio_rest_upload:
10 | storages:
11 | default:
12 | type: flysystem
13 | filesystem: oneup_flysystem.test_filesystem
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/config_gaufrette.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: config.yml }
3 |
4 | framework:
5 | test: ~
6 | session:
7 | storage_id: session.storage.filesystem
8 |
9 | srio_rest_upload:
10 | storages:
11 | default:
12 | type: gaufrette
13 | filesystem: gaufrette.test_filesystem
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/config_test.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: config.yml }
3 |
4 | framework:
5 | test: ~
6 | session:
7 | storage_id: session.storage.filesystem
8 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/parameters.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | database_driver: pdo_mysql
3 | database_host: 127.0.0.1
4 | database_port: ~
5 | database_name: restuploadbundle
6 | database_user: root
7 | database_password: ~
8 |
9 | mailer_transport: smtp
10 | mailer_host: 127.0.0.1
11 | mailer_user: ~
12 | mailer_password: ~
13 |
14 | locale: en
15 | secret: ThisTokenIsNotSoSecretChangeIt
16 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/parameters.yml.dist:
--------------------------------------------------------------------------------
1 | parameters:
2 | database_driver: pdo_mysql
3 | database_host: 127.0.0.1
4 | database_port: ~
5 | database_name: restuploadbundle
6 | database_user: ~
7 | database_password: ~
8 |
9 | mailer_transport: smtp
10 | mailer_host: 127.0.0.1
11 | mailer_user: ~
12 | mailer_password: ~
13 |
14 | locale: en
15 | secret: ThisTokenIsNotSoSecretChangeIt
16 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/config/routing.yml:
--------------------------------------------------------------------------------
1 | srio_rest_upload_test_bundle:
2 | resource: "@SRIORestUploadBundle/Tests/Fixtures/Controller/"
3 | type: annotation
4 | prefix: /
5 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/app/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev');
19 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod';
20 |
21 | if ($debug) {
22 | Debug::enable();
23 | }
24 |
25 | $kernel = new AppKernel($env, $debug);
26 | $application = new Application($kernel);
27 | $application->run($input);
28 |
--------------------------------------------------------------------------------
/Tests/Fixtures/App/web/.gitignore:
--------------------------------------------------------------------------------
1 | /uploads
--------------------------------------------------------------------------------
/Tests/Fixtures/App/web/uploads/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sroze/SRIORestUploadBundle/ba3e25acd038f51b886f6587629596037c931119/Tests/Fixtures/App/web/uploads/.gitkeep
--------------------------------------------------------------------------------
/Tests/Fixtures/Controller/UploadController.php:
--------------------------------------------------------------------------------
1 | createForm(new MediaFormType());
33 |
34 | /** @var $uploadHandler UploadHandler */
35 | $uploadHandler = $this->get('srio_rest_upload.upload_handler');
36 | $result = $uploadHandler->handleRequest($request, $form);
37 |
38 | if (($response = $result->getResponse()) != null) {
39 | return $response;
40 | }
41 |
42 | if (!$form->isValid()) {
43 | throw new BadRequestHttpException();
44 | }
45 |
46 | if (($file = $result->getFile()) !== null) {
47 | /** @var $media Media */
48 | $media = $form->getData();
49 | $media->setFile($file);
50 |
51 | $em = $this->getDoctrine()->getManager();
52 | $em->persist($media);
53 | $em->flush();
54 |
55 | return new JsonResponse($media);
56 | }
57 |
58 | throw new NotAcceptableHttpException();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/Fixtures/Entity/Media.php:
--------------------------------------------------------------------------------
1 | path = $uploaded->getFile()->getName();
63 | $this->size = $uploaded->getFile()->getSize();
64 |
65 | // TODO Add mimetype on `UploadedFile`
66 | $this->mimeType = $uploaded->getStorage()->getFilesystem()->getMimeType($this->path);
67 |
68 | // TODO Add original name
69 | $this->originalName = $uploaded->getFile()->getName();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Tests/Fixtures/Entity/ResumableUploadSession.php:
--------------------------------------------------------------------------------
1 | add('name', 'text', array(
19 | 'required' => true,
20 | 'constraints' => array(new NotBlank()),
21 | ))
22 | ;
23 | }
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public function setDefaultOptions(OptionsResolverInterface $resolver)
29 | {
30 | $resolver->setDefaults(array(
31 | 'csrf_protection' => false,
32 | 'data_class' => 'SRIO\RestUploadBundle\Tests\Fixtures\Entity\Media',
33 | ));
34 | }
35 |
36 | public function getName()
37 | {
38 | return;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/Fixtures/Resources/apple.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sroze/SRIORestUploadBundle/ba3e25acd038f51b886f6587629596037c931119/Tests/Fixtures/Resources/apple.gif
--------------------------------------------------------------------------------
/Tests/Fixtures/Resources/lorem.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tristique felis tellus, ac fermentum turpis vulputate cursus. Quisque eget eros ac mi lobortis mollis in ut mauris. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec dignissim vulputate tortor ac lobortis. Cras ac ante ut sapien mollis porta eu vel leo. Aenean varius leo quis nulla vehicula, quis euismod diam interdum. Aenean et justo nec tortor volutpat volutpat. Curabitur et risus at mi rutrum posuere. Duis neque purus, condimentum quis dolor sed, tincidunt fringilla lorem. Proin gravida urna nec orci porttitor, sed condimentum arcu iaculis. Sed placerat eget sapien nec hendrerit.
2 |
3 | Nulla accumsan interdum nunc, at accumsan velit tristique ac. Vivamus vulputate convallis mi in tristique. Aliquam vehicula, purus at luctus dignissim, velit sem consectetur magna, a sagittis est nibh sed metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In hac habitasse platea dictumst. Fusce accumsan tincidunt ullamcorper. Nulla facilisi.
4 |
5 | Maecenas non bibendum nunc. Vivamus in adipiscing lectus, sed sagittis mi. Sed ut odio ultricies, mattis tellus nec, consectetur eros. Morbi eu interdum risus. Duis pellentesque tortor erat, eu vulputate nulla consectetur non. Aenean fermentum imperdiet enim, pulvinar volutpat magna eleifend id. Etiam condimentum, ipsum vel mollis dictum, nibh diam dictum sapien, nec aliquam purus odio vitae nulla. Vestibulum rutrum ullamcorper velit id suscipit. In pretium vel sem in feugiat. Ut id rhoncus eros.
6 |
7 | In sit amet sagittis lacus. Integer quis mi lobortis, euismod purus ac, pharetra ligula. Fusce ut orci sodales, viverra diam in, mollis justo. Aliquam feugiat facilisis accumsan. Cras viverra a ante eget vestibulum. In hac habitasse platea dictumst. Nullam mattis eleifend diam et volutpat. Nulla sed condimentum augue. Nulla mollis condimentum dolor, ut scelerisque tellus blandit pellentesque.
8 |
9 | Ut egestas venenatis turpis quis ultrices. Integer blandit nisl ut nibh semper cursus. Nulla in ligula euismod, venenatis metus a, feugiat mi. Praesent interdum, risus condimentum fermentum tincidunt, neque nisl pulvinar nulla, ut dignissim purus neque quis lacus. Aenean eget sapien eu tortor convallis pretium. Etiam felis ligula, commodo vel lacus sit amet, auctor porttitor quam. Sed mollis enim in elit luctus sagittis. Pellentesque vestibulum adipiscing lacus, at venenatis ipsum dapibus vel. Etiam rhoncus massa mollis aliquam tincidunt. Quisque quam sapien, aliquet nec nibh eu, pretium tempor sapien.
--------------------------------------------------------------------------------
/Tests/Processor/AbstractProcessorTestCase.php:
--------------------------------------------------------------------------------
1 | getMethod(get_class($object), $methodName);
21 |
22 | return $method->invokeArgs($object, $arguments);
23 | }
24 |
25 | /**
26 | * Get a protected method as public.
27 | *
28 | * @param $name
29 | *
30 | * @return \ReflectionMethod
31 | */
32 | protected function getMethod($className, $name)
33 | {
34 | $class = new \ReflectionClass($className);
35 |
36 | $method = $class->getMethod($name);
37 | $method->setAccessible(true);
38 |
39 | return $method;
40 | }
41 |
42 | /**
43 | * @return \PHPUnit_Framework_MockObject_MockObject
44 | */
45 | protected function getMockEntityManager()
46 | {
47 | return $this->getMock('\Doctrine\ORM\EntityManager',
48 | array('getRepository', 'getClassMetadata', 'persist', 'flush'), array(), '', false);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/Processor/MultipartUploadProcessorTest.php:
--------------------------------------------------------------------------------
1 | getNewClient();
12 | $image = $this->getResource($client, 'apple.gif');
13 | $data = array('test' => 'OK');
14 | $jsonData = json_encode($data);
15 |
16 | $multipartUploadProcessor = $this->getProcessor();
17 | $request = $this->createMultipartRequest($jsonData, $image);
18 |
19 | $partOne = $this->callMethod($multipartUploadProcessor, 'getPart', array($request, 1));
20 | $this->assertTrue(is_array($partOne));
21 | list($contentType, $body) = $partOne;
22 |
23 | $this->assertEquals('application/json; charset=UTF-8', $contentType);
24 | $this->assertEquals($jsonData, $body);
25 |
26 | $partTwo = $this->callMethod($multipartUploadProcessor, 'getPart', array($request, 2));
27 | $this->assertTrue(is_array($partTwo));
28 | list($contentType, $body) = $partTwo;
29 |
30 | $this->assertEquals('image/gif', $contentType);
31 | $this->assertEquals($image, $body);
32 | }
33 |
34 | public function testGetPartsResource()
35 | {
36 | $client = $this->getNewClient();
37 | $image = $this->getResource($client, 'apple.gif');
38 | $data = array('test' => 'OK');
39 | $jsonData = json_encode($data);
40 | $boundary = uniqid();
41 | $content = $this->createMultipartContent($boundary, $jsonData, $image);
42 |
43 | $tempFile = $this->getResourcePath($client, 'test.tmp');
44 | file_put_contents($tempFile, $content);
45 | $resource = fopen($tempFile, 'r');
46 |
47 | $multipartUploadProcessor = $this->getProcessor();
48 | $request = $this->createMultipartRequestWithContent($boundary, $resource);
49 |
50 | $partOne = $this->callMethod($multipartUploadProcessor, 'getPart', array($request, 1));
51 | $this->assertTrue(is_array($partOne));
52 | list($contentType, $body) = $partOne;
53 |
54 | $this->assertEquals('application/json; charset=UTF-8', $contentType);
55 | $this->assertEquals($jsonData, $body);
56 |
57 | $partTwo = $this->callMethod($multipartUploadProcessor, 'getPart', array($request, 2));
58 | $this->assertTrue(is_array($partTwo));
59 | list($contentType, $body) = $partTwo;
60 |
61 | $this->assertEquals('image/gif', $contentType);
62 | $this->assertEquals($image, $body);
63 |
64 | // Clean up
65 | fclose($resource);
66 | unlink($tempFile);
67 | }
68 |
69 | protected function createMultipartRequest($jsonData, $binaryContent)
70 | {
71 | $boundary = uniqid();
72 | $content = $this->createMultipartContent($boundary, $jsonData, $binaryContent);
73 |
74 | return $this->createMultipartRequestWithContent($boundary, $content);
75 | }
76 |
77 | protected function createMultipartContent($boundary, $jsonData, $binaryContent)
78 | {
79 | $content = '--'.$boundary."\r\n".'Content-Type: application/json; charset=UTF-8'."\r\n\r\n".$jsonData."\r\n\r\n";
80 | $content .= '--'.$boundary."\r\n".'Content-Type: image/gif'."\r\n\r\n".$binaryContent."\r\n\r\n";
81 | $content .= '--'.$boundary.'--';
82 |
83 | return $content;
84 | }
85 |
86 | protected function createMultipartRequestWithContent($boundary, $content)
87 | {
88 | $request = $this->getMock('\Symfony\Component\HttpFoundation\Request');
89 | $request->expects($this->any())
90 | ->method('getContent')
91 | ->will($this->returnValue($content));
92 |
93 | $request->headers = new HeaderBag(array(
94 | 'Content-Type' => 'multipart/related; boundary="'.$boundary.'"',
95 | ));
96 |
97 | return $request;
98 | }
99 |
100 | protected function getProcessor()
101 | {
102 | $voter = $this->getMock(
103 | 'SRIO\RestUploadBundle\Voter\StorageVoter'
104 | );
105 |
106 | $storageHandler = $this->getMock(
107 | '\SRIO\RestUploadBundle\Upload\StorageHandler',
108 | array(),
109 | array($voter)
110 | );
111 |
112 | $processor = $this->getMock(
113 | '\SRIO\RestUploadBundle\Processor\MultipartUploadProcessor',
114 | array(),
115 | array($storageHandler)
116 | );
117 |
118 | return $processor;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Tests/Processor/ResumableUploadProcessorTest.php:
--------------------------------------------------------------------------------
1 | callParseContentRange($string);
15 | $this->assertTrue(is_array($result));
16 | $this->assertEquals($start, $result['start']);
17 | $this->assertEquals($end, $result['end']);
18 | $this->assertEquals($length, $result['total']);
19 | }
20 |
21 | /**
22 | * @dataProvider contentErrorRangeDataProvider
23 | * @expectedException \SRIO\RestUploadBundle\Exception\UploadProcessorException
24 | *
25 | * @param $string
26 | */
27 | public function testErrorComputeContentRange($string)
28 | {
29 | $this->callParseContentRange($string);
30 | }
31 |
32 | /**
33 | * Call parseContentRange function.
34 | *
35 | * @param $string
36 | *
37 | * @return mixed
38 | */
39 | protected function callParseContentRange($string)
40 | {
41 | $voter = $this->getMock(
42 | 'SRIO\RestUploadBundle\Voter\StorageVoter'
43 | );
44 |
45 | $storageHandler = $this->getMock(
46 | '\SRIO\RestUploadBundle\Upload\StorageHandler',
47 | array(),
48 | array($voter)
49 | );
50 |
51 | $method = $this->getMethod('\SRIO\RestUploadBundle\Processor\ResumableUploadProcessor', 'parseContentRange');
52 | $em = $this->getMockEntityManager();
53 | $uploadProcessor = new ResumableUploadProcessor($storageHandler, $em, 'SRIO\RestUploadBundle\Tests\Fixtures\Entity\ResumableUploadSession');
54 |
55 | return $method->invokeArgs($uploadProcessor, array($string));
56 | }
57 |
58 | /**
59 | * Data Provider for success Content-Range test.
60 | */
61 | public function contentSuccessRangeDataProvider()
62 | {
63 | return array(
64 | array('bytes 1-2/12', 1, 2, 12),
65 | array('bytes */1000', '*', null, 1000),
66 | array('bytes 0-1000/1000', 0, 1000, 1000),
67 | );
68 | }
69 |
70 | /**
71 | * Data Provider for error Content-Range test.
72 | */
73 | public function contentErrorRangeDataProvider()
74 | {
75 | return array(
76 | array('bytes 2-1/12'),
77 | array('bytes 12/12'),
78 | array('bytes 0-13/12'),
79 | array('1-2/12'),
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/Request/RequestContentHandlerTest.php:
--------------------------------------------------------------------------------
1 | getNewClient();
13 | $filePath = $this->getResourcePath($client, 'apple.gif');
14 | $content = file_get_contents($filePath);
15 |
16 | $this->doTest($content, $content);
17 | }
18 |
19 | public function testBinaryResourceContent()
20 | {
21 | $client = $this->getNewClient();
22 | $filePath = $this->getResourcePath($client, 'apple.gif');
23 | $content = fopen($filePath, 'r');
24 | $expectedContent = file_get_contents($filePath);
25 |
26 | $this->doTest($expectedContent, $content);
27 | }
28 |
29 | public function testStringContent()
30 | {
31 | $client = $this->getNewClient();
32 | $filePath = $this->getResourcePath($client, 'lorem.txt');
33 | $content = file_get_contents($filePath);
34 |
35 | $this->doTest($content, $content);
36 | }
37 |
38 | public function testStringResourceContent()
39 | {
40 | $client = $this->getNewClient();
41 | $filePath = $this->getResourcePath($client, 'lorem.txt');
42 | $content = fopen($filePath, 'r');
43 | $expectedContent = file_get_contents($filePath);
44 |
45 | $this->doTest($expectedContent, $content);
46 | }
47 |
48 | protected function doTest($expectedContent, $content)
49 | {
50 | $request = $this->getMock('\Symfony\Component\HttpFoundation\Request');
51 | $request->expects($this->any())
52 | ->method('getContent')
53 | ->will($this->returnValue($content));
54 |
55 | $handler = new RequestContentHandler($request);
56 | $this->assertFalse($handler->eof());
57 |
58 | $foundContent = '';
59 | while (!$handler->eof()) {
60 | $foundContent .= $handler->gets();
61 | }
62 |
63 | $this->assertEquals($expectedContent, $foundContent);
64 | $this->assertTrue($handler->eof());
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/Upload/AbstractUploadTestCase.php:
--------------------------------------------------------------------------------
1 | getResponse();
18 | $this->assertEquals(400, $response->getStatusCode());
19 | }
20 |
21 | /**
22 | * Get content of a resource.
23 | *
24 | * @param Client $client
25 | * @param $name
26 | *
27 | * @return string
28 | *
29 | * @throws \RuntimeException
30 | */
31 | protected function getResource(Client $client, $name)
32 | {
33 | $filePath = $this->getResourcePath($client, $name);
34 | if (!file_exists($filePath)) {
35 | throw new \RuntimeException(sprintf(
36 | 'File %s do not exists',
37 | $filePath
38 | ));
39 | }
40 |
41 | return file_get_contents($filePath);
42 | }
43 |
44 | /**
45 | * Get uploaded file path.
46 | *
47 | * @param Client $client
48 | * @param $name
49 | *
50 | * @return string
51 | */
52 | protected function getUploadedFilePath(Client $client)
53 | {
54 | return $client->getContainer()->getParameter('kernel.root_dir').'/../web/uploads';
55 | }
56 |
57 | /**
58 | * Get resource path.
59 | *
60 | * @param Client $client
61 | * @param $name
62 | *
63 | * @return string
64 | */
65 | protected function getResourcePath(Client $client, $name)
66 | {
67 | return $client->getContainer()->getParameter('kernel.root_dir').'/../../Resources/'.$name;
68 | }
69 |
70 | /**
71 | * Creates a Client.
72 | *
73 | * @param array $options An array of options to pass to the createKernel class
74 | * @param array $server An array of server parameters
75 | *
76 | * @return Client A Client instance
77 | */
78 | protected function getNewClient(array $options = array(), array $server = array())
79 | {
80 | $options = array_merge(array('environment' => isset($_SERVER['TEST_FILESYSTEM']) ? strtolower($_SERVER['TEST_FILESYSTEM']) : 'gaufrette'), $options);
81 |
82 | return static::createClient($options, $server);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Tests/Upload/MultipartUploadTest.php:
--------------------------------------------------------------------------------
1 | getNewClient();
10 | $queryParameters = array('name' => 'test');
11 |
12 | $boundary = uniqid();
13 | $content = '--'.$boundary."\r\n".'Content-Type: application/json; charset=UTF-8'."\r\n\r\n".json_encode($queryParameters)."\r\n\r\n";
14 | $content .= '--'.$boundary.'--';
15 |
16 | $client->request('POST', '/upload?uploadType=multipart', array(), array(), array(
17 | 'CONTENT_TYPE' => 'multipart/related; boundary="'.$boundary.'"',
18 | 'CONTENT_LENGTH' => strlen($content),
19 | ), $content);
20 | $this->assertResponseHasErrors($client);
21 | }
22 |
23 | public function testWithoutHeaders()
24 | {
25 | $client = $this->getNewClient();
26 | $queryParameters = array('name' => 'test');
27 |
28 | $boundary = uniqid();
29 | $image = $this->getResource($client, 'apple.gif');
30 | $content = '--'.$boundary."\r\n".'Content-Type: image/gif'."\r\n\r\n".$image."\r\n\r\n";
31 | $content .= '--'.$boundary."\r\n".'Content-Type: application/json; charset=UTF-8'."\r\n\r\n".json_encode($queryParameters)."\r\n\r\n";
32 | $content .= '--'.$boundary.'--';
33 |
34 | $client->request('POST', '/upload?uploadType=multipart', array(), array(), array(), $content);
35 | $this->assertResponseHasErrors($client);
36 | }
37 |
38 | public function testWithoutBoundary()
39 | {
40 | $client = $this->getNewClient();
41 | $queryParameters = array('name' => 'test');
42 |
43 | $boundary = uniqid();
44 | $image = $this->getResource($client, 'apple.gif');
45 | $content = '--'.$boundary."\r\n".'Content-Type: image/gif'."\r\n\r\n".$image."\r\n\r\n";
46 | $content .= '--'.$boundary."\r\n".'Content-Type: application/json; charset=UTF-8'."\r\n\r\n".json_encode($queryParameters)."\r\n\r\n";
47 | $content .= '--'.$boundary.'--';
48 |
49 | $client->request('POST', '/upload?uploadType=multipart', array(), array(), array(
50 | 'CONTENT_TYPE' => 'multipart/related',
51 | 'CONTENT_LENGTH' => strlen($content),
52 | ), $content);
53 | $this->assertResponseHasErrors($client);
54 | }
55 |
56 | public function testBinaryBeforeMeta()
57 | {
58 | $client = $this->getNewClient();
59 | $queryParameters = array('name' => 'test');
60 |
61 | $boundary = uniqid();
62 | $image = $this->getResource($client, 'apple.gif');
63 | $content = '--'.$boundary."\r\n".'Content-Type: image/gif'."\r\n\r\n".$image."\r\n\r\n";
64 | $content .= '--'.$boundary."\r\n".'Content-Type: application/json; charset=UTF-8'."\r\n\r\n".json_encode($queryParameters)."\r\n\r\n";
65 | $content .= '--'.$boundary.'--';
66 |
67 | $client->request('POST', '/upload?uploadType=multipart', array(), array(), array(
68 | 'CONTENT_TYPE' => 'multipart/related; boundary="'.$boundary.'"',
69 | 'CONTENT_LENGTH' => strlen($content),
70 | ), $content);
71 | $this->assertResponseHasErrors($client);
72 | }
73 |
74 | public function testMultipartUpload()
75 | {
76 | $client = $this->getNewClient();
77 | $queryParameters = array('name' => 'test');
78 |
79 | $boundary = uniqid();
80 | $image = $this->getResource($client, 'apple.gif');
81 | $content = '--'.$boundary."\r\n".'Content-Type: application/json; charset=UTF-8'."\r\n\r\n".json_encode($queryParameters)."\r\n\r\n";
82 | $content .= '--'.$boundary."\r\n".'Content-Type: image/gif'."\r\n\r\n".$image."\r\n\r\n";
83 | $content .= '--'.$boundary.'--';
84 |
85 | $client->request('POST', '/upload?uploadType=multipart', array(), array(), array(
86 | 'CONTENT_TYPE' => 'multipart/related; boundary="'.$boundary.'"',
87 | 'CONTENT_LENGTH' => strlen($content),
88 | ), $content);
89 |
90 | $response = $client->getResponse();
91 | $this->assertEquals(200, $response->getStatusCode());
92 | $jsonContent = json_decode($response->getContent(), true);
93 | $this->assertNotEmpty($jsonContent);
94 | $this->assertFalse(array_key_exists('errors', $jsonContent));
95 | $this->assertTrue(array_key_exists('path', $jsonContent));
96 | $this->assertTrue(array_key_exists('size', $jsonContent));
97 | $this->assertTrue(array_key_exists('name', $jsonContent));
98 | $this->assertEquals('test', $jsonContent['name']);
99 | $this->assertEquals(strlen($image), $jsonContent['size']);
100 |
101 | $filePath = $this->getUploadedFilePath($client).$jsonContent['path'];
102 | $this->assertTrue(file_exists($filePath));
103 | $this->assertEquals($image, file_get_contents($filePath));
104 | $this->assertTrue(array_key_exists('id', $jsonContent));
105 | $this->assertNotEmpty($jsonContent['id']);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Tests/Upload/ResumableUploadTest.php:
--------------------------------------------------------------------------------
1 | startSession();
12 |
13 | $response = $client->getResponse();
14 | $location = $response->headers->get('Location');
15 | $content = $this->getResource($client, 'apple.gif');
16 | $client->request('PUT', $location, array(), array(), array(
17 | 'CONTENT_TYPE' => 'image/gif',
18 | 'CONTENT_LENGTH' => strlen($content),
19 | ), $content);
20 |
21 | $this->assertSuccessful($client, $content);
22 | }
23 |
24 | public function testChunkedUpload()
25 | {
26 | $client = $this->startSession();
27 |
28 | $response = $client->getResponse();
29 | $location = $response->headers->get('Location');
30 | $content = $this->getResource($client, 'apple.gif');
31 | $chunkSize = 256;
32 |
33 | for ($start = 0; $start < strlen($content); $start += $chunkSize) {
34 | $part = substr($content, $start, $chunkSize);
35 | $end = $start + strlen($part) - 1;
36 | $client->request('PUT', $location, array(), array(), array(
37 | 'CONTENT_TYPE' => 'image/gif',
38 | 'CONTENT_LENGTH' => strlen($part),
39 | 'HTTP_Content-Range' => 'bytes '.$start.'-'.$end.'/'.strlen($content),
40 | ), $part);
41 |
42 | $response = $client->getResponse();
43 | if (($start + $chunkSize) < strlen($content)) {
44 | $this->assertEquals(308, $response->getStatusCode());
45 | $this->assertEquals('0-'.$end, $response->headers->get('Range'));
46 |
47 | $client->request('PUT', $location, array(), array(), array(
48 | 'CONTENT_LENGTH' => 0,
49 | 'HTTP_Content-Range' => 'bytes */'.strlen($content),
50 | ));
51 |
52 | $response = $client->getResponse();
53 | $this->assertEquals(308, $response->getStatusCode());
54 | $this->assertEquals('0-'.$end, $response->headers->get('Range'));
55 | }
56 | }
57 |
58 | $this->assertSuccessful($client, $content);
59 | }
60 |
61 | protected function startSession()
62 | {
63 | $client = $this->getNewClient();
64 | $content = $this->getResource($client, 'apple.gif');
65 | $parameters = array('name' => 'test');
66 | $json = json_encode($parameters);
67 |
68 | $client->request('POST', '/upload?uploadType=resumable', array(), array(), array(
69 | 'CONTENT_TYPE' => 'application/json',
70 | 'CONTENT_LENGTH' => strlen($json),
71 | 'HTTP_X-Upload-Content-Type' => 'image/gif',
72 | 'HTTP_X-Upload-Content-Length' => strlen($content),
73 | ), $json);
74 |
75 | $response = $client->getResponse();
76 | $this->assertEquals(200, $response->getStatusCode());
77 | $this->assertTrue($response->headers->has('Location'));
78 | $this->assertEquals(0, $response->headers->get('Content-Length', 0));
79 |
80 | return $client;
81 | }
82 |
83 | protected function assertSuccessful(Client $client, $content)
84 | {
85 | $response = $client->getResponse();
86 | $this->assertEquals(200, $response->getStatusCode());
87 | $jsonContent = json_decode($response->getContent(), true);
88 | $this->assertNotEmpty($jsonContent);
89 | $this->assertTrue(array_key_exists('path', $jsonContent));
90 | $this->assertTrue(array_key_exists('size', $jsonContent));
91 | $this->assertTrue(array_key_exists('name', $jsonContent));
92 | $this->assertEquals('test', $jsonContent['name']);
93 | $this->assertEquals(strlen($content), $jsonContent['size']);
94 |
95 | $filePath = $this->getUploadedFilePath($client).$jsonContent['path'];
96 | $this->assertTrue(file_exists($filePath));
97 | $this->assertEquals($content, file_get_contents($filePath));
98 | $this->assertTrue(array_key_exists('id', $jsonContent));
99 | $this->assertNotEmpty($jsonContent['id']);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Tests/Upload/SimpleUploadTest.php:
--------------------------------------------------------------------------------
1 | getNewClient();
10 | $queryParameters = array('uploadType' => 'simple', 'name' => 'test');
11 |
12 | $content = $this->getResource($client, 'apple.gif');
13 |
14 | // Set empty content type header since Request defaults to application/x-www-form-urlencoded
15 | $client->request('POST', '/upload?'.http_build_query($queryParameters), array(), array(), array('CONTENT_TYPE' => ''), $content);
16 |
17 | $response = $client->getResponse();
18 | $this->assertEquals(400, $response->getStatusCode());
19 | }
20 |
21 | public function testWithoutFormSimpleUpload()
22 | {
23 | $client = $this->getNewClient();
24 | $queryParameters = array('uploadType' => 'simple');
25 | $content = $this->getResource($client, 'apple.gif');
26 | $client->request('POST', '/upload?'.http_build_query($queryParameters), array(), array(), array(
27 | 'CONTENT_TYPE' => 'image/gif',
28 | 'CONTENT_LENGTH' => strlen($content),
29 | ));
30 |
31 | $response = $client->getResponse();
32 | $this->assertEquals(400, $response->getStatusCode());
33 | }
34 |
35 | public function testWithoutContentSimpleUpload()
36 | {
37 | $client = $this->getNewClient();
38 | $queryParameters = array('uploadType' => 'simple', 'name' => 'test');
39 | $client->request('POST', '/upload?'.http_build_query($queryParameters));
40 |
41 | $response = $client->getResponse();
42 | $this->assertEquals(400, $response->getStatusCode());
43 | }
44 |
45 | public function testSimpleUpload()
46 | {
47 | $client = $this->getNewClient();
48 | $queryParameters = array('uploadType' => 'simple', 'name' => 'test');
49 |
50 | $content = $this->getResource($client, 'apple.gif');
51 | $client->request('POST', '/upload?'.http_build_query($queryParameters), array(), array(), array(
52 | 'CONTENT_TYPE' => 'image/gif',
53 | 'CONTENT_LENGTH' => strlen($content),
54 | ), $content);
55 |
56 | $response = $client->getResponse();
57 | $this->assertEquals(200, $response->getStatusCode());
58 | $jsonContent = json_decode($response->getContent(), true);
59 | $this->assertNotEmpty($jsonContent);
60 | $this->assertTrue(array_key_exists('path', $jsonContent));
61 | $this->assertTrue(array_key_exists('size', $jsonContent));
62 | $this->assertTrue(array_key_exists('name', $jsonContent));
63 | $this->assertEquals('test', $jsonContent['name']);
64 | $this->assertEquals(strlen($content), $jsonContent['size']);
65 |
66 | $localPath = $this->getUploadedFilePath($client).$jsonContent['path'];
67 | $this->assertTrue(file_exists($localPath));
68 | $this->assertEquals($content, file_get_contents($localPath));
69 | $this->assertTrue(array_key_exists('id', $jsonContent));
70 | $this->assertNotEmpty($jsonContent['id']);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | voter = $voter;
29 | }
30 |
31 | /**
32 | * Store a file's content.
33 | *
34 | * @param UploadContext $context
35 | * @param string $content
36 | * @param array $config
37 | * @param bool $overwrite
38 | *
39 | * @return UploadedFile
40 | */
41 | public function store(UploadContext $context, $contents, array $config = array(), $overwrite = false)
42 | {
43 | return $this->getStorage($context)->store($context, $contents, $config, $overwrite);
44 | }
45 |
46 | /**
47 | * Store a file's content.
48 | *
49 | * @param UploadContext $context
50 | * @param resource $resource
51 | * @param array $config
52 | * @param bool $overwrite
53 | *
54 | * @return UploadedFile
55 | */
56 | public function storeStream(UploadContext $context, $resource, array $config = array(), $overwrite = false)
57 | {
58 | return $this->getStorage($context)->storeStream($context, $resource, $config, $overwrite);
59 | }
60 |
61 | /**
62 | * @return FilesystemAdapterInterface
63 | */
64 | public function getFilesystem(UploadContext $context)
65 | {
66 | return $this->getStorage($context)->getFilesystem();
67 | }
68 |
69 | /**
70 | * Get storage by upload context.
71 | *
72 | * @param UploadContext $context
73 | *
74 | * @return FileStorage
75 | *
76 | * @throws \SRIO\RestUploadBundle\Exception\UploadException
77 | */
78 | public function getStorage(UploadContext $context)
79 | {
80 | $storage = $this->voter->getStorage($context);
81 | if (!$storage instanceof FileStorage) {
82 | throw new UploadException('Storage returned by voter isn\'t instanceof FileStorage');
83 | }
84 |
85 | return $storage;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Upload/UploadContext.php:
--------------------------------------------------------------------------------
1 | request = $request;
45 | $this->form = $form;
46 | $this->config = $config;
47 | }
48 |
49 | /**
50 | * @param array $config
51 | */
52 | public function setConfig($config)
53 | {
54 | $this->config = $config;
55 | }
56 |
57 | /**
58 | * @return array
59 | */
60 | public function getConfig()
61 | {
62 | return $this->config;
63 | }
64 |
65 | /**
66 | * @param \SRIO\RestUploadBundle\Storage\UploadedFile $file
67 | */
68 | public function setFile($file)
69 | {
70 | $this->file = $file;
71 | }
72 |
73 | /**
74 | * @return \SRIO\RestUploadBundle\Storage\UploadedFile
75 | */
76 | public function getFile()
77 | {
78 | return $this->file;
79 | }
80 |
81 | /**
82 | * @param \Symfony\Component\Form\FormInterface $form
83 | */
84 | public function setForm($form)
85 | {
86 | $this->form = $form;
87 | }
88 |
89 | /**
90 | * @return \Symfony\Component\Form\FormInterface
91 | */
92 | public function getForm()
93 | {
94 | return $this->form;
95 | }
96 |
97 | /**
98 | * @param \Symfony\Component\HttpFoundation\Request $request
99 | */
100 | public function setRequest($request)
101 | {
102 | $this->request = $request;
103 | }
104 |
105 | /**
106 | * @return \Symfony\Component\HttpFoundation\Request
107 | */
108 | public function getRequest()
109 | {
110 | return $this->request;
111 | }
112 |
113 | /**
114 | * @param string $storageName
115 | */
116 | public function setStorageName($storageName)
117 | {
118 | $this->storageName = $storageName;
119 | }
120 |
121 | /**
122 | * @return string
123 | */
124 | public function getStorageName()
125 | {
126 | return $this->storageName;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Upload/UploadHandler.php:
--------------------------------------------------------------------------------
1 | uploadTypeParameter = $uploadTypeParameter;
32 | }
33 |
34 | /**
35 | * Add an upload processor.
36 | *
37 | * @param $uploadType
38 | * @param ProcessorInterface $processor
39 | *
40 | * @throws \LogicException
41 | */
42 | public function addProcessor($uploadType, ProcessorInterface $processor)
43 | {
44 | if (array_key_exists($uploadType, $this->processors)) {
45 | throw new \LogicException(sprintf(
46 | 'A processor is already registered for type %s',
47 | $uploadType
48 | ));
49 | }
50 |
51 | $this->processors[$uploadType] = $processor;
52 | }
53 |
54 | /**
55 | * Handle the upload request.
56 | *
57 | * @param Request $request
58 | * @param \Symfony\Component\Form\FormInterface $form
59 | * @param array $config
60 | *
61 | * @throws \SRIO\RestUploadBundle\Exception\UploadException
62 | *
63 | * @return UploadResult
64 | */
65 | public function handleRequest(Request $request, FormInterface $form = null, array $config = array())
66 | {
67 | try {
68 | $processor = $this->getProcessor($request, $config);
69 |
70 | return $processor->handleUpload($request, $form, $config);
71 | } catch (UploadException $e) {
72 | if ($form != null) {
73 | $form->addError(new FormError($e->getMessage()));
74 | }
75 |
76 | $result = new UploadResult();
77 | $result->setException($e);
78 | $result->setForm($form);
79 |
80 | return $result;
81 | }
82 | }
83 |
84 | /**
85 | * Get the upload processor.
86 | *
87 | * @param \Symfony\Component\HttpFoundation\Request $request
88 | * @param array $config
89 | *
90 | * @throws \SRIO\RestUploadBundle\Exception\UploadProcessorException
91 | *
92 | * @return ProcessorInterface
93 | */
94 | protected function getProcessor(Request $request, array $config)
95 | {
96 | $uploadType = $request->get($this->getUploadTypeParameter($config));
97 |
98 | if (!array_key_exists($uploadType, $this->processors)) {
99 | throw new UploadProcessorException(sprintf(
100 | 'Unknown upload processor for upload type %s',
101 | $uploadType
102 | ));
103 | }
104 |
105 | return $this->processors[$uploadType];
106 | }
107 |
108 | /**
109 | * Get the current upload type parameter.
110 | *
111 | * @param array $extraConfiguration
112 | *
113 | * @internal param $parameter
114 | * @internal param $config
115 | *
116 | * @return mixed
117 | */
118 | protected function getUploadTypeParameter(array $extraConfiguration)
119 | {
120 | return array_key_exists('uploadTypeParameter', $extraConfiguration)
121 | ? $extraConfiguration['uploadTypeParameter']
122 | : $this->uploadTypeParameter;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Upload/UploadResult.php:
--------------------------------------------------------------------------------
1 | exception = $exception;
23 | }
24 |
25 | /**
26 | * @return \SRIO\RestUploadBundle\Exception\UploadException
27 | */
28 | public function getException()
29 | {
30 | return $this->exception;
31 | }
32 |
33 | /**
34 | * @param \Symfony\Component\HttpFoundation\Response $response
35 | */
36 | public function setResponse($response)
37 | {
38 | $this->response = $response;
39 | }
40 |
41 | /**
42 | * @return \Symfony\Component\HttpFoundation\Response
43 | */
44 | public function getResponse()
45 | {
46 | return $this->response;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Voter/StorageVoter.php:
--------------------------------------------------------------------------------
1 | defaultStorage = $defaultStorage;
34 | }
35 |
36 | /**
37 | * Add a storage.
38 | *
39 | * @param FileStorage $storage
40 | *
41 | * @throws \RuntimeException
42 | */
43 | public function addStorage(FileStorage $storage)
44 | {
45 | if (array_key_exists($storage->getName(), $this->storages)) {
46 | throw new \RuntimeException(sprintf(
47 | 'Storage with name %s already exists',
48 | $storage->getName()
49 | ));
50 | }
51 |
52 | $this->storages[$storage->getName()] = $storage;
53 | }
54 |
55 | /**
56 | * Get the best storage based on request and/or parameters.
57 | *
58 | * @param UploadContext $context
59 | *
60 | * @throws \SRIO\RestUploadBundle\Exception\UploadException
61 | * @throws \RuntimeException
62 | *
63 | * @return FileStorage
64 | */
65 | public function getStorage(UploadContext $context)
66 | {
67 | if (count($this->storages) == 0) {
68 | throw new UploadException('No storage found');
69 | }
70 |
71 | if (($storageName = $context->getStorageName()) !== null
72 | || (($storageName = $this->defaultStorage) !== null)) {
73 | if (!array_key_exists($storageName, $this->storages)) {
74 | throw new \RuntimeException(sprintf(
75 | 'Storage with name %s do not exists',
76 | $storageName
77 | ));
78 | }
79 |
80 | return $this->storages[$storageName];
81 | }
82 |
83 | return current($this->storages);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "srio/rest-upload-bundle",
3 | "description": "Handle multiple rest upload ways",
4 | "keywords": ["symfony", "file", "upload", "api", "rest", "multipart", "resumable", "form-data"],
5 | "type": "symfony-bundle",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Samuel ROZE",
10 | "email": "samuel@sroze.io",
11 | "homepage": "http://www.sroze.io"
12 | }
13 | ],
14 | "require": {
15 | "php": ">=5.3.3",
16 | "symfony/symfony": ">=2.3.0",
17 | "incenteev/composer-parameter-handler": "~2.0"
18 | },
19 | "require-dev": {
20 | "sensio/framework-extra-bundle": ">=2.3.0",
21 | "doctrine/orm": "~2.2,>=2.2.3",
22 | "doctrine/doctrine-bundle": ">=1.2.0",
23 | "phpunit/phpunit": "3.7.*",
24 | "knplabs/knp-gaufrette-bundle": ">=0.1.7",
25 | "oneup/flysystem-bundle": "~1.4"
26 | },
27 | "suggest": {
28 | "doctrine/orm": ">=2.2.3,<2.4-dev",
29 | "doctrine/doctrine-bundle": ">=1.2.0",
30 | "knplabs/knp-gaufrette-bundle": "Required if you use the knplabs/gaufrette filesystem abstraction layer",
31 | "oneup/flysystem-bundle": "Required if you use the league/flysystem filesystem abstraction layer"
32 | },
33 | "autoload": {
34 | "psr-0": { "SRIO\\RestUploadBundle": "" }
35 | },
36 | "scripts": {
37 | "test": "./test.sh",
38 | "contrib": [
39 | "php-cs-fixer fix .",
40 | "./test.sh"
41 | ]
42 | },
43 | "target-dir": "SRIO/RestUploadBundle"
44 | }
45 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | ./Tests/
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ./
25 |
26 | ./Resources
27 | ./Tests
28 | ./vendor
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export TEST_FILESYSTEM="Gaufrette"
4 | vendor/bin/phpunit
5 |
6 | export TEST_FILESYSTEM="Flysystem"
7 | vendor/bin/phpunit
--------------------------------------------------------------------------------