Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Command/GenerateThumbnailsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Artgris\Bundle\FileManagerBundle\Command;

use Artgris\Bundle\FileManagerBundle\Helpers\FileManagerUploadHandler;
use Artgris\Bundle\FileManagerBundle\Service\FilemanagerService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

#[AsCommand(name: 'artgris:filemanager:thumbnails', description: 'Creates the missing image versions (upload.image_versions) of existing images')]
class GenerateThumbnailsCommand extends Command {

public function __construct(private FilemanagerService $filemanagerService) {
parent::__construct();
}

protected function configure(): void {
$this
->addArgument('conf', InputArgument::REQUIRED, 'Name of the configuration (artgris_file_manager.conf.<name>)')
->addOption('force', null, InputOption::VALUE_NONE, 'Recreate the versions that already exist');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$conf = $this->filemanagerService->getBasePath(['conf' => $input->getArgument('conf')]);
$upload = $conf['upload'] ?? [];
// The empty key holds the options of the original, it is not a version
$versions = array_values(array_diff(array_keys($upload['image_versions'] ?? []), ['']));
if (!$versions) {
$output->writeln('<error>No image version is defined in upload.image_versions.</error>');

return Command::FAILURE;
}

// Same extensions as UploadHandler::has_image_file_extension()
$images = (new Finder())->files()->in($conf['dir'])->name('/\.(gif|jpe?g|png)$/i')
->filter(function (SplFileInfo $file) use ($versions) {
return !\in_array(basename($file->getRelativePath()), $versions, true);
});

$created = 0;
$failed = 0;
foreach ($images as $image) {
$directory = $image->getPath().\DIRECTORY_SEPARATOR;
// One handler per image: the GD backend keeps every opened image in memory as long as the handler lives
$handler = new FileManagerUploadHandler(['upload_dir' => $directory] + $upload, false);
foreach ($versions as $version) {
if (!$input->getOption('force') && is_file($directory.$version.\DIRECTORY_SEPARATOR.$image->getFilename())) {
continue;
}
if ($handler->createVersion($image->getFilename(), $version)) {
++$created;
} else {
++$failed;
$output->writeln(sprintf('<error>Cannot create the "%s" version of %s</error>', $version, $image->getPathname()));
}
}
}

$output->writeln(sprintf('%d image version(s) created, %d failed.', $created, $failed));

return $failed ? Command::FAILURE : Command::SUCCESS;
}
}
26 changes: 26 additions & 0 deletions Controller/ManagerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public function indexAction(Request $request, FileTypeService $fileTypeService):
// File search
$finderFiles = new Finder();
$finderFiles->in($fileManager->getCurrentPath())->depth(0);
$this->excludeImageVersionDirectories($fileManager, $finderFiles);
$regex = $fileManager->getRegex();

$orderBy = $fileManager->getQueryParameter('orderby');
Expand Down Expand Up @@ -220,6 +221,13 @@ public function renameFileAction(Request $request, string $fileName): RedirectRe
try {
$this->dispatch(FileManagerEvents::RENAME_FILE, ['oldFile'=> $oldfilePath,'newFile'=> $newfilePath]);
$fs->rename($oldfilePath, $newfilePath);
// Generated image versions (e.g. thumbnail/<name>) follow the original
foreach ($fileManager->getImageVersionNames() as $version) {
$versionDirectory = $fileManager->getCurrentPath().\DIRECTORY_SEPARATOR.$version.\DIRECTORY_SEPARATOR;
if (file_exists($versionDirectory.basename($oldfilePath))) {
$fs->rename($versionDirectory.basename($oldfilePath), $versionDirectory.$newfileName);
}
}
$this->addFlash('success', $this->translator->trans('file.renamed.success'));
//File has been renamed successfully
} catch (IOException $exception) {
Expand Down Expand Up @@ -311,6 +319,10 @@ public function deleteAction(Request $request): RedirectResponse {
try {
$this->dispatch(FileManagerEvents::PRE_DELETE_FILE);
$fs->remove($filePath);
// Generated image versions (e.g. thumbnail/<name>) go together with the original
foreach ($fileManager->getImageVersionNames() as $version) {
$fs->remove(\dirname($filePath).\DIRECTORY_SEPARATOR.$version.\DIRECTORY_SEPARATOR.basename($filePath));
}
$is_delete = true;
$this->dispatch(FileManagerEvents::POST_DELETE_FILE);
} catch (IOException $exception) {
Expand Down Expand Up @@ -388,6 +400,8 @@ private function retrieveSubDirectories(FileManager $fileManager, string $path,

if ($baseFolderName) {
$directories->name($baseFolderName);
} else {
$this->excludeImageVersionDirectories($fileManager, $directories);
}
$directoriesList = null;

Expand Down Expand Up @@ -428,6 +442,18 @@ private function retrieveSubDirectories(FileManager $fileManager, string $path,
return $directoriesList;
}

/**
* Image version directories (e.g. thumbnail/) belong to the originals, so they are not listed.
*/
private function excludeImageVersionDirectories(FileManager $fileManager, Finder $finder): void {
$versionNames = $fileManager->getImageVersionNames();
if ($versionNames) {
$finder->filter(function (SplFileInfo $file) use ($versionNames) {
return !($file->isDir() && \in_array($file->getFilename(), $versionNames, true));
});
}
}

/**
* Tree Iterator.
*/
Expand Down
1 change: 1 addition & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function getConfigTreeBuilder(): TreeBuilder {
->scalarNode('root_name')->defaultValue(null)->end()
->scalarNode('twig_extension')->end()
->booleanNode('cachebreaker')->defaultValue(true)->end()
->scalarNode('thumbnail_version')->defaultNull()->end()
->enumNode('view')->values(['thumbnail', 'list'])->defaultValue('list')->end()
->scalarNode('regex')->end()
->scalarNode('service')->end()
Expand Down
13 changes: 13 additions & 0 deletions Helpers/FileManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ public function getQueryParameter(string $parameter)
return $this->getQueryParameters()[$parameter] ?? null;
}

/**
* Names of the image version directories (non-empty keys of upload.image_versions).
* They are managed together with the originals only when thumbnail_version is set.
*/
public function getImageVersionNames(): array
{
if (null === $this->getConfigurationParameter('thumbnail_version')) {
return [];
}

return array_values(array_filter(array_keys($this->getConfiguration()['upload']['image_versions'] ?? []), 'strlen'));
}

public function getConfigurationParameter(string $parameter)
{
return $this->getConfiguration()[$parameter] ?? null;
Expand Down
27 changes: 27 additions & 0 deletions Helpers/FileManagerUploadHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@

class FileManagerUploadHandler extends UploadHandler {

/**
* Creates one image version (upload.image_versions) of a file already stored in upload_dir,
* without an upload request (used by the artgris:filemanager:thumbnails command).
*/
public function createVersion(string $fileName, string $version): bool {
return (bool) $this->create_scaled_image($fileName, $version, $this->options['image_versions'][$version]);
}

protected function get_full_url(): string {
// On the console there is no HTTP host to build the URL from, and the URL is not needed there
if (!isset($_SERVER['HTTP_HOST']) && !isset($_SERVER['SERVER_NAME'])) {
return '';
}

return parent::get_full_url();
}

protected function get_server_var($id) {
// A missing key (e.g. HTTP_REFERER on the console) gave null, which string functions no longer accept
return $_SERVER[$id] ?? '';
}

protected function basename($filepath, $suffix = '') {
// The parent passes its default null suffix to basename(), deprecated since PHP 8.1
return parent::basename($filepath, $suffix);
}

protected function get_unique_filename($file_path, $name, $size, $type, $error,
$index, $content_range) {

Expand Down
3 changes: 3 additions & 0 deletions Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ services:
Artgris\Bundle\FileManagerBundle\Controller\:
resource: '../../Controller/*'

Artgris\Bundle\FileManagerBundle\Command\:
resource: '../../Command/*'

Artgris\Bundle\FileManagerBundle\Twig\FileTypeExtension:
tags:
- { name: twig.extension }
Expand Down
50 changes: 50 additions & 0 deletions Resources/doc/book/1-basic-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,56 @@ Having a large number of files in a (sub-)directory can have a huge impact on pe
|:------------------|:--------:|:--------:|:-------------:|
| `root_name` | `String` | False | null |

## `thumbnail_version` to preview images from a smaller image version

| Option | Type | Required | Default value |
|:--------------------|:--------:|:--------:|:-------------:|
| `thumbnail_version` | `String` | False | null |

By default the list shows each image preview from the original file, so a folder of large photos downloads all of them in full size.
Set `thumbnail_version` to the name of one of the [`upload.image_versions`](#image_versions-image-version) to load the previews from that version instead:

```yml
artgris_file_manager:
conf:
public:
dir: '%kernel.project_dir%/public/uploads'
thumbnail_version: thumbnail
upload:
image_versions:
'': { auto_orient: true }
thumbnail: { max_width: 200, max_height: 200 }
```

Each version is stored in a subfolder named after the version, next to the original (the layout of the upload widget), so every folder with images gets its own `thumbnail/`:

```
uploads/
├── photo.jpg
├── thumbnail/photo.jpg
└── 2024/
├── scan.png
└── thumbnail/scan.png
```

In a public folder the versions are reachable by URL like the originals (here `/uploads/2024/thumbnail/scan.png`).

When `thumbnail_version` is set:

* the preview uses `<version>/<file name>` when that file exists, and the original otherwise. The preview modal, the download link and the path sent to TinyMCE/CKEditor still use the original;
* the version folders (every non-empty key of `upload.image_versions`, here `thumbnail/`) are not listed;
* deleting or renaming a file also deletes or renames its versions.

New uploads create the versions. For images that already exist, run:

```bash
php bin/console artgris:filemanager:thumbnails public # creates the missing versions
php bin/console artgris:filemanager:thumbnails public --force # recreates all versions
```

The command uses `image_library` from `upload` (GD, Imagick or ImageMagick), like the upload does.
Run it as the user of the web server (e.g. `sudo -u www-data php bin/console ...`): the version folders it creates belong to the user running it, and the upload must be able to write into them later.

## `upload` A non-exhaustive configuration of the File Upload widget [blueimp/jQuery-File-Upload](https://github.com/blueimp/jQuery-File-Upload)
> [Exhaustive options](https://github.com/blueimp/jQuery-File-Upload/blob/master/server/php/UploadHandler.php) can only be defined with [The service configuration](2-service-configuration.md)

Expand Down
27 changes: 25 additions & 2 deletions Service/FileTypeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Twig\Environment;

class FileTypeService {
const IMAGE_SIZE = [
public const IMAGE_SIZE = [
FileManager::VIEW_LIST => '22',
FileManager::VIEW_THUMBNAIL => '100',
];
Expand All @@ -34,7 +34,11 @@ public function preview(FileManager $fileManager, SplFileInfo $file) {
if ('file' === $type) {
$size = $this::IMAGE_SIZE[$fileManager->getView()];

return $this->fileIcon($filePath, $extension, $size, true, $fileManager->getConfigurationParameter('twig_extension'), $fileManager->getConfigurationParameter('cachebreaker'));
$preview = $this->fileIcon($this->thumbnailPath($fileManager, $file) ?? $filePath, $extension, $size, true, $fileManager->getConfigurationParameter('twig_extension'), $fileManager->getConfigurationParameter('cachebreaker'));
// Only the list preview uses the thumbnail; the modal, editors and download keep the original
$preview['path'] = $filePath;

return $preview;
}
if ('dir' === $type) {

Expand All @@ -53,6 +57,25 @@ public function preview(FileManager $fileManager, SplFileInfo $file) {
}
}

/**
* URL of the thumbnail_version image of the file, when it has been generated.
*/
private function thumbnailPath(FileManager $fileManager, SplFileInfo $file): ?string {
$version = $fileManager->getConfigurationParameter('thumbnail_version');
if (null === $version || !is_file($file->getPath().\DIRECTORY_SEPARATOR.$version.\DIRECTORY_SEPARATOR.$file->getFilename())) {
return null;
}

if ($fileManager->getImagePath()) {
return $fileManager->getImagePath().rawurlencode($version).'/'.rawurlencode($file->getFilename());
}

return $this->router->generate('file_manager_file', array_merge($fileManager->getQueryParameters(), [
'fileName' => $file->getFilename(),
'route' => $fileManager->getRoute().'/'.$version,
]));
}

public function accept($type): bool|string {
switch ($type) {
case 'image':
Expand Down
Loading
Loading