diff --git a/src/Controller/Backend/FileEditController.php b/src/Controller/Backend/FileEditController.php index 9fba242a1..c22f7e9f0 100644 --- a/src/Controller/Backend/FileEditController.php +++ b/src/Controller/Backend/FileEditController.php @@ -37,11 +37,15 @@ class FileEditController extends TwigAwareController implements BackendZoneInter /** @var EntityManagerInterface */ private $em; + /** @var Filesystem */ + private $filesystem; + public function __construct(CsrfTokenManagerInterface $csrfTokenManager, MediaRepository $mediaRepository, EntityManagerInterface $em) { $this->csrfTokenManager = $csrfTokenManager; $this->mediaRepository = $mediaRepository; $this->em = $em; + $this->filesystem = new Filesystem(); } /** @@ -109,12 +113,12 @@ public function save(Request $request, UrlGeneratorInterface $urlGenerator): Res } /** - * @Route("/delete", name="bolt_file_delete", methods={"POST", "GET"}) + * @Route("/file-delete/", name="bolt_file_delete", methods={"POST", "GET"}) */ public function handleDelete(Request $request): Response { try { - $this->validateCsrf($request, 'delete'); + $this->validateCsrf($request, 'file-delete'); } catch (InvalidCsrfTokenException $e) { return new JsonResponse([ 'error' => [ @@ -123,8 +127,6 @@ public function handleDelete(Request $request): Response ], Response::HTTP_FORBIDDEN); } - $filesystem = new Filesystem(); - $locationName = $request->get('location', ''); $path = $request->get('path', ''); @@ -139,7 +141,40 @@ public function handleDelete(Request $request): Response $filePath = Path::canonicalize($locationName . '/' . $path); try { - $filesystem->remove($filePath); + $this->filesystem->remove($filePath); + } catch (\Throwable $e) { + // something wrong happened, we don't need the uploaded files anymore + throw $e; + } + + $this->addFlash('success', 'file.delete_success'); + return $this->redirectToRoute('bolt_filemanager', ['location' => $locationName]); + } + + /** + * @Route("/file-duplicate/", name="bolt_file_duplicate", methods={"POST", "GET"}) + */ + public function handleDuplicate(Request $request): Response + { + try { + $this->validateCsrf($request, 'file-duplicate'); + } catch (InvalidCsrfTokenException $e) { + return new JsonResponse([ + 'error' => [ + 'message' => 'Invalid CSRF token', + ], + ], Response::HTTP_FORBIDDEN); + } + + $locationName = $request->get('location', ''); + $path = $request->get('path', ''); + + $originalFilepath = Path::canonicalize($locationName . '/' . $path); + + $copyFilePath = $this->getCopyFilepath($originalFilepath); + + try { + $this->filesystem->copy($originalFilepath, $copyFilePath); } catch (\Throwable $e) { // something wrong happened, we don't need the uploaded files anymore throw $e; @@ -149,6 +184,24 @@ public function handleDelete(Request $request): Response return $this->redirectToRoute('bolt_filemanager', ['location' => $locationName]); } + /** + * @return string Returns the copy file path. E.g. 'files/foal.jpg' -> 'files/foal (1).jpg' + */ + private function getCopyFilepath(string $path): string + { + $copyPath = $path; + + $i = 1; + while ($this->filesystem->exists($copyPath)) { + $pathinfo = pathinfo($path); + $basename = basename($pathinfo['basename'], '.' . $pathinfo['extension']) . ' (' . $i . ')'; + $copyPath = Path::canonicalize($pathinfo['dirname'] . '/' . $basename . '.' . $pathinfo['extension']); + $i++; + } + + return $copyPath; + } + private function verifyYaml(string $yaml): bool { $yamlParser = new Parser(); diff --git a/templates/finder/_files_actions.html.twig b/templates/finder/_files_actions.html.twig index 893b392a7..c02b94cfa 100644 --- a/templates/finder/_files_actions.html.twig +++ b/templates/finder/_files_actions.html.twig @@ -24,14 +24,16 @@ {{ 'files_cards.action_view_original'|trans }} - + {{ 'files_cards.action_duplicate'|trans }} {{ file.getRelativePathname|excerpt(22) }} {{ 'files_cards.action_delete'|trans }} {{ file.getRelativePathname|excerpt(22) }} diff --git a/tests/e2e/filemanager.feature b/tests/e2e/filemanager.feature index 35d4f9cff..a56aa8259 100644 --- a/tests/e2e/filemanager.feature +++ b/tests/e2e/filemanager.feature @@ -42,4 +42,40 @@ Feature: Filemanager When I press "Cancel" Then I should not see "File deleted successfully" - And I should see "_a-sunrise.jpeg" in the 2nd ".admin__body--main tr" element \ No newline at end of file + And I should see "_a-sunrise.jpeg" in the 2nd ".admin__body--main tr" element + + @javascript + @testme + Scenario: As an Admin I want to duplicate a file + Given I am logged in as "admin" + And I am on "/bolt/filemanager/files" + + Then I should see "_a-sunrise.jpeg" in the 2nd ".admin__body--main tr" element + And I should not see "I should see _a-sunrise (1).jpeg" + + When I click the 1st ".edit-actions__dropdown-toggler" + And I follow "Duplicate _a-sunrise.jpeg" + + Then I should be on "/bolt/filemanager/files" + And I should see "_a-sunrise (1).jpeg" + And I should see "_a-sunrise.jpeg" + + When I click the 2nd ".edit-actions__dropdown-toggler" + And I follow "Duplicate _a-sunrise.jpeg" + + Then I should be on "/bolt/filemanager/files" + And I should see "_a-sunrise (2).jpeg" + And I should see "_a-sunrise (1).jpeg" + And I should see "_a-sunrise.jpeg" + + # This is the end of the test. Below is cleanup. + Then I click the 2nd ".edit-actions__dropdown-toggler" + And I follow "Delete _a-sunrise (2).jpeg" + And I wait for ".modal-dialog" + And I press "OK" + + Then I click the 1st ".edit-actions__dropdown-toggler" + And I follow "Delete _a-sunrise (1).jpeg" + And I wait for ".modal-dialog" + And I press "OK" +