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"
+