Skip to content

Commit 1c6e420

Browse files
rthideawayklausi
authored andcommitted
feat(file-upload): Support saving multiple files at once. (drupal-graphql#1139)
1 parent fb40b3d commit 1c6e420

3 files changed

Lines changed: 153 additions & 24 deletions

File tree

src/GraphQL/Response/FileUploadResponse.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
class FileUploadResponse extends Response {
1313

1414
/**
15-
* The file entity in case of successful file upload.
15+
* The file entities in case of successful file upload.
1616
*
17-
* @var \Drupal\file\FileInterface|null
17+
* @var \Drupal\file\FileInterface[]
1818
*/
19-
protected $fileEntity;
19+
protected $fileEntities = [];
2020

2121
/**
2222
* Sets file entity.
@@ -25,14 +25,37 @@ class FileUploadResponse extends Response {
2525
* File entity.
2626
*/
2727
public function setFileEntity(FileInterface $fileEntity): void {
28-
$this->fileEntity = $fileEntity;
28+
$this->fileEntities[] = $fileEntity;
2929
}
3030

3131
/**
32-
* Get the file entity if there is one.
32+
* Sets file entities.
33+
*
34+
* @param \Drupal\file\FileInterface[] $fileEntities
35+
* File entities.
36+
*/
37+
public function setFileEntities(array $fileEntities): void {
38+
$this->fileEntities = $fileEntities;
39+
}
40+
41+
/**
42+
* Get the first file entity if there is one.
43+
*
44+
* @return \Drupal\file\FileInterface|null
45+
* First file entity or NULL.
3346
*/
3447
public function getFileEntity(): ?FileInterface {
35-
return $this->fileEntity;
48+
return $this->fileEntities[0] ?? NULL;
49+
}
50+
51+
/**
52+
* Get the file entities.
53+
*
54+
* @return \Drupal\file\FileInterface[]
55+
* File entities.
56+
*/
57+
public function getFileEntities(): array {
58+
return $this->fileEntities;
3659
}
3760

3861
}

src/GraphQL/Utility/FileUpload.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,47 @@ public function saveFileUpload(UploadedFile $uploaded_file, array $settings): Fi
281281
}
282282
}
283283

284+
/**
285+
* Validates uploaded files, saves them and returns a file upload response.
286+
*
287+
* @param \Symfony\Component\HttpFoundation\File\UploadedFile[] $uploaded_files
288+
* The file entities to upload.
289+
* @param array $settings
290+
* File settings as specified in regular file field config. Contains keys:
291+
* - file_directory: Where to upload the file
292+
* - uri_scheme: Uri scheme to upload the file to (eg public://, private://)
293+
* - file_extensions: List of valid file extensions (eg [xml, pdf])
294+
* - max_filesize: Maximum allowed size of uploaded file.
295+
*
296+
* @return \Drupal\graphql\GraphQL\Response\FileUploadResponse
297+
* The file upload response containing file entities or list of violations.
298+
*/
299+
public function saveMultipleFileUploads(array $uploaded_files, array $settings): FileUploadResponse {
300+
$response = new FileUploadResponse();
301+
foreach ($uploaded_files as $uploaded_file) {
302+
if (!$uploaded_file instanceof UploadedFile) {
303+
continue;
304+
}
305+
$file_upload_response = $this->saveFileUpload($uploaded_file, $settings);
306+
$file_entity = $file_upload_response->getFileEntity();
307+
if ($file_entity) {
308+
$response->setFileEntity($file_entity);
309+
}
310+
else {
311+
// If one file upload fails we need to delete any other uploaded files
312+
// before that. Avoids file orphans that don't belong to any entity.
313+
foreach ($response->getFileEntities() as $saved_file_entity) {
314+
$saved_file_entity->delete();
315+
}
316+
// Reset list of file entities as this is a violation response.
317+
$response->setFileEntities([]);
318+
$response->mergeViolations($file_upload_response);
319+
return $response;
320+
}
321+
}
322+
return $response;
323+
}
324+
284325
/**
285326
* Validates the file.
286327
*

tests/src/Kernel/Framework/UploadFileServiceTest.php

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,31 @@ class UploadFileServiceTest extends GraphQLTestBase {
2828
protected $uploadService;
2929

3030
/**
31-
* Path to temporary test file.
31+
* Gets the file path of the source file.
3232
*
33-
* @var string
33+
* @param string $filename
34+
* Filename of the source file to be get the file path for.
35+
*
36+
* @return string
37+
* File path of the source file.
3438
*/
35-
protected $file;
39+
protected function getSourceTestFilePath(string $filename): string {
40+
$file_system = $this->container->get('file_system');
41+
// Create dummy file, since symfony will test if it exists.
42+
$filepath = $file_system->getTempDirectory() . '/' . $filename;
43+
touch($filepath);
44+
return $filepath;
45+
}
3646

3747
/**
3848
* {@inheritdoc}
3949
*/
4050
protected function setUp() {
4151
parent::setUp();
4252
$this->installEntitySchema('file');
53+
$this->installSchema('file', ['file_usage']);
4354

4455
$this->uploadService = $this->container->get('graphql.file_upload');
45-
46-
$file_system = $this->container->get('file_system');
47-
// Create dummy file, since symfony will test if it exists.
48-
$this->file = $file_system->getTempDirectory() . '/graphql_upload_test.txt';
49-
touch($this->file);
5056
}
5157

5258
/**
@@ -119,12 +125,12 @@ public function testMissingSettings() {
119125
* Tests that the file must not be larger than the file size limit.
120126
*/
121127
public function testSizeValidation() {
122-
// Create a file with 4 bytes.
123-
file_put_contents($this->file, 'test');
124-
125128
// Create a Symfony dummy uploaded file in test mode.
126129
$uploadFile = $this->getUploadedFile(UPLOAD_ERR_OK, 4);
127130

131+
// Create a file with 4 bytes.
132+
file_put_contents($uploadFile->getRealPath(), 'test');
133+
128134
$file_upload_response = $this->uploadService->saveFileUpload($uploadFile, [
129135
'uri_scheme' => 'public',
130136
'file_directory' => 'test',
@@ -200,12 +206,12 @@ public function testLockReleased() {
200206
\Drupal::service('renderer')
201207
);
202208

203-
// Create a file with 4 bytes.
204-
file_put_contents($this->file, 'test');
205-
206209
// Create a Symfony dummy uploaded file in test mode.
207210
$uploadFile = $this->getUploadedFile(UPLOAD_ERR_OK, 4);
208211

212+
// Create a file with 4 bytes.
213+
file_put_contents($uploadFile->getRealPath(), 'test');
214+
209215
$upload_service->saveFileUpload($uploadFile, [
210216
'uri_scheme' => 'public',
211217
'file_directory' => 'test',
@@ -214,6 +220,63 @@ public function testLockReleased() {
214220
]);
215221
}
216222

223+
/**
224+
* Tests successful scenario with multiple file uploads.
225+
*/
226+
public function testSuccessWithMultipleFileUploads() {
227+
$uploadFiles = [
228+
$this->getUploadedFile(UPLOAD_ERR_OK, 0, 'test1.txt', 'graphql_upload_test1.txt'),
229+
$this->getUploadedFile(UPLOAD_ERR_OK, 0, 'test2.txt', 'graphql_upload_test2.txt'),
230+
$this->getUploadedFile(UPLOAD_ERR_OK, 0, 'test3.txt', 'graphql_upload_test3.txt'),
231+
];
232+
233+
$file_upload_response = $this->uploadService->saveMultipleFileUploads($uploadFiles, [
234+
'uri_scheme' => 'public',
235+
'file_directory' => 'test',
236+
'file_extensions' => 'txt',
237+
]);
238+
239+
// There must be no violations.
240+
$violations = $file_upload_response->getViolations();
241+
$this->assertEmpty($violations);
242+
243+
// There must be three file entities.
244+
$file_entities = $file_upload_response->getFileEntities();
245+
$this->assertCount(3, $file_entities);
246+
foreach ($file_entities as $index => $file_entity) {
247+
$this->assertSame('public://test/test' . ($index + 1) . '.txt', $file_entity->getFileUri());
248+
$this->assertFileExists($file_entity->getFileUri());
249+
}
250+
}
251+
252+
/**
253+
* Tests unsuccessful scenario with multiple file uploads.
254+
*/
255+
public function testUnsuccessWithMultipleFileUploads() {
256+
$uploadFiles = [
257+
$this->getUploadedFile(UPLOAD_ERR_OK, 0, 'test1.txt', 'graphql_upload_test1.txt'),
258+
$this->getUploadedFile(UPLOAD_ERR_OK, 0, 'test2.txt', 'graphql_upload_test2.txt'),
259+
$this->getUploadedFile(UPLOAD_ERR_OK, 0, 'test3.jpg', 'graphql_upload_test3.jpg'),
260+
];
261+
262+
$file_upload_response = $this->uploadService->saveMultipleFileUploads($uploadFiles, [
263+
'uri_scheme' => 'public',
264+
'file_directory' => 'test',
265+
'file_extensions' => 'txt',
266+
]);
267+
268+
// There must be violation regarding forbidden file extension.
269+
$violations = $file_upload_response->getViolations();
270+
$this->assertStringMatchesFormat(
271+
'Only files with the following extensions are allowed: <em class="placeholder">txt</em>.',
272+
$violations[0]['message']
273+
);
274+
275+
// There must be no file entities.
276+
$file_entities = $file_upload_response->getFileEntities();
277+
$this->assertEmpty($file_entities);
278+
}
279+
217280
/**
218281
* Helper method to prepare the UploadedFile depending on core version.
219282
*
@@ -223,16 +286,18 @@ public function testLockReleased() {
223286
protected function getUploadedFile(
224287
int $error_status,
225288
int $size = 0,
226-
string $name = 'test.txt'
289+
string $dest_filename = 'test.txt',
290+
string $source_filename = 'graphql_upload_test.txt'
227291
): UploadedFile {
228292

229-
list($version) = explode('.', \Drupal::VERSION, 2);
293+
$source_filepath = $this->getSourceTestFilePath($source_filename);
294+
[$version] = explode('.', \Drupal::VERSION, 2);
230295
switch ($version) {
231296
case 8:
232-
return new UploadedFile($this->file, $name, 'text/plain', $size, $error_status, TRUE);
297+
return new UploadedFile($source_filepath, $dest_filename, 'text/plain', $size, $error_status, TRUE);
233298

234299
}
235-
return new UploadedFile($this->file, $name, 'text/plain', $error_status, TRUE);
300+
return new UploadedFile($source_filepath, $dest_filename, 'text/plain', $error_status, TRUE);
236301
}
237302

238303
}

0 commit comments

Comments
 (0)