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

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new df3edd6ec7 [ZEPPELIN-5989] Resolve a local notebook dir as a file path 
instead of parsing it as a URI
df3edd6ec7 is described below

commit df3edd6ec727921b5abc718ce5d50382dcd22567
Author: κΉ€λ™ν™˜ <[email protected]>
AuthorDate: Mon Oct 5 14:01:30 2026 +0900

    [ZEPPELIN-5989] Resolve a local notebook dir as a file path instead of 
parsing it as a URI
    
    ### What is this PR for?
    
    `VFSNotebookRepo.setNotebookDirectory()` parses the notebook dir with `new 
URI()` unless it is a Windows absolute path like `C:\...`. A local path is not 
a URI and can contain characters that a URI does not allow:
    
    * On Windows the default dir is built as `./` + `\` + `notebook`, so the 
server fails to start with `URISyntaxException: Illegal character in path at 
index 2: ./\notebook`.
    * On any OS, a dir with a space, like `/home/me/My Notebooks`, fails the 
same way.
    
    The issue suggests joining the dir with `Paths.get`, but on Windows that 
gives `.\notebook`, which `new URI()` rejects too.
    
    This PR:
    
    * Resolves a notebook dir without a scheme as a file path, `new 
File(getAbsoluteDir(path)).toURI()`, and parses only a dir with a scheme as a 
URI. The branch that handled a URI without a scheme is removed, since that case 
no longer reaches it.
    * Keeps `rootNotebookFolder` as a decoded path for a local dir. It was the 
root's URI with `file:///` replaced, so it kept `%20` for a space. `list()` 
strips it from note file names that are decoded since ZEPPELIN-6202, which 
would shift note paths by two characters per space, and `GitNotebookRepo` opens 
the repository at `rootNotebookFolder/.git`, which would be a different 
directory. `list()` now resolves the root by its URI instead of by this path. A 
dir with another scheme keeps i [...]
    
    For a local dir without such characters, both values are the same as before.
    
    I could not run it on Windows. On other systems the same code path is 
reached with a space, and Commons VFS treats a backslash in the file URI as a 
separator. It would help if someone on Windows could start the server from this 
branch without a configuration file, which is how the issue reproduces it.
    
    ### What type of PR is it?
    Bug Fix
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-5989
    
    ### How should this be tested?
    
    * `VFSNotebookRepoTest.testNotebookDirWithCharactersThatAUriDoesNotAllow` 
uses a dir with a space and one with a backslash, saves and lists a note, and 
checks its path.
    * `GitNotebookRepoTest.notebookDirWithSpace` checks that `.git` is created 
in the dir with a space and that a checkpoint is recorded.
    
    Without the first change both tests fail with the `URISyntaxException` 
above; without the second the path and `.git` checks fail.
    
    Run locally: `VFSNotebookRepoTest` (8), `GitNotebookRepoTest` (12), 
`NotebookRepoSyncTest` (10), `NotebookServiceTest` (13), 
`NotebookServiceRaceConditionTest` (1), `NoteManagerMoveResaveRaceTest` (1), 
`NotebookTest` (42). `testSchedule`, `testScheduleDisabledWithName` and 
`testSchedulePoolUsage` fail locally with and without this change: they wait 
five seconds for the cron run, and the interpreter process here takes about six 
seconds to sync its libraries and register.
    
    
    Closes #5518 from dev-donghwan/ZEPPELIN-5989.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../zeppelin/notebook/repo/VFSNotebookRepo.java    | 29 ++++++++++++++--------
 .../notebook/repo/GitNotebookRepoTest.java         | 16 ++++++++++++
 .../notebook/repo/VFSNotebookRepoTest.java         | 25 +++++++++++++++++++
 3 files changed, 59 insertions(+), 11 deletions(-)

diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java
 
b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java
index 420a1c148a..8861aa5078 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java
@@ -70,19 +70,16 @@ public class VFSNotebookRepo extends AbstractNotebookRepo {
     URI filesystemRoot = null;
     try {
       LOGGER.info("Using notebookDir: {}", notebookDirPath);
-      if (zConf.isWindowsPath(notebookDirPath)) {
-        filesystemRoot = new File(notebookDirPath).toURI();
+      if (zConf.isWindowsPath(notebookDirPath) || 
!zConf.isPathWithScheme(notebookDirPath)) {
+        // A local path is not parsed as a URI: it can contain characters that 
a URI does not
+        // allow, such as spaces or the backslashes of a Windows path like the 
default ./\notebook.
+        filesystemRoot = new 
File(zConf.getAbsoluteDir(notebookDirPath)).toURI();
       } else {
         filesystemRoot = new URI(notebookDirPath);
       }
     } catch (URISyntaxException e) {
       throw new IOException(e);
     }
-
-    if (filesystemRoot.getScheme() == null) { // it is local path
-      File f = new File(zConf.getAbsoluteDir(filesystemRoot.getPath()));
-      filesystemRoot = f.toURI();
-    }
     this.fsManager = VFS.getManager();
     this.rootNotebookFileObject = fsManager.resolveFile(filesystemRoot);
     if (!this.rootNotebookFileObject.exists()) {
@@ -90,19 +87,29 @@ public class VFSNotebookRepo extends AbstractNotebookRepo {
       LOGGER.info("Notebook dir doesn't exist: {}, creating it.",
           rootNotebookFileObject.getName().getPath());
     }
-    // getPath() method returns a string without root directory in windows, so 
we use getURI() instead
-    // windows does not support paths with "file:///" prepended, so we replace 
it by "/"
-    this.rootNotebookFolder = 
rootNotebookFileObject.getName().getURI().replace("file:///", "/");
+    this.rootNotebookFolder = 
toRootNotebookFolder(rootNotebookFileObject.getName().getURI());
   }
 
   @Override
   public Map<String, NoteInfo> list(AuthenticationInfo subject) throws 
IOException {
     // Must to create rootNotebookFileObject each time when call method list, 
otherwise we can not
     // get the updated data under this folder.
-    this.rootNotebookFileObject = 
fsManager.resolveFile(this.rootNotebookFolder);
+    this.rootNotebookFileObject = 
fsManager.resolveFile(rootNotebookFileObject.getName().getURI());
     return listFolder(rootNotebookFileObject);
   }
 
+  /**
+   * A local root is kept as a decoded path, like the note file names in 
{@link #listFolder}, so
+   * that {@link #getNotePath} can strip it from them and GitNotebookRepo can 
open the repository
+   * in it. Windows does not support paths with "file:///" prepended, so it 
starts with "/".
+   */
+  private static String toRootNotebookFolder(String rootUri) {
+    if (rootUri.startsWith("file:")) {
+      return URI.create(rootUri).getPath();
+    }
+    return rootUri;
+  }
+
   private Map<String, NoteInfo> listFolder(FileObject fileObject) throws 
IOException {
     Map<String, NoteInfo> noteInfos = new HashMap<>();
 
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java
index 3996ed2b03..78a2bc6d6e 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java
@@ -96,6 +96,22 @@ class GitNotebookRepoTest {
     FileUtils.deleteDirectory(zeppelinDir);
   }
 
+  @Test
+  void notebookDirWithSpace() throws IOException {
+    File dirWithSpace = new File(zeppelinDir, "my notebooks");
+    FileUtils.moveDirectory(notebooksDir, dirWithSpace);
+    zConf.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(), 
dirWithSpace.getAbsolutePath());
+
+    notebookRepo = new GitNotebookRepo();
+    notebookRepo.init(zConf, noteParser);
+
+    // The repository is opened in the notebook dir, not in a sibling named 
after its URI form.
+    assertTrue(new File(dirWithSpace, ".git").isDirectory());
+    assertEquals(TEST_NOTE_PATH, 
notebookRepo.list(null).get(TEST_NOTE_ID).getPath());
+    notebookRepo.checkpoint(TEST_NOTE_ID, TEST_NOTE_PATH, "first commit", 
null);
+    assertEquals(1, notebookRepo.revisionHistory(TEST_NOTE_ID, TEST_NOTE_PATH, 
null).size());
+  }
+
   @Test
   void initNonemptyNotebookDir() throws IOException, GitAPIException {
     //given - .git does not exit
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java
index c7dcc9bdc4..fb8d7d2527 100644
--- 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java
@@ -30,16 +30,19 @@ import org.apache.zeppelin.user.AuthenticationInfo;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import java.io.File;
 import java.nio.file.Files;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class VFSNotebookRepoTest {
 
@@ -115,6 +118,28 @@ class VFSNotebookRepoTest {
     assertEquals(1, notebookRepo.list(AuthenticationInfo.ANONYMOUS).size());
   }
 
+  @Test
+  void testNotebookDirWithCharactersThatAUriDoesNotAllow(@TempDir Path 
tempDir) throws IOException {
+    // A space anywhere, or the backslashes of a Windows path such as the 
default "./\\notebook".
+    // Commons VFS treats a backslash as a separator, so only check that the 
repo works.
+    for (String dirName : new String[] {"my notebooks", "back\\slash"}) {
+      
zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_NOTEBOOK_DIR.getVarName(),
+          tempDir.resolve(dirName).toString());
+      VFSNotebookRepo repo = new VFSNotebookRepo();
+      repo.init(zConf, noteParser);
+
+      assertTrue(new File(repo.rootNotebookFolder).isDirectory(), dirName);
+      Note note = new Note();
+      note.setPath("/note1");
+      note.setNoteParser(noteParser);
+      repo.save(note, AuthenticationInfo.ANONYMOUS);
+      Map<String, NoteInfo> noteInfos = 
repo.list(AuthenticationInfo.ANONYMOUS);
+      assertEquals(1, noteInfos.size(), dirName);
+      assertEquals("/note1", noteInfos.get(note.getId()).getPath(), dirName);
+    }
+    assertTrue(tempDir.resolve("my notebooks").toFile().isDirectory());
+  }
+
   @Test
   void testNoteNameWithColon() throws IOException {
     assertEquals(0, notebookRepo.list(AuthenticationInfo.ANONYMOUS).size());

Reply via email to