This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 3894961  Allow creation of a new directory as a file movement 
destination
3894961 is described below

commit 3894961cf1d707d8ecbb2972ae6d7a361f191c22
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue May 27 19:48:23 2025 +0100

    Allow creation of a new directory as a file movement destination
---
 atr/routes/finish.py                      | 26 +++++++++++++++++++++-----
 atr/static/js/finish-selected-move.js     | 23 ++++++++++++++++++++++-
 atr/static/js/finish-selected-move.js.map |  2 +-
 atr/static/ts/finish-selected-move.ts     | 25 ++++++++++++++++++++++++-
 4 files changed, 68 insertions(+), 8 deletions(-)

diff --git a/atr/routes/finish.py b/atr/routes/finish.py
index 8dda9c5..6475ff8 100644
--- a/atr/routes/finish.py
+++ b/atr/routes/finish.py
@@ -66,7 +66,7 @@ class MoveFileForm(util.QuartFormTyped):
         validators=[wtforms.validators.DataRequired(message="Please select at 
least one file to move.")],
     )
     target_directory = wtforms.SelectField(
-        "Target directory", choices=[], 
validators=[wtforms.validators.DataRequired()]
+        "Target directory", choices=[], 
validators=[wtforms.validators.DataRequired()], validate_choice=False
     )
     submit = wtforms.SubmitField("Move file")
 
@@ -415,6 +415,24 @@ async def _setup_revision(
     moved_files_names: list[str],
     skipped_files_names: list[str],
 ) -> None:
+    target_path = creating.interim_path / target_dir_rel
+    try:
+        target_path.resolve().relative_to(creating.interim_path.resolve())
+    except ValueError:
+        # Path traversal detected
+        creating.failed = True
+        return
+
+    if not await aiofiles.os.path.exists(target_path):
+        try:
+            await aiofiles.os.makedirs(target_path)
+        except OSError:
+            creating.failed = True
+            return
+    elif not await aiofiles.os.path.isdir(target_path):
+        creating.failed = True
+        return
+
     for source_file_rel in source_files_rel:
         if source_file_rel.parent == target_dir_rel:
             skipped_files_names.append(source_file_rel.name)
@@ -422,15 +440,13 @@ async def _setup_revision(
 
         related_files = _related_files(source_file_rel)
         bundle = [f for f in related_files if await 
aiofiles.os.path.exists(creating.interim_path / f)]
-        collisions = [
-            f.name for f in bundle if await 
aiofiles.os.path.exists(creating.interim_path / target_dir_rel / f.name)
-        ]
+        collisions = [f.name for f in bundle if await 
aiofiles.os.path.exists(target_path / f.name)]
         if collisions:
             creating.failed = True
             return
 
         for f in bundle:
-            await aiofiles.os.rename(creating.interim_path / f, 
creating.interim_path / target_dir_rel / f.name)
+            await aiofiles.os.rename(creating.interim_path / f, target_path / 
f.name)
             if f == source_file_rel:
                 moved_files_names.append(f.name)
 
diff --git a/atr/static/js/finish-selected-move.js 
b/atr/static/js/finish-selected-move.js
index c35a6ad..8ccf4c1 100644
--- a/atr/static/js/finish-selected-move.js
+++ b/atr/static/js/finish-selected-move.js
@@ -55,6 +55,9 @@ function includesCaseInsensitive(haystack, needle) {
         return false;
     return toLower(haystack).includes(toLower(needle));
 }
+function isValidNewDirName(d) {
+    return d.length > 0 && !d.includes("..") && !d.startsWith("/") && 
!d.endsWith("/");
+}
 function getParentPath(filePathString) {
     if (!filePathString || typeof filePathString !== "string")
         return ".";
@@ -110,6 +113,12 @@ function updateMoveSelectionInfo() {
         const strongDest = document.createElement("strong");
         strongDest.textContent = destinationDir;
         currentMoveSelectionInfoElement.appendChild(strongDest);
+        if (destinationDir && uiState.allTargetDirs.indexOf(destinationDir) 
=== -1 && isValidNewDirName(destinationDir)) {
+            const newDirSpan = document.createElement("span");
+            newDirSpan.textContent = " (will be created)";
+            newDirSpan.className = "text-muted small";
+            currentMoveSelectionInfoElement.appendChild(newDirSpan);
+        }
         message = "";
         disableConfirmButton = false;
     }
@@ -164,6 +173,12 @@ function renderListItems(tbodyElement, items, config) {
                 else {
                     row.setAttribute("aria-selected", "false");
                 }
+                if (itemPathString === uiState.filters.dir.trim() && 
uiState.allTargetDirs.indexOf(itemPathString) === -1 && 
isValidNewDirName(itemPathString)) {
+                    const newDirSpan = document.createElement("span");
+                    newDirSpan.textContent = " (new directory)";
+                    newDirSpan.className = "text-muted small";
+                    span.appendChild(newDirSpan);
+                }
                 controlCell.appendChild(radio);
                 break;
             }
@@ -192,7 +207,13 @@ function renderAllLists() {
         moreInfoId: ID.fileListMoreInfo
     };
     renderListItems(fileListTableBody, filteredFilePaths, filesConfig);
-    const filteredDirs = uiState.allTargetDirs.filter(dirP => 
includesCaseInsensitive(dirP, uiState.filters.dir));
+    const displayDirs = [...uiState.allTargetDirs];
+    const trimmedDirFilter = uiState.filters.dir.trim();
+    if (isValidNewDirName(trimmedDirFilter) && 
uiState.allTargetDirs.indexOf(trimmedDirFilter) === -1) {
+        displayDirs.push(trimmedDirFilter);
+        displayDirs.sort();
+    }
+    const filteredDirs = displayDirs.filter(dirP => 
includesCaseInsensitive(dirP, uiState.filters.dir));
     const dirsConfig = {
         itemType: ItemType.Dir,
         selectedItem: uiState.currentlyChosenDirectoryPath,
diff --git a/atr/static/js/finish-selected-move.js.map 
b/atr/static/js/finish-selected-move.js.map
index 2887f6f..8f6d5e9 100644
--- a/atr/static/js/finish-selected-move.js.map
+++ b/atr/static/js/finish-selected-move.js.map
@@ -1 +1 @@
-{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,
 [...]
+{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,
 [...]
diff --git a/atr/static/ts/finish-selected-move.ts 
b/atr/static/ts/finish-selected-move.ts
index c5089d7..5d81587 100644
--- a/atr/static/ts/finish-selected-move.ts
+++ b/atr/static/ts/finish-selected-move.ts
@@ -70,6 +70,10 @@ function includesCaseInsensitive(haystack: string | null | 
undefined, needle: st
     return toLower(haystack).includes(toLower(needle));
 }
 
+function isValidNewDirName(d: string): boolean {
+  return d.length > 0 && !d.includes("..") && !d.startsWith("/") && 
!d.endsWith("/");
+}
+
 function getParentPath(filePathString: string | null | undefined): string {
     if (!filePathString || typeof filePathString !== "string") return ".";
     const lastSlash = filePathString.lastIndexOf("/");
@@ -126,6 +130,12 @@ function updateMoveSelectionInfo(): void {
         const strongDest = document.createElement("strong");
         strongDest.textContent = destinationDir;
         currentMoveSelectionInfoElement.appendChild(strongDest);
+        if (destinationDir && uiState.allTargetDirs.indexOf(destinationDir) 
=== -1 && isValidNewDirName(destinationDir)) {
+            const newDirSpan = document.createElement("span");
+            newDirSpan.textContent = " (will be created)";
+            newDirSpan.className = "text-muted small";
+            currentMoveSelectionInfoElement.appendChild(newDirSpan);
+        }
         message = "";
         disableConfirmButton = false;
     }
@@ -191,6 +201,12 @@ function renderListItems(
                 } else {
                     row.setAttribute("aria-selected", "false");
                 }
+                if (itemPathString === uiState.filters.dir.trim() && 
uiState.allTargetDirs.indexOf(itemPathString) === -1 && 
isValidNewDirName(itemPathString)){
+                    const newDirSpan = document.createElement("span");
+                    newDirSpan.textContent = " (new directory)";
+                    newDirSpan.className = "text-muted small";
+                    span.appendChild(newDirSpan);
+                }
                 controlCell.appendChild(radio);
                 break;
             }
@@ -225,7 +241,14 @@ function renderAllLists(): void {
     };
     renderListItems(fileListTableBody, filteredFilePaths, filesConfig);
 
-    const filteredDirs = uiState.allTargetDirs.filter(dirP =>
+    const displayDirs = [...uiState.allTargetDirs];
+    const trimmedDirFilter = uiState.filters.dir.trim();
+    if (isValidNewDirName(trimmedDirFilter) && 
uiState.allTargetDirs.indexOf(trimmedDirFilter) === -1) {
+        displayDirs.push(trimmedDirFilter);
+        displayDirs.sort();
+    }
+
+    const filteredDirs = displayDirs.filter(dirP =>
         includesCaseInsensitive(dirP, uiState.filters.dir)
     );
     const dirsConfig: RenderListDisplayConfig = {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to