Copilot commented on code in PR #12562:
URL: https://github.com/apache/cloudstack/pull/12562#discussion_r2767281324


##########
utils/src/main/java/com/cloud/utils/FileUtil.java:
##########
@@ -160,4 +160,19 @@ public static boolean writeToFile(String fileName, String 
content) {
     public static String readResourceFile(String resource) throws IOException {
         return 
IOUtils.toString(Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResourceAsStream(resource)),
 com.cloud.utils.StringUtils.getPreferredCharset());
     }
+
+    public static boolean deleteRecursively(Path path) throws IOException {
+        LOGGER.debug("Deleting path: {}", path);
+        if (Files.isDirectory(path)) {

Review Comment:
   The deleteRecursively method may behave unexpectedly when encountering 
symbolic links to directories. Files.isDirectory(path) follows symlinks by 
default, which means if a symlink points to a directory, this method will 
recursively delete the contents of the target directory instead of just 
removing the symlink. Consider using Files.isDirectory(path, 
LinkOption.NOFOLLOW_LINKS) to treat symlinks as files and delete them directly 
without following them.



##########
services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java:
##########
@@ -356,6 +364,30 @@ public Answer 
handleDeleteEntityDownloadURLCommand(DeleteEntityDownloadURLComman
         return new Answer(cmd, true, "");
     }
 
+    protected void 
deleteEntitySymlinkRootPathIfNeeded(DeleteEntityDownloadURLCommand cmd, String 
linkPath) {
+        if (StringUtils.isEmpty(linkPath)) {
+            return;
+        }
+        String[] parts = linkPath.split("/");
+        if (parts.length == 0) {
+            return;
+        }
+        String rootDir = parts[0];
+        if (StringUtils.isEmpty(rootDir) || !UuidUtils.isUuid(rootDir)) {
+            return;
+        }
+        logger.info("Deleting symlink root directory: {} for {}", rootDir, 
cmd.getExtractUrl());
+        Path rootDirPath = Path.of(BASE_EXTRACT_DIR + rootDir);

Review Comment:
   Consider using Path.of(BASE_EXTRACT_DIR, rootDir) instead of string 
concatenation for path construction. This is more robust and handles path 
separators correctly across different operating systems, even if 
BASE_EXTRACT_DIR is modified in the future to not include a trailing separator.
   ```suggestion
           Path rootDirPath = Path.of(BASE_EXTRACT_DIR, rootDir);
   ```



##########
services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java:
##########
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.storage.template;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import java.nio.file.Path;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.agent.api.storage.DeleteEntityDownloadURLCommand;
+import com.cloud.utils.FileUtil;
+
+@RunWith(MockitoJUnitRunner.class)
+public class UploadManagerImplTest {
+
+    @InjectMocks
+    UploadManagerImpl uploadManager;
+
+    MockedStatic<FileUtil> fileUtilMock;
+
+    @Before
+    public void setup() {
+        fileUtilMock = mockStatic(FileUtil.class, Mockito.CALLS_REAL_METHODS);
+        fileUtilMock.when(() -> 
FileUtil.deleteRecursively(any(Path.class))).thenReturn(true);
+    }
+
+    @After
+    public void tearDown() {
+        fileUtilMock.close();
+    }
+
+    @Test
+    public void doesNotDeleteWhenLinkPathIsEmpty() {
+        String emptyLinkPath = "";
+        
uploadManager.deleteEntitySymlinkRootPathIfNeeded(mock(DeleteEntityDownloadURLCommand.class),
 emptyLinkPath);
+        fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), 
never());
+    }
+
+    @Test
+    public void doesNotDeleteWhenRootDirIsNotUuid() {
+        String invalidLinkPath = "invalidRootDir/file";
+        
uploadManager.deleteEntitySymlinkRootPathIfNeeded(mock(DeleteEntityDownloadURLCommand.class),
 invalidLinkPath);
+        fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), 
never());
+    }
+
+    @Test
+    public void deletesSymlinkRootDirectoryWhenValidUuid() {
+        String validLinkPath = "123e4567-e89b-12d3-a456-426614174000/file";
+        
uploadManager.deleteEntitySymlinkRootPathIfNeeded(mock(DeleteEntityDownloadURLCommand.class),
 validLinkPath);
+        fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), 
times(1));
+    }

Review Comment:
   Consider adding a test case for linkPath that contains only a UUID without a 
filename (e.g., "123e4567-e89b-12d3-a456-426614174000"). While this may not 
occur in normal operation given the URL format, it's a valid edge case that 
should be handled correctly.



##########
services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java:
##########
@@ -330,12 +335,15 @@ public Answer 
handleDeleteEntityDownloadURLCommand(DeleteEntityDownloadURLComman
         String extractUrl = cmd.getExtractUrl();
         String result;
         if (extractUrl != null) {
-            command.add("unlink /var/www/html/userdata/" + 
extractUrl.substring(extractUrl.lastIndexOf(File.separator) + 1));
+            String linkPath = 
extractUrl.substring(extractUrl.lastIndexOf(File.separator) + 1);
+            command.add("unlink " + BASE_EXTRACT_DIR + linkPath);
             result = command.execute();
             if (result != null) {
                 // FIXME - Ideally should bail out if you can't delete 
symlink. Not doing it right now.
                 // This is because the ssvm might already be destroyed and the 
symlinks do not exist.
                 logger.warn("Error in deleting symlink :" + result);
+            } else {
+                deleteEntitySymlinkRootPathIfNeeded(cmd, linkPath);

Review Comment:
   The linkPath extraction logic is incorrect. Given extractUrl format 
`http://host/userdata/UUID/filename`, using `lastIndexOf(File.separator)` will 
find the last "/" before the filename, resulting in linkPath being just 
"filename" instead of "UUID/filename". This breaks the 
deleteEntitySymlinkRootPathIfNeeded method which expects linkPath to contain 
the UUID directory name as the first path component. The code should extract 
everything after "/userdata/" using 
`extractUrl.substring(extractUrl.indexOf("/userdata/") + 
"/userdata/".length())` instead, with proper validation that "/userdata/" 
exists in the URL.
   ```suggestion
               final String userDataSegment = "/userdata/";
               int userDataIndex = extractUrl.indexOf(userDataSegment);
               if (userDataIndex == -1) {
                   logger.warn("Extract URL does not contain expected segment 
'/userdata/': " + extractUrl);
               } else {
                   String linkPath = extractUrl.substring(userDataIndex + 
userDataSegment.length());
                   if (StringUtils.isBlank(linkPath)) {
                       logger.warn("Extract URL does not contain a valid link 
path after '/userdata/': " + extractUrl);
                   } else {
                       command.add("unlink " + BASE_EXTRACT_DIR + linkPath);
                       result = command.execute();
                       if (result != null) {
                           // FIXME - Ideally should bail out if you can't 
delete symlink. Not doing it right now.
                           // This is because the ssvm might already be 
destroyed and the symlinks do not exist.
                           logger.warn("Error in deleting symlink :" + result);
                       } else {
                           deleteEntitySymlinkRootPathIfNeeded(cmd, linkPath);
                       }
                   }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to