diff --git a/api/src/org/labkey/api/files/FileContentService.java b/api/src/org/labkey/api/files/FileContentService.java index 9ae586b1fe7..8ef4771131d 100644 --- a/api/src/org/labkey/api/files/FileContentService.java +++ b/api/src/org/labkey/api/files/FileContentService.java @@ -68,10 +68,19 @@ static void setInstance(FileContentService impl) } /** - * Returns a list of Container in which the path resides. + * Returns the container whose file root holds the path, or null if none does. */ + @Nullable + Container getContainerForFilePath(java.nio.file.Path path); + + /** @deprecated use {@link #getContainerForFilePath(java.nio.file.Path)} */ + @Deprecated @NotNull - List getContainersForFilePath(java.nio.file.Path path); + default List getContainersForFilePath(java.nio.file.Path path) + { + Container c = getContainerForFilePath(path); + return c == null ? List.of() : List.of(c); + } /** * Returns the file root of the specified container. If not explicitly defined, diff --git a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java index 0ad11b2e8bb..c0b45ec9592 100644 --- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java +++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java @@ -2453,8 +2453,8 @@ protected ModelAndView getDataView(DataForm form, BindException errors) throws I if (!underFileRoot) { // Issue 35649: ImmPort module "publish" creates exp.data object in this container for paths that originate in a different container - List containers = fileSvc.getContainersForFilePath(dataPath); - if (containers.isEmpty() || containers.stream().noneMatch(c -> c.hasPermission(getUser(), ReadPermission.class))) + Container fileContainer = fileSvc.getContainerForFilePath(dataPath); + if (fileContainer == null || !fileContainer.hasPermission(getUser(), ReadPermission.class)) throw new UnauthorizedException("Data file is not under the pipeline root for this folder"); } } diff --git a/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java b/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java index ca6ef515ee3..974a993daf2 100644 --- a/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java +++ b/filecontent/src/org/labkey/filecontent/FileContentServiceImpl.java @@ -193,57 +193,63 @@ private FileContentServiceImpl() } @Override - @NotNull - public List getContainersForFilePath(java.nio.file.Path path) + public @Nullable Container getContainerForFilePath(java.nio.file.Path path) { // Ignore cloud files for now if (FileUtil.hasCloudScheme(path)) - return Collections.emptyList(); + return null; + + // Anchor on the most specific root holding the path: the site default root or a custom file root + Container anchor = null; + java.nio.file.Path anchorPath = null; + java.nio.file.Path defaultRootPath = getSiteDefaultRoot().toPath(); + if (URIUtil.isDescendant(defaultRootPath.toUri(), path.toUri())) + { + anchor = ContainerManager.getRoot(); + anchorPath = defaultRootPath; + } - // If the path is under the default root, do optimistic simple match for containers under the default root - File defaultRoot = getSiteDefaultRoot(); - java.nio.file.Path defaultRootPath = defaultRoot.toPath(); - if (path.startsWith(defaultRootPath)) + for (FileRoot root : FileRootManager.get().getFileRoots()) { - java.nio.file.Path rel = defaultRootPath.relativize(path); - if (rel.getNameCount() > 0) + // Skip cloud roots before resolving: resolution goes through CloudStoreService, which may be absent or misconfigured + if (isCloudFileRoot(root.getPath())) + continue; + Container c = ContainerManager.getForId(root.getContainer()); + if (c == null || c.isRoot() || isUseDefaultRoot(c)) + continue; + java.nio.file.Path rootPath = getExistingFileRootPath(c); + if (rootPath != null && URIUtil.isDescendant(rootPath.toUri(), path.toUri()) && (anchorPath == null || URIUtil.isDescendant(anchorPath.toUri(), rootPath.toUri()))) { - Container root = ContainerManager.getRoot(); - Container next = root; - while (rel.getNameCount() > 0) - { - // check if there exists a child container that matches the next path segment - java.nio.file.Path top = rel.subpath(0, 1); - assert top != null; - Container child = next.getChild(top.getFileName().toString()); - if (child == null) - break; + anchor = c; + anchorPath = rootPath; + } + } - next = child; + if (anchor == null) + return null; - if(rel.getNameCount() > 1) - { - rel = rel.subpath(1, rel.getNameCount()); - } - else - { - break; - } - } + // Child folder roots default to a matching subdirectory, so walk the path's segments down the container tree + Container owner = anchor; - if (next != null && !next.equals(root)) - { - // verify our naive file path is correct for the container -- it may have a file root other than the default - java.nio.file.Path fileRoot = getFileRootPath(next); - if (fileRoot != null && path.startsWith(fileRoot)) - return Collections.singletonList(next); - } - } + for (java.nio.file.Path segment : anchorPath.relativize(path)) + { + Container child = owner.getChild(segment.toString()); + if (child == null) + break; + owner = child; } - // TODO: Create cache of file root and pipeline root paths -> list of containers + // A child's default root sits beside the parent's @files, so a moved child's old location belongs to no folder + if (owner.isRoot()) + return null; + + java.nio.file.Path fileRoot = getExistingFileRootPath(owner); + return fileRoot != null && URIUtil.isDescendant(fileRoot.toUri(), path.toUri()) ? owner : null; + } - return Collections.emptyList(); + private @Nullable java.nio.file.Path getExistingFileRootPath(@NotNull Container c) + { + return isFileRootDisabled(c) ? null : resolveFileRootPath(c, false); } @Override @@ -1849,6 +1855,41 @@ public void fileRootsTest() } + @Test + public void testContainerForFilePath() + { + //pre-clean + cleanup(); + + FileContentService svc = FileContentService.get(); + Assert.assertNotNull(svc); + + Container project1 = ContainerManager.createContainer(ContainerManager.getRoot(), PROJECT1, TestContext.get().getUser()); + Container subfolder1 = ContainerManager.createContainer(project1, PROJECT1_SUBFOLDER1, TestContext.get().getUser()); + Container project2 = ContainerManager.createContainer(ContainerManager.getRoot(), PROJECT2, TestContext.get().getUser()); + + java.nio.file.Path project2File = svc.getFileRootPath(project2, ContentType.files).resolve(TXT_FILE); + Assert.assertEquals("Default root path", project2, svc.getContainerForFilePath(project2File)); + + File testRoot = getTestRoot(); + svc.setFileRoot(project1, testRoot); + + java.nio.file.Path subfolderFile = svc.getFileRootPath(subfolder1, ContentType.files).resolve(TXT_FILE); + Assert.assertEquals("Inherited custom root path", subfolder1, svc.getContainerForFilePath(subfolderFile)); + Assert.assertEquals("Custom root path", project1, svc.getContainerForFilePath(testRoot.toPath().resolve("unmatched").resolve(TXT_FILE))); + + java.nio.file.Path unmanaged = svc.getSiteDefaultRoot().toPath().resolve(FILE_ROOT_SUFFIX + "Unmanaged").resolve(TXT_FILE); + Assert.assertNull("Unmanaged path", svc.getContainerForFilePath(unmanaged)); + + File subfolderRoot = new File(testRoot, "CustomSubfolder"); + subfolderRoot.mkdirs(); + svc.setFileRoot(subfolder1, subfolderRoot); + + java.nio.file.Path movedSubfolderFile = svc.getFileRootPath(subfolder1, ContentType.files).resolve(TXT_FILE); + Assert.assertEquals("Moved custom root path", subfolder1, svc.getContainerForFilePath(movedSubfolderFile)); + Assert.assertNull("Stale default root path", svc.getContainerForFilePath(subfolderFile)); + } + private void assertPathsEqual(String msg, File expected, File actual) { String expectedPath = FileUtil.getAbsoluteCaseSensitiveFile(expected).getPath(); diff --git a/filecontent/src/org/labkey/filecontent/FileRootManager.java b/filecontent/src/org/labkey/filecontent/FileRootManager.java index 03eec39df27..7f4c307482a 100644 --- a/filecontent/src/org/labkey/filecontent/FileRootManager.java +++ b/filecontent/src/org/labkey/filecontent/FileRootManager.java @@ -28,6 +28,7 @@ import org.labkey.api.files.FileRoot; import org.labkey.api.security.User; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -72,6 +73,11 @@ public FileRoot getFileRoot(Container c) return null == root ? new FileRoot(c) : root; } + public Collection getFileRoots() + { + return CACHE.get(CACHE_KEY).values(); + } + public void deleteFileRoot(Container c) { SimpleFilter filter = SimpleFilter.createContainerFilter(c);