Skip to content

Commit 15295ef

Browse files
nginx: uploaded files now download as attachments (#4660)
2 parents 295ed7b + 5e3e615 commit 15295ef

7 files changed

Lines changed: 182 additions & 17 deletions

File tree

src/Component/FileUpload/Exception/MissingFileClassDirectoryMappingException.php renamed to src/Component/FileUpload/Exception/MissingFileConfigByFileClassException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use Exception;
88

9-
class MissingFileClassDirectoryMappingException extends Exception
9+
class MissingFileConfigByFileClassException extends Exception
1010
{
1111
public function __construct(string $message = '', ?Exception $previous = null)
1212
{

src/Component/FileUpload/FileUpload.php

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
namespace Shopsys\FrameworkBundle\Component\FileUpload;
66

77
use InvalidArgumentException;
8+
use League\Flysystem\Config;
89
use League\Flysystem\FilesystemException;
910
use League\Flysystem\FilesystemOperator;
1011
use League\Flysystem\MountManager;
1112
use League\Flysystem\StorageAttributes;
13+
use League\Flysystem\Visibility;
1214
use Shopsys\FrameworkBundle\Component\Cache\InMemoryCache;
1315
use Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFile;
1416
use Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFileRepository;
1517
use Shopsys\FrameworkBundle\Component\Doctrine\Exception\UnexpectedTypeException;
16-
use Shopsys\FrameworkBundle\Component\FileUpload\Exception\MissingFileClassDirectoryMappingException;
18+
use Shopsys\FrameworkBundle\Component\FileUpload\Exception\MissingFileConfigByFileClassException;
1719
use Shopsys\FrameworkBundle\Component\FileUpload\Exception\MoveToEntityFailedException;
1820
use Shopsys\FrameworkBundle\Component\FileUpload\Exception\UploadFailedException;
1921
use Shopsys\FrameworkBundle\Component\Image\Image;
@@ -30,9 +32,15 @@ class FileUpload
3032
protected const int DELETE_OLD_FILES_SECONDS = 86400;
3133
protected const string POSITION_BY_ENTITY_AND_TYPE_CACHE_NAMESPACE = 'positionByEntityAndType';
3234

35+
/**
36+
* @param array<class-string<\Shopsys\FrameworkBundle\Component\FileUpload\EntityFileUploadInterface>, array{
37+
* directory: non-empty-string,
38+
* visibility: 'public'|'private',
39+
* }> $filesConfigByFileClass
40+
*/
3341
public function __construct(
3442
protected readonly string $temporaryDir,
35-
protected array $directoriesByFileClass,
43+
protected array $filesConfigByFileClass,
3644
protected readonly FileNamingConvention $fileNamingConvention,
3745
protected readonly MountManager $mountManager,
3846
protected readonly FilesystemOperator $filesystem,
@@ -54,6 +62,9 @@ public function upload(UploadedFile $file): string
5462
$this->mountManager->move(
5563
'local://' . $file->getRealPath(),
5664
'main://' . $this->getTemporaryDirectory() . '/' . $temporaryFilename,
65+
[
66+
Config::OPTION_VISIBILITY => Visibility::PRIVATE,
67+
],
5768
);
5869

5970
return $temporaryFilename;
@@ -174,7 +185,13 @@ public function postFlushEntity(EntityFileUploadInterface $entity): void
174185
$this->filesystem->delete($targetFilename);
175186
}
176187

177-
$this->mountManager->move('main://' . $sourceFilepath, 'main://' . $targetFilename);
188+
$this->mountManager->move(
189+
'main://' . $sourceFilepath,
190+
'main://' . $targetFilename,
191+
[
192+
Config::OPTION_VISIBILITY => $this->getVisibilityByFileClass($fileForUpload->getFileClass()),
193+
],
194+
);
178195
$entity->setFileKeyAsUploaded($key);
179196
} catch (IOException $ex) {
180197
$message = 'Failed to rename file from temporary directory to entity';
@@ -266,20 +283,36 @@ protected function getUploadEntityType(EntityFileUploadInterface $entity): strin
266283
return $uploadEntityType;
267284
}
268285

269-
protected function getDirectoryByFileClass(string $fileClass): string
286+
/**
287+
* @return array{
288+
* directory: non-empty-string,
289+
* visibility: 'public'|'private',
290+
* }
291+
*/
292+
protected function getFileConfigByFileClass(string $fileClass): array
270293
{
271-
if (array_key_exists($fileClass, $this->directoriesByFileClass)) {
272-
return $this->directoriesByFileClass[$fileClass];
294+
if (array_key_exists($fileClass, $this->filesConfigByFileClass)) {
295+
return $this->filesConfigByFileClass[$fileClass];
273296
}
274297

275-
foreach ($this->directoriesByFileClass as $class => $dir) {
298+
foreach ($this->filesConfigByFileClass as $class => $config) {
276299
if (is_subclass_of($fileClass, $class)) {
277-
return $dir;
300+
return $config;
278301
}
279302
}
280303

281-
throw new MissingFileClassDirectoryMappingException(
282-
sprintf('Missing directory mapping for file class "%s"', $fileClass),
304+
throw new MissingFileConfigByFileClassException(
305+
sprintf('Missing file config mapping for file class "%s"', $fileClass),
283306
);
284307
}
308+
309+
protected function getDirectoryByFileClass(string $fileClass): string
310+
{
311+
return $this->getFileConfigByFileClass($fileClass)['directory'];
312+
}
313+
314+
protected function getVisibilityByFileClass(string $fileClass): string
315+
{
316+
return $this->getFileConfigByFileClass($fileClass)['visibility'];
317+
}
285318
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopsys\FrameworkBundle\Component\PostDeploy\Task;
6+
7+
use Doctrine\ORM\EntityManagerInterface;
8+
use League\Flysystem\FilesystemOperator;
9+
use League\Flysystem\Visibility;
10+
use Override;
11+
use Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFile;
12+
use Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFileLocator;
13+
use Symfony\Component\Console\Style\SymfonyStyle;
14+
15+
class SetCustomerUploadedFilesPrivateVisibilityTask implements PostDeployTaskInterface
16+
{
17+
protected const int BATCH_SIZE = 100;
18+
19+
public function __construct(
20+
protected readonly EntityManagerInterface $entityManager,
21+
protected readonly FilesystemOperator $filesystem,
22+
protected readonly CustomerUploadedFileLocator $customerUploadedFileLocator,
23+
) {
24+
}
25+
26+
#[Override]
27+
public function run(SymfonyStyle $style): void
28+
{
29+
$style->section('Customer uploaded files visibility');
30+
31+
$processedCount = $this->setCustomerUploadedFilesPrivateVisibility($style);
32+
33+
if ($processedCount === 0) {
34+
$style->success('No customer uploaded files found.');
35+
36+
return;
37+
}
38+
39+
$style->success(sprintf('Set private visibility for %d customer uploaded files.', $processedCount));
40+
}
41+
42+
protected function setCustomerUploadedFilesPrivateVisibility(SymfonyStyle $style): int
43+
{
44+
$tableName = $this->getCustomerUploadedFilesTableName();
45+
$totalCount = (int)$this->entityManager->getConnection()->fetchOne(
46+
sprintf('SELECT COUNT(id) FROM %s', $tableName),
47+
);
48+
49+
if ($totalCount === 0) {
50+
return 0;
51+
}
52+
53+
$style->progressStart($totalCount);
54+
55+
$processedCount = 0;
56+
$lastProcessedId = 0;
57+
58+
while (true) {
59+
$rows = $this->getCustomerUploadedFileRows($tableName, $lastProcessedId);
60+
61+
if ($rows === []) {
62+
break;
63+
}
64+
65+
foreach ($rows as $row) {
66+
$this->filesystem->setVisibility(
67+
$this->getCustomerUploadedFilepath($row),
68+
Visibility::PRIVATE,
69+
);
70+
71+
$lastProcessedId = (int)$row['id'];
72+
$processedCount++;
73+
$style->progressAdvance();
74+
}
75+
}
76+
77+
$style->progressFinish();
78+
79+
return $processedCount;
80+
}
81+
82+
protected function getCustomerUploadedFilesTableName(): string
83+
{
84+
return $this->entityManager->getClassMetadata(CustomerUploadedFile::class)->getTableName();
85+
}
86+
87+
/**
88+
* @return array<int, array<string, mixed>>
89+
*/
90+
protected function getCustomerUploadedFileRows(string $tableName, int $lastProcessedId): array
91+
{
92+
return $this->entityManager->getConnection()->fetchAllAssociative(
93+
sprintf(
94+
'SELECT id, entity_name, extension FROM %s WHERE id > :lastProcessedId ORDER BY id ASC LIMIT %d',
95+
$tableName,
96+
static::BATCH_SIZE,
97+
),
98+
['lastProcessedId' => $lastProcessedId],
99+
);
100+
}
101+
102+
/**
103+
* @param array<string, mixed> $row
104+
*/
105+
protected function getCustomerUploadedFilepath(array $row): string
106+
{
107+
return $this->customerUploadedFileLocator->getAbsoluteFilePath(
108+
sprintf('%s/%d.%s', (string)$row['entity_name'], (int)$row['id'], (string)$row['extension']),
109+
);
110+
}
111+
}

src/DependencyInjection/ShopsysFrameworkExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\PostDeployTaskDescriptor;
1616
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\PostDeployTaskRunEnum;
1717
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\RecalculateFileSizesTask;
18+
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\SetCustomerUploadedFilesPrivateVisibilityTask;
1819
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlDataProviderInterface;
1920
use Shopsys\FrameworkBundle\Model\Category\AutomatedFilter\CategoryAutomatedFilterInterface;
2021
use Shopsys\FrameworkBundle\Model\Mail\MailTemplateSender\MailTemplateSenderInterface;
@@ -129,6 +130,11 @@ public function prepend(ContainerBuilder $container): void
129130
'priority' => 100,
130131
'service' => RecalculateFileSizesTask::class,
131132
],
133+
'set_customer_uploaded_files_private_visibility' => [
134+
'run' => PostDeployTaskRunEnum::ONE_TIME,
135+
'priority' => 90,
136+
'service' => SetCustomerUploadedFilesPrivateVisibilityTask::class,
137+
],
132138
],
133139
],
134140
]);

src/Resources/config/services.yaml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,16 @@ services:
297297
Shopsys\FrameworkBundle\Component\FileUpload\FileUpload:
298298
arguments:
299299
$temporaryDir: '%shopsys.var_dir%'
300-
$directoriesByFileClass:
301-
Shopsys\FrameworkBundle\Component\Image\Image: '%shopsys.image_dir%'
302-
Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFile: '%shopsys.uploaded_file_dir%'
303-
Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFile: '%shopsys.customer_uploaded_file_dir%'
300+
$filesConfigByFileClass:
301+
Shopsys\FrameworkBundle\Component\Image\Image:
302+
directory: '%shopsys.image_dir%'
303+
visibility: !php/const League\Flysystem\Visibility::PUBLIC
304+
Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFile:
305+
directory: '%shopsys.uploaded_file_dir%'
306+
visibility: !php/const League\Flysystem\Visibility::PUBLIC
307+
Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFile:
308+
directory: '%shopsys.customer_uploaded_file_dir%'
309+
visibility: !php/const League\Flysystem\Visibility::PRIVATE
304310

305311
Shopsys\FrameworkBundle\Component\Filesystem\MainFilesystemFactory:
306312
arguments:

tests/Unit/Component/Image/ImageFactoryTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use League\Flysystem\FilesystemOperator;
88
use League\Flysystem\MountManager;
9+
use League\Flysystem\Visibility;
910
use PHPUnit\Framework\TestCase;
1011
use Shopsys\FrameworkBundle\Component\Cache\InMemoryCache;
1112
use Shopsys\FrameworkBundle\Component\CustomerUploadedFile\CustomerUploadedFileRepository;
@@ -73,7 +74,7 @@ private function getFileUpload(): FileUpload
7374

7475
return new FileUpload(
7576
'temporaryDir',
76-
[Image::class => 'imageDir'],
77+
[Image::class => ['directory' => 'imageDir', 'visibility' => Visibility::PUBLIC]],
7778
$fileNamingConvention,
7879
$mountManager,
7980
$abstractFilesystem,

tests/Unit/DependencyInjection/ShopsysFrameworkExtensionTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\PostDeployTaskDescriptor;
1010
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\PostDeployTaskRunEnum;
1111
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\RecalculateFileSizesTask;
12+
use Shopsys\FrameworkBundle\Component\PostDeploy\Task\SetCustomerUploadedFilesPrivateVisibilityTask;
1213
use Shopsys\FrameworkBundle\DependencyInjection\Configuration;
1314
use Shopsys\FrameworkBundle\DependencyInjection\ShopsysFrameworkExtension;
1415
use Symfony\Component\Config\Definition\Processor;
@@ -83,9 +84,16 @@ public function testFrameworkPrependedTaskComesBeforeProjectTaskWithSamePriority
8384
->getDefinition(PostDeployTaskConfig::class)
8485
->getArgument('$descriptors');
8586

86-
$this->assertCount(2, $descriptorDefinitions);
87+
$this->assertCount(3, $descriptorDefinitions);
8788
$this->assertDescriptorDefinition($descriptorDefinitions[0], 'recalculate_file_sizes', PostDeployTaskRunEnum::ONE_TIME, 100, RecalculateFileSizesTask::class);
8889
$this->assertDescriptorDefinition($descriptorDefinitions[1], 'project_task', PostDeployTaskRunEnum::ALWAYS, 100, 'project_service');
90+
$this->assertDescriptorDefinition(
91+
$descriptorDefinitions[2],
92+
'set_customer_uploaded_files_private_visibility',
93+
PostDeployTaskRunEnum::ONE_TIME,
94+
90,
95+
SetCustomerUploadedFilesPrivateVisibilityTask::class,
96+
);
8997
}
9098

9199
private function assertDescriptorDefinition(

0 commit comments

Comments
 (0)