From fecd8d2389f25331338403d8984388afde49642b Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Mon, 17 Nov 2025 11:43:54 -0800 Subject: [PATCH 01/18] Continue adopting FileLike instead of File or Path --- .../api/assay/AbstractAssayProvider.java | 6 +- .../api/assay/DefaultAssayRunCreator.java | 2 +- .../pipeline/AssayUploadPipelineJob.java | 10 +- .../org/labkey/api/docker/DockerService.java | 3 +- api/src/org/labkey/api/exp/api/ExpRun.java | 10 ++ .../moduleeditor/api/ModuleEditorService.java | 3 +- .../AppPipelineJobNotificationProvider.java | 8 +- .../labkey/api/pipeline/LocalDirectory.java | 157 ++++++++---------- .../org/labkey/api/pipeline/ParamParser.java | 5 +- api/src/org/labkey/api/pipeline/PipeRoot.java | 8 +- .../org/labkey/api/pipeline/PipelineJob.java | 33 ++-- .../api/pipeline/PipelineProtocolFactory.java | 12 +- .../labkey/api/pipeline/PipelineProvider.java | 10 +- .../labkey/api/pipeline/RecordedAction.java | 15 ++ .../labkey/api/pipeline/WorkDirectory.java | 66 +++++--- .../file/AbstractFileAnalysisJob.java | 64 +++---- .../AbstractFileAnalysisProtocolFactory.java | 14 -- .../pipeline/file/FileAnalysisJobSupport.java | 78 +-------- .../labkey/api/pipeline/file/PathMapper.java | 3 - .../api/pipeline/file/PathMapperImpl.java | 12 -- .../api/query/AbstractQueryImportAction.java | 2 +- .../api/query/QueryImportPipelineJob.java | 8 +- .../labkey/api/reader/ColumnDescriptor.java | 9 +- .../org/labkey/api/reader/ExcelLoader.java | 20 ++- .../labkey/api/reader/FastaDataLoader.java | 6 +- .../org/labkey/api/reader/FastaLoader.java | 14 +- .../api/reports/ExternalScriptEngine.java | 55 +++--- .../reports/report/DockerScriptReport.java | 3 +- .../report/ExternalScriptEngineReport.java | 62 ++++--- .../report/InternalScriptEngineReport.java | 20 +-- .../reports/report/ScriptEngineReport.java | 107 ++++-------- .../reports/report/ScriptProcessReport.java | 69 ++------ .../api/reports/report/ScriptReport.java | 5 +- .../reports/report/python/IpynbReport.java | 97 +++++------ .../report/r/AbstractParamReplacement.java | 14 +- .../reports/report/r/ParamReplacement.java | 14 +- .../reports/report/r/ParamReplacementSvc.java | 62 +++---- .../reports/report/r/RDockerScriptEngine.java | 16 +- .../labkey/api/reports/report/r/RReport.java | 80 +++++---- .../api/reports/report/r/RReportJob.java | 56 +++---- .../api/reports/report/r/RScriptEngine.java | 36 ++-- .../reports/report/r/RserveScriptEngine.java | 31 ++-- .../reports/report/r/view/ConsoleOutput.java | 16 +- .../report/r/view/DownloadOutputView.java | 44 +++-- .../r/view/DownloadParamReplacement.java | 12 +- .../api/reports/report/r/view/FileOutput.java | 24 +-- .../api/reports/report/r/view/HrefOutput.java | 8 +- .../api/reports/report/r/view/HtmlOutput.java | 20 ++- .../reports/report/r/view/ImageOutput.java | 27 +-- .../reports/report/r/view/IpynbOutput.java | 14 +- .../api/reports/report/r/view/JsonOutput.java | 22 +-- .../reports/report/r/view/KnitrOutput.java | 15 +- .../api/reports/report/r/view/PdfOutput.java | 26 ++- .../report/r/view/PostscriptOutput.java | 23 +-- .../reports/report/r/view/ROutputView.java | 23 +-- .../api/reports/report/r/view/SvgOutput.java | 6 +- .../api/reports/report/r/view/TextOutput.java | 24 +-- .../api/reports/report/r/view/TsvOutput.java | 32 ++-- .../view/RenderBackgroundRReportView.java | 7 +- .../labkey/api/study/SpecimenTransform.java | 7 +- api/src/org/labkey/api/util/FileType.java | 57 ++----- api/src/org/labkey/api/util/FileUtil.java | 36 +++- api/src/org/labkey/api/util/HashHelpers.java | 15 +- api/src/org/labkey/api/util/ImageUtil.java | 3 +- .../api/util/MaintenancePipelineJob.java | 2 +- api/src/org/labkey/api/writer/ZipUtil.java | 7 +- api/src/org/labkey/vfs/FileLike.java | 22 ++- api/src/org/labkey/vfs/FileSystemLike.java | 15 ++ api/src/org/labkey/vfs/FileSystemLocal.java | 4 +- api/src/org/labkey/vfs/FileSystemVFS.java | 4 +- .../assay/pipeline/AssayImportRunTask.java | 84 ++++------ .../sitevalidation/SiteValidationJob.java | 8 +- .../controllers/exp/ExperimentController.java | 7 +- .../pipeline/ExperimentPipelineProvider.java | 29 ++-- .../pipeline/MoveRunsPipelineJob.java | 11 +- .../experiment/pipeline/SampleReloadTask.java | 12 +- .../experiment/pipeline/XarGeneratorTask.java | 2 +- .../labkey/list/pipeline/ListReloadJob.java | 10 +- .../labkey/list/pipeline/ListReloadTask.java | 14 +- .../pipeline/analysis/CommandTaskImpl.java | 109 ++++++------ .../pipeline/analysis/ConvertTaskFactory.java | 22 +-- .../pipeline/analysis/FileAnalysisJob.java | 29 ++-- .../pipeline/api/AbstractWorkDirectory.java | 131 +++++++-------- .../labkey/pipeline/api/ParamParserImpl.java | 22 +-- .../org/labkey/pipeline/api/PipeRootImpl.java | 28 ++-- .../labkey/pipeline/api/ScriptTaskImpl.java | 24 +-- .../pipeline/api/WorkDirectoryLocal.java | 10 +- .../pipeline/api/WorkDirectoryRemote.java | 53 +++--- .../pipeline/importer/FolderImportTask.java | 8 +- .../pipeline/mule/test/DummyPipelineJob.java | 2 +- .../query/reports/ReportsController.java | 4 +- .../org/labkey/specimen/SpecimenModule.java | 4 +- .../importer/AbstractSpecimenTask.java | 61 +++---- .../pipeline/FileAnalysisSpecimenTask.java | 21 ++- .../specimen/pipeline/SpecimenBatch.java | 10 +- .../specimen/pipeline/SpecimenJobSupport.java | 5 +- .../specimen/SpecimenMigrationService.java | 4 +- .../pipeline/AbstractStudyPipelineJob.java | 6 +- .../labkey/api/study/pipeline/StudyBatch.java | 2 +- .../study/importer/StudyImportContext.java | 8 +- .../study/importer/StudyImporterFactory.java | 7 +- .../labkey/study/pipeline/DatasetBatch.java | 10 +- .../pipeline/DatasetInferSchemaReader.java | 15 +- .../pipeline/FileAnalysisDatasetTask.java | 7 +- .../MasterPatientIndexUpdateTask.java | 4 +- .../visitmanager/PurgeParticipantsJob.java | 2 +- 106 files changed, 1209 insertions(+), 1396 deletions(-) diff --git a/api/src/org/labkey/api/assay/AbstractAssayProvider.java b/api/src/org/labkey/api/assay/AbstractAssayProvider.java index 5dc3c2cf5a1..973df2aa79e 100644 --- a/api/src/org/labkey/api/assay/AbstractAssayProvider.java +++ b/api/src/org/labkey/api/assay/AbstractAssayProvider.java @@ -673,7 +673,7 @@ else if (inputDatas.size() > 1) { Map.Entry entry = iter.next(); // If it's not under the current pipeline root - if (pipeRoot == null || !pipeRoot.isUnderRoot(entry.getValue().toNioPathForRead())) + if (pipeRoot == null || !pipeRoot.isUnderRoot(entry.getValue())) { // Remove it from both collections iter.remove(); @@ -1267,7 +1267,7 @@ public Pair> setValidationAndAnalysisS for (AnalysisScript script : scripts) { - File scriptFile = script.getScript().toNioPathForRead().toFile(); + FileLike scriptFile = script.getScript(); String ext = FileUtil.getExtension(scriptFile); if (scriptFile.isFile() && ext != null) { @@ -1280,7 +1280,7 @@ public Pair> setValidationAndAnalysisS String scriptText; try { - scriptText = Files.readString(scriptFile.toPath(), StringUtilsLabKey.DEFAULT_CHARSET); + scriptText = Files.readString(scriptFile.toNioPathForRead(), StringUtilsLabKey.DEFAULT_CHARSET); } catch (IOException e) { diff --git a/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java b/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java index f7fbae5aaa8..46ae2d3bf88 100644 --- a/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java +++ b/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java @@ -229,7 +229,7 @@ private ExpExperiment saveExperimentRunAsync(AssayRunUploadContext batch, forceSaveBatchProps, PipelineService.get().getPipelineRootSetting(context.getContainer()), - primaryFile.toNioPathForRead().toFile() + primaryFile ); context.setPipelineJobGUID(pipelineJob.getJobGUID()); diff --git a/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java b/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java index dbc66f79de4..c9ccf8c94c2 100644 --- a/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java +++ b/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java @@ -37,8 +37,8 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.URLHelper; import org.labkey.api.view.ViewBackgroundInfo; +import org.labkey.vfs.FileLike; -import java.io.File; import java.util.HashMap; import java.util.Map; @@ -51,7 +51,7 @@ public class AssayUploadPipelineJob extends private long _batchId; private AssayRunAsyncContext _context; - private File _primaryFile; + private FileLike _primaryFile; private boolean _forceSaveBatchProps; private ExpRun _run; @@ -62,7 +62,7 @@ protected AssayUploadPipelineJob() /** * @param forceSaveBatchProps whether we need to save the batch properties, or if it's already been handled */ - public AssayUploadPipelineJob(AssayRunAsyncContext context, ViewBackgroundInfo info, @NotNull ExpExperiment batch, boolean forceSaveBatchProps, PipeRoot root, File primaryFile) + public AssayUploadPipelineJob(AssayRunAsyncContext context, ViewBackgroundInfo info, @NotNull ExpExperiment batch, boolean forceSaveBatchProps, PipeRoot root, FileLike primaryFile) { super(context.getProvider().getName(), info, root); String baseName = primaryFile.getName(); @@ -138,7 +138,7 @@ public void doWork() } // Create the basic run - _run = AssayService.get().createExperimentRun(_context.getName(), getContainer(), _context.getProtocol(), _primaryFile); + _run = AssayService.get().createExperimentRun(_context.getName(), getContainer(), _context.getProtocol(), _primaryFile.toNioPathForRead().toFile()); _run.setComments(_context.getComments()); _run.setWorkflowTaskId(_context.getWorkflowTask()); // remember which job created the run so we can show this run on the job details page @@ -224,7 +224,7 @@ protected String getJobNotificationProvider() return _context._jobNotificationProvider; } - public File getPrimaryFile() + public FileLike getPrimaryFile() { return _primaryFile; } diff --git a/api/src/org/labkey/api/docker/DockerService.java b/api/src/org/labkey/api/docker/DockerService.java index 9e742b18df4..f92732529d8 100644 --- a/api/src/org/labkey/api/docker/DockerService.java +++ b/api/src/org/labkey/api/docker/DockerService.java @@ -22,6 +22,7 @@ import org.labkey.api.security.UserManager; import org.labkey.api.services.ServiceRegistry; import org.labkey.api.util.HeartBeat; +import org.labkey.vfs.FileLike; import java.io.File; import java.io.FileFilter; @@ -484,7 +485,7 @@ public DockerContainer( boolean isUseDockerVolumes(); - default void executeR(DockerImage dockerImage, File scriptFile, String localWorkingDir, String remoteWorkingDir, FileFilter inputScripts) throws IOException + default void executeR(DockerImage dockerImage, FileLike scriptFile, String localWorkingDir, String remoteWorkingDir, FileFilter inputScripts) throws IOException { throw new UnsupportedOperationException(NO_DOCKER); } diff --git a/api/src/org/labkey/api/exp/api/ExpRun.java b/api/src/org/labkey/api/exp/api/ExpRun.java index 4ff577354d1..05105e37d45 100644 --- a/api/src/org/labkey/api/exp/api/ExpRun.java +++ b/api/src/org/labkey/api/exp/api/ExpRun.java @@ -20,6 +20,8 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.exp.Identifiable; import org.labkey.api.security.User; +import org.labkey.vfs.FileLike; +import org.labkey.vfs.FileSystemLike; import java.io.File; import java.nio.file.Path; @@ -52,7 +54,15 @@ public interface ExpRun extends ExpObject, Identifiable List getInputDatas(@Nullable String inputRole, @Nullable ExpProtocol.ApplicationType appType); File getFilePathRoot(); void setFilePathRoot(File filePathRoot); + default void setFilePathRoot(FileLike f) + { + setFilePathRoot(FileSystemLike.toFile(f)); + } Path getFilePathRootPath(); + default FileLike getFilePathFileLike() + { + return FileSystemLike.wrapFile(getFilePathRoot()); + }; void setFilePathRootPath(Path filePathRoot); void setProtocol(ExpProtocol protocol); void setJobId(Long jobId); diff --git a/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java b/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java index 9fba064f9f9..894e4b806be 100644 --- a/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java +++ b/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.module.Module; import org.labkey.api.services.ServiceRegistry; +import org.labkey.api.util.FileUtil; import org.labkey.api.util.Path; import org.labkey.api.view.ActionURL; @@ -63,6 +64,6 @@ default File getFileForModuleResource(Module module, Path path) File resources = getUpdatableResourcesRoot(module, null); if (null == resources) return null; - return new File(resources, path.toString("","")); + return FileUtil.appendPath(resources, path); } } \ No newline at end of file diff --git a/api/src/org/labkey/api/pipeline/AppPipelineJobNotificationProvider.java b/api/src/org/labkey/api/pipeline/AppPipelineJobNotificationProvider.java index e0e87c14dab..2829f43f2d1 100644 --- a/api/src/org/labkey/api/pipeline/AppPipelineJobNotificationProvider.java +++ b/api/src/org/labkey/api/pipeline/AppPipelineJobNotificationProvider.java @@ -194,7 +194,7 @@ private String getJobSuccessMsg(PipelineJob job, @NotNull ImportType importType, return successMsg.toString(); } - else if (job instanceof AssayUploadPipelineJob) + else if (job instanceof AssayUploadPipelineJob assayJob) { String successMsg = "Successfully imported assay run"; @@ -202,7 +202,7 @@ else if (job instanceof AssayUploadPipelineJob) { String assayName = (String) info.get("assayName"); - String filename = ((AssayUploadPipelineJob) job).getPrimaryFile().getName(); + String filename = assayJob.getPrimaryFile().getName(); if (!filename.endsWith(".tmp")) { successMsg += " from " + filename; @@ -229,10 +229,10 @@ private String getJobErrorMsg(PipelineJob job, String rawErrorMsg) "\n" + rawErrorMsg;// resolveErrorMessage on client } - else if (job instanceof AssayUploadPipelineJob) + else if (job instanceof AssayUploadPipelineJob assayJob) { return "Failed to import assay run from " + - ((AssayUploadPipelineJob) job).getPrimaryFile().getName() + + assayJob.getPrimaryFile().getName() + "\n" + rawErrorMsg; } diff --git a/api/src/org/labkey/api/pipeline/LocalDirectory.java b/api/src/org/labkey/api/pipeline/LocalDirectory.java index 68c67ed130f..1e87d40e028 100644 --- a/api/src/org/labkey/api/pipeline/LocalDirectory.java +++ b/api/src/org/labkey/api/pipeline/LocalDirectory.java @@ -22,13 +22,11 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; import org.labkey.api.util.FileUtil; +import org.labkey.vfs.FileLike; -import java.io.File; import java.io.IOException; import java.io.Serializable; -import java.nio.file.Files; import java.nio.file.NoSuchFileException; -import java.nio.file.Path; import java.nio.file.StandardCopyOption; /** @@ -42,43 +40,37 @@ //@JsonIgnoreProperties(value={"_moduleName"}) public class LocalDirectory implements Serializable { - @NotNull private final File _localDirectoryFile; + @NotNull private final FileLike _localDirectoryFile; private final boolean _isTemporary; private final PipeRoot _pipeRoot; - private final Path _remoteDir; - private Path _logFile; + private final FileLike _remoteDir; + private FileLike _logFile; private final String _baseLogFileName; public static LocalDirectory create(@NotNull PipeRoot root) { - return create(root, "dummyLogFile", root.isCloudRoot() ? "dummy" : root.getRootPath().getPath()); + return create(root, "dummyLogFile", root.isCloudRoot() ? root.getRootFileLike().resolveChild("dummy") : root.getRootFileLike()); } - @Deprecated //Prefer to use a Path for workingDir -- can be local or remote, but should match with root - public static LocalDirectory create(@NotNull PipeRoot root, @NotNull String baseLogFileName, @NotNull String workingDir) - { - return create(root, baseLogFileName, Path.of(workingDir)); - } - - public static LocalDirectory create(@NotNull PipeRoot root, @NotNull String baseLogFileName, @NotNull Path workingDir) + public static LocalDirectory create(@NotNull PipeRoot root, @NotNull String baseLogFileName, @NotNull FileLike workingDir) { return !root.isCloudRoot() ? - new LocalDirectory(workingDir.toFile(), baseLogFileName) : + new LocalDirectory(workingDir, baseLogFileName) : new LocalDirectory(root.getContainer(), root, baseLogFileName); } @JsonCreator private LocalDirectory( - @JsonProperty("_localDirectoryFile") File localDirectoryFile, + @JsonProperty("_localDirectoryFile") FileLike localDirectoryFile, @JsonProperty("_isTemporary") boolean isTemporary, @JsonProperty("_pipeRoot") PipeRoot pipeRoot, @JsonProperty("_baseLogFileName") String baseLogFileName, - @JsonProperty("_remoteDir") Path remoteDir) + @JsonProperty("_remoteDir") FileLike remoteDir) { _localDirectoryFile = localDirectoryFile; _isTemporary = isTemporary; _pipeRoot = pipeRoot; - _remoteDir = remoteDir != null ? remoteDir : _pipeRoot == null ? null : _pipeRoot.getRootNioPath(); //Using _piperoot as default for backwards compatability + _remoteDir = remoteDir != null ? remoteDir : _pipeRoot == null ? null : _pipeRoot.getRootFileLike(); //Using _piperoot as default for backwards compatability _baseLogFileName = baseLogFileName; } @@ -88,17 +80,17 @@ public LocalDirectory(Container container, PipeRoot pipeRoot, String basename) this(container, pipeRoot, basename, null); } - public LocalDirectory(Container container, PipeRoot pipeRoot, String basename, Path remoteDir) + public LocalDirectory(Container container, PipeRoot pipeRoot, String basename, FileLike remoteDir) { _isTemporary = true; _pipeRoot = pipeRoot; - _remoteDir = remoteDir != null ? remoteDir : _pipeRoot == null ? null : _pipeRoot.getRootNioPath(); //Using _piperoot as default for backwards compatability + _remoteDir = remoteDir != null ? remoteDir : _pipeRoot == null ? null : _pipeRoot.getRootFileLike(); //Using _piperoot as default for backwards compatability _baseLogFileName = basename; try { - File containerDir = ensureContainerDir(container); - _localDirectoryFile = FileUtil.appendName(containerDir, FileUtil.makeFileNameWithTimestamp("_temp_")); + FileLike containerDir = ensureContainerDir(container); + _localDirectoryFile = containerDir.resolveChild(FileUtil.makeFileNameWithTimestamp("_temp_")); ensureLocalDirectory(); } @@ -109,7 +101,7 @@ public LocalDirectory(Container container, PipeRoot pipeRoot, String basename, P } // Constructor when pipeline root not in cloud - public LocalDirectory(@NotNull File localDirectory, String basename) + public LocalDirectory(@NotNull FileLike localDirectory, String basename) { _localDirectoryFile = localDirectory; _isTemporary = false; @@ -119,27 +111,27 @@ public LocalDirectory(@NotNull File localDirectory, String basename) } @NotNull - public File getLocalDirectoryFile() + public FileLike getLocalDirectoryFile() { return _localDirectoryFile; } - public Path determineLogFile() + public FileLike determineLogFile() { // If _isTemporary, look for existing file in the parent - _logFile = PipelineJob.FT_LOG.newFile(_localDirectoryFile, _baseLogFileName).toPath(); + _logFile = PipelineJob.FT_LOG.newFile(_localDirectoryFile, _baseLogFileName); if (_isTemporary && null != _remoteDir) { try { - Path remoteLogFilePath = FileUtil.appendName(_remoteDir, _logFile.getFileName().toString()); - if (Files.exists(remoteLogFilePath)) + FileLike remoteLogFilePath = _remoteDir.resolveChild(_logFile.getName()); + if (remoteLogFilePath.exists()) { - Files.copy(remoteLogFilePath, _logFile); + FileUtil.copyFile(remoteLogFilePath, _logFile); } else { - FileUtil.createFile(_logFile); + FileUtil.createNewFile(_logFile, true); } } catch (IOException e) @@ -150,7 +142,7 @@ public Path determineLogFile() return _logFile; } - public Path restore() + public FileLike restore() { try { @@ -163,29 +155,28 @@ public Path restore() } } - public File copyToLocalDirectory(String url, Logger log) + public FileLike copyToLocalDirectory(String url, Logger log) { if (_isTemporary && null != _pipeRoot) { - // File elsewhere (on S3); make a copy a return File object for copy - Path path = _pipeRoot.resolveToNioPathFromUrl(url); - if (null != path) + try { - String filename = FileUtil.getFileName(path); - try + // File elsewhere (on S3); make a copy a return File object for copy + FileLike path = _pipeRoot.resolveToFileLikeFromUrl(url); + if (null != path) { - File tempFile = FileUtil.appendName(_localDirectoryFile, filename); - if (!Files.exists(tempFile.toPath())) + FileLike tempFile = _localDirectoryFile.resolveChild(path.getName()); + if (!tempFile.exists()) { - Files.copy(path, tempFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES); - log.info("Created temp file because input is from cloud: " + FileUtil.pathToString(path)); + FileUtil.copyFile(path, tempFile, StandardCopyOption.COPY_ATTRIBUTES); + log.info("Created temp file because input is from cloud: " + path); } return tempFile; } - catch (IOException e) - { - log.error("IO Error: " + e.getMessage()); - } + } + catch (IOException e) + { + log.error("IO Error: " + e.getMessage()); } } return null; @@ -193,35 +184,35 @@ public File copyToLocalDirectory(String url, Logger log) private void ensureLocalDirectory() throws IOException { - if (_isTemporary && !Files.exists(_localDirectoryFile.toPath())) - FileUtil.createDirectory(_localDirectoryFile.toPath()); // TODO Should we set file permissions? + if (_isTemporary && !_localDirectoryFile.exists()) + FileUtil.createDirectory(_localDirectoryFile); // TODO Should we set file permissions? } // If LocalDirectory isTemporary, copies the file to the temp directory for this container. // That temp directory will live as long as the the LabKey container lives, or until the system temp is cleared @Nullable - public static File copyToContainerDirectory(@NotNull Container container, @NotNull Path remotePath, @NotNull Logger log) + public static FileLike copyToContainerDirectory(@NotNull Container container, @NotNull FileLike remotePath, @NotNull Logger log) { // File elsewhere (on S3); make a copy a return File object for copy - String fileName = FileUtil.getFileName(remotePath); + String fileName = remotePath.getName(); String tempFileName = FileUtil.makeFileNameWithTimestamp(FileUtil.getBaseName(fileName), FileUtil.getExtension(fileName)); try { - File containerDir = ensureContainerDir(container); - File tempFile = FileUtil.appendName(containerDir, tempFileName); - if (!Files.exists(tempFile.toPath())) + FileLike containerDir = ensureContainerDir(container); + FileLike tempFile = containerDir.resolveChild(tempFileName); + if (!tempFile.exists()) { - log.debug("Copying file to container's temp directory: "+ FileUtil.pathToString(remotePath)); - Files.copy(remotePath, tempFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES); - log.debug("Copied " + Files.size(tempFile.toPath()) + " bytes."); + log.debug("Copying file to container's temp directory: " + remotePath); + FileUtil.copyFile(remotePath, tempFile, StandardCopyOption.COPY_ATTRIBUTES); + log.debug("Copied " + tempFile.getSize() + " bytes."); } return tempFile; } catch (NoSuchFileException e) { // Avoid a separate round-trip just to determine if file is available, as it adds ~1 overhead per call - log.debug("Could not find remote file: " + FileUtil.pathToString(remotePath) + ", unable to copy locally"); + log.debug("Could not find remote file: " + remotePath + ", unable to copy locally"); return null; } catch (IOException e) @@ -231,37 +222,37 @@ public static File copyToContainerDirectory(@NotNull Container container, @NotNu } } - private static File ensureContainerDir(Container container) throws IOException + private static FileLike ensureContainerDir(Container container) throws IOException { - File moduleDir = getModuleLocalTempDirectory(); - if (!Files.exists(moduleDir.toPath())) + FileLike moduleDir = getModuleLocalTempDirectory(); + if (!moduleDir.exists()) { - FileUtil.createDirectory(moduleDir.toPath()); + FileUtil.createDirectory(moduleDir); } - File containerDir = getContainerLocalTempDirectory(container); - if (!Files.exists(containerDir.toPath())) + FileLike containerDir = getContainerLocalTempDirectory(container); + if (!containerDir.exists()) { - FileUtil.createDirectory(containerDir.toPath()); + FileUtil.createDirectory(containerDir); } return containerDir; } @Nullable - public Path cleanUpLocalDirectory() + public FileLike cleanUpLocalDirectory() { - Path remoteLogFilePath = null; - if (_isTemporary && Files.exists(_localDirectoryFile.toPath())) + FileLike remoteLogFilePath = null; + if (_isTemporary && _localDirectoryFile.exists()) { - if (null != _logFile && Files.exists(_logFile)) + if (null != _logFile && _logFile.exists()) { // Copy file back to the cloud - remoteLogFilePath = getRemoteLogFilePath(); + remoteLogFilePath = getRemoteLogFile(); if (null != remoteLogFilePath) { try { - Files.copy(_logFile, remoteLogFilePath, StandardCopyOption.REPLACE_EXISTING); + FileUtil.copyFile(_logFile, remoteLogFilePath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { @@ -269,39 +260,27 @@ public Path cleanUpLocalDirectory() } } } - deleteDirectory(_localDirectoryFile.toPath()); + FileUtil.deleteDir(_localDirectoryFile); } return remoteLogFilePath; } - private void deleteDirectory(Path dir) - { - try - { - FileUtil.deleteDir(dir); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - private static File getModuleLocalTempDirectory() + private static FileLike getModuleLocalTempDirectory() { - File tempDir = FileUtil.getTempDirectory(); // tomcat/temp or similar - return FileUtil.appendName(tempDir, FileUtil.makeLegalName(PipelineService.MODULE_NAME + "_temp")); + FileLike tempDir = FileUtil.getTempDirectoryFileLike(); // tomcat/temp or similar + return tempDir.resolveChild(FileUtil.makeLegalName(PipelineService.MODULE_NAME + "_temp")); } - public static File getContainerLocalTempDirectory(Container container) + public static FileLike getContainerLocalTempDirectory(Container container) { - return FileUtil.appendName(getModuleLocalTempDirectory(), FileUtil.makeLegalName(container.getName() + "_" + container.getId())); + return getModuleLocalTempDirectory().resolveChild(FileUtil.makeLegalName(container.getName() + "_" + container.getId())); } - public Path getRemoteLogFilePath() + public FileLike getRemoteLogFile() { if (_remoteDir == null) return _logFile; - return FileUtil.appendName(_remoteDir, _logFile.getFileName().toString()); + return _remoteDir.resolveChild(_logFile.getName()); } } diff --git a/api/src/org/labkey/api/pipeline/ParamParser.java b/api/src/org/labkey/api/pipeline/ParamParser.java index cf9d46f1370..4d96bd8f1d0 100644 --- a/api/src/org/labkey/api/pipeline/ParamParser.java +++ b/api/src/org/labkey/api/pipeline/ParamParser.java @@ -15,7 +15,8 @@ */ package org.labkey.api.pipeline; -import java.io.File; +import org.labkey.vfs.FileLike; + import java.io.IOException; import java.io.InputStream; import java.util.Map; @@ -51,7 +52,7 @@ public interface ParamParser String getXMLFromMap(Map params); - void writeFromMap(Map params, File fileDest) throws IOException; + void writeFromMap(Map params, FileLike fileDest) throws IOException; /** * Error diff --git a/api/src/org/labkey/api/pipeline/PipeRoot.java b/api/src/org/labkey/api/pipeline/PipeRoot.java index 80138b4b122..7bcbb2ebc95 100644 --- a/api/src/org/labkey/api/pipeline/PipeRoot.java +++ b/api/src/org/labkey/api/pipeline/PipeRoot.java @@ -27,6 +27,7 @@ import org.labkey.vfs.FileLike; import java.io.File; +import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.util.List; @@ -56,18 +57,15 @@ public interface PipeRoot extends SecurableResource @NotNull FileLike getRootFileLike(); - @Deprecated // prefer getRootFileLike() - @NotNull - File getLogDirectory(); - @NotNull - FileLike getLogDirectoryFileLike(boolean forWrite); + FileLike getLogDirectory(boolean forWrite); @Nullable Path resolveToNioPath(String path); @Nullable Path resolveToNioPathFromUrl(String url); + FileLike resolveToFileLikeFromUrl(String url) throws IOException; /** * @return the file that's at the given relativePath from the pipeline root. Will be null if the relative path diff --git a/api/src/org/labkey/api/pipeline/PipelineJob.java b/api/src/org/labkey/api/pipeline/PipelineJob.java index f1c2a45aa9f..1d99d99d80b 100644 --- a/api/src/org/labkey/api/pipeline/PipelineJob.java +++ b/api/src/org/labkey/api/pipeline/PipelineJob.java @@ -72,7 +72,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -481,22 +480,22 @@ public Path getRemoteLogPath() if (dir == null) return getLogFilePath(); - return dir.getRemoteLogFilePath(); + return dir.getRemoteLogFile().toNioPathForRead(); } /** Finds a file name that hasn't been used yet, appending ".2", ".3", etc as needed */ - public static File findUniqueLogFile(File primaryFile, String baseName) + public static FileLike findUniqueLogFile(FileLike primaryFile, String baseName) { String validBaseName = FileUtil.makeLegalName(baseName); // need to look in current and archived dirs for any unused log file names (issue 20987) - File fileLog = FT_LOG.newFile(primaryFile.getParentFile(), validBaseName); - File archivedDir = FileUtil.appendName(primaryFile.getParentFile(), AssayFileWriter.ARCHIVED_DIR_NAME); - File fileLogArchived = FT_LOG.newFile(archivedDir, validBaseName); + FileLike fileLog = FT_LOG.newFile(primaryFile.getParent(), validBaseName); + FileLike archivedDir = primaryFile.getParent().resolveChild(AssayFileWriter.ARCHIVED_DIR_NAME); + FileLike fileLogArchived = FT_LOG.newFile(archivedDir, validBaseName); int index = 1; while (NetworkDrive.exists(fileLog) || NetworkDrive.exists(fileLogArchived)) { - fileLog = FT_LOG.newFile(primaryFile.getParentFile(), validBaseName + "." + (index)); + fileLog = FT_LOG.newFile(primaryFile.getParent(), validBaseName + "." + (index)); fileLogArchived = FT_LOG.newFile(archivedDir, validBaseName + "." + (index++)); } @@ -1131,7 +1130,7 @@ protected void finallyCleanUpLocalDirectory() { try { - Path remoteLogFilePath = _localDirectory.cleanUpLocalDirectory(); + FileLike remoteLogFilePath = _localDirectory.cleanUpLocalDirectory(); //Update job log entry's log location to remote path if (null != remoteLogFilePath) @@ -1246,14 +1245,14 @@ private void join() // Support for running processes @Nullable - private PrintWriter createPrintWriter(@Nullable File outputFile, boolean append) throws PipelineJobException + private PrintWriter createPrintWriter(@Nullable FileLike outputFile, boolean append) throws PipelineJobException { if (outputFile == null) return null; try { - return new PrintWriter(new BufferedWriter(new FileWriter(outputFile, append))); + return new PrintWriter(new BufferedWriter(PrintWriters.getPrintWriter(outputFile.openOutputStream()))); } catch (IOException e) { @@ -1261,7 +1260,7 @@ private PrintWriter createPrintWriter(@Nullable File outputFile, boolean append) } } - public void runSubProcess(ProcessBuilder pb, File dirWork) throws PipelineJobException + public void runSubProcess(ProcessBuilder pb, FileLike dirWork) throws PipelineJobException { runSubProcess(pb, dirWork, null, 0, false); } @@ -1270,13 +1269,13 @@ public void runSubProcess(ProcessBuilder pb, File dirWork) throws PipelineJobExc * If logLineInterval is greater than 1, the first logLineInterval lines of output will be written to the * job's main log file. */ - public void runSubProcess(ProcessBuilder pb, File dirWork, File outputFile, int logLineInterval, boolean append) + public void runSubProcess(ProcessBuilder pb, FileLike dirWork, FileLike outputFile, int logLineInterval, boolean append) throws PipelineJobException { runSubProcess(pb, dirWork, outputFile, logLineInterval, append, 0, null); } - public void runSubProcess(ProcessBuilder pb, File dirWork, File outputFile, int logLineInterval, boolean append, long timeout, TimeUnit timeoutUnit) + public void runSubProcess(ProcessBuilder pb, FileLike dirWork, FileLike outputFile, int logLineInterval, boolean append, long timeout, TimeUnit timeoutUnit) throws PipelineJobException { Process proc; @@ -1333,12 +1332,12 @@ public void runSubProcess(ProcessBuilder pb, File dirWork, File outputFile, int try { - pb.directory(dirWork); + pb.directory(dirWork.toNioPathForRead().toFile()); // TODO: Errors should go to log even when output is redirected to a file. pb.redirectErrorStream(true); - info("Working directory is " + dirWork.getAbsolutePath()); + info("Working directory is " + dirWork); info("running: " + StringUtils.join(pb.command().iterator(), " ")); proc = pb.start(); @@ -2010,9 +2009,9 @@ else if (!FileUtil.getAbsoluteCaseSensitiveFile(job1._logFile.toFile()).getAbsol /** * @return Path String for a local working directory, temporary if root is cloud based */ - protected Path getWorkingDirectoryString() + protected FileLike getWorkingDirectoryString() { - return !getPipeRoot().isCloudRoot() ? getPipeRoot().getRootNioPath() : FileUtil.getTempDirectory().toPath(); + return !getPipeRoot().isCloudRoot() ? getPipeRoot().getRootFileLike() : FileUtil.getTempDirectoryFileLike(); } /** diff --git a/api/src/org/labkey/api/pipeline/PipelineProtocolFactory.java b/api/src/org/labkey/api/pipeline/PipelineProtocolFactory.java index 94dced631af..683707d5219 100644 --- a/api/src/org/labkey/api/pipeline/PipelineProtocolFactory.java +++ b/api/src/org/labkey/api/pipeline/PipelineProtocolFactory.java @@ -20,10 +20,8 @@ import org.apache.xmlbeans.XmlOptions; import org.fhcrc.cpas.pipeline.protocol.xml.PipelineProtocolPropsDocument; import org.labkey.api.util.FileUtil; -import org.labkey.api.util.NetworkDrive; import org.labkey.vfs.FileLike; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Arrays; @@ -48,16 +46,12 @@ public abstract class PipelineProtocolFactory public static FileLike getProtocolRootDir(PipeRoot root) { FileLike systemDir = root.ensureSystemDirectory(); - return systemDir.resolveChild(_pipelineProtocolDir); + return getProtocolRootDir(systemDir); } - public static File locateProtocolRootDir(File rootDir, File systemDir) + public static FileLike getProtocolRootDir(FileLike systemDir) { - File protocolRootDir = FileUtil.appendName(systemDir, _pipelineProtocolDir); - File protocolRootDirLegacy = FileUtil.appendName(rootDir, _pipelineProtocolDir); - if (NetworkDrive.exists(protocolRootDirLegacy)) - protocolRootDirLegacy.renameTo(protocolRootDir); - return protocolRootDir; + return systemDir.resolveChild(_pipelineProtocolDir); } public abstract String getName(); diff --git a/api/src/org/labkey/api/pipeline/PipelineProvider.java b/api/src/org/labkey/api/pipeline/PipelineProvider.java index 618b46eed5a..72328948eea 100644 --- a/api/src/org/labkey/api/pipeline/PipelineProvider.java +++ b/api/src/org/labkey/api/pipeline/PipelineProvider.java @@ -29,6 +29,7 @@ import org.labkey.api.view.ActionURL; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; import org.springframework.web.servlet.mvc.Controller; import java.io.File; @@ -69,6 +70,13 @@ public boolean fileExists(File f) return _entry.fileExists(f); } + public boolean fileExists(FileLike f) + { + if (_entry == null) + return f.exists(); + return _entry.fileExists(f.toNioPathForRead()); + } + public boolean fileExists(Path f) { if (_entry == null) @@ -208,7 +216,7 @@ public String getName() * @param rootDir the pipeline root directory on disk * @param systemDir the system directory itself */ - public void initSystemDirectory(Path rootDir, Path systemDir) + public void initSystemDirectory(FileLike rootDir, FileLike systemDir) { } diff --git a/api/src/org/labkey/api/pipeline/RecordedAction.java b/api/src/org/labkey/api/pipeline/RecordedAction.java index 23abf52bcdb..fc403c038c9 100644 --- a/api/src/org/labkey/api/pipeline/RecordedAction.java +++ b/api/src/org/labkey/api/pipeline/RecordedAction.java @@ -122,6 +122,11 @@ public void addInputIfNotPresent(File input, String role) addInput(input.toURI(), role, false); } + public void addInputIfNotPresent(FileLike input, String role) + { + addInput(input.toURI(), role, false); + } + /** * Exp.data has a constraint that will only allow a given file * once per action, so by default this will throw an exception @@ -151,6 +156,11 @@ public void addOutput(File output, String role, boolean transientFile) addOutput(output.toURI(), role, transientFile, false); } + public void addOutput(FileLike output, String role, boolean transientFile) + { + addOutput(output.toURI(), role, transientFile, false); + } + public void addOutputIfNotPresent(File output, String role, boolean transientFile) { addOutput(output.toURI(), role, transientFile, false, false); @@ -161,6 +171,11 @@ public void addOutput(File output, String role, boolean transientFile, boolean g addOutput(output.toURI(), role, transientFile, generated); } + public void addOutput(FileLike output, String role, boolean transientFile, boolean generated) + { + addOutput(output.toURI(), role, transientFile, generated); + } + public void addOutput(URI output, String role, boolean transientFile) { addOutput(output, role, transientFile, false); diff --git a/api/src/org/labkey/api/pipeline/WorkDirectory.java b/api/src/org/labkey/api/pipeline/WorkDirectory.java index e60c24985db..8795a256dbb 100644 --- a/api/src/org/labkey/api/pipeline/WorkDirectory.java +++ b/api/src/org/labkey/api/pipeline/WorkDirectory.java @@ -17,7 +17,9 @@ import org.labkey.api.pipeline.cmd.TaskPath; import org.labkey.api.util.FileType; +import org.labkey.api.util.UnexpectedException; import org.labkey.vfs.FileLike; +import org.labkey.vfs.FileSystemLike; import java.io.File; import java.io.IOException; @@ -46,60 +48,76 @@ enum Function { /** * @return the directory where the input files live and where the output files will end up */ - File getDir(); + FileLike getDir(); - /** Informs the WorkDirectory that a new file is being created. It is treated as a Function.output */ - File newFile(String name); + /** + * Informs the WorkDirectory that a new file is being created. It is treated as a Function.output + */ + FileLike newFile(String name); - /** Informs the WorkDirectory that a new file is being created. */ - File newFile(Function f, String name); + /** + * Informs the WorkDirectory that a new file is being created. + */ + FileLike newFile(Function f, String name); - /** Informs the WorkDirectory that a new file is being created. It is treated as a Function.output */ - File newFile(FileType type); + /** + * Informs the WorkDirectory that a new file is being created. It is treated as a Function.output + */ + FileLike newFile(FileType type); /** * Indicates that a file is to be used as input. The implementation can choose whether it needs to be copied, unless * forceCopy is true (in which case it will always be copied to the work directory + * * @return the full path to the file where it is available for use */ - File inputFile(File fileInput, boolean forceCopy) throws IOException; - - default File inputFile(FileLike fileInput, boolean forceCopy) throws IOException - { - return inputFile(fileInput.toNioPathForRead().toFile(), forceCopy); - } - + FileLike inputFile(FileLike fileInput, boolean forceCopy) throws IOException; /** * Indicates that a file is to be used as input. The implementation can choose whether it needs to be copied, unless * forceCopy is true (in which case it will always be copied to the work directory. This version of the method allows the caller * to manually specify the destination file, which allows callers to place files into subdirectories of the work directory + * * @return the full path to the file where it is available for use */ - File inputFile(File fileInput, File fileWork, boolean forceCopy) throws IOException; - + FileLike inputFile(FileLike fileInput, FileLike fileWork, boolean forceCopy) throws IOException; + + /** Use the FileLike version instead */ + @Deprecated + default File inputFile(File fileInput, File fileWork, boolean forceCopy) + { + try + { + return inputFile(FileSystemLike.wrapFile(fileInput), FileSystemLike.wrapFile(fileWork), forceCopy).toNioPathForRead().toFile(); + } + catch (IOException e) + { + throw UnexpectedException.wrap(e); + } + } + /** @return the relative path of the file relative to the work directory itself. The file is presumed to be under the work directory. */ - String getRelativePath(File fileWork) throws IOException; + String getRelativePath(FileLike fileWork) throws IOException; /** * @return the final location for file after it's copied out of the work directory */ - File outputFile(File fileWork) throws IOException; + FileLike outputFile(FileLike fileWork) throws IOException; /** * @return the final location for file after it's copied out of the work directory */ - File outputFile(File fileWork, String nameDest) throws IOException; + FileLike outputFile(FileLike fileWork, String nameDest) throws IOException; /** * @return copies the file to the specified location */ - File outputFile(File fileWork, File dest) throws IOException; + FileLike outputFile(FileLike fileWork, FileLike dest) throws IOException; /** * Delete a file from the working directory */ - void discardFile(File fileWork) throws IOException; + void discardFile(FileLike fileWork) throws IOException; /** Deletes any inputs that were copied into this working directory */ void discardCopiedInputs() throws IOException; @@ -121,7 +139,7 @@ default File inputFile(FileLike fileInput, boolean forceCopy) throws IOException * Pipeline inputs are copied to the working directory. If the passed file was already copied to the work directory, this will * return the local copy. */ - File getWorkingCopyForInput(File f); + FileLike getWorkingCopyForInput(FileLike f); /** * Ensures that we have a lock, if needed. The lock must be released by the caller. Locks can be configured so that @@ -129,9 +147,9 @@ default File inputFile(FileLike fileInput, boolean forceCopy) throws IOException */ CopyingResource ensureCopyingLock() throws IOException; - List getWorkFiles(Function f, TaskPath tp); + List getWorkFiles(Function f, TaskPath tp); - File newWorkFile(Function output, TaskPath taskPath, String baseName); + FileLike newWorkFile(Function output, TaskPath taskPath, String baseName); /** A lock for copying files over a network share, for convenient use with try-with-resources */ interface CopyingResource extends AutoCloseable diff --git a/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisJob.java b/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisJob.java index 396a2b31348..dfc9b844271 100644 --- a/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisJob.java +++ b/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisJob.java @@ -38,9 +38,7 @@ import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.vfs.FileLike; -import java.io.File; import java.io.IOException; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -136,9 +134,9 @@ public AbstractFileAnalysisJob(@NotNull AbstractFileAnalysisProtocol protocol * @return Path String for a local working directory, temporary if root is cloud based */ @Override - protected Path getWorkingDirectoryString() + protected FileLike getWorkingDirectoryString() { - return _dirAnalysis.toNioPathForWrite().toAbsolutePath(); + return _dirAnalysis; } public AbstractFileAnalysisJob(AbstractFileAnalysisJob job, FileLike fileInput) @@ -186,7 +184,7 @@ public void setSplittable(boolean splittable) @Override public boolean isSplittable() { - return _splittable && getInputFilePaths().size() > 1; + return _splittable && getInputFiles().size() > 1; } @Override @@ -202,7 +200,7 @@ public List createSplitJobs() } @Override - public TaskPipeline getTaskPipeline() + public TaskPipeline getTaskPipeline() { return PipelineJobService.get().getTaskPipeline(getTaskPipelineId()); } @@ -263,69 +261,75 @@ public String getBaseNameForFileType(FileType fileType) } @Override - public FileLike getDataDirectoryFileLike() + public FileLike getDataDirectory() { return _dirData; } @Override - public File getAnalysisDirectory() + public FileLike getAnalysisDirectory() { - return _dirAnalysis.toNioPathForWrite().toFile(); + return _dirAnalysis; } @Override - public File findOutputFile(@NotNull String outputDir, @NotNull String fileName) + public FileLike findOutputFile(@NotNull String outputDir, @NotNull String fileName) { return getOutputFile(outputDir, fileName, getPipeRoot(), getLogger(), getAnalysisDirectory()); } - public static File getOutputFile(@NotNull String outputDir, @NotNull String fileName, PipeRoot root, Logger log, File analysisDirectory) + public static FileLike getOutputFile(@NotNull String outputDir, @NotNull String fileName, PipeRoot root, Logger log, FileLike analysisDirectory) { - File dir; + FileLike dir; if (outputDir.startsWith("/")) { - dir = root.resolvePath(outputDir); + dir = root.resolvePathToFileLike(outputDir); if (dir == null) throw new RuntimeException("Output directory not under pipeline root: " + outputDir); if (!NetworkDrive.exists(dir)) { log.info("Creating output directory under pipeline root: " + dir); - if (!dir.mkdirs()) - throw new RuntimeException("Failed to create output directory under pipeline root: " + outputDir); + try + { + dir.mkdirs(); + } + catch (IOException e) + { + throw new RuntimeException("Failed to create output directory under pipeline root: " + outputDir, e); + } } } else { - dir = new File(analysisDirectory, outputDir); + dir = analysisDirectory.resolveChild(outputDir); if (!NetworkDrive.exists(dir)) { log.info("Creating output directory under pipeline analysis dir: " + dir); - if (!dir.mkdirs()) - throw new RuntimeException("Failed to create output directory under analysis dir: " + outputDir); + try + { + dir.mkdirs(); + } + catch (IOException e) + { + throw new RuntimeException("Failed to create output directory under analysis dir: " + outputDir, e); + } } } - return new File(dir, fileName); - } - - @Override - public List getInputFiles() - { - return getInputFilePaths().stream().map(Path::toFile).collect(Collectors.toList()); + return dir.resolveChild(fileName); } @Override - public List getInputFilePaths() + public List getInputFiles() { - return _filesInput.stream().map(FileLike::toNioPathForRead).toList(); + return _filesInput; } @Override - public File getParametersFile() + public FileLike getParametersFile() { - return _fileParameters.toNioPathForRead().toFile(); + return _fileParameters; } @Override @@ -392,7 +396,7 @@ public ParamParser createParamParser() @Override public String getDescription() { - return getDataDescription(getDataDirectoryFileLike(), getBaseName(), getJoinedBaseName(), getProtocolName(), _filesInput); + return getDataDescription(getDataDirectory(), getBaseName(), getJoinedBaseName(), getProtocolName(), _filesInput); } @Override diff --git a/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisProtocolFactory.java b/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisProtocolFactory.java index b370206472c..147446bf3b7 100644 --- a/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisProtocolFactory.java +++ b/api/src/org/labkey/api/pipeline/file/AbstractFileAnalysisProtocolFactory.java @@ -161,20 +161,6 @@ public String[] getProtocolNames(PipeRoot root, FileLike dirData, boolean archiv return ArrayUtils.removeElement(protocolNames, DEFAULT_PARAMETERS_NAME); } - public void initSystemDirectory(File rootDir, File systemDir) - { - // Make sure the root protocol directory is in the right place. - File protocolRootDir = locateProtocolRootDir(rootDir, systemDir); - - // Make sure the defaults for this particular protocol are in the right place. - File fileLegacyDefaults = FileUtil.appendName(rootDir, getLegacyDefaultParametersFileName()); - if (NetworkDrive.exists(fileLegacyDefaults)) - { - File protocolDir = FileUtil.appendName(protocolRootDir, getName()); - fileLegacyDefaults.renameTo(FileUtil.appendName(protocolDir, getDefaultParametersFileName())); - } - } - /** * Override to set a custom validator. * diff --git a/api/src/org/labkey/api/pipeline/file/FileAnalysisJobSupport.java b/api/src/org/labkey/api/pipeline/file/FileAnalysisJobSupport.java index 0a23361774e..6175edc1778 100644 --- a/api/src/org/labkey/api/pipeline/file/FileAnalysisJobSupport.java +++ b/api/src/org/labkey/api/pipeline/file/FileAnalysisJobSupport.java @@ -19,16 +19,10 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.pipeline.ParamParser; import org.labkey.api.util.FileType; -import org.labkey.api.util.UnexpectedException; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * FileAnalysisJobSupport @@ -67,84 +61,34 @@ public interface FileAnalysisJobSupport /** * @return the directory in which the original input file resides. */ - @Deprecated //Prefer the getDataDirectoryFileLike version as File return type doesn't support full URIs very well - default File getDataDirectory() - { - return getDataDirectoryFileLike().toNioPathForWrite().toFile(); - } - - FileLike getDataDirectoryFileLike(); + FileLike getDataDirectory(); /** * @return the directory where the input files reside, and where the * final analysis should end up. */ - @Deprecated // Please use getAnalysisDirectoryFileLike - File getAnalysisDirectory(); - - default FileLike getAnalysisDirectoryFileLike() - { - // TODO This needs implementation in derived classes... - // This is typically safe but may cause an error if FileSystem provider isn't configured - return FileSystemLike.wrapFile(getAnalysisDirectory()); - } + FileLike getAnalysisDirectory(); /** * Returns a file for use as input in the pipeline, given its name. * This allows the task definitions to name files they require as input, * and the pipeline definition to specify where those files should come from. */ - @Deprecated // Please use findInputFileLike instead, as File objects may have issues with full URIs - File findInputFile(String name); - @Deprecated // Please use findInputFileLike instead, as File objects may have issues with full URIs - default Path findInputPath(String filepath) - { - // TODO This needs implementation in derived classes... - // This is typically safe but may cause an error if FileSystem provider isn't configured - return findInputFile(filepath).toPath(); - } - default FileLike findInputFileLike(String filepath) - { - File file = findInputFile(filepath); - if (file != null) - { - try - { - return FileSystemLike.wrapFile(getDataDirectory(), file); - } - catch (IOException e) - { - throw UnexpectedException.wrap(e); - } - } - return null; - } + FileLike findInputFile(String name); /** * Returns a file for use as output in the pipeline, given its name. * This allows the task definitions to name files they create as output, * and the pipeline definition to specify where those files should end up. */ - @Deprecated //Please switch to use findOutputPath - File findOutputFile(String name); //TODO update implementations to return nio.Path directly - default Path findOutputPath(String name) - { - //This is generally safe, but may fail if the appropriate filesystem providers are not registered. - return findOutputFile(name).toPath(); - } + FileLike findOutputFile(String name); /** * Returns a file for the output dir and file name. * The output dir is a directory path relative to the analysis directory, * or, if the path starts with "/", relative to the pipeline root. */ - @Deprecated //Please switch to use findOutputPath - File findOutputFile(@NotNull String outputDir, @NotNull String fileName); - default Path findOutputPath(@NotNull String outputDir, @NotNull String filename) - { - //This is generally safe, but may fail if the appropriate filesystem providers are not registered. - return findOutputFile(outputDir, filename).toPath(); - } + FileLike findOutputFile(@NotNull String outputDir, @NotNull String fileName); /** * @return a parameter parser object for writing parameters to a file. @@ -160,20 +104,12 @@ default Path findOutputPath(@NotNull String outputDir, @NotNull String filename) * @return the parameters input file used to drive the pipeline. */ @Nullable - @Deprecated //Use Path based versions - File getParametersFile(); + FileLike getParametersFile(); /** * @return a list of all input files analyzed. */ - @Deprecated - List getInputFiles(); - - default List getInputFilePaths() - { - //Implemented as such for backwards compatibility - return getInputFiles().stream().map(File::toPath).collect(Collectors.toList()); - } + List getInputFiles(); /** * returns support level for .xml.gz handling: diff --git a/api/src/org/labkey/api/pipeline/file/PathMapper.java b/api/src/org/labkey/api/pipeline/file/PathMapper.java index 17660a36e6d..1fd2a5431aa 100644 --- a/api/src/org/labkey/api/pipeline/file/PathMapper.java +++ b/api/src/org/labkey/api/pipeline/file/PathMapper.java @@ -31,9 +31,6 @@ */ public interface PathMapper { - @Deprecated //Please use getURIPathMap - Map getPathMap(); - Map getURIPathMap(); @Deprecated //Please use the URI version diff --git a/api/src/org/labkey/api/pipeline/file/PathMapperImpl.java b/api/src/org/labkey/api/pipeline/file/PathMapperImpl.java index edbea7a9423..649d25590b8 100644 --- a/api/src/org/labkey/api/pipeline/file/PathMapperImpl.java +++ b/api/src/org/labkey/api/pipeline/file/PathMapperImpl.java @@ -74,18 +74,6 @@ public PathMapperImpl(Map pathMap, boolean remoteIgnoreCase, boo _localIgnoreCase = localIgnoreCase; } - /** - * Returns a copy of the path map as strings - */ - @Override - @Deprecated //Use getURIPathMap instead - public Map getPathMap() - { - Map stringMap = new LinkedHashMap<>(); - _uriMap.forEach((k, v) -> stringMap.put(k.toString(), v.toString())); - return stringMap; - } - /** * * @return unmodifiable copy of underlying uri map diff --git a/api/src/org/labkey/api/query/AbstractQueryImportAction.java b/api/src/org/labkey/api/query/AbstractQueryImportAction.java index 624fd4b03a5..71641555b46 100644 --- a/api/src/org/labkey/api/query/AbstractQueryImportAction.java +++ b/api/src/org/labkey/api/query/AbstractQueryImportAction.java @@ -591,7 +591,7 @@ else if (!dataFileDir.exists()) QueryImportPipelineJob.QueryImportAsyncContextBuilder importContextBuilder = new QueryImportPipelineJob.QueryImportAsyncContextBuilder(); importContextBuilder - .setPrimaryFile(dataFile.toNioPathForRead().toFile()) + .setPrimaryFile(dataFile) .setHasColumnHeaders(_hasColumnHeaders) .setFileContentType(multipartfile.getContentType()) .setSchemaName(schemaName) diff --git a/api/src/org/labkey/api/query/QueryImportPipelineJob.java b/api/src/org/labkey/api/query/QueryImportPipelineJob.java index a4820826fab..28295ba5041 100644 --- a/api/src/org/labkey/api/query/QueryImportPipelineJob.java +++ b/api/src/org/labkey/api/query/QueryImportPipelineJob.java @@ -16,8 +16,8 @@ import org.labkey.api.reader.DataLoader; import org.labkey.api.util.URLHelper; import org.labkey.api.view.ViewBackgroundInfo; +import org.labkey.vfs.FileLike; -import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -47,7 +47,7 @@ public QueryImportPipelineJob(@Nullable String provider, ViewBackgroundInfo info public static class QueryImportAsyncContextBuilder { - File _primaryFile; + FileLike _primaryFile; boolean _hasColumnHeaders; String _fileContentType; @@ -74,7 +74,7 @@ public QueryImportAsyncContextBuilder() } - public File getPrimaryFile() + public FileLike getPrimaryFile() { return _primaryFile; } @@ -146,7 +146,7 @@ public void setJobNotificationProvider(String jobNotificationProvider) _jobNotificationProvider = jobNotificationProvider; } - public QueryImportAsyncContextBuilder setPrimaryFile(File primaryFile) + public QueryImportAsyncContextBuilder setPrimaryFile(FileLike primaryFile) { _primaryFile = primaryFile; return this; diff --git a/api/src/org/labkey/api/reader/ColumnDescriptor.java b/api/src/org/labkey/api/reader/ColumnDescriptor.java index 59ca2faf923..f26db4d9584 100644 --- a/api/src/org/labkey/api/reader/ColumnDescriptor.java +++ b/api/src/org/labkey/api/reader/ColumnDescriptor.java @@ -37,13 +37,13 @@ public ColumnDescriptor(String name) this.clazz = String.class; } - public ColumnDescriptor(String name, Class type) + public ColumnDescriptor(String name, Class type) { this.name = name; this.clazz = type; } - public ColumnDescriptor(String name, Class type, Object defaultValue) + public ColumnDescriptor(String name, Class type, Object defaultValue) { this.name = name; this.clazz = type; @@ -84,6 +84,11 @@ public String getRangeURI() return type.getXsdType(); } + public Class getDataClass() + { + return clazz; + } + public boolean isMvEnabled() { return mvEnabled; diff --git a/api/src/org/labkey/api/reader/ExcelLoader.java b/api/src/org/labkey/api/reader/ExcelLoader.java index de2535a195d..16c4bd89324 100644 --- a/api/src/org/labkey/api/reader/ExcelLoader.java +++ b/api/src/org/labkey/api/reader/ExcelLoader.java @@ -46,6 +46,7 @@ import org.labkey.api.util.FileUtil; import org.labkey.api.util.JunitUtil; import org.labkey.api.util.StringUtilsLabKey; +import org.labkey.vfs.FileLike; import org.labkey.vfs.FileSystemLike; import org.xml.sax.Attributes; import org.xml.sax.InputSource; @@ -109,6 +110,21 @@ public DataLoader createLoader(InputStream is, boolean hasColumnHeaders, Contain public FileType getFileType() { return FILE_TYPE; } } + public static boolean isExcel(final FileLike dataFile) + { + try + { + try (InputStream inputStream = dataFile.openInputStream()) + { + return ExcelLoader.FILE_TYPE.isType(dataFile.toNioPathForRead(), null, FileUtil.readHeader(inputStream, 8 * 1024)); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + public static boolean isExcel(final File dataFile) { try @@ -136,8 +152,7 @@ public static boolean isExcel(final File dataFile) private boolean shouldDeleteFile = false; - private ExcelLoader() - {} + private ExcelLoader() {} public static ExcelLoader create(Path path, boolean hasColumnHeaders, Container mvIndicatorContainer) throws IOException { @@ -878,6 +893,7 @@ public void testExtraHasNext() throws Exception int rowCount = 0; while (iter.hasNext()) { + //noinspection ConstantValue assertTrue(iter.hasNext()); iter.next(); rowCount++; diff --git a/api/src/org/labkey/api/reader/FastaDataLoader.java b/api/src/org/labkey/api/reader/FastaDataLoader.java index e425383d551..09335f9392a 100644 --- a/api/src/org/labkey/api/reader/FastaDataLoader.java +++ b/api/src/org/labkey/api/reader/FastaDataLoader.java @@ -21,6 +21,8 @@ import org.labkey.api.iterator.CloseableIterator; import org.labkey.api.util.FileType; import org.labkey.api.util.FileUtil; +import org.labkey.vfs.FileLike; +import org.labkey.vfs.FileSystemLike; import java.io.File; import java.io.FileOutputStream; @@ -85,7 +87,7 @@ public FastaDataLoader(File inputFile, boolean hasColumnHeaders, Container mvInd setScrollable(true); setHasColumnHeaders(hasColumnHeaders); - _loader = new GenericFastaLoader(inputFile); + _loader = new GenericFastaLoader(FileSystemLike.wrapFile(inputFile)); } public void setCharacterFilter(FastaLoader.CharacterFilter characterFilter) @@ -142,7 +144,7 @@ public void close() private static class GenericFastaLoader extends FastaLoader> { - public GenericFastaLoader(File fastaFile) + public GenericFastaLoader(FileLike fastaFile) { super(fastaFile, new FastaIteratorElementFactory<>() { diff --git a/api/src/org/labkey/api/reader/FastaLoader.java b/api/src/org/labkey/api/reader/FastaLoader.java index 15fbd8447c8..eeb38831846 100644 --- a/api/src/org/labkey/api/reader/FastaLoader.java +++ b/api/src/org/labkey/api/reader/FastaLoader.java @@ -15,10 +15,10 @@ */ package org.labkey.api.reader; +import org.labkey.vfs.FileLike; + import java.io.BufferedReader; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.util.Iterator; import java.util.zip.GZIPInputStream; @@ -29,11 +29,11 @@ */ public abstract class FastaLoader implements Iterable { - private final File _fastaFile; + private final FileLike _fastaFile; private final FastaIteratorElementFactory _factory; private CharacterFilter _characterFilter = new UppercaseCharacterFilter(); - protected FastaLoader(File fastaFile, FastaIteratorElementFactory factory) + protected FastaLoader(FileLike fastaFile, FastaIteratorElementFactory factory) { _fastaFile = fastaFile; _factory = factory; @@ -72,7 +72,7 @@ private void init() try { // Detect Charset encoding based on BOM - _reader = Readers.getBOMDetectingReader(_fastaFile.getName().toLowerCase().endsWith(".gz") ? new GZIPInputStream(new FileInputStream(_fastaFile)): new FileInputStream(_fastaFile)); + _reader = Readers.getBOMDetectingReader(_fastaFile.getName().toLowerCase().endsWith(".gz") ? new GZIPInputStream(_fastaFile.openInputStream()): _fastaFile.openInputStream()); String line = getLine(); @@ -103,7 +103,7 @@ private void init() } _beforeFirst = false; - _fileLength = _fastaFile.length(); + _fileLength = _fastaFile.getSize(); } private String getLine() throws IOException @@ -224,7 +224,7 @@ public void close() { _reader.close(); } - catch (IOException x) {} + catch (IOException ignored) {} _reader = null; _header = null; diff --git a/api/src/org/labkey/api/reports/ExternalScriptEngine.java b/api/src/org/labkey/api/reports/ExternalScriptEngine.java index 0f1fec57695..158611084b0 100644 --- a/api/src/org/labkey/api/reports/ExternalScriptEngine.java +++ b/api/src/org/labkey/api/reports/ExternalScriptEngine.java @@ -28,6 +28,8 @@ import org.labkey.api.util.FileUtil; import org.labkey.api.util.QuietCloser; import org.labkey.api.util.URIUtil; +import org.labkey.vfs.FileLike; +import org.labkey.vfs.FileSystemLike; import javax.script.AbstractScriptEngine; import javax.script.Bindings; @@ -40,9 +42,11 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -79,7 +83,7 @@ public class ExternalScriptEngine extends AbstractScriptEngine implements LabKey public static final String DEFAULT_WORKING_DIRECTORY = "ExternalScript"; private static final Pattern scriptCmdPattern = Pattern.compile("'([^']+)'|\\\"([^\\\"]+)\\\"|(^[^\\s]+)|(\\s[^\\s^'^\\\"]+)"); - private File _workingDirectory; + private FileLike _workingDirectory; protected ExternalScriptEngineDefinition _def; protected Writer _originalWriter; @@ -98,7 +102,7 @@ public ScriptEngineFactory getFactory() return new ExternalScriptEngineFactory(_def); } - public boolean isBinary(File file) + public boolean isBinary(FileLike file) { String ext = FileUtil.getExtension(file); @@ -117,18 +121,18 @@ public Object eval(String script, ScriptContext context) throws ScriptException if (!extensions.isEmpty()) { // write out the script file to disk using the first extension as the default - File scriptFile = writeScriptFile(script, context, extensions); + FileLike scriptFile = writeScriptFile(script, context, extensions); return eval(scriptFile, context); } else throw new ScriptException("There are no file name extensions registered for this ScriptEngine : " + getFactory().getLanguageName()); } - protected Object eval(File scriptFile, ScriptContext context) throws ScriptException + protected Object eval(FileLike scriptFile, ScriptContext context) throws ScriptException { String[] params = formatCommand(scriptFile, context); ProcessBuilder pb = new ProcessBuilder(params); - pb = pb.directory(getWorkingDir(context)); + pb = pb.directory(getWorkingDir(context).toNioPathForRead().toFile()); final long timeout = getTimeout(context); @@ -178,7 +182,7 @@ public Bindings createBindings() return new SimpleBindings(); } - protected File getWorkingDir(ScriptContext context) + protected FileLike getWorkingDir(ScriptContext context) { if (_workingDirectory == null) { @@ -186,12 +190,12 @@ protected File getWorkingDir(ScriptContext context) if (bindings.containsKey(WORKING_DIRECTORY)) { String dir = (String)bindings.get(WORKING_DIRECTORY); - _workingDirectory = new File(dir); + _workingDirectory = FileSystemLike.wrapFile(new File(dir)); } else { - File tempDir = new File(System.getProperty("java.io.tmpdir")); - _workingDirectory = FileUtil.appendName(tempDir, DEFAULT_WORKING_DIRECTORY); + FileLike tempDir = FileUtil.getTempDirectoryFileLike(); + _workingDirectory = tempDir.resolveChild(DEFAULT_WORKING_DIRECTORY); } if (!_workingDirectory.exists()) @@ -224,7 +228,7 @@ protected long getTimeout(ScriptContext context) * * @return an array of command parameters */ - protected String[] formatCommand(File scriptFile, ScriptContext context) throws ScriptException + protected String[] formatCommand(FileLike scriptFile, ScriptContext context) throws ScriptException { List params = new ArrayList<>(); String exe = _def.getExePath(); @@ -232,7 +236,7 @@ protected String[] formatCommand(File scriptFile, ScriptContext context) throws params.add(exe); - String scriptFilePath = scriptFile.getAbsolutePath(); + String scriptFilePath = scriptFile.toNioPathForRead().toFile().getAbsolutePath(); // Issue 19545: R pipeline scripts don't support spaces in directory names // The bash shell script wrappers around the R executable don't correctly handle spaces @@ -240,7 +244,7 @@ protected String[] formatCommand(File scriptFile, ScriptContext context) throws // To avoid issues with executing scripts from within directories that contain spaces, // try to get the file name relative to the working directory if possible. // This doesn't fix executing scripts that contains a space in the file name, but is better than failing completely. - File workingDir = getWorkingDir(context); + FileLike workingDir = getWorkingDir(context); if (workingDir != null && URIUtil.isDescendant(workingDir.toURI(), scriptFile.toURI())) { try @@ -282,7 +286,7 @@ protected String[] formatCommand(File scriptFile, ScriptContext context) throws if (cmd.contains("workingDir")) { - cmd = ParamReplacementSvc.get().processInputReplacement(cmd, "workingDir", workingDir.getAbsolutePath().replaceAll("\\\\", "/")); + cmd = ParamReplacementSvc.get().processInputReplacement(cmd, "workingDir", workingDir.toNioPathForRead().toFile().getAbsolutePath().replaceAll("\\\\", "/")); } // finally clean up the script @@ -422,17 +426,17 @@ protected int runProcess(ScriptContext context, ProcessBuilder pb, StringBuffer } } - protected File writeScriptFile(String script, ScriptContext context, List extensions) + protected FileLike writeScriptFile(String script, ScriptContext context, List extensions) { // write out the script file to disk using the first extension as the default - File scriptFile; + FileLike scriptFile; boolean isBinaryScript = false; Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); if (bindings.containsKey(ExternalScriptEngine.SCRIPT_PATH)) { - File path = new File((String)bindings.get(ExternalScriptEngine.SCRIPT_PATH)); + FileLike path = FileSystemLike.wrapFile(new File((String)bindings.get(ExternalScriptEngine.SCRIPT_PATH))); isBinaryScript = isBinary(path); // if the script is a binary file, no parameter replacement can be performed on the script, so we @@ -440,10 +444,10 @@ protected File writeScriptFile(String script, ScriptContext context, List outputSubst, return "I'm abstract"; } - protected JSONObject createReportConfig(ViewContext context, File ipynb) + protected JSONObject createReportConfig(ViewContext context, FileLike ipynb) { ReportDescriptor descriptor = getDescriptor(); JSONObject sourceQuery = null; diff --git a/api/src/org/labkey/api/reports/report/ExternalScriptEngineReport.java b/api/src/org/labkey/api/reports/report/ExternalScriptEngineReport.java index ae77e2e53b8..119e8c0ebc7 100644 --- a/api/src/org/labkey/api/reports/report/ExternalScriptEngineReport.java +++ b/api/src/org/labkey/api/reports/report/ExternalScriptEngineReport.java @@ -54,7 +54,6 @@ import org.labkey.api.writer.ContainerUser; import org.labkey.api.writer.PrintWriters; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; import javax.script.Bindings; import javax.script.ScriptContext; @@ -62,7 +61,6 @@ import javax.script.ScriptException; import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.PrintWriter; @@ -121,7 +119,7 @@ public boolean handleRuntimeException(Exception e) @Override public HttpView render(List parameters) throws IOException { - return renderViews(ExternalScriptEngineReport.this, view, parameters, false); + return renderViews(ExternalScriptEngineReport.this, view, parameters); } }); @@ -153,7 +151,7 @@ public boolean handleRuntimeException(Exception e) public List render(List parameters) throws IOException { - return renderParameters(ExternalScriptEngineReport.this, scriptOutputs, parameters, false ); + return renderParameters(ExternalScriptEngineReport.this, scriptOutputs, parameters); } }); @@ -251,13 +249,13 @@ protected K renderReport(ViewContext context, Map inputParam return renderer.render(outputSubst); } - private HttpView handleException(Exception e) + private HttpView handleException(Exception e) { return HtmlView.unsafe(ReportUtil.makeExceptionString(e, "%s
%s
")); } @Override - public String runScript(ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws ScriptException + public String runScript(ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws ScriptException { ScriptEngine engine = getScriptEngine(context.getContainer()); @@ -284,16 +282,15 @@ protected void saveConsoleOutput(Object output, List outputSub if (null != output) { FileLike console = getReportDirFileLike(context.getContainer().getId()).resolveChild(CONSOLE_OUTPUT); - File consoleFile = FileSystemLike.toFile(console); - try (PrintWriter pw = PrintWriters.getPrintWriter(consoleFile)) + try (PrintWriter pw = PrintWriters.getPrintWriter(console.openOutputStream())) { pw.write(output.toString()); } ParamReplacement param = ParamReplacementSvc.get().getHandlerInstance(ConsoleOutput.ID); param.setName("console"); - param.addFile(consoleFile); + param.addFile(console); outputSubst.add(param); } } @@ -303,18 +300,18 @@ protected void saveConsoleOutput(Object output, List outputSub */ protected void saveAdditionalFileOutput(List outputSubst, @NotNull ContainerUser context) { - FileLike reportDirFileLike = getReportDirFileLike(context.getContainer().getId()); - File reportDir = FileSystemLike.toFile(reportDirFileLike); + FileLike reportDir = getReportDirFileLike(context.getContainer().getId()); if (reportDir != null && reportDir.exists()) { Set boundFiles = new CaseInsensitiveHashSet(); for (ParamReplacement param : outputSubst) { - for (File file : param.getFiles()) + for (FileLike file : param.getFiles()) boundFiles.add(file.getName()); } - File[] additionalFiles = reportDir.listFiles((dir, name) -> { + List additionalFiles = reportDir.getChildren(x -> { + String name = x.getName(); if (boundFiles.contains(name)) return false; else if ("input_data.tsv".equalsIgnoreCase(name)) @@ -326,7 +323,7 @@ else if ("script.rout".equalsIgnoreCase(name)) return true; }); - for (File file : additionalFiles) + for (FileLike file : additionalFiles) { for (ParamReplacement param : outputSubst) { @@ -340,7 +337,7 @@ else if ("script.rout".equalsIgnoreCase(name)) } } - protected Object runScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws ScriptException + protected Object runScript(ScriptEngine engine, ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws ScriptException { RConnectionHolder rh = null; try (TransformSession session = SecurityManager.createTransformSession(context)) @@ -413,27 +410,27 @@ protected void cacheResults(ViewContext context, List replacem if (getDescriptor().getReportId() != null && BooleanUtils.toBoolean(getDescriptor().getProperty(ReportDescriptor.Prop.cached))) { synchronized (_cachedReportURLMap) { - File cacheDir = getCacheDir(context.getContainer().getId()); + FileLike cacheDir = getCacheDir(context.getContainer().getId()); if (null == cacheDir) return; try { - File mapFile = FileUtil.appendName(cacheDir, SUBSTITUTION_MAP); + FileLike mapFile = cacheDir.resolveChild(SUBSTITUTION_MAP); for (ParamReplacement param : replacements) { - List> changes = new ArrayList<>(); - for (File src : param.getFiles()) + List> changes = new ArrayList<>(); + for (FileLike src : param.getFiles()) { - File dst = FileUtil.appendName(cacheDir, src.getName()); + FileLike dst = cacheDir.resolveChild(src.getName()); if (src.exists() && FileUtil.createTempFile(dst)) { FileUtil.copyFile(src, dst); if (param.getId().equals(ConsoleOutput.ID)) { - try (BufferedWriter bw = new BufferedWriter(new FileWriter(dst, true))) + try (BufferedWriter bw = new BufferedWriter(PrintWriters.getPrintWriter(dst.openOutputStream()))) { bw.write("\nLast cached update : " + DateUtil.formatDateTime(context.getContainer()) + "\n"); } @@ -442,7 +439,7 @@ protected void cacheResults(ViewContext context, List replacem } } - for (Pair change : changes) + for (Pair change : changes) { param.getFiles().remove(change.getKey()); param.addFile(change.getValue()); @@ -459,7 +456,7 @@ protected void cacheResults(ViewContext context, List replacem public void clearCache(@NotNull String executingContainerId) { - File cacheDir = getCacheDir(executingContainerId); + FileLike cacheDir = getCacheDir(executingContainerId); if (null != cacheDir && cacheDir.exists()) FileUtil.deleteDir(cacheDir); } @@ -476,13 +473,13 @@ protected boolean getCachedReport(ViewContext context, List re clearCache(context.getContainer().getId()); return false; } - File cacheDir = getCacheDir(context.getContainer().getId()); + FileLike cacheDir = getCacheDir(context.getContainer().getId()); if (null == cacheDir) return false; try { - replacements.addAll(ParamReplacementSvc.get().fromFile(FileUtil.appendName(cacheDir, SUBSTITUTION_MAP))); + replacements.addAll(ParamReplacementSvc.get().fromFile(cacheDir.resolveChild(SUBSTITUTION_MAP))); return !replacements.isEmpty(); } catch (Exception e) @@ -538,12 +535,15 @@ public void beforeDelete(ContainerUser context) super.beforeDelete(context); } - protected File getCacheDir(@NotNull String executingContainerId) + protected FileLike getCacheDir(@NotNull String executingContainerId) { if (getDescriptor().getReportId() == null) return null; - File cacheDir = new File(getTempRoot(getDescriptor()), executingContainerId + File.separator + "Report_" + FileUtil.makeLegalName(getDescriptor().getReportId().toString()) + File.separator + CACHE_DIR); + FileLike cacheDir = getTempRootFileLike(getDescriptor()). + resolveChild(executingContainerId). + resolveChild("Report_" + FileUtil.makeLegalName(getDescriptor().getReportId().toString())). + resolveChild(CACHE_DIR); if (!cacheDir.exists()) { @@ -561,7 +561,7 @@ protected File getCacheDir(@NotNull String executingContainerId) } @Override - public HttpView renderDataView(ViewContext context) throws Exception + public HttpView renderDataView(ViewContext context) throws Exception { QueryView view = createQueryView(context, getDescriptor()); @@ -606,10 +606,4 @@ else if (filter.accept(part.getParentFile(), part.getName())) } return appPath; } - - @Override - public boolean isSandboxed() - { - return false; - } } diff --git a/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java b/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java index 268ecde4005..fce5d6f5fc3 100644 --- a/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java +++ b/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java @@ -30,13 +30,11 @@ import org.labkey.api.writer.ContainerUser; import org.labkey.api.writer.PrintWriters; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptException; -import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -59,7 +57,7 @@ public String getType() } @Override - public HttpView renderReport(ViewContext context) throws Exception + public HttpView renderReport(ViewContext context) throws Exception { VBox view = new VBox(); String script = getDescriptor().getProperty(ScriptReportDescriptor.Prop.script); @@ -89,17 +87,17 @@ public HttpView renderReport(ViewContext context) throws Exception errors.add(error2); String err = "" + error1 + "
" + error2 + "
"; - HttpView errView = HtmlView.unsafe(err); + HttpView errView = HtmlView.unsafe(err); view.addView(errView); } - renderViews(this, view, outputSubst, false); + renderViews(this, view, outputSubst); return view; } @Override - public String runScript(ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws ScriptException + public String runScript(ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws ScriptException { ScriptEngine engine = getScriptEngine(context.getContainer()); if (engine != null) @@ -140,7 +138,7 @@ public String runScript(ViewContext context, List outputSubst, ParamReplacement param = ParamReplacementSvc.get().getHandlerInstance(ConsoleOutput.ID); param.setName("console"); - param.addFile(FileSystemLike.toFile(console)); + param.addFile(console); outputSubst.add(param); } @@ -149,7 +147,7 @@ public String runScript(ViewContext context, List outputSubst, catch(Exception e) { if (!errors.getBuffer().isEmpty()) - throw new ScriptException(e.getMessage() + errors.getBuffer().toString()); + throw new ScriptException(e.getMessage() + errors.getBuffer()); else throw new ScriptException(e); } @@ -166,10 +164,4 @@ public void beforeDelete(ContainerUser context) deleteReportDir(context); super.beforeDelete(context); } - - @Override - public boolean isSandboxed() - { - return false; - } } diff --git a/api/src/org/labkey/api/reports/report/ScriptEngineReport.java b/api/src/org/labkey/api/reports/report/ScriptEngineReport.java index dbf0cdf6269..edb0cdc1e2d 100644 --- a/api/src/org/labkey/api/reports/report/ScriptEngineReport.java +++ b/api/src/org/labkey/api/reports/report/ScriptEngineReport.java @@ -68,7 +68,6 @@ import org.labkey.api.view.template.PageConfig; import org.labkey.api.writer.ContainerUser; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; import javax.script.ScriptEngine; import javax.script.ScriptException; @@ -189,7 +188,7 @@ protected boolean validateScript(String text, List errors) /* * Create the .tsv associated with the data grid for this report. */ - public File createInputDataFile(@NotNull ViewContext context) throws SQLException, IOException, ValidationException + public FileLike createInputDataFile(@NotNull ViewContext context) throws SQLException, IOException, ValidationException { FileLike resultFile = getReportDirFileLike(context.getContainer().getId()).resolveChild(DATA_INPUT); ResultsFactory factory = () -> { @@ -202,52 +201,10 @@ public File createInputDataFile(@NotNull ViewContext context) throws SQLExceptio throw new RuntimeException(e); } }; - return _createInputDataFile(context, factory, FileSystemLike.toFile(resultFile)); + return _createInputDataFile(context, factory, resultFile); } - /** - * @param executingContainerId id of the container in which the report is running - * @return directory, which has been created, to contain the generated report - *

- * Note: This method used to stash results in members (_tempFolder and _tempFolderPipeline), but that no longer works - * now that we cache reports between threads (e.g., Thread.currentThread().getId() is part of the path). - * - * Consider using the variant which returns a FileLike object - */ - @Deprecated - public File getReportDir(@NotNull String executingContainerId) - { - boolean isPipeline = BooleanUtils.toBoolean(getDescriptor().getProperty(ScriptReportDescriptor.Prop.runInBackground)); - return getReportDir(executingContainerId, isPipeline); - } - - @Deprecated - protected File getReportDir(@NotNull String executingContainerId, boolean isPipeline) - { - - File tempRoot = getTempRoot(getDescriptor()); - String reportId = FileUtil.makeLegalName(String.valueOf(getDescriptor().getReportId())).replaceAll(" ", "_"); - - File tempFolder; - - if (isPipeline) - { - String identifier = RReportJob.getJobIdentifier(); - if (identifier != null) - tempFolder = new File(tempRoot.getAbsolutePath() + File.separator + executingContainerId + File.separator + "Report_" + reportId, identifier); - else - tempFolder = new File(tempRoot, executingContainerId + File.separator + "Report_" + reportId); - } - else - tempFolder = new File(tempRoot.getAbsolutePath() + File.separator + executingContainerId + File.separator + "Report_" + reportId, String.valueOf(Thread.currentThread().getId())); - - if (!tempFolder.exists()) - tempFolder.mkdirs(); - - return tempFolder; - } - /** * @param executingContainerId id of the container in which the report is running * @return directory, which has been created, to contain the generated report @@ -312,9 +269,9 @@ public void deleteReportDir(@NotNull ContainerUser context) public static void scheduledFileCleanup(Logger log) { final long cutoff = System.currentTimeMillis() - (1000 * 3600 * 24); - File root = getTempRoot(ReportService.get().createDescriptorInstance(RReportDescriptor.TYPE)); + FileLike root = getTempRootFileLike(ReportService.get().createDescriptorInstance(RReportDescriptor.TYPE)); - for (File file : root.listFiles()) + for (FileLike file : root.getChildren()) { if (file.isDirectory()) { @@ -323,8 +280,15 @@ public static void scheduledFileCleanup(Logger log) } else { - // shouldn't be loose files here, so delete anyway - file.delete(); + try + { + // shouldn't be loose files here, so delete anyway + file.delete(); + } + catch (IOException e) + { + log.info("Unable to delete temporary report file: " + file.getPath(), e); + } } } @@ -337,14 +301,14 @@ public static void scheduledFileCleanup(Logger log) * specified cutoff, and if there are no thread subfolders, delete the parent. * */ - protected static void deleteReportDir(File dir, long cutoff) + protected static void deleteReportDir(FileLike dir, long cutoff) { if (dir.isDirectory()) { boolean empty = true; - for (File child : dir.listFiles()) + for (FileLike child : dir.getChildren()) { - if (child.lastModified() < cutoff) + if (child.getLastModified() < cutoff) { FileUtil.deleteDir(child); } @@ -442,7 +406,7 @@ private String oldLegalName(FieldKey fkey, @NotNull SqlDialect dialect) /** * used to render output parameters for script execution without rendering the output into HTML */ - public static List renderParameters(ScriptEngineReport report, final List scriptOutputs, List parameters, boolean deleteTempFiles) throws IOException + public static List renderParameters(ScriptEngineReport report, final List scriptOutputs, List parameters) throws IOException { return handleParameters(report, parameters, new ParameterHandler<>() { @@ -452,7 +416,7 @@ public boolean handleParameter(ViewContext context, Report report, ParamReplacem param.setReport(report); try { - for (File file : param.getFiles()) + for (FileLike file : param.getFiles()) { // Even if there is a script error we keep processing the output parameters. If the parameter // files don't exist then don't create a parameter object. @@ -487,7 +451,7 @@ public List cleanup(ScriptEngineReport report, ContainerUser conte }); } - public static HttpView renderViews(ScriptEngineReport report, final VBox view, Collection parameters, boolean deleteTempFiles) throws IOException + public static HttpView renderViews(ScriptEngineReport report, final VBox view, Collection parameters) throws IOException { return handleParameters(report, parameters, new ParameterHandler<>() { @@ -581,7 +545,7 @@ private interface ParameterHandler protected static boolean isViewable(ParamReplacement param, List sectionNames) { - for (File data : param.getFiles()) + for (FileLike data : param.getFiles()) { if (data.exists()) { @@ -593,7 +557,7 @@ protected static boolean isViewable(ParamReplacement param, List section return false; } - protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws Exception + protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws Exception { return createScript(engine, context, outputSubst, inputDataTsv, inputParameters, false); } @@ -602,24 +566,18 @@ protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters, boolean isRStudio) throws Exception + protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters, boolean isRStudio) throws Exception { return processScript(engine, context, getDescriptor().getProperty(ScriptReportDescriptor.Prop.script), inputDataTsv, outputSubst, inputParameters, true, isRStudio); } - public abstract String runScript(ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws ScriptException; - + public abstract String runScript(ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws ScriptException; - protected String processScript(ScriptEngine engine, ViewContext context, String script, File inputFile, List outputSubst, Map inputParameters, boolean includeProlog) throws Exception - { - return processScript(engine, context, script, inputFile, outputSubst, inputParameters, includeProlog, false); - } /** * Takes a script source, adds a prolog, processes any input and output replacement parameters - * */ - protected String processScript(ScriptEngine engine, ViewContext context, String script, File inputFile, List outputSubst, Map inputParameters, boolean includeProlog, boolean isRStudio) throws Exception + protected String processScript(ScriptEngine engine, ViewContext context, String script, FileLike inputFile, List outputSubst, Map inputParameters, boolean includeProlog, boolean isRStudio) throws Exception { if (!StringUtils.isEmpty(script) && isRStudio) script = ParamReplacementSvc.get().transformInlineReplacements(script); // transform old inline syntax to comment syntax @@ -634,30 +592,25 @@ protected String processScript(ScriptEngine engine, ViewContext context, String return script; } - protected String getScriptProlog(ScriptEngine engine, ViewContext context, File inputFile, Map inputParameters) + protected String getScriptProlog(ScriptEngine engine, ViewContext context, FileLike inputFile, Map inputParameters) { return null; } - protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, File inputFile, Map inputParameters) - { - return concatScriptProlog(engine, context, script, inputFile, inputParameters, false); - } - - protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, File inputFile, Map inputParameters, boolean isRStudio) + protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, FileLike inputFile, Map inputParameters, boolean isRStudio) { return StringUtils.defaultString(getScriptProlog(engine, context, inputFile, inputParameters)) + script; } - protected String processInputReplacement(ScriptEngine engine, String script, @Nullable File inputFile, boolean isRStudio) + protected String processInputReplacement(ScriptEngine engine, String script, @Nullable FileLike inputFile, boolean isRStudio) { - return ParamReplacementSvc.get().processInputReplacement(script, INPUT_FILE_TSV, inputFile == null ? null : inputFile.getAbsolutePath().replaceAll("\\\\", "/"), isRStudio, null); + return ParamReplacementSvc.get().processInputReplacement(script, INPUT_FILE_TSV, inputFile == null ? null : inputFile.toNioPathForRead().toFile().getAbsolutePath().replaceAll("\\\\", "/"), isRStudio, null); } protected String processOutputReplacements(ScriptEngine engine, String script, List replacements, @NotNull ContainerUser context, boolean isRStudio) throws Exception { FileLike reportDir = getReportDirFileLike(context.getContainer().getId()); - return ParamReplacementSvc.get().processParamReplacement(script, FileSystemLike.toFile(reportDir), null, replacements, isRStudio); + return ParamReplacementSvc.get().processParamReplacement(script, reportDir, null, replacements, isRStudio); } @@ -704,7 +657,7 @@ public String getTsvFormattedValue(RenderContext ctx) } } - protected static class TempFileCleanup extends HttpView + protected static class TempFileCleanup extends HttpView { private final FileLike _dir; diff --git a/api/src/org/labkey/api/reports/report/ScriptProcessReport.java b/api/src/org/labkey/api/reports/report/ScriptProcessReport.java index 188c52dc30e..a25ff262569 100644 --- a/api/src/org/labkey/api/reports/report/ScriptProcessReport.java +++ b/api/src/org/labkey/api/reports/report/ScriptProcessReport.java @@ -17,7 +17,6 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.admin.FolderExportContext; @@ -32,10 +31,10 @@ import org.labkey.api.security.User; import org.labkey.api.thumbnail.Thumbnail; import org.labkey.api.util.FileUtil; -import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; import org.labkey.api.writer.ContainerUser; +import org.labkey.vfs.FileLike; import javax.script.ScriptEngine; import javax.script.ScriptException; @@ -64,12 +63,9 @@ */ public abstract class ScriptProcessReport extends ScriptReport implements Report.ResultSetGenerator { - private static final Logger LOG = LogHelper.getLogger(ScriptProcessReport.class, "External script interpreter report"); - final String reportType; final String defaultDescriptorType; - final ReportContext reportContext = new ReportContext(); - private File workingDirectory; + private FileLike workingDirectory; /* this is where we gather the context that will be passed to the script runner as a json file */ @@ -125,32 +121,32 @@ public Results generateResults(ViewContext context, boolean allowAsyncQuery) thr * Note: This method used to stash results in members (_tempFolder and _tempFolderPipeline), but that no longer works * now that we cache reports between threads (e.g., Thread.currentThread().getId() is part of the path). */ - public File getReportDir(@NotNull String executingContainerId) + public FileLike getReportDir(@NotNull String executingContainerId) throws IOException { boolean isPipeline = BooleanUtils.toBoolean(getDescriptor().getProperty(ScriptReportDescriptor.Prop.runInBackground)); return getReportDir(executingContainerId, isPipeline); } - protected File getReportDir(@NotNull String executingContainerId, boolean isPipeline) + protected FileLike getReportDir(@NotNull String executingContainerId, boolean isPipeline) throws IOException { if (null == workingDirectory) { - File tempRoot = getTempRoot(getDescriptor()); + FileLike tempRoot = getTempRootFileLike(getDescriptor()); String reportId = FileUtil.makeLegalName(String.valueOf(getDescriptor().getReportId())).replaceAll(" ", "_"); - File tempFolder; + FileLike tempFolder; if (isPipeline) { String identifier = RReportJob.getJobIdentifier(); if (identifier != null) - tempFolder = new File(tempRoot.getAbsolutePath() + File.separator + executingContainerId + File.separator + "Report_" + reportId, identifier); + tempFolder = tempRoot.resolveChild(executingContainerId).resolveChild("Report_" + reportId).resolveChild(identifier); else - tempFolder = new File(tempRoot, executingContainerId + File.separator + "Report_" + reportId); + tempFolder = tempRoot.resolveChild(executingContainerId).resolveChild("Report_" + reportId); } else - tempFolder = new File(tempRoot.getAbsolutePath() + File.separator + executingContainerId + File.separator + "Report_" + reportId, String.valueOf(Thread.currentThread().getId())); + tempFolder = tempRoot.resolveChild(executingContainerId).resolveChild("Report_" + reportId).resolveChild(String.valueOf(Thread.currentThread().getId())); if (!tempFolder.exists()) tempFolder.mkdirs(); @@ -161,18 +157,6 @@ protected File getReportDir(@NotNull String executingContainerId, boolean isPipe } - public void deleteReportDir(@NotNull ContainerUser context) - { - boolean isPipeline = BooleanUtils.toBoolean(getDescriptor().getProperty(ScriptReportDescriptor.Prop.runInBackground)); - - File dir = getReportDir(context.getContainer().getId()); - - if (!isPipeline) - dir = dir.getParentFile(); - - FileUtil.deleteDir(dir); - } - public Thumbnail getThumbnail(List parameters) throws IOException { return handleParameters(this, parameters, new ParameterHandler<>() @@ -232,7 +216,7 @@ private interface ParameterHandler protected static boolean isViewable(ParamReplacement param, List sectionNames) { - for (File data : param.getFiles()) + for (FileLike data : param.getFiles()) { if (data.exists()) { @@ -244,26 +228,8 @@ protected static boolean isViewable(ParamReplacement param, List section return false; } - protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws Exception - { - return createScript(engine, context, outputSubst, inputDataTsv, inputParameters, false); - } - /** - * Create the script to be executed by the scripting engine - */ - protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters, boolean isRStudio) throws Exception - { - return processScript(engine, context, getDescriptor().getProperty(ScriptReportDescriptor.Prop.script), inputDataTsv, outputSubst, inputParameters, true, isRStudio); - } - public abstract String runScript(ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws ScriptException; - - protected String processScript(ScriptEngine engine, ViewContext context, String script, File inputFile, List outputSubst, Map inputParameters, boolean includeProlog) throws Exception - { - return processScript(engine, context, script, inputFile, outputSubst, inputParameters, includeProlog, false); - } - /** * Takes a script source, adds a prolog, processes any input and output replacement parameters */ @@ -272,12 +238,12 @@ protected String processScript(ScriptEngine engine, ViewContext context, String if (!StringUtils.isEmpty(script) && isRStudio) script = ParamReplacementSvc.get().transformInlineReplacements(script); // transform old inline syntax to comment syntax if (includeProlog && (!StringUtils.isEmpty(script) || isRStudio)) - script = concatScriptProlog(engine, context, script == null ? "" : script, inputFile, inputParameters, isRStudio); + script = concatScriptProlog(engine, context, script == null ? "" : script, inputFile, inputParameters); if (!StringUtils.isEmpty(script)) { if (inputFile != null || isRStudio) - script = processInputReplacement(engine, script, inputFile, isRStudio); - script = processOutputReplacements(engine, script, outputSubst, context, isRStudio); + script = processInputReplacement(script, inputFile, isRStudio); + script = processOutputReplacements(script, outputSubst, context, isRStudio); } return script; } @@ -288,21 +254,16 @@ protected String getScriptProlog(ScriptEngine engine, ViewContext context, File } protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, File inputFile, Map inputParameters) - { - return concatScriptProlog(engine, context, script, inputFile, inputParameters, false); - } - - protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, File inputFile, Map inputParameters, boolean isRStudio) { return StringUtils.defaultString(getScriptProlog(engine, context, inputFile, inputParameters)) + script; } - protected String processInputReplacement(ScriptEngine engine, String script, @Nullable File inputFile, boolean isRStudio) + protected String processInputReplacement(String script, @Nullable File inputFile, boolean isRStudio) { return ParamReplacementSvc.get().processInputReplacement(script, INPUT_FILE_TSV, inputFile == null ? null : inputFile.getAbsolutePath().replaceAll("\\\\", "/"), isRStudio, null); } - protected String processOutputReplacements(ScriptEngine engine, String script, List replacements, @NotNull ContainerUser context, boolean isRStudio) throws Exception + protected String processOutputReplacements(String script, List replacements, @NotNull ContainerUser context, boolean isRStudio) throws Exception { return ParamReplacementSvc.get().processParamReplacement(script, getReportDir(context.getContainer().getId()), null, replacements, isRStudio); } diff --git a/api/src/org/labkey/api/reports/report/ScriptReport.java b/api/src/org/labkey/api/reports/report/ScriptReport.java index 42278885423..863180ecda3 100644 --- a/api/src/org/labkey/api/reports/report/ScriptReport.java +++ b/api/src/org/labkey/api/reports/report/ScriptReport.java @@ -171,7 +171,7 @@ public Results _generateResults(ViewContext context, boolean allowAsyncQuery) th /* * Create the .tsv associated with the data grid for this report. */ - public File _createInputDataFile(@NotNull ViewContext context, ResultsFactory factory, File resultFile) throws SQLException, IOException, ValidationException + public FileLike _createInputDataFile(@NotNull ViewContext context, ResultsFactory factory, FileLike resultFile) throws SQLException, IOException, ValidationException { try (StashingResultsFactory srf = new StashingResultsFactory(factory)) { @@ -192,8 +192,7 @@ public File _createInputDataFile(@NotNull ViewContext context, ResultsFactory fa try (TSVGridWriter tsv = new TSVGridWriter(srf, dataColumns)) { tsv.setColumnHeaderType(ColumnHeaderType.Name); // CONSIDER: Use FieldKey instead - FileUtil.createTempFile(resultFile); - tsv.write(resultFile); + tsv.write(resultFile.openOutputStream()); } } } diff --git a/api/src/org/labkey/api/reports/report/python/IpynbReport.java b/api/src/org/labkey/api/reports/report/python/IpynbReport.java index 0ca3d44437b..5dd1b9fe894 100644 --- a/api/src/org/labkey/api/reports/report/python/IpynbReport.java +++ b/api/src/org/labkey/api/reports/report/python/IpynbReport.java @@ -37,8 +37,6 @@ import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.FileUtil; import org.labkey.api.util.Pair; -import org.labkey.api.util.Path; -import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.util.URLHelper; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.HtmlView; @@ -46,18 +44,19 @@ import org.labkey.api.view.JspTemplate; import org.labkey.api.view.VBox; import org.labkey.api.view.ViewContext; +import org.labkey.api.writer.PrintWriters; +import org.labkey.vfs.FileLike; import org.springframework.validation.BindException; import javax.script.ScriptEngine; import jakarta.servlet.http.HttpServletResponse; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.io.PrintWriter; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -65,9 +64,8 @@ import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; -import java.util.HashSet; +import java.util.Collection; import java.util.List; -import java.util.Set; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.labkey.api.util.DOM.DIV; @@ -183,36 +181,29 @@ public HttpView renderReport(ViewContext context) throws Exception if (context.getRequest() == null) throw new IllegalStateException("Invalid report context"); String apikey = SessionApiKeyManager.get().getApiKey(context.getRequest(), "ipynb report"); - File workingDirectory = getReportDir(context.getContainer().getId()); + FileLike workingDirectory = getReportDir(context.getContainer().getId()); - assert workingDirectory.isAbsolute(); + assert workingDirectory.toNioPathForRead().isAbsolute(); if (!workingDirectory.isDirectory()) throw new IOException("Could not create working directory"); - FileUtils.cleanDirectory(workingDirectory); + FileUtil.deleteDirectoryContents(workingDirectory); // write the script out to the working directory var descriptor = getDescriptor(); String script = descriptor.getProperty(ScriptReportDescriptor.Prop.script); - File scriptFile = FileUtil.appendName(workingDirectory, FileUtil.makeLegalName(descriptor.getReportName()) + ".ipynb"); + FileLike scriptFile = workingDirectory.resolveChild(FileUtil.makeLegalName(descriptor.getReportName()) + ".ipynb"); FileUtil.createTempFile(scriptFile); - IOUtil.copyCompletely(new StringReader(script), new FileWriter(scriptFile, StringUtilsLabKey.DEFAULT_CHARSET)); + IOUtil.copyCompletely(new StringReader(script), PrintWriters.getPrintWriter(scriptFile.openOutputStream())); - Set beforeExecute = new HashSet<>(FileUtils.listFiles(workingDirectory, null, true)); - LOG.trace("BEFORE: " + workingDirectory.getPath() + "\n\t" + - StringUtils.join(beforeExecute.stream().map(f -> - f.getPath().replace(workingDirectory.toString(), "") + " : " + f.length()).toArray(), "\n\t")); + logFiles(workingDirectory, "BEFORE"); ExecuteStrategy ex = new WebServiceExecuteStrategy(); int exitCode = ex.execute(context, apikey, workingDirectory, scriptFile); LOG.trace("EXIT: " + exitCode); - File outputFile = ex.getOutputDocument(); + FileLike outputFile = ex.getOutputDocument(); LOG.trace("OUTPUT: " + outputFile); - - Set afterExecute = new HashSet<>(FileUtils.listFiles(workingDirectory, null, true)); - LOG.trace("AFTER: " + workingDirectory.getPath() + "\n\t" + - StringUtils.join(afterExecute.stream().map(f -> - f.getPath().replace(workingDirectory.toString(), "") + " : " + f.length()).toArray(), "\n\t")); + logFiles(workingDirectory, "AFTER"); try { @@ -228,7 +219,7 @@ public HttpView renderReport(ViewContext context) throws Exception } else { - BasicFileAttributes outputFileAttributes = Files.readAttributes(outputFile.toPath(), BasicFileAttributes.class); + BasicFileAttributes outputFileAttributes = Files.readAttributes(outputFile.toNioPathForRead(), BasicFileAttributes.class); if (outputFileAttributes.isRegularFile() && 0 < outputFileAttributes.size()) { vbox.addView(new IpynbOutput(outputFile).getView(context)); @@ -240,12 +231,12 @@ public HttpView renderReport(ViewContext context) throws Exception } // if there is console.txt or errors.txt file render them - File console = FileUtil.appendName(workingDirectory, ScriptEngineReport.CONSOLE_OUTPUT); - if (console.isFile() && console.length() > 0) + FileLike console = workingDirectory.resolveChild(ScriptEngineReport.CONSOLE_OUTPUT); + if (console.isFile() && console.getSize() > 0) vbox.addView(new ConsoleOutput(console).getView(context)); - File error = FileUtil.appendName(workingDirectory, ERROR_OUTPUT); - if (error.isFile() && error.length() > 0) + FileLike error = workingDirectory.resolveChild(ERROR_OUTPUT); + if (error.isFile() && error.getSize() > 0) vbox.addView(new ConsoleOutput(error).getView(context)); LOG.trace("VIEWS: " + vbox.getViews().size()); @@ -258,9 +249,18 @@ public HttpView renderReport(ViewContext context) throws Exception } } + private static void logFiles(FileLike parentDir, String label) + { + File dir = parentDir.toNioPathForRead().toFile(); + Collection files = FileUtils.listFiles(dir, null, true); + LOG.trace(label + ": " + dir.getPath() + "\n\t" + + StringUtils.join(files.stream().map(f -> + f.getPath().replace(dir.getPath(), "") + " : " + f.length()).toArray(), "\n\t")); + } + @Override - protected JSONObject createReportConfig(ViewContext context, File scriptFile) + protected JSONObject createReportConfig(ViewContext context, FileLike scriptFile) { return super.createReportConfig(context, scriptFile); } @@ -292,22 +292,22 @@ URL getServiceAddress(Container c) throws ConfigurationException } - private static void extractTar(InputStream in, File targetDirectory) throws IOException + private static void extractTar(InputStream in, FileLike targetDirectory) throws IOException { try (TarArchiveInputStream tar = new TarArchiveInputStream(in)) { TarArchiveEntry entry; while ((entry = tar.getNextEntry()) != null) { - File path = FileUtil.appendPath(targetDirectory, new Path(entry.getName())); + FileLike path = targetDirectory.resolveFile(org.labkey.api.util.Path.parse(entry.getName())); if (entry.isDirectory()) { - FileUtils.forceMkdir(path); + FileUtil.mkdir(path); } else { FileUtil.createTempFile(path); - try (FileOutputStream os = new FileOutputStream(path)) + try (OutputStream os = path.openOutputStream()) { IOUtils.copy(tar, os); } @@ -325,10 +325,10 @@ private static void extractTar(InputStream in, File targetDirectory) throws IOEx private interface ExecuteStrategy { IpynbReport getReport(); - int execute(ViewContext context, String apiKey, File working, File ipynb) throws IOException; + int execute(ViewContext context, String apiKey, FileLike working, FileLike ipynb) throws IOException; // document could be .html .ipynb or .md - @Nullable File getOutputDocument(); + @Nullable FileLike getOutputDocument(); } @@ -337,8 +337,8 @@ private interface ExecuteStrategy class WebServiceExecuteStrategy implements ExecuteStrategy { - File inputScript; - File outputDocument; + FileLike inputScript; + FileLike outputDocument; @Override public IpynbReport getReport() @@ -359,14 +359,18 @@ private void tryPing(URL service) @Override - public int execute(ViewContext context, String apiKey, File working, File ipynb) throws IOException + public int execute(ViewContext context, String apiKey, FileLike working, FileLike ipynb) throws IOException { inputScript = ipynb; JSONObject reportConfig = createReportConfig(context, ipynb); // I tried "putting" a fake tar entry, but TarArchiveOutputStream seems to actually want the file to exist - FileUtils.write(FileUtil.appendName(working, CONFIG_FILE), reportConfig.toString(), StringUtilsLabKey.DEFAULT_CHARSET); + try (OutputStream configOut = working.resolveChild(CONFIG_FILE).openOutputStream(); + PrintWriter writer = PrintWriters.getPrintWriter(configOut)) + { + writer.print(reportConfig.toString()); + } URL service = getServiceAddress(context.getContainer()); // For testing, just return if the remoteURL host is "noop.test" @@ -396,19 +400,16 @@ public int execute(ViewContext context, String apiKey, File working, File ipynb) final DbScope.RetryPassthroughException[] bgException = new DbScope.RetryPassthroughException[1]; final Thread t = new Thread(() -> { - try ( - TarArchiveOutputStream tar = new TarArchiveOutputStream(pipeOutput) - ) + try (TarArchiveOutputStream tar = new TarArchiveOutputStream(pipeOutput)) { - File[] listFiles = working.listFiles(); - List files = null == listFiles ? List.of() : Arrays.asList(listFiles); + List files = working.getChildren(); for (var file : files) { - TarArchiveEntry entry = tar.createArchiveEntry(file, file.getName()); + TarArchiveEntry entry = tar.createArchiveEntry(file.toNioPathForRead(), file.getName()); tar.putArchiveEntry(entry); - try(FileInputStream fis = new FileInputStream(file)) + try(InputStream is = file.openInputStream()) { - IOUtils.copy(fis, tar); + IOUtils.copy(is, tar); } tar.closeArchiveEntry(); } @@ -435,7 +436,7 @@ public int execute(ViewContext context, String apiKey, File working, File ipynb) bgException[0].throwRuntimeException(); } // delete script to avoid returning unprocessed ipynb in case of error - FileUtils.delete(ipynb); + ipynb.delete(); if (200 != response.getCode()) return response.getCode(); @@ -450,7 +451,7 @@ public int execute(ViewContext context, String apiKey, File working, File ipynb) } @Override - public @Nullable File getOutputDocument() + public @Nullable FileLike getOutputDocument() { if (null != outputDocument && outputDocument.isFile()) return outputDocument; diff --git a/api/src/org/labkey/api/reports/report/r/AbstractParamReplacement.java b/api/src/org/labkey/api/reports/report/r/AbstractParamReplacement.java index b62c8d4e9e7..9eef67e887f 100644 --- a/api/src/org/labkey/api/reports/report/r/AbstractParamReplacement.java +++ b/api/src/org/labkey/api/reports/report/r/AbstractParamReplacement.java @@ -20,8 +20,8 @@ import org.labkey.api.reports.Report; import org.labkey.api.thumbnail.Thumbnail; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -41,7 +41,7 @@ public abstract class AbstractParamReplacement implements ParamReplacement protected Map _properties = Collections.emptyMap(); protected boolean _isRemote = false; protected String _regex; - protected List _files = new ArrayList<>(); + protected List _files = new ArrayList<>(); public AbstractParamReplacement(String id) { @@ -126,13 +126,13 @@ public void setRemote(boolean isRemote) } @Override - public List getFiles() + public List getFiles() { return _files; } @Override - public void addFile(File file) + public void addFile(FileLike file) { _files.add(file); } @@ -156,9 +156,8 @@ public void setRegex(String regex) _regex = regex; } - @Nullable @Override - public final File convertSubstitution(File directory) throws Exception + public final @Nullable FileLike convertSubstitution(FileLike directory) throws IOException { // if there isn't a token specified, the substitution may be using a regex substitution, // regardless we can't explicitly map a file without a valid token identifier @@ -166,6 +165,5 @@ public final File convertSubstitution(File directory) throws Exception return (token != null) ? getSubstitution(directory) : null; } - @Nullable - protected abstract File getSubstitution(File directory) throws Exception; + protected abstract @Nullable FileLike getSubstitution(FileLike directory) throws IOException; } diff --git a/api/src/org/labkey/api/reports/report/r/ParamReplacement.java b/api/src/org/labkey/api/reports/report/r/ParamReplacement.java index 817387a2ab5..1728e486b0e 100644 --- a/api/src/org/labkey/api/reports/report/r/ParamReplacement.java +++ b/api/src/org/labkey/api/reports/report/r/ParamReplacement.java @@ -22,8 +22,8 @@ import org.labkey.api.thumbnail.Thumbnail; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; @@ -60,16 +60,16 @@ public interface ParamReplacement /** * Convert the substitution to its eventual generated file. + * * @param directory - the parent directory to create the generated file (if any, can be null) */ - @Nullable - File convertSubstitution(File directory) throws Exception; + @Nullable FileLike convertSubstitution(FileLike directory) throws Exception; /** * Get and set files associated with the replacement */ - void addFile(File file); - List getFiles(); + void addFile(FileLike file); + List getFiles(); void clearFiles(); String toString(); @@ -80,8 +80,8 @@ public interface ParamReplacement void setHeaderVisible(boolean visible); boolean getHeaderVisible(); - HttpView getView(ViewContext context); + HttpView getView(ViewContext context); @Nullable Thumbnail renderThumbnail(ViewContext context) throws IOException; - ScriptOutput renderAsScriptOutput(File file) throws Exception; + ScriptOutput renderAsScriptOutput(FileLike file) throws Exception; } diff --git a/api/src/org/labkey/api/reports/report/r/ParamReplacementSvc.java b/api/src/org/labkey/api/reports/report/r/ParamReplacementSvc.java index 27c62357e45..aaec41af51d 100644 --- a/api/src/org/labkey/api/reports/report/r/ParamReplacementSvc.java +++ b/api/src/org/labkey/api/reports/report/r/ParamReplacementSvc.java @@ -17,6 +17,7 @@ package org.labkey.api.reports.report.r; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; @@ -31,10 +32,14 @@ import org.labkey.api.util.HelpTopic; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.Pair; +import org.labkey.api.util.Path; import org.labkey.api.writer.PrintWriters; +import org.labkey.vfs.FileLike; +import org.labkey.vfs.FileSystemLike; import java.io.BufferedReader; import java.io.File; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; @@ -42,11 +47,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.apache.commons.lang3.StringUtils.startsWith; import static org.labkey.api.reports.report.ScriptEngineReport.DATA_INPUT; import static org.labkey.api.reports.report.ScriptEngineReport.INPUT_FILE_TSV; @@ -65,11 +70,11 @@ public class ParamReplacementSvc /* Inline substitution parameters "${}" are deprecated, use comment substitution "#${}" instead. */ // the default inline param replacement pattern : ${} - private static final String REPLACEMENT_PARAM = "\\$\\{(.*?)\\}"; /* Deprecated */ + private static final String REPLACEMENT_PARAM = "\\$\\{(.*?)}"; /* Deprecated */ private static final Pattern DEFAULT_INLINE_SCRIPT_PATTERN = Pattern.compile(REPLACEMENT_PARAM); /* Deprecated */ // Enable ${} or equivalent escape sequences for {}. Note that this // makes the match group 2 for the token name instead of 1 - private static final String REPLACEMENT_PARAM_ESC = "\\$(\\{|%7[bB])(.*?)(\\}|%7[dD])"; /* Deprecated */ + private static final String REPLACEMENT_PARAM_ESC = "\\$(\\{|%7[bB])(.*?)(}|%7[dD])"; /* Deprecated */ private static final Pattern ESC_INLINE_SCRIPT_PATTERN = Pattern.compile(REPLACEMENT_PARAM_ESC); /* Deprecated */ private static final String REGEX_PARAM_REGEX = "(regex\\()(.*)(\\))"; /* Deprecated */ @@ -78,7 +83,7 @@ public class ParamReplacementSvc // the default comment param replacement pattern : #${:} \n public static final String COMMENT_LINE_REGEX = "#\\p{Space}*" + REPLACEMENT_PARAM + "\\p{Space}+"; public static final Pattern COMMENT_LINE_PATTERN = Pattern.compile(COMMENT_LINE_REGEX); - public static final String COMMENT_REGEX_REGEX = "#\\p{Space}*\\$\\{(\\p{Print}*" + REGEX_PARAM_REGEX + ")\\p{Print}*\\}"; + public static final String COMMENT_REGEX_REGEX = "#\\p{Space}*\\$\\{(\\p{Print}*" + REGEX_PARAM_REGEX + ")\\p{Print}*}"; public static final String COMMENT_BLOCK_REGEX = COMMENT_LINE_REGEX + "(\\p{Print}+\"\\p{Print}+\")"; public static final String COMMENT_PARAM_REGEX = "(" + COMMENT_BLOCK_REGEX + ")|(" + COMMENT_REGEX_REGEX + ")"; public static final Pattern DEFAULT_COMMENT_PATTERN = Pattern.compile(COMMENT_PARAM_REGEX); @@ -251,8 +256,8 @@ private String formatDeprecatedMessage(String paramName, String message, @Nullab { StringBuilder sb = new StringBuilder("Deprecated replacement '" + paramName + "'"); if (sourceName != null) - sb.append(" in " + sourceName); - sb.append(": " + message); + sb.append(" in ").append(sourceName); + sb.append(": ").append(message); return sb.toString(); } @@ -273,7 +278,7 @@ public ParamReplacement getHandlerInstance(String id) { try { String className = _outputSubstitutions.get(id); - return (ParamReplacement)Class.forName(className).newInstance(); + return (ParamReplacement)Class.forName(className).getDeclaredConstructor().newInstance(); } catch (Exception e) { @@ -481,7 +486,7 @@ public String clearUnusedReplacements(String script) * @param parentDirectory - the parent directory to create the output files for each param replacement. * @param outputReplacements - the list of processed replacements found in the source script. */ - public String processParamReplacement(String script, File parentDirectory, String remoteParentDirectoryPath, List outputReplacements, boolean isRStudio) throws Exception + public String processParamReplacement(String script, FileLike parentDirectory, String remoteParentDirectoryPath, List outputReplacements, boolean isRStudio) throws Exception { if (isRStudio) { @@ -499,7 +504,7 @@ public String processParamReplacement(String script, File parentDirectory, Strin * @param remoteParentDirectoryPath - the remote reference to this path if specified; may be null * @param outputReplacements - the list of processed replacements found in the source script. */ - public String processParamReplacement(String script, File parentDirectory, String remoteParentDirectoryPath, List outputReplacements, SubstitutionSyntax pattern) throws Exception + public String processParamReplacement(String script, FileLike parentDirectory, String remoteParentDirectoryPath, List outputReplacements, SubstitutionSyntax pattern) throws Exception { Matcher m = pattern.getMatchPattern().matcher(script); StringBuilder sb = new StringBuilder(); @@ -510,7 +515,7 @@ public String processParamReplacement(String script, File parentDirectory, Strin ParamReplacement param = fromToken(tokenString); if (param != null) { - File resultFile = param.convertSubstitution(parentDirectory); + FileLike resultFile = param.convertSubstitution(parentDirectory); String resultFileName = ""; if (resultFile != null) @@ -527,13 +532,13 @@ public String processParamReplacement(String script, File parentDirectory, Strin } else { - resultFileName = resultFile.getAbsolutePath().replaceAll("\\\\", "/"); + resultFileName = resultFile.toNioPathForRead().toFile().getAbsolutePath().replaceAll("\\\\", "/"); } // NOTE: dollar signs in the path will cause "java.lang.IllegalArgumentException: Illegal group reference" from Matcher.appendReplacement resultFileName = resultFileName.replaceAll("\\$", "\\\\\\$"); - _log.debug("Found output parameter '" + param.getName() + "'. Mapping local file '" + resultFile.getAbsolutePath() + "' to '" + resultFileName + "'"); + _log.debug("Found output parameter '" + param.getName() + "'. Mapping local file '" + resultFile + "' to '" + resultFileName + "'"); } String replacementStr = pattern.getReplacementStr(resultFileName, m.group(0), param.getName()); outputReplacements.add(param); @@ -545,7 +550,7 @@ public String processParamReplacement(String script, File parentDirectory, Strin return sb.toString(); } - public String processHrefParamReplacement(Report report, String script, File parentDirectory) + public String processHrefParamReplacement(Report report, String script, FileLike parentDirectory) throws IOException { String commentProcessedScript = processHrefParamReplacement(report, script, parentDirectory, SubstitutionSyntax.COMMENT); return processHrefParamReplacement(report, commentProcessedScript, parentDirectory, SubstitutionSyntax.INLINE); @@ -561,7 +566,7 @@ public String processHrefParamReplacement(Report report, String script, File par * @param parentDirectory - the parent directory to create the output files for each param replacement. * @param pattern - the remote reference to this path if specified; may be null */ - public String processHrefParamReplacement(Report report, String script, File parentDirectory, SubstitutionSyntax pattern) + public String processHrefParamReplacement(Report report, String script, FileLike parentDirectory, SubstitutionSyntax pattern) throws IOException { Matcher m = pattern.getEscapedMatchPattern().matcher(script); StringBuilder sb = new StringBuilder(); @@ -570,11 +575,10 @@ public String processHrefParamReplacement(Report report, String script, File par { String tokenString = m.group(pattern.getEscapedTokenGroupIndex()); ParamReplacement param = fromToken(tokenString); - if (param != null && HrefOutput.class.isInstance(param)) + if (param instanceof HrefOutput href) { - HrefOutput href = (HrefOutput) param; href.setReport(report); - File file = new File(parentDirectory, href.getName()); + FileLike file = parentDirectory.resolveFile(Path.parse(href.getName())); if (file.exists()) { href.addFile(file); @@ -596,7 +600,7 @@ public String processHrefParamReplacement(Report report, String script, File par private boolean isRelativeHref(String href) { - return !(startsWith(href,"$") || startsWith(href,"http:") || startsWith(href,"https:") || startsWith(href,"/")); + return !(Strings.CS.startsWith(href,"$") || Strings.CS.startsWith(href,"http:") || Strings.CS.startsWith(href,"https:") || Strings.CS.startsWith(href,"/")); } /** @@ -607,7 +611,7 @@ private boolean isRelativeHref(String href) * @param script - the script upon which to replace the Href parameters * @param parentDirectory - the parent directory to create the output files for each param replacement. */ - public String processRelativeHrefReplacement(Report report, String script, File parentDirectory) + public String processRelativeHrefReplacement(Report report, String script, FileLike parentDirectory) throws IOException { Matcher m = Pattern.compile("(href|src)=\"([^\"]*)\"").matcher(script); StringBuilder sb = new StringBuilder(); @@ -626,7 +630,7 @@ public String processRelativeHrefReplacement(Report report, String script, File HrefOutput href = new HrefOutput(); href.setName(path); href.setReport(report); - File file = new File(parentDirectory, href.getName()); + FileLike file = parentDirectory.resolveFile(Path.parse(href.getName())); ScriptOutput so = null; if (file.exists()) { @@ -644,23 +648,23 @@ public String processRelativeHrefReplacement(Report report, String script, File return sb.toString(); } - public void toFile(List outputSubst, File file) throws Exception + public void toFile(List outputSubst, FileLike file) throws Exception { - try (PrintWriter bw = PrintWriters.getPrintWriter(file)) + try (PrintWriter bw = PrintWriters.getPrintWriter(file.openOutputStream())) { outputSubst.stream(). filter(output -> output.getName() != null). - forEach(output -> output.getFiles().stream().filter(outputFile -> outputFile != null). - forEach(outputFile -> bw.write(output.getId() + '\t' + output.getName() + '\t' + outputFile.getAbsolutePath() + '\t' + PageFlowUtil.toQueryString(output.getProperties().entrySet()) + '\n'))); + forEach(output -> output.getFiles().stream().filter(Objects::nonNull). + forEach(outputFile -> bw.write(output.getId() + '\t' + output.getName() + '\t' + outputFile.toNioPathForRead().toFile().getAbsolutePath() + '\t' + PageFlowUtil.toQueryString(output.getProperties().entrySet()) + '\n'))); } } - public Collection fromFile(File file) throws Exception + public Collection fromFile(FileLike file) throws Exception { Map outputSubstMap = new HashMap<>(); if (file.exists()) { - try (BufferedReader br = Readers.getReader(file)) + try (BufferedReader br = Readers.getReader(file.openInputStream())) { String l; while ((l = br.readLine()) != null) @@ -676,7 +680,7 @@ public Collection fromFile(File file) throws Exception if (handler != null) { handler.setName(parts[1]); - handler.addFile(new File(parts[2])); + handler.addFile(FileSystemLike.wrapFile(new File(parts[2]))); handler.setProperties(PageFlowUtil.mapFromQueryString(parts[3])); } } @@ -720,8 +724,8 @@ public String transformInlineReplacements(@NotNull String script, InlineTransfor } private static final String TRANSFORMED_INPUT_FILE_NAME_PREFIX = "rStudioInputFileName"; - private static final String INLINE_INPUT_DATA_REGEX = "(?m)^[^#\\r\\n]*(\\$\\{input_data\\}).*$"; - private static final String INLINE_REGEX_REGEX = "(?m)^[^#\\r\\n]*\\$\\{.*regex.*\\}.*$"; + private static final String INLINE_INPUT_DATA_REGEX = "(?m)^[^#\\r\\n]*(\\$\\{input_data}).*$"; + private static final String INLINE_REGEX_REGEX = "(?m)^[^#\\r\\n]*\\$\\{.*regex.*}.*$"; private static final String INLINE_OUTPUT_REGEX = "(?m)^[^#\\r\\n]*(" + REPLACEMENT_PARAM + ").*$"; private static final Pattern INLINE_INPUT_DATA_PATTERN = Pattern.compile(INLINE_INPUT_DATA_REGEX); diff --git a/api/src/org/labkey/api/reports/report/r/RDockerScriptEngine.java b/api/src/org/labkey/api/reports/report/r/RDockerScriptEngine.java index c50790661f2..926d88801db 100644 --- a/api/src/org/labkey/api/reports/report/r/RDockerScriptEngine.java +++ b/api/src/org/labkey/api/reports/report/r/RDockerScriptEngine.java @@ -30,6 +30,7 @@ import org.labkey.api.reports.LabKeyScriptEngineManager; import org.labkey.api.util.FileUtil; import org.labkey.api.util.UnexpectedException; +import org.labkey.vfs.FileLike; import javax.script.ScriptContext; import javax.script.ScriptException; @@ -38,14 +39,12 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; -import java.util.Map; /** * Created by matthew on 7/14/17. */ public class RDockerScriptEngine extends RScriptEngine { - private static final Logger LOG = LogManager.getLogger(RDockerScriptEngine.class); private static DockerService _ds; private final String _remoteWorkingDir; @@ -66,19 +65,12 @@ public RDockerScriptEngine(@NotNull ExternalScriptEngineDefinition def, @Nullabl void setMapping() { - String wd = getWorkingDir(getContext()).getAbsolutePath().replace("\\","/").replace("/./","/"); + String wd = getWorkingDir(getContext()).toNioPathForRead().toFile().getAbsolutePath().replace("\\","/").replace("/./","/"); super.setPathMap(Collections.singletonMap( _remoteWorkingDir, new File(wd).toURI().toString())); } - @Override - public Map getPathMap() - { - setMapping(); - return super.getPathMap(); - } - @Override public String remoteToLocal(String remoteURI) { @@ -103,7 +95,7 @@ public ValidationException getValidationErrors() } @Override - protected Object eval(File scriptFile, ScriptContext context) throws ScriptException + protected Object eval(FileLike scriptFile, ScriptContext context) throws ScriptException { StringBuffer output = new StringBuffer(); if (null != _ds) @@ -141,7 +133,7 @@ private static FileFilter InputFiles() } @Override - public String getRemotePath(File localFile) + public String getRemotePath(FileLike localFile) { // get absolute path to make sure the paths are consistent URI localUri = FileUtil.getAbsoluteCaseSensitiveFile(localFile).toURI(); diff --git a/api/src/org/labkey/api/reports/report/r/RReport.java b/api/src/org/labkey/api/reports/report/r/RReport.java index bf485752e85..9bf523eb35a 100644 --- a/api/src/org/labkey/api/reports/report/r/RReport.java +++ b/api/src/org/labkey/api/reports/report/r/RReport.java @@ -346,12 +346,12 @@ public String getKnitrEndChunk() } @Override - protected String getScriptProlog(ScriptEngine engine, ViewContext context, File inputFile, Map inputParameters) + protected String getScriptProlog(ScriptEngine engine, ViewContext context, FileLike inputFile, Map inputParameters) { return getScriptProlog(engine, context, inputFile, inputParameters, false); } - protected String getScriptProlog(ScriptEngine engine, ViewContext context, File inputFile, Map inputParameters, boolean isRStudio) + protected String getScriptProlog(ScriptEngine engine, ViewContext context, FileLike inputFile, Map inputParameters, boolean isRStudio) { StringBuilder labkey = new StringBuilder(); @@ -373,8 +373,10 @@ else if (isRStudio) } } - labkey.append("labkey.url <- function (controller, action, list){paste(labkey.url.base,controller,labkey.url.path,action,\".view?\",paste(names(list),list,sep=\"=\",collapse=\"&\"),sep=\"\")}\n" + - "labkey.resolveLSID <- function(lsid){paste(labkey.url.base,\"experiment/resolveLSID.view?lsid=\",lsid,sep=\"\");}\n"); + labkey.append(""" + labkey.url <- function (controller, action, list){paste(labkey.url.base,controller,labkey.url.path,action,".view?",paste(names(list),list,sep="=",collapse="&"),sep="")} + labkey.resolveLSID <- function(lsid){paste(labkey.url.base,"experiment/resolveLSID.view?lsid=",lsid,sep="");} + """); labkey.append("labkey.user.email=").append(toR(context.getUser().getEmail())).append("\n"); ActionURL url = context.getActionURL(); @@ -452,7 +454,7 @@ else if (isRStudio) } @Override - protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, File inputFile, Map inputParameters, boolean isRStudio) + protected String concatScriptProlog(ScriptEngine engine, ViewContext context, String script, FileLike inputFile, Map inputParameters, boolean isRStudio) { String yamlScript = ""; String yamlSyntaxPrefix = "---\n"; @@ -573,14 +575,14 @@ public int[] getPrologAnchors(String script) } // append the pipeline roots to the prolog - public static File getPipelineRoot(ViewContext context) + public static FileLike getPipelineRoot(ViewContext context) { // // currently we ignore the supplemental directory and only return the primary directory/override // if the supplemental directory is important then consider making this a list // PipeRoot pipelineRoot = PipelineService.get().findPipelineRoot(context.getContainer()); - return pipelineRoot.getRootPath(); + return pipelineRoot.getRootFileLike(); } public void setScriptSource(String script) @@ -588,14 +590,14 @@ public void setScriptSource(String script) getDescriptor().setProperty(ScriptReportDescriptor.Prop.script, script); } - public static String getLocalPath(File f) + public static String getLocalPath(FileLike f) { - File fAbsolute = FileUtil.getAbsoluteCaseSensitiveFile(f); + File fAbsolute = FileUtil.getAbsoluteCaseSensitiveFile(f.toNioPathForRead().toFile()); return fAbsolute.getAbsolutePath().replaceAll("\\\\", "/"); } @Override - protected String processInputReplacement(ScriptEngine engine, String script, @Nullable File inputFile, boolean isRStudio) + protected String processInputReplacement(ScriptEngine engine, String script, @Nullable FileLike inputFile, boolean isRStudio) { RScriptEngine rengine = (RScriptEngine) engine; String remotePath = inputFile == null ? null : rengine.getRemotePath(inputFile); @@ -606,21 +608,20 @@ protected String processInputReplacement(ScriptEngine engine, String script, @Nu protected String processOutputReplacements(ScriptEngine engine, String script, List replacements, @NotNull ContainerUser context, boolean isRStudio) throws Exception { FileLike reportDirFileLike = getReportDirFileLike(context.getContainer().getId()); - File reportDir = FileSystemLike.toFile(reportDirFileLike); RScriptEngine rengine = (RScriptEngine)engine; - String localPath = getLocalPath(reportDir); + String localPath = getLocalPath(reportDirFileLike); String remoteRoot = rengine.getRemotePath(localPath); - return ParamReplacementSvc.get().processParamReplacement(script, reportDir, remoteRoot, replacements, isRStudio); + return ParamReplacementSvc.get().processParamReplacement(script, reportDirFileLike, remoteRoot, replacements, isRStudio); } @Override - protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws Exception + protected String createScript(ScriptEngine engine, ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws Exception { return createScript(engine, context, outputSubst, inputDataTsv, inputParameters, false); } @Override - public String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters, boolean isRStudio) throws Exception + public String createScript(ScriptEngine engine, ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters, boolean isRStudio) throws Exception { String script = super.createScript(engine, context, outputSubst, inputDataTsv, inputParameters, isRStudio); FileLike inputData = getReportDirFileLike(context.getContainer().getId()).resolveChild(DATA_INPUT); @@ -645,7 +646,7 @@ public String createScript(ScriptEngine engine, ViewContext context, List outputSubst, File inputDataTsv, Map inputParameters) throws ScriptException + public String runScript(ViewContext context, List outputSubst, FileLike inputDataTsv, Map inputParameters) throws ScriptException { ScriptEngine engine = getScriptEngine(context.getContainer()); if (engine != null) @@ -741,7 +728,7 @@ public String runScript(ViewContext context, List outputSubst, } else { - File knitrOutput = new File((String)bindings.get(RScriptEngine.KNITR_OUTPUT)); + FileLike knitrOutput = FileSystemLike.wrapFile(new File((String)bindings.get(RScriptEngine.KNITR_OUTPUT))); saveKnitrOutput(knitrOutput, outputSubst); } saveAdditionalFileOutput(outputSubst, context); @@ -757,7 +744,7 @@ public String runScript(ViewContext context, List outputSubst, throw new ScriptException("A script engine implementation was not found for the specified report"); } - private void saveKnitrOutput(File knitrOutput, List outputSubst) + private void saveKnitrOutput(FileLike knitrOutput, List outputSubst) { KnitrOutput param = new KnitrOutput(); param.setName("Knitr"); @@ -864,10 +851,13 @@ public String getDefaultScript() // issue 27527: only use the write.table default script if we have a labkey.data data frame return getDescriptor().getProperty("dataRegionName") == null ? "" - : "# This sample code returns the query data in tab-separated values format, which LabKey then\n" + - "# renders as HTML. Replace this code with your R script. See the Help tab for more details.\n\n" + - "# ${tsvout:tsvfile}\n" + - "write.table(labkey.data, file = \"tsvfile\", sep = \"\\t\", qmethod = \"double\", col.names=NA)\n"; + : """ + # This sample code returns the query data in tab-separated values format, which LabKey then + # renders as HTML. Replace this code with your R script. See the Help tab for more details. + + # ${tsvout:tsvfile} + write.table(labkey.data, file = "tsvfile", sep = "\\t", qmethod = "double", col.names=NA) + """; } @Override @@ -1004,10 +994,14 @@ public void testPrologMd() RScriptEngine r = (RScriptEngine)LabKeyScriptEngineManager.get().getEngineByExtension(context.getContainer(), "r"); //r.getBindings(ScriptContext.ENGINE_SCOPE).put(RScriptEngine.KNITR_FORMAT, RReportDescriptor.KnitrFormat.Markdown); Map params = PageFlowUtil.map("a", "1", "b", "2"); - String pre = "---\n" + - "title: My Report\n" + - "---\n" + - "hello \n\nworld\n"; + String pre = """ + --- + title: My Report + --- + hello\s + + world + """; boolean isRStudio = false; String post = report.concatScriptProlog(r, context, pre, null, (Map)params, isRStudio); diff --git a/api/src/org/labkey/api/reports/report/r/RReportJob.java b/api/src/org/labkey/api/reports/report/r/RReportJob.java index 8090a2bada0..e9f984a6672 100644 --- a/api/src/org/labkey/api/reports/report/r/RReportJob.java +++ b/api/src/org/labkey/api/reports/report/r/RReportJob.java @@ -16,7 +16,6 @@ package org.labkey.api.reports.report.r; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.labkey.api.data.JdbcType; @@ -40,7 +39,6 @@ import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.api.view.ViewContext; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; import java.io.File; import java.io.Serializable; @@ -51,8 +49,6 @@ import java.util.Objects; import java.util.TreeMap; -import static org.labkey.api.util.StringUtilsLabKey.DEFAULT_CHARSET; - /** * User: Karl Lum * Date: Jun 11, 2007 @@ -182,9 +178,9 @@ public void run() public Task createPipelineTask(PipelineJob job, Report report, Map params) { // overrride to support different kind of report - if (!(report instanceof RReport)) + if (!(report instanceof RReport rReport)) throw new UnsupportedOperationException("Expected R report"); - return new RunRReportTask(job, ((RReport)report), params); + return new RunRReportTask(job, rReport, params); } @@ -246,7 +242,7 @@ protected void runReport(ViewContext context) throws PipelineJobException try { // get the input file which should have been previously created - File inputFile = inputFile(_report, context); + FileLike inputFile = inputFile(_report, context); List outputSubst = new ArrayList<>(); ActionURL url = context.cloneActionURL(); @@ -266,10 +262,9 @@ protected void runReport(ViewContext context) throws PipelineJobException } } - protected File inputFile(RReport report, @NotNull ViewContext context) throws Exception + protected FileLike inputFile(RReport report, @NotNull ViewContext context) throws Exception { - FileLike inputFile = report.getReportDirFileLike(context.getContainer().getId()).resolveChild(RReport.DATA_INPUT); - return FileSystemLike.toFile(inputFile); + return report.getReportDirFileLike(context.getContainer().getId()).resolveChild(RReport.DATA_INPUT); } protected void processOutputs(RReport report, List outputSubst) throws Exception @@ -277,15 +272,14 @@ protected void processOutputs(RReport report, List outputSubst if (!outputSubst.isEmpty()) { // write the output substitution map to disk so we can render the view later - FileLike reportDirFileLike = report.getReportDirFileLike(getJob().getContainerId()); - File reportDir = FileSystemLike.toFile(reportDirFileLike); - File substitutionMap; + FileLike reportDir = report.getReportDirFileLike(getJob().getContainerId()); + FileLike substitutionMap; if (reportDir.getName().equals(getJobIdentifier())) { - File parentDir = reportDir.getParentFile(); + FileLike parentDir = reportDir.getParent(); // clean up the destination folder - for (File file : parentDir.listFiles()) + for (FileLike file : parentDir.getChildren()) { if (!file.isDirectory() && !"log".equalsIgnoreCase(FileUtil.getExtension(file))) file.delete(); @@ -294,25 +288,25 @@ protected void processOutputs(RReport report, List outputSubst // rewrite the parameter replacement files to point to the destination folder for (ParamReplacement replacement : outputSubst) { - List newFiles = new ArrayList<>(); - for (File file : replacement.getFiles()) + List newFiles = new ArrayList<>(); + for (FileLike file : replacement.getFiles()) { - File newFile = FileUtil.appendName(parentDir, file.getName()); - FileUtils.moveFile(file, newFile); + FileLike newFile = parentDir.resolveChild(file.getName()); + file.move(newFile); newFiles.add(newFile); } replacement.clearFiles(); - for (File file : newFiles) + for (FileLike file : newFiles) { replacement.addFile(file); } } // move the remaining files and delete the pipeline specific directory - for (File file : reportDir.listFiles()) + for (FileLike file : reportDir.getChildren()) { - File newFile = FileUtil.appendName(parentDir, file.getName()); + FileLike newFile = parentDir.resolveChild(file.getName()); // special handling for log file if (LOG_FILE_NAME.equalsIgnoreCase(file.getName())) { @@ -321,27 +315,27 @@ protected void processOutputs(RReport report, List outputSubst { newFile = FileUtil.createTempFile(LOG_FILE_PREFIX, ".log", parentDir); getJob().setLogFile(newFile); - FileUtils.copyFile(file, newFile); + FileUtil.copyFile(file, newFile); } // report.log != getLogFile(), just regular file else { - String logFileContenxt = FileUtils.readFileToString(file, DEFAULT_CHARSET); - if (!StringUtils.isEmpty(logFileContenxt)) - getJob().info("REPORT.LOG CONTENTS:\n" + logFileContenxt); - FileUtils.moveFile(file, newFile); + String logFileContent = PageFlowUtil.getStreamContentsAsString(file.openInputStream()); + if (!StringUtils.isEmpty(logFileContent)) + getJob().info("REPORT.LOG CONTENTS:\n" + logFileContent); + file.move(newFile); } } else { - FileUtils.moveFile(file, newFile); + file.move(newFile); } } - FileUtils.deleteDirectory(reportDir); - substitutionMap = FileUtil.appendName(reportDir.getParentFile(), RReport.SUBSTITUTION_MAP); + FileUtil.deleteDir(reportDir); + substitutionMap = reportDir.getParent().resolveChild(RReport.SUBSTITUTION_MAP); } else - substitutionMap = FileUtil.appendName(reportDir, RReport.SUBSTITUTION_MAP); + substitutionMap = reportDir.getParent().resolveChild(RReport.SUBSTITUTION_MAP); ParamReplacementSvc.get().toFile(outputSubst, substitutionMap); } } diff --git a/api/src/org/labkey/api/reports/report/r/RScriptEngine.java b/api/src/org/labkey/api/reports/report/r/RScriptEngine.java index 95018b063f5..3684522b19b 100644 --- a/api/src/org/labkey/api/reports/report/r/RScriptEngine.java +++ b/api/src/org/labkey/api/reports/report/r/RScriptEngine.java @@ -19,12 +19,12 @@ import org.labkey.api.data.JdbcType; import org.labkey.api.reports.ExternalScriptEngine; import org.labkey.api.reports.ExternalScriptEngineDefinition; +import org.labkey.vfs.FileLike; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; -import java.io.File; import java.util.Arrays; import java.util.List; @@ -59,14 +59,9 @@ public ScriptEngineFactory getFactory() return new RScriptEngineFactory(_def); } - public File prepareScript(String script) + protected FileLike prepareScriptFile(String script, ScriptContext context, List extensions) { - return prepareScriptFile(script, context, Arrays.asList(_def.getExtensions()), false); - } - - protected File prepareScriptFile(String script, ScriptContext context, List extensions, boolean createWrapper) - { - File scriptFile; + FileLike scriptFile; if (getKnitrFormat(context) != RReportDescriptor.KnitrFormat.None) { // @@ -78,13 +73,10 @@ protected File prepareScriptFile(String script, ScriptContext context, List preprocessExtensions = Arrays.asList(getKnitrExtension(context, extensions)); scriptFile = writeScriptFile(script, context, preprocessExtensions); - if (createWrapper) - { - // write a new script (the acutal .R script to be run) as the preprocessing script and use - // this as the script file we pass to the script engine - String preprocessScript = createKnitrScript(context, scriptFile); - scriptFile = writeScriptFile(preprocessScript, context, extensions); - } + // write a new script (the actual .R script to be run) as the preprocessing script and use + // this as the script file we pass to the script engine + String preprocessScript = createKnitrScript(context, scriptFile); + scriptFile = writeScriptFile(preprocessScript, context, extensions); } else { @@ -101,7 +93,7 @@ public Object eval(String script, ScriptContext context) throws ScriptException if (!extensions.isEmpty()) { - File scriptFile = prepareScriptFile(script, context, extensions, true); + FileLike scriptFile = prepareScriptFile(script, context, extensions); return eval(scriptFile, context); } else @@ -162,19 +154,19 @@ protected String getPandocOutputOptionsList(ScriptContext context) return (String) v; } - protected String getInputFilename(File inputScript) + protected String getInputFilename(FileLike inputScript) { - return inputScript.getAbsolutePath().replaceAll("\\\\", "/"); + return inputScript.toNioPathForRead().toFile().getAbsolutePath().replaceAll("\\\\", "/"); } - protected String getOutputFilename(File inputScript) + protected String getOutputFilename(FileLike inputScript) { String outputFilename; // do not call getInputFilename here as we do not want to invoke // any overrides. The output file name should be the local path even // in the Rserve case since this file is manipulated on the labkey // server - String inputFilename = inputScript.getAbsolutePath().replaceAll("\\\\", "/"); + String inputFilename = inputScript.toNioPathForRead().toFile().getAbsolutePath().replaceAll("\\\\", "/"); String ext = "html"; if (inputFilename.lastIndexOf('.') != -1) @@ -197,7 +189,7 @@ protected String getRWorkingDir(ScriptContext context) } - public String getRemotePath(File localFile) + public String getRemotePath(FileLike localFile) { return RReport.getLocalPath(localFile); } @@ -209,7 +201,7 @@ public String getRemotePath(String localURI) } - protected String createKnitrScript(ScriptContext context, File inputScript) + protected String createKnitrScript(ScriptContext context, FileLike inputScript) { if (getKnitrFormat(context) == RReportDescriptor.KnitrFormat.None) return null; diff --git a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java index 633ba3cdcf1..5a39018d7dd 100644 --- a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java +++ b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java @@ -26,6 +26,7 @@ import org.labkey.api.util.Pair; import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; import org.rosuda.REngine.REXP; import org.rosuda.REngine.REXPMismatchException; import org.rosuda.REngine.Rserve.RConnection; @@ -35,7 +36,6 @@ import javax.script.ScriptException; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -126,17 +126,17 @@ static ModusOperandi getModusOperandi(ExternalScriptEngineDefinition def) @Override - protected String getInputFilename(File inputScript) + protected String getInputFilename(FileLike inputScript) { return getRemotePath(inputScript); } // clean absolute path - File workingDirectory; + FileLike workingDirectory; @Override - public File getWorkingDir(ScriptContext context) + public FileLike getWorkingDir(ScriptContext context) { if (null == workingDirectory) workingDirectory = FileUtil.getAbsoluteCaseSensitiveFile(super.getWorkingDir(getContext())); @@ -147,7 +147,7 @@ public File getWorkingDir(ScriptContext context) @Override protected String getRWorkingDir(ScriptContext context) { - File workingDir = getWorkingDir(context); + FileLike workingDir = getWorkingDir(context); if (!mo.requiresFileRemap()) return workingDir.toString(); else @@ -163,7 +163,7 @@ public void appendScriptProlog(StringBuilder labkey, ViewContext context) { if (!mo.requiresCopyFiles()) // requiresCopyFiles implies there is no shared file-system { - File pipelineRoot = RReport.getPipelineRoot(context); + FileLike pipelineRoot = RReport.getPipelineRoot(context); String localPath = getLocalPath(pipelineRoot); labkey.append("labkey.pipeline.root <- \"").append(localPath).append("\"\n"); @@ -230,7 +230,7 @@ protected void copyWorkingDirectoryToRemote(RConnection rconn) throws IOExceptio return; LOG.debug("Copy files in working directory to remote server"); - final Path localWorkingDirectoryPath = getWorkingDir(getContext()).toPath(); + final Path localWorkingDirectoryPath = getWorkingDir(getContext()).toNioPathForRead(); try (Stream pathStream = Files.find(localWorkingDirectoryPath, Integer.MAX_VALUE, (filePath, fileAttr) -> fileAttr.isRegularFile())) { List> listing = pathStream @@ -260,7 +260,7 @@ protected void copyWorkingDirectoryFromRemote(RConnection rconn) throws IOExcept return; LOG.debug("Copy files from remote server to local working directory"); - File wd = getWorkingDir(getContext()); + FileLike wd = getWorkingDir(getContext()); try { String[] paths = rconn.eval("dir("+ toR(defaultIfBlank(rserveWorkingDirectory, ".")) +", all.files = TRUE, full.names = TRUE, recursive = TRUE, include.dirs = FALSE, no.. = FALSE)").asStrings(); @@ -270,10 +270,9 @@ protected void copyWorkingDirectoryFromRemote(RConnection rconn) throws IOExcept continue; if ("script.R".equalsIgnoreCase(remotePath)) continue; - File file = new File(wd,remotePath); - FileUtil.createTempFile(file); + FileLike file = wd.resolveFile(org.labkey.api.util.Path.parse(remotePath)); try (InputStream is = rconn.openFile(remotePath); - FileOutputStream fos = new FileOutputStream(file)) + OutputStream fos = file.openOutputStream()) { IOUtil.copyCompletely(is, fos); } @@ -311,7 +310,7 @@ public Object eval(String script, ScriptContext context) throws ScriptException LOG.info("Reusing RServe connection in use: " + rh.isInUse()); } - File scriptFile = prepareScriptFile(script, context, extensions, true); + FileLike scriptFile = prepareScriptFile(script, context, extensions); try { @@ -435,7 +434,7 @@ public static String getRserveOutput(REXP rexp) @Override - public String getRemotePath(File localFile) + public String getRemotePath(FileLike localFile) { // get absolute path to make sure the paths are consistent localFile = FileUtil.getAbsoluteCaseSensitiveFile(localFile); @@ -460,14 +459,14 @@ public String getRemotePath(String local) } // generate path relative to the working directory, return null for paths outside directory - static String relativizeWorkingDirectory(File workingDirectory, String strPath) + static String relativizeWorkingDirectory(FileLike workingDirectory, String strPath) { Path path = new File(strPath).toPath().normalize(); if (path.equals(path.getRoot()) || path.startsWith("..")) return null; if (!path.isAbsolute()) return path.toString(); - Path wd = workingDirectory.toPath(); + Path wd = workingDirectory.toNioPathForRead(); Path relative = wd.relativize(path); if (relative.startsWith("../") || relative.startsWith("/")) return null; @@ -477,7 +476,7 @@ static String relativizeWorkingDirectory(File workingDirectory, String strPath) // It's confusing to have methods that take URI and return path (or vice versa) // Let's stick to methods that take/return the same type (using URI here since pathMap.localToRemote() wants URI) - static public URI makeLocalToRemotePath(ExternalScriptEngineDefinition def, File workingDirectory, URI localURI) + static public URI makeLocalToRemotePath(ExternalScriptEngineDefinition def, FileLike workingDirectory, URI localURI) { // let's first try to relative relative to the working directory // We could do this in the other order. However, since pathMap.localToRemote() doesn't tell us when it did not do anything, diff --git a/api/src/org/labkey/api/reports/report/r/view/ConsoleOutput.java b/api/src/org/labkey/api/reports/report/r/view/ConsoleOutput.java index e00f100bc04..0304797809e 100644 --- a/api/src/org/labkey/api/reports/report/r/view/ConsoleOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/ConsoleOutput.java @@ -16,6 +16,7 @@ package org.labkey.api.reports.report.r.view; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.Report; import org.labkey.api.reports.report.r.RReport; import org.labkey.api.reports.report.ScriptOutput; @@ -23,8 +24,9 @@ import org.labkey.api.util.FileUtil; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; /** * User: Karl Lum @@ -40,27 +42,27 @@ public ConsoleOutput() } /* create an output for a known file */ - public ConsoleOutput(File output) + public ConsoleOutput(FileLike output) { super(ID); addFile(output); } @Override - protected File getSubstitution(File directory) throws Exception + protected @Nullable FileLike getSubstitution(FileLike directory) throws IOException { - File file; + FileLike file; if (directory != null) file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.txt", directory); else - file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.txt"); + file = FileUtil.createTempFileLike(RReport.FILE_PREFIX, "Result.txt"); addFile(file); return file; } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { ROutputView view = new TextOutput.TextOutputView(this); view.setLabel("Console output"); @@ -71,7 +73,7 @@ public HttpView getView(ViewContext context) } @Override - public ScriptOutput renderAsScriptOutput(File file) throws Exception + public ScriptOutput renderAsScriptOutput(FileLike file) throws Exception { ROutputView view = new TextOutput.TextOutputView(this); String console = view.renderInternalAsString(file); diff --git a/api/src/org/labkey/api/reports/report/r/view/DownloadOutputView.java b/api/src/org/labkey/api/reports/report/r/view/DownloadOutputView.java index 3ee179b603b..1a56443f632 100644 --- a/api/src/org/labkey/api/reports/report/r/view/DownloadOutputView.java +++ b/api/src/org/labkey/api/reports/report/r/view/DownloadOutputView.java @@ -17,13 +17,13 @@ package org.labkey.api.reports.report.r.view; import org.apache.commons.lang3.StringUtils; -import org.labkey.api.attachments.AttachmentParent; import org.labkey.api.reports.report.ReportUrls; import org.labkey.api.reports.report.r.ParamReplacement; import org.labkey.api.util.ImageUtil; import org.labkey.api.util.PageFlowUtil; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; import java.io.PrintWriter; /** @@ -33,13 +33,10 @@ public abstract class DownloadOutputView extends ROutputView { private final String _fileType; - private final AttachmentParent _parent; - private String _lastError; - DownloadOutputView(ParamReplacement param, AttachmentParent parent, String fileType) + DownloadOutputView(ParamReplacement param, String fileType) { super(param); - _parent = parent; _fileType = fileType; setLabel("Attachment output"); } @@ -56,13 +53,13 @@ protected String renderException(Exception e) } @Override - protected String renderInternalAsString(File file) + protected String renderInternalAsString(FileLike file) throws IOException { String downloadUrl = null; - if (file != null && file.exists() && (file.length() > 0)) + if (file != null && file.exists() && (file.getSize() > 0)) { - File newFile = moveToTemp(file); + FileLike newFile = moveToTemp(file); // file hasn't been saved yet String key = ImageUtil.setFileInSession(getViewContext().getRequest(), newFile); downloadUrl = PageFlowUtil.urlProvider(ReportUrls.class).urlStreamFile(getViewContext().getContainer()). @@ -75,9 +72,9 @@ protected String renderInternalAsString(File file) } @Override - protected void renderInternal(Object model, PrintWriter out) + protected void renderInternal(Object model, PrintWriter out) throws IOException { - for (File file : getFiles()) + for (FileLike file : getFiles()) { String downloadUrl = renderInternalAsString(file); @@ -95,22 +92,19 @@ protected void renderInternal(Object model, PrintWriter out) else out.write(""); - if (null != downloadUrl) + out.write(""); + if (StringUtils.stripToNull(filename) == null) { - out.write(""); - if (StringUtils.stripToNull(filename) == null) - { - out.write(_fileType); - out.write(" output file (click to download)"); - } - else - { - out.write(filename); - } - out.write(""); + out.write(_fileType); + out.write(" output file (click to download)"); } + else + { + out.write(filename); + } + out.write(""); out.write(""); out.write(""); diff --git a/api/src/org/labkey/api/reports/report/r/view/DownloadParamReplacement.java b/api/src/org/labkey/api/reports/report/r/view/DownloadParamReplacement.java index c86fb853632..6cf1e810b70 100644 --- a/api/src/org/labkey/api/reports/report/r/view/DownloadParamReplacement.java +++ b/api/src/org/labkey/api/reports/report/r/view/DownloadParamReplacement.java @@ -20,8 +20,10 @@ import org.labkey.api.reports.report.r.AbstractParamReplacement; import org.labkey.api.util.FileUtil; import org.labkey.api.util.Path; +import org.labkey.vfs.FileLike; import java.io.File; +import java.io.IOException; /** * User: Dax Hawkins @@ -36,11 +38,11 @@ public DownloadParamReplacement(String id) super(id); } - protected final File getSubstitution(File directory, String extension) + protected final FileLike getSubstitution(FileLike directory, String extension) { String fileName; String tokenName = getName(); - File file = null; + FileLike file = null; if (tokenName != null) { String tokenExtension = FileUtil.getExtension(tokenName); @@ -50,16 +52,16 @@ protected final File getSubstitution(File directory, String extension) fileName = getName().concat(extension); if (directory != null) - file = FileUtil.appendPath(directory, Path.parse(fileName)); + file = directory.resolveFile(Path.parse(fileName)); } if (file != null) addFile(file); return file; } - protected ScriptOutput renderAsScriptOutput(File file, DownloadOutputView view, ScriptOutput.ScriptOutputType scriptOutputType) + protected ScriptOutput renderAsScriptOutput(FileLike file, DownloadOutputView view, ScriptOutput.ScriptOutputType scriptOutputType) throws IOException { - String downloadUrl = view.renderInternalAsString(file); + String downloadUrl = view.renderInternalAsString(file); if (null != downloadUrl) return new ScriptOutput(scriptOutputType, getName(), downloadUrl); diff --git a/api/src/org/labkey/api/reports/report/r/view/FileOutput.java b/api/src/org/labkey/api/reports/report/r/view/FileOutput.java index 98bfed381df..986015489b7 100644 --- a/api/src/org/labkey/api/reports/report/r/view/FileOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/FileOutput.java @@ -16,14 +16,15 @@ package org.labkey.api.reports.report.r.view; -import org.labkey.api.attachments.AttachmentParent; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.report.ScriptOutput; import org.labkey.api.reports.report.r.ParamReplacement; import org.labkey.api.view.HtmlView; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; /** * User: Karl Lum @@ -39,36 +40,35 @@ public FileOutput() } @Override - protected File getSubstitution(File directory) + protected @Nullable FileLike getSubstitution(FileLike directory) { return getSubstitution(directory, ".txt"); } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { - if (getReport() instanceof AttachmentParent) - return renderAsScriptOutput(file, new FileoutReportView(this, getReport()), + if (getReport() != null) + return renderAsScriptOutput(file, new FileoutReportView(this), ScriptOutput.ScriptOutputType.file); - else return renderAsScriptOutputError(); } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { - if (getReport() instanceof AttachmentParent) - return new FileoutReportView(this, getReport()); + if (getReport() != null) + return new FileoutReportView(this); else return HtmlView.of(DownloadParamReplacement.UNABLE_TO_RENDER); } public static class FileoutReportView extends DownloadOutputView { - FileoutReportView(ParamReplacement param, AttachmentParent parent) + FileoutReportView(ParamReplacement param) { - super(param, parent, "Text"); + super(param, "Text"); } } } diff --git a/api/src/org/labkey/api/reports/report/r/view/HrefOutput.java b/api/src/org/labkey/api/reports/report/r/view/HrefOutput.java index 1e263a09985..88fd08ce0ab 100644 --- a/api/src/org/labkey/api/reports/report/r/view/HrefOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/HrefOutput.java @@ -16,13 +16,13 @@ package org.labkey.api.reports.report.r.view; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.Report; import org.labkey.api.reports.report.r.RReportDescriptor; import org.labkey.api.reports.report.ScriptReportDescriptor; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; - -import java.io.File; +import org.labkey.vfs.FileLike; /** * User: Karl Lum @@ -38,13 +38,13 @@ public HrefOutput() } @Override - protected File getSubstitution(File directory) + protected @Nullable FileLike getSubstitution(FileLike directory) { return null; } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { return null; } diff --git a/api/src/org/labkey/api/reports/report/r/view/HtmlOutput.java b/api/src/org/labkey/api/reports/report/r/view/HtmlOutput.java index 61ba89e4e9f..2904d3a7283 100644 --- a/api/src/org/labkey/api/reports/report/r/view/HtmlOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/HtmlOutput.java @@ -16,6 +16,7 @@ package org.labkey.api.reports.report.r.view; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.report.r.RReport; import org.labkey.api.reports.report.ScriptOutput; import org.labkey.api.reports.report.r.AbstractParamReplacement; @@ -24,8 +25,9 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; import java.io.PrintWriter; /** @@ -47,13 +49,13 @@ protected HtmlOutput(String id) } @Override - protected File getSubstitution(File directory) throws Exception + protected @Nullable FileLike getSubstitution(FileLike directory) throws IOException { - File file; + FileLike file; if (directory != null) file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.html", directory); else - file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.html"); + file = FileUtil.createTempFileLike(RReport.FILE_PREFIX, "Result.html"); addFile(file); return file; @@ -65,7 +67,7 @@ protected String getLabel() } @Override - public ScriptOutput renderAsScriptOutput(File file) throws Exception + public ScriptOutput renderAsScriptOutput(FileLike file) throws Exception { HtmlOutputView view = new HtmlOutputView(this, getLabel()); String html = view.renderInternalAsString(file); @@ -94,10 +96,10 @@ public HtmlOutputView(ParamReplacement param, String label) * Loads an HTML file and adds nonces to any embedded <script> tags. Don't call this with files that aren't HTML. */ @Override - protected String renderInternalAsString(File file) throws Exception + protected String renderInternalAsString(FileLike file) throws Exception { - if (exists(file)) - return PageFlowUtil.addScriptNonces(PageFlowUtil.getFileContentsAsString(file)); + if (file.exists()) + return PageFlowUtil.addScriptNonces(PageFlowUtil.getStreamContentsAsString(file.openInputStream())); return null; } @@ -105,7 +107,7 @@ protected String renderInternalAsString(File file) throws Exception @Override protected void renderInternal(Object model, PrintWriter out) throws Exception { - for (File file : getFiles()) + for (FileLike file : getFiles()) { String html = renderInternalAsString(file); if (null != html) diff --git a/api/src/org/labkey/api/reports/report/r/view/ImageOutput.java b/api/src/org/labkey/api/reports/report/r/view/ImageOutput.java index b89061204d9..2027bd302be 100644 --- a/api/src/org/labkey/api/reports/report/r/view/ImageOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/ImageOutput.java @@ -17,6 +17,7 @@ package org.labkey.api.reports.report.r.view; import org.apache.commons.lang3.BooleanUtils; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.Report; import org.labkey.api.reports.report.ReportDescriptor; import org.labkey.api.reports.report.ReportUrls; @@ -32,9 +33,9 @@ import org.labkey.api.view.ActionURL; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; import javax.imageio.ImageIO; -import java.io.File; import java.io.IOException; import java.io.PrintWriter; @@ -62,26 +63,26 @@ protected String getExtension() } @Override - protected File getSubstitution(File directory) throws Exception + protected @Nullable FileLike getSubstitution(FileLike directory) throws IOException { - File file; + FileLike file; if (directory != null) file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result." + getExtension(), directory); else - file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result." + getExtension()); + file = FileUtil.createTempFileLike(RReport.FILE_PREFIX, "Result." + getExtension()); addFile(file); return file; } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { return new ImgReportView(this, canDeleteFile()); } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { ImgReportView view = new ImgReportView(this, canDeleteFile()); String image = view.renderInternalAsString(file); @@ -118,13 +119,13 @@ public static class ImgReportView extends ROutputView } @Override - protected String renderInternalAsString(File file) + protected String renderInternalAsString(FileLike file) throws IOException { String imgUrl = null; - if (exists(file)) + if (file.exists()) { - File imgFile; + FileLike imgFile; if (!_deleteFile) imgFile = file; else @@ -147,9 +148,9 @@ protected String renderInternalAsString(File file) } @Override - protected void renderInternal(Object model, PrintWriter out) + protected void renderInternal(Object model, PrintWriter out) throws IOException { - for (File file : getFiles()) + for (FileLike file : getFiles()) { String imgUrl = renderInternalAsString(file); @@ -174,11 +175,11 @@ protected void renderInternal(Object model, PrintWriter out) @Override public Thumbnail renderThumbnail(ViewContext context) throws IOException { - for (File file : getFiles()) + for (FileLike file : getFiles()) { // just render the first file, in most cases this is appropriate if (file.exists()) - return ImageUtil.renderThumbnail(ImageIO.read(file)); + return ImageUtil.renderThumbnail(ImageIO.read(file.openInputStream())); } return null; } diff --git a/api/src/org/labkey/api/reports/report/r/view/IpynbOutput.java b/api/src/org/labkey/api/reports/report/r/view/IpynbOutput.java index 6a58911b498..069aeda4362 100644 --- a/api/src/org/labkey/api/reports/report/r/view/IpynbOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/IpynbOutput.java @@ -34,8 +34,8 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; import java.io.PrintWriter; import java.net.URI; import java.util.List; @@ -57,14 +57,14 @@ public IpynbOutput() } /* create an output for a known file */ - public IpynbOutput(File output) + public IpynbOutput(FileLike output) { super(ID); addFile(output); } @Override - public ScriptOutput renderAsScriptOutput(File file) throws Exception + public ScriptOutput renderAsScriptOutput(FileLike file) throws Exception { IpynbOutputView view = new IpynbOutputView(this, getLabel()); String html = view.renderInternalAsString(file); @@ -84,7 +84,7 @@ public HttpView getView(ViewContext context) @Override public @Nullable Thumbnail renderThumbnail(ViewContext context) { - for (File file : getFiles()) + for (FileLike file : getFiles()) { IpynbOutputView view = new IpynbOutputView(this, getLabel()); Thumbnail thumb = null; @@ -148,10 +148,10 @@ String stripAnsiColors(String s) @Override - protected String renderInternalAsString(File file) throws Exception + protected String renderInternalAsString(FileLike file) throws Exception { // Don't call super.renderInternalAsString(file) since we expect JSON - String result = file.exists() ? StringUtils.trimToEmpty(PageFlowUtil.getFileContentsAsString(file)) : ""; + String result = file.exists() ? StringUtils.trimToEmpty(PageFlowUtil.getStreamContentsAsString(file.openInputStream())) : ""; try { final JSONObject obj = new JSONObject(result); @@ -410,7 +410,7 @@ private void renderHeaderCell(HtmlStringBuilder sb, JSONObject cell) protected void renderInternal(Object model, PrintWriter out) throws Exception { String delim = ""; - for (File file : getFiles()) + for (FileLike file : getFiles()) { String html = renderInternalAsString(file); if (null != html) diff --git a/api/src/org/labkey/api/reports/report/r/view/JsonOutput.java b/api/src/org/labkey/api/reports/report/r/view/JsonOutput.java index 1ce444f889a..b07e48b4f5d 100644 --- a/api/src/org/labkey/api/reports/report/r/view/JsonOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/JsonOutput.java @@ -16,6 +16,7 @@ package org.labkey.api.reports.report.r.view; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.report.r.RReport; import org.labkey.api.reports.report.ScriptOutput; import org.labkey.api.reports.report.r.AbstractParamReplacement; @@ -24,8 +25,9 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; import java.io.PrintWriter; /** @@ -42,13 +44,13 @@ public JsonOutput() } @Override - protected File getSubstitution(File directory) throws Exception + protected @Nullable FileLike getSubstitution(FileLike directory) throws IOException { - File file; + FileLike file; if (directory != null) file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.json", directory); else - file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.json"); + file = FileUtil.createTempFileLike(RReport.FILE_PREFIX, "Result.json"); addFile(file); return file; @@ -61,7 +63,7 @@ public HttpView getView(ViewContext context) } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { JsonOutputView view = new JsonOutputView(this); String json = view.renderInternalAsString(file); @@ -81,18 +83,18 @@ public JsonOutputView(ParamReplacement param) } @Override - protected String renderInternalAsString(File file) + protected String renderInternalAsString(FileLike file) throws IOException { - if (exists(file)) - return PageFlowUtil.getFileContentsAsString(file); + if (file.exists()) + return PageFlowUtil.getStreamContentsAsString(file.openInputStream()); return null; } @Override - protected void renderInternal(Object model, PrintWriter out) + protected void renderInternal(Object model, PrintWriter out) throws IOException { - for (File file : getFiles()) + for (FileLike file : getFiles()) { String rawValue = renderInternalAsString(file); diff --git a/api/src/org/labkey/api/reports/report/r/view/KnitrOutput.java b/api/src/org/labkey/api/reports/report/r/view/KnitrOutput.java index 09100101c3e..a1b5c20f06c 100644 --- a/api/src/org/labkey/api/reports/report/r/view/KnitrOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/KnitrOutput.java @@ -49,7 +49,7 @@ public KnitrOutput() } @Override - public ScriptOutput renderAsScriptOutput(File file) throws Exception + public ScriptOutput renderAsScriptOutput(FileLike file) throws Exception { KnitrOutputView view = new KnitrOutputView(this, getLabel()); String html = view.renderInternalAsString(file); @@ -61,7 +61,7 @@ public ScriptOutput renderAsScriptOutput(File file) throws Exception } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { return new KnitrOutputView(this, getLabel()); } @@ -69,7 +69,7 @@ public HttpView getView(ViewContext context) @Override public @Nullable Thumbnail renderThumbnail(ViewContext context) { - for (File file : getFiles()) + for (FileLike file : getFiles()) { KnitrOutputView view = new KnitrOutputView(this, getLabel()); Thumbnail thumb = null; @@ -78,7 +78,7 @@ public HttpView getView(ViewContext context) { String html = view.renderInternalAsString(file); URI baseURI = new URI(AppProps.getInstance().getBaseServerUrl()); - if (html != null && baseURI != null) + if (html != null) thumb = ImageUtil.webThumbnail(context, html, baseURI); } catch(Exception ignore){}// if we can't get a thumbnail then that is okay; LabKey should use a default @@ -99,7 +99,7 @@ public KnitrOutputView(KnitrOutput param, String label) } @Override - protected String renderInternalAsString(File file) throws Exception + protected String renderInternalAsString(FileLike file) throws Exception { String htmlIn = super.renderInternalAsString(file); @@ -107,8 +107,7 @@ protected String renderInternalAsString(File file) throws Exception if (null != htmlIn) { // replace all ${hrefout:} with the appropriate url - FileLike reportDirFileLike = _report.getReportDirFileLike(this.getViewContext().getContainer().getId()); - File reportDir = FileSystemLike.toFile(reportDirFileLike); + FileLike reportDir = _report.getReportDirFileLike(this.getViewContext().getContainer().getId()); String htmlOut = ParamReplacementSvc.get().processHrefParamReplacement(_report, htmlIn, reportDir); htmlOut = ParamReplacementSvc.get().processRelativeHrefReplacement( _report, @@ -124,7 +123,7 @@ protected String renderInternalAsString(File file) throws Exception protected void renderInternal(Object model, PrintWriter out) throws Exception { String delim = ""; - for (File file : getFiles()) + for (FileLike file : getFiles()) { String html = renderInternalAsString(file); if (null != html) diff --git a/api/src/org/labkey/api/reports/report/r/view/PdfOutput.java b/api/src/org/labkey/api/reports/report/r/view/PdfOutput.java index 9a4426f23fa..5edf682e7e0 100644 --- a/api/src/org/labkey/api/reports/report/r/view/PdfOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/PdfOutput.java @@ -17,7 +17,6 @@ package org.labkey.api.reports.report.r.view; import org.jetbrains.annotations.Nullable; -import org.labkey.api.attachments.AttachmentParent; import org.labkey.api.attachments.DocumentConversionService; import org.labkey.api.reports.report.ScriptOutput; import org.labkey.api.reports.report.r.ParamReplacement; @@ -26,9 +25,9 @@ import org.labkey.api.view.HtmlView; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; import java.awt.image.BufferedImage; -import java.io.File; import java.io.IOException; public class PdfOutput extends DownloadParamReplacement @@ -41,25 +40,25 @@ public PdfOutput() } @Override - protected File getSubstitution(File directory) + protected @Nullable FileLike getSubstitution(FileLike directory) { return getSubstitution(directory, ".pdf"); } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { - if (getReport() instanceof AttachmentParent) - return new PdfReportView(this, getReport()); + if (getReport() != null) + return new PdfReportView(this); else return HtmlView.of(DownloadParamReplacement.UNABLE_TO_RENDER); } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { - if (getReport() instanceof AttachmentParent) - return renderAsScriptOutput(file, new PdfReportView(this, getReport()), + if (getReport() != null) + return renderAsScriptOutput(file, new PdfReportView(this), ScriptOutput.ScriptOutputType.pdf); else return renderAsScriptOutputError(); @@ -67,9 +66,9 @@ public ScriptOutput renderAsScriptOutput(File file) public static class PdfReportView extends DownloadOutputView { - PdfReportView(ParamReplacement param, AttachmentParent parent) + PdfReportView(ParamReplacement param) { - super(param, parent, "PDF"); + super(param, "PDF"); } } @@ -81,13 +80,12 @@ public static class PdfReportView extends DownloadOutputView if (null == svc) return null; - for (File file : getFiles()) + for (FileLike file : getFiles()) { // just render the first file, in most cases this is appropriate if (file.exists()) { - BufferedImage image = svc.pdfToImage(file, 0); - + BufferedImage image = svc.pdfToImage(file.toNioPathForRead().toFile(), 0); return ImageUtil.renderThumbnail(image); } } diff --git a/api/src/org/labkey/api/reports/report/r/view/PostscriptOutput.java b/api/src/org/labkey/api/reports/report/r/view/PostscriptOutput.java index c64c88feca4..8a058c24347 100644 --- a/api/src/org/labkey/api/reports/report/r/view/PostscriptOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/PostscriptOutput.java @@ -16,14 +16,15 @@ package org.labkey.api.reports.report.r.view; -import org.labkey.api.attachments.AttachmentParent; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.report.ScriptOutput; import org.labkey.api.reports.report.r.ParamReplacement; import org.labkey.api.view.HtmlView; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; /** * User: Karl Lum @@ -39,35 +40,35 @@ public PostscriptOutput() } @Override - protected File getSubstitution(File directory) + protected @Nullable FileLike getSubstitution(FileLike directory) { return getSubstitution(directory, ".ps"); } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { - if (getReport() instanceof AttachmentParent) - return renderAsScriptOutput(file, new PostscriptReportView(this, getReport()), + if (getReport() != null) + return renderAsScriptOutput(file, new PostscriptReportView(this), ScriptOutput.ScriptOutputType.postscript); else return renderAsScriptOutputError(); } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { - if (getReport() instanceof AttachmentParent) - return new PostscriptReportView(this, getReport()); + if (getReport() != null) + return new PostscriptReportView(this); else return HtmlView.of(DownloadParamReplacement.UNABLE_TO_RENDER); } public static class PostscriptReportView extends DownloadOutputView { - PostscriptReportView(ParamReplacement param, AttachmentParent parent) + PostscriptReportView(ParamReplacement param) { - super(param, parent, "Postscript"); + super(param, "Postscript"); } } } diff --git a/api/src/org/labkey/api/reports/report/r/view/ROutputView.java b/api/src/org/labkey/api/reports/report/r/view/ROutputView.java index a20d34d6ffb..71c0101625f 100644 --- a/api/src/org/labkey/api/reports/report/r/view/ROutputView.java +++ b/api/src/org/labkey/api/reports/report/r/view/ROutputView.java @@ -26,6 +26,7 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.UniqueID; import org.labkey.api.view.HttpView; +import org.labkey.vfs.FileLike; import java.io.File; import java.io.FileFilter; @@ -41,14 +42,14 @@ * User: Karl Lum * Date: May 5, 2008 */ -public class ROutputView extends HttpView +public class ROutputView extends HttpView { private String _label; private final String _name; private boolean _collapse; private boolean _showHeader; - private boolean _isRemote; - private List _files; + private final boolean _isRemote; + private final List _files; private Map _properties; protected static Logger LOG = LogManager.getLogger(ROutputView.class); private static final boolean ALLOW_REMOTE_FILESIZE_BYPASS = false; @@ -97,12 +98,12 @@ public void setShowHeader(boolean showHeader) _showHeader = showHeader; } - public List getFiles() + public List getFiles() { return _files; } - public void addFile(File file) + public void addFile(FileLike file) { _files.add(file); } @@ -122,7 +123,7 @@ protected String getUniqueId(String id) return id.concat(String.valueOf(UniqueID.getServerSessionScopedUID())); } - protected String renderInternalAsString(File file) throws Exception + protected String renderInternalAsString(FileLike file) throws Exception { return null; } @@ -148,18 +149,18 @@ protected void renderTitle(Object model, PrintWriter out) out.write(sb.toString()); } - protected File moveToTemp(File file) + protected FileLike moveToTemp(FileLike file) throws IOException { - File root = ScriptEngineReport.getTempRoot(ReportService.get().createDescriptorInstance(RReportDescriptor.TYPE)); + FileLike root = ScriptEngineReport.getTempRootFileLike(ReportService.get().createDescriptorInstance(RReportDescriptor.TYPE)); - File newFile = FileUtil.appendName(root, FileUtil.makeFileNameWithTimestamp(FileUtil.getBaseName(file.getName()), FileUtil.getExtension(file))); + FileLike newFile = root.resolveChild(FileUtil.makeFileNameWithTimestamp(FileUtil.getBaseName(file.getName()), FileUtil.getExtension(file))); newFile.delete(); - LOG.debug("Moving '" + file.getAbsolutePath() + "' to '" + newFile.getAbsolutePath() + "'"); + LOG.debug("Moving '" + file + "' to '" + newFile + "'"); if (file.renameTo(newFile)) return newFile; - LOG.debug("Failed to move " + file.getAbsolutePath() + "' to '" + newFile.getAbsolutePath() + "'"); + LOG.debug("Failed to move " + file + "' to '" + newFile + "'"); return null; } diff --git a/api/src/org/labkey/api/reports/report/r/view/SvgOutput.java b/api/src/org/labkey/api/reports/report/r/view/SvgOutput.java index 3fc26942c73..9540c04b7dd 100644 --- a/api/src/org/labkey/api/reports/report/r/view/SvgOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/SvgOutput.java @@ -24,8 +24,8 @@ import org.labkey.api.thumbnail.ThumbnailOutputStream; import org.labkey.api.thumbnail.ThumbnailService.ImageType; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; import java.io.IOException; /** @@ -57,11 +57,11 @@ public Thumbnail renderThumbnail(ViewContext context) throws IOException { ThumbnailOutputStream os = new ThumbnailOutputStream(); - for (File file : getFiles()) + for (FileLike file : getFiles()) { try { - svc.svgToPng(SvgSource.of(Readers.getXmlReader(file)), os, ImageType.Large.getHeight()); + svc.svgToPng(SvgSource.of(Readers.getXmlReader(file.openInputStream())), os, ImageType.Large.getHeight()); return os.getThumbnail("image/png"); } diff --git a/api/src/org/labkey/api/reports/report/r/view/TextOutput.java b/api/src/org/labkey/api/reports/report/r/view/TextOutput.java index a2e8f5c85ad..5b60f8d8144 100644 --- a/api/src/org/labkey/api/reports/report/r/view/TextOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/TextOutput.java @@ -16,6 +16,7 @@ package org.labkey.api.reports.report.r.view; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reports.report.r.RReport; import org.labkey.api.reports.report.ScriptOutput; import org.labkey.api.reports.report.r.AbstractParamReplacement; @@ -24,8 +25,9 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; import java.io.PrintWriter; /** @@ -42,26 +44,26 @@ public TextOutput() } @Override - protected File getSubstitution(File directory) throws Exception + protected @Nullable FileLike getSubstitution(FileLike directory) throws IOException { - File file; + FileLike file; if (directory != null) file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.txt", directory); else - file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.txt"); + file = FileUtil.createTempFileLike(RReport.FILE_PREFIX, "Result.txt"); addFile(file); return file; } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { return new TextOutputView(this); } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { TextOutputView view = new TextOutputView(this); String text = view.renderInternalAsString(file); @@ -81,18 +83,18 @@ public TextOutputView(ParamReplacement param) } @Override - protected String renderInternalAsString(File file) + protected String renderInternalAsString(FileLike file) throws IOException { - if (exists(file)) - return PageFlowUtil.getFileContentsAsString(file); + if (file.exists()) + return PageFlowUtil.getStreamContentsAsString(file.openInputStream()); return null; } @Override - protected void renderInternal(Object model, PrintWriter out) + protected void renderInternal(Object model, PrintWriter out) throws IOException { - for (File file : getFiles()) + for (FileLike file : getFiles()) { String rawValue = renderInternalAsString(file); diff --git a/api/src/org/labkey/api/reports/report/r/view/TsvOutput.java b/api/src/org/labkey/api/reports/report/r/view/TsvOutput.java index 573b767b2e8..3528e1a624f 100644 --- a/api/src/org/labkey/api/reports/report/r/view/TsvOutput.java +++ b/api/src/org/labkey/api/reports/report/r/view/TsvOutput.java @@ -17,6 +17,7 @@ package org.labkey.api.reports.report.r.view; import org.apache.commons.beanutils.ConvertUtils; +import org.jetbrains.annotations.Nullable; import org.labkey.api.reader.ColumnDescriptor; import org.labkey.api.reader.TabLoader; import org.labkey.api.reports.report.r.RReport; @@ -27,8 +28,9 @@ import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.HttpView; import org.labkey.api.view.ViewContext; +import org.labkey.vfs.FileLike; -import java.io.File; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; @@ -49,20 +51,20 @@ public TsvOutput() } @Override - protected File getSubstitution(File directory) throws Exception + protected @Nullable FileLike getSubstitution(FileLike directory) throws IOException { - File file; + FileLike file; if (directory != null) file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.tsv", directory); else - file = FileUtil.createTempFile(RReport.FILE_PREFIX, "Result.tsv"); + file = FileUtil.createTempFileLike(RReport.FILE_PREFIX, "Result.tsv"); addFile(file); return file; } @Override - public ScriptOutput renderAsScriptOutput(File file) + public ScriptOutput renderAsScriptOutput(FileLike file) throws IOException { TabReportView view = new TabReportView(this); String tsv = view.renderInternalAsString(file); @@ -74,16 +76,16 @@ public ScriptOutput renderAsScriptOutput(File file) } @Override - public HttpView getView(ViewContext context) + public HttpView getView(ViewContext context) { return new TabReportView(this); } - public static TabLoader createTabLoader(File file) + public static TabLoader createTabLoader(FileLike file) throws IOException { - if (file != null && file.exists() && (file.length() > 0)) + if (file != null && file.exists() && (file.getSize() > 0)) { - TabLoader tabLoader = new TabLoader(file, true) { + TabLoader tabLoader = new TabLoader(file.openInputStream(), true, null) { @Override protected String getDefaultColumnName(int col) { @@ -108,10 +110,10 @@ public static class TabReportView extends ROutputView } @Override - protected String renderInternalAsString(File file) + protected String renderInternalAsString(FileLike file) throws IOException { - if (exists(file)) - return PageFlowUtil.getFileContentsAsString(file); + if (file.exists()) + return PageFlowUtil.getStreamContentsAsString(file.openInputStream()); return null; } @@ -119,7 +121,7 @@ protected String renderInternalAsString(File file) @Override protected void renderInternal(Object model, PrintWriter out) throws Exception { - for (File file : getFiles()) + for (FileLike file : getFiles()) { TabLoader tabLoader = createTabLoader(file); if (tabLoader != null) @@ -167,7 +169,7 @@ protected void renderInternal(Object model, PrintWriter out) throws Exception out.write(""); for (ColumnDescriptor col : display) { - if (Number.class.isAssignableFrom(col.getClass())) + if (Number.class.isAssignableFrom(col.getDataClass())) out.write(""); else out.write(""); @@ -177,7 +179,7 @@ protected void renderInternal(Object model, PrintWriter out) throws Exception } out.write(""); - for (Map m : data) + for (Map m : data) { if (row % 2 == 0) out.write(""); diff --git a/api/src/org/labkey/api/reports/report/view/RenderBackgroundRReportView.java b/api/src/org/labkey/api/reports/report/view/RenderBackgroundRReportView.java index 7884a3c13b0..0c4ad9c9d41 100644 --- a/api/src/org/labkey/api/reports/report/view/RenderBackgroundRReportView.java +++ b/api/src/org/labkey/api/reports/report/view/RenderBackgroundRReportView.java @@ -23,7 +23,6 @@ import org.labkey.api.view.JspView; import org.labkey.api.view.VBox; import org.labkey.vfs.FileLike; -import org.labkey.vfs.FileSystemLike; import java.io.PrintWriter; import java.util.Collection; @@ -32,7 +31,7 @@ * User: Karl Lum * Date: Feb 21, 2008 */ -public class RenderBackgroundRReportView extends HttpView +public class RenderBackgroundRReportView extends HttpView { private final RReport _report; @@ -54,10 +53,10 @@ protected void renderInternal(Object model, PrintWriter out) throws Exception // if the job is complete, show the results of the job if (substitutionMap.exists()) { - Collection outputSubst = ParamReplacementSvc.get().fromFile(FileSystemLike.toFile(substitutionMap)); + Collection outputSubst = ParamReplacementSvc.get().fromFile(substitutionMap); VBox innerView = new VBox(); view.addView(innerView); - RReport.renderViews(_report, view, outputSubst, false); + RReport.renderViews(_report, view, outputSubst); } include(view); } diff --git a/api/src/org/labkey/api/study/SpecimenTransform.java b/api/src/org/labkey/api/study/SpecimenTransform.java index e62f3af113e..9573eaa4c90 100644 --- a/api/src/org/labkey/api/study/SpecimenTransform.java +++ b/api/src/org/labkey/api/study/SpecimenTransform.java @@ -25,9 +25,6 @@ import org.labkey.api.view.ActionURL; import org.labkey.vfs.FileLike; -import java.io.File; -import java.nio.file.Path; - /** * User: klum * Date: 11/12/13 @@ -54,12 +51,12 @@ public interface SpecimenTransform */ FileType getFileType(); - void transform(@Nullable PipelineJob job, Path input, Path outputArchive) throws PipelineJobException; + void transform(@Nullable PipelineJob job, FileLike input, FileLike outputArchive) throws PipelineJobException; /** * An optional post transform step. */ - void postTransform(@Nullable PipelineJob job, File input, File outputArchive) throws PipelineJobException; + void postTransform(@Nullable PipelineJob job, FileLike input, FileLike outputArchive) throws PipelineJobException; @Nullable ActionURL getManageAction(Container c, User user); diff --git a/api/src/org/labkey/api/util/FileType.java b/api/src/org/labkey/api/util/FileType.java index d874803027c..b968ccf776d 100644 --- a/api/src/org/labkey/api/util/FileType.java +++ b/api/src/org/labkey/api/util/FileType.java @@ -51,14 +51,14 @@ public class FileType implements Serializable // For serialization protected FileType() {} - public File findInputFile(FileAnalysisJobSupport support, String baseName) + public FileLike findInputFile(FileAnalysisJobSupport support, String baseName) { if (_suffixes.size() > 1) { for (String suffix : _suffixes) { - File f = support.findInputFile(baseName + suffix); - if (f != null && NetworkDrive.exists(f)) + FileLike f = support.findInputFile(baseName + suffix); + if (NetworkDrive.exists(f)) { return f; } @@ -264,8 +264,8 @@ private String tryName(Path parentDir, String name) return name; } - /** Uses the preferred suffix, useful when there's not a directory of existing files to reference */ - /** if _preferGZ is set, will use preferred suffix.gz since TPP treats .gz as native format, + /** Uses the preferred suffix, useful when there's not a directory of existing files to reference. + * if _preferGZ is set, will use preferred suffix.gz since TPP treats .gz as native format, * unless non-gz file exists */ public String getDefaultName(String basename) { @@ -288,10 +288,7 @@ public boolean supportGZ(gzSupportLevel doSupportGZ) public int addSuffix(String newsuffix) { List s = new ArrayList<>(_suffixes.size()+1); - for (String suffix : _suffixes) - { - s.add(suffix); - } + s.addAll(_suffixes); s.add(newsuffix); _suffixes = s; return _suffixes.size(); @@ -303,10 +300,7 @@ public int addSuffix(String newsuffix) public int addAntiFileType(FileType anti) { List s = new ArrayList<>(_antiTypes.size()+1); - for (FileType a : _antiTypes) - { - s.add(a); - } + s.addAll(_antiTypes); s.add(anti); _antiTypes = s; return _antiTypes.size(); @@ -371,15 +365,9 @@ public String getName(String parentDirName, String basename) * Looks for a file in the parentDir that matches, in priority order. If one is found, returns its file name. * If nothing matches, uses the defaultSuffix to build a file name. */ - @Deprecated //please switch to using the nio.Path version as the File class can have issues using full URIs - public File getFile(File parentDir, String basename) - { - return new File(parentDir, getName(parentDir, basename)); - } - - public Path getPath(Path parentDir, String basename) + public FileLike getFile(FileLike parentDir, String basename) { - return FileUtil.appendName(parentDir, getName(parentDir, basename)); + return parentDir.resolveChild(getName(parentDir, basename)); } /** @@ -532,7 +520,7 @@ public boolean isType(String filePath) /** * Checks if the path matches any of the suffixes and the file header if provided. */ - public boolean isType(@Nullable String filePath, @Nullable String contentType, @Nullable byte[] header) + public boolean isType(@Nullable String filePath, @Nullable String contentType, byte @Nullable[] header) { String providedContentType = contentType; // Save it for later @@ -624,7 +612,7 @@ public boolean isMatch(String name, String basename) * @param header First few K of the file. * @return True if the header matches, false otherwise. */ - public boolean isHeaderMatch(@NotNull byte[] header) + public boolean isHeaderMatch(byte @NotNull [] header) { return false; } @@ -711,25 +699,6 @@ public void setExtensionsMutuallyExclusive(boolean extensionsMutuallyExclusive) _extensionsMutuallyExclusive = extensionsMutuallyExclusive; } - /** - * @return a FileType that will only match on the default suffix for this FileType - */ - public FileType getDefaultFileType() - { - if (!_suffixes.isEmpty()) - { - FileType ft = new FileType(_defaultSuffix); - ft._dir = _dir; - ft._supportGZ = _supportGZ.booleanValue(); - ft._preferGZ = _preferGZ.booleanValue(); - return ft; - } - else - { - return this; - } - } - public String getDefaultRole() { if (_defaultSuffix.contains(".")) @@ -762,7 +731,7 @@ public void test() // simple case FileType ft = new FileType(".foo"); assertTrue(ft.isType("test.foo")); - assertTrue(!ft.isType("test.foo.gz")); + assertFalse(ft.isType("test.foo.gz")); assertEquals("test.foo",ft.getDefaultName("test")); // support for .gz @@ -792,7 +761,7 @@ public void test() // antitypes - for example avoid mistaking protxml ".pep-prot.xml" for pepxml ".xml" assertTrue(ftt.isType("test.foo.bar")); ftt.addAntiFileType(new FileType(".foo.bar")); - assertTrue(!ftt.isType("test.foo.bar")); + assertFalse(ftt.isType("test.foo.bar")); assertTrue(ftt.isType("test.foo")); assertTrue(ftt.isType("test.bar")); diff --git a/api/src/org/labkey/api/util/FileUtil.java b/api/src/org/labkey/api/util/FileUtil.java index fd358042a19..89a2a4f8078 100644 --- a/api/src/org/labkey/api/util/FileUtil.java +++ b/api/src/org/labkey/api/util/FileUtil.java @@ -64,6 +64,7 @@ import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.ReadableByteChannel; +import java.nio.file.CopyOption; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -304,6 +305,11 @@ public static boolean deleteDir(@NotNull Path dir) throws IOException return true; } + public static void copyDirectory(FileLike src, FileLike dest) throws IOException + { + copyDirectory(src.toNioPathForRead(), dest.toNioPathForWrite()); + } + public static void copyDirectory(Path srcPath, Path destPath) throws IOException { @@ -514,7 +520,6 @@ public static Path createDirectories(Path path, boolean checkFileName) throws IO public static boolean renameTo(FileLike from, FileLike to) { - // TODO FileLike.renameTo() return toFileForRead(from).renameTo(toFileForWrite(to)); } @@ -654,6 +659,16 @@ public static String getExtension(File file) return getExtension(file.getName()); } + /** + * Returns the file name extension without the dot, null if there + * isn't one. + */ + @Nullable + public static String getExtension(FileLike file) + { + return getExtension(file.getName()); + } + /** * Returns the file name extension without the dot, null if there @@ -997,6 +1012,11 @@ private static String getPathStringWithoutAccessId(URI uri) } + public static String relativize(FileLike home, FileLike file, boolean canonicalize) throws IOException + { + return relativize(home.toNioPathForRead().toFile(), file.toNioPathForRead().toFile(), canonicalize); + } + /** * Get relative path of File 'file' with respect to 'home' directory *

@@ -1121,16 +1141,11 @@ public static String matchPathLists(List home, List file)
         return path.toString();
     }
 
-    public static void copyFile(FileLike src, FileLike dst) throws IOException
+    public static void copyFile(FileLike src, FileLike dst, CopyOption... options) throws IOException
     {
-        try (InputStream in = src.openInputStream();
-            OutputStream out = dst.openOutputStream())
-        {
-            copyData(in, out);
-        }
+        Files.copy(src.toNioPathForRead(), dst.toNioPathForWrite(), options);
     }
 
-
     public static void copyFile(File src, File dst) throws IOException
     {
         try (FileInputStream is = new FileInputStream(src);
@@ -1805,6 +1820,11 @@ public static boolean createTempFile(File file) throws IOException
         return true;
     }
 
+    public static boolean createTempFile(FileLike file) throws IOException
+    {
+        return createTempFile(file.toNioPathForWrite().toFile());
+    }
+
 
     public static void deleteTempFile(File f)
     {
diff --git a/api/src/org/labkey/api/util/HashHelpers.java b/api/src/org/labkey/api/util/HashHelpers.java
index a3dc564f380..78ed11f308f 100644
--- a/api/src/org/labkey/api/util/HashHelpers.java
+++ b/api/src/org/labkey/api/util/HashHelpers.java
@@ -15,9 +15,9 @@
  */
 package org.labkey.api.util;
 
+import org.labkey.vfs.FileLike;
+
 import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.MessageDigest;
@@ -31,14 +31,12 @@ public class HashHelpers
 {
     private static final int BYTE_ARRAY_SIZE = 4096;
 
-    public static String hashFileContents(File file)  throws IOException
+    public static String hashFileContents(FileLike file)  throws IOException
     {
-        InputStream is = null;
-        try
+        try (InputStream is = new BufferedInputStream(file.openInputStream()))
         {
             MessageDigest md = MessageDigest.getInstance("SHA");
             byte[] byteArray = new byte[BYTE_ARRAY_SIZE];
-            is = new BufferedInputStream(new FileInputStream(file));
             int len;
             while ((len = is.read(byteArray)) > 0)
                md.update(byteArray, 0, len);
@@ -48,11 +46,6 @@ public static String hashFileContents(File file)  throws IOException
         {
             throw new RuntimeException(e);
         }
-        finally
-        {
-            if (is != null)
-                try { is.close(); } catch (IOException e) {}
-        }
     }
 
     public static String hash(String data)
diff --git a/api/src/org/labkey/api/util/ImageUtil.java b/api/src/org/labkey/api/util/ImageUtil.java
index e76a8e7ec51..778d0ffe583 100644
--- a/api/src/org/labkey/api/util/ImageUtil.java
+++ b/api/src/org/labkey/api/util/ImageUtil.java
@@ -27,6 +27,7 @@
 import org.labkey.api.thumbnail.ThumbnailOutputStream;
 import org.labkey.api.thumbnail.ThumbnailService.ImageType;
 import org.labkey.api.view.ViewContext;
+import org.labkey.vfs.FileLike;
 import org.w3c.dom.Document;
 import org.xhtmlrenderer.resource.ImageResource;
 import org.xhtmlrenderer.swing.Java2DRenderer;
@@ -270,7 +271,7 @@ public static File getFileFromSession(HttpServletRequest request, String key)
      * Adds a file to the request session and returns the generated session attribute key.
      */
     @Nullable
-    public static String setFileInSession(HttpServletRequest request, File file)
+    public static String setFileInSession(HttpServletRequest request, FileLike file)
     {
         if (file != null && file.exists())
         {
diff --git a/api/src/org/labkey/api/util/MaintenancePipelineJob.java b/api/src/org/labkey/api/util/MaintenancePipelineJob.java
index ae0d00215ef..360660b1fcf 100644
--- a/api/src/org/labkey/api/util/MaintenancePipelineJob.java
+++ b/api/src/org/labkey/api/util/MaintenancePipelineJob.java
@@ -42,7 +42,7 @@ protected MaintenancePipelineJob(@JsonProperty("_tasks") Collection tasks)
     {
         super("SystemMaintenance", info, pipeRoot);
-        setLogFile(pipeRoot.getLogDirectoryFileLike(true).resolveChild(FileUtil.makeFileNameWithTimestamp("system_maintenance", "log")));
+        setLogFile(pipeRoot.getLogDirectory(true).resolveChild(FileUtil.makeFileNameWithTimestamp("system_maintenance", "log")));
         _tasks = tasks;
     }
 
diff --git a/api/src/org/labkey/api/writer/ZipUtil.java b/api/src/org/labkey/api/writer/ZipUtil.java
index 5bafbf1c8c4..3f5d43e7e71 100644
--- a/api/src/org/labkey/api/writer/ZipUtil.java
+++ b/api/src/org/labkey/api/writer/ZipUtil.java
@@ -56,7 +56,12 @@ public static List unzipToDirectory(Path zipFile, Path unzipDir) throws IO
 
     public static List unzipToDirectory(FileLike zipFile, FileLike unzipDir) throws IOException
     {
-        List paths = unzipToDirectory(zipFile.toNioPathForRead(), unzipDir.toNioPathForWrite(), null);
+        return unzipToDirectory(zipFile, unzipDir, null);
+    }
+
+        public static List unzipToDirectory(FileLike zipFile, FileLike unzipDir, @Nullable Logger logger) throws IOException
+    {
+        List paths = unzipToDirectory(zipFile.toNioPathForRead(), unzipDir.toNioPathForWrite(), logger);
         File rootFile = unzipDir.toNioPathForRead().toFile();
         List result = new ArrayList<>();
         for (Path path : paths)
diff --git a/api/src/org/labkey/vfs/FileLike.java b/api/src/org/labkey/vfs/FileLike.java
index 56854a42eb1..78639f02774 100644
--- a/api/src/org/labkey/vfs/FileLike.java
+++ b/api/src/org/labkey/vfs/FileLike.java
@@ -13,6 +13,7 @@
 import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 import org.jetbrains.annotations.NotNull;
+import org.labkey.api.util.FileUtil;
 import org.labkey.api.util.Path;
 import org.labkey.api.view.UnauthorizedException;
 
@@ -20,6 +21,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
 import java.util.List;
 import java.util.function.Predicate;
@@ -47,6 +49,11 @@ default URI toURI()
         return getFileSystem().getURI(this);
     }
 
+    default boolean renameTo(FileLike target)
+    {
+        return FileUtil.renameTo(this, target);
+    }
+
     default boolean isDescendant(URI uri)
     {
         return getFileSystem().isDescendant(this, uri);
@@ -123,10 +130,23 @@ default List getChildren(Predicate filter)
 
     long getLastModified();
 
-    OutputStream openOutputStream() throws IOException;
+    default OutputStream openOutputStream() throws IOException
+    {
+        return openOutputStream(false);
+    }
+
+    OutputStream openOutputStream(boolean append) throws IOException;
 
     InputStream openInputStream() throws IOException;
 
+    default void move(FileLike dest) throws IOException
+    {
+        if (!renameTo(dest))
+        {
+            Files.move(this.toNioPathForRead(), dest.toNioPathForWrite());
+        }
+    }
+
     class FileLikeSerializer extends StdSerializer
     {
         public FileLikeSerializer()
diff --git a/api/src/org/labkey/vfs/FileSystemLike.java b/api/src/org/labkey/vfs/FileSystemLike.java
index 14d9d0edcdd..ef5f63a2d6a 100644
--- a/api/src/org/labkey/vfs/FileSystemLike.java
+++ b/api/src/org/labkey/vfs/FileSystemLike.java
@@ -17,6 +17,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.net.URI;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -272,6 +273,20 @@ static FileLike wrapFile(File root, File f) throws IOException
         return fs.getRoot().resolveFile(Path.parse(rel));
     }
 
+    /** Helper for partially converted code. root must exist. */
+    static FileLike wrapFile(java.nio.file.Path root, java.nio.file.Path f) throws IOException
+    {
+        if (!Files.isDirectory(root))
+            throw new FileNotFoundException(root.toString());
+        FileSystemLike fs = new Builder(root.toUri()).build();
+        URI relative = URIUtil.relativize(root.toUri(), f.toUri());
+        if (null == relative)
+        {
+            throw new IOException("File '" + f.toUri().getPath() + "' is outside the allowed root '" + root.toUri().getPath() + "'");
+        }
+        return fs.resolveFile(new Path(relative.getPath()));
+    }
+
     /** Helper for partially converted code. May throw if the FileLike does not wrap a local file system. */
     static File toFile(FileLike f)
     {
diff --git a/api/src/org/labkey/vfs/FileSystemLocal.java b/api/src/org/labkey/vfs/FileSystemLocal.java
index 284b9173b29..6f2c4313ec3 100644
--- a/api/src/org/labkey/vfs/FileSystemLocal.java
+++ b/api/src/org/labkey/vfs/FileSystemLocal.java
@@ -220,11 +220,11 @@ public long getLastModified()
         }
 
         @Override
-        public OutputStream openOutputStream() throws IOException
+        public OutputStream openOutputStream(boolean append) throws IOException
         {
             if (!canWriteFiles())
                 throw new UnauthorizedException();
-            return new FileOutputStream(file);
+            return new FileOutputStream(file, append);
         }
 
         @Override
diff --git a/api/src/org/labkey/vfs/FileSystemVFS.java b/api/src/org/labkey/vfs/FileSystemVFS.java
index 71fe21c406d..1465cf77309 100644
--- a/api/src/org/labkey/vfs/FileSystemVFS.java
+++ b/api/src/org/labkey/vfs/FileSystemVFS.java
@@ -261,10 +261,12 @@ public long getLastModified()
         }
 
         @Override
-        public OutputStream openOutputStream() throws IOException
+        public OutputStream openOutputStream(boolean append) throws IOException
         {
             if (!canWriteFiles())
                 throw new UnauthorizedException();
+            if (append)
+                throw new UnsupportedOperationException("Append not supported");
             return getContent().getOutputStream();
         }
 
diff --git a/assay/src/org/labkey/assay/pipeline/AssayImportRunTask.java b/assay/src/org/labkey/assay/pipeline/AssayImportRunTask.java
index 97a69e27694..736481badb4 100644
--- a/assay/src/org/labkey/assay/pipeline/AssayImportRunTask.java
+++ b/assay/src/org/labkey/assay/pipeline/AssayImportRunTask.java
@@ -143,29 +143,11 @@ public String getStatusName()
         }
 
         @Override
-        public boolean isJobComplete(PipelineJob job)
-        {
-            return false;
-        }
-
-        @Override
-        public PipelineJob.Task createTask(PipelineJob job)
+        public AssayImportRunTask createTask(PipelineJob job)
         {
             return new AssayImportRunTask(this, job);
         }
 
-        @Override
-        public List getProtocolActionNames()
-        {
-            return Collections.emptyList();
-        }
-
-        @Override
-        public List getInputTypes()
-        {
-            return Collections.emptyList();
-        }
-
         @Override
         protected @NotNull ExpProtocol getProtocol(PipelineJob job, AssayProvider provider) throws PipelineJobException
         {
@@ -233,17 +215,17 @@ else if (params.containsKey(PROTOCOL_NAME_KEY))
          * triggered location
          */
         @Override
-        List getOutputs(PipelineJob job) throws PipelineJobException
+        List getOutputs(PipelineJob job)
         {
             List outputs = new ArrayList<>();
-            File dataFile = getDataFile(job);
+            FileLike dataFile = getDataFile(job);
             job.getLogger().info("Importing output data file : " + dataFile.getName());
             outputs.add(new RecordedAction.DataFile(dataFile.toURI(), "RESULTS-DATA", false, false));
 
             return outputs;
         }
 
-        private File getDataFile(PipelineJob job)
+        private FileLike getDataFile(PipelineJob job)
         {
             FileAnalysisJobSupport support = job.getJobSupport(FileAnalysisJobSupport.class);
 
@@ -256,14 +238,14 @@ private File getDataFile(PipelineJob job)
         @Nullable
         List> getRawData(PipelineJob job) throws PipelineJobException
         {
-            File dataFile = getDataFile(job);
+            FileLike dataFile = getDataFile(job);
             try
             {
                 if (ExcelLoader.isExcel(dataFile))
                 {
                     job.getLogger().info("Processing excel file: " + dataFile.getName());
                     // check to see if this is a multi-sheet format
-                    try (ExcelLoader loader = new ExcelLoader(dataFile, true))
+                    try (ExcelLoader loader = new ExcelLoader(dataFile.openInputStream(), true, null))
                     {
                         List sheets = loader.getSheetNames();
                         if (sheets.size() > 1)
@@ -286,12 +268,12 @@ List> getRawData(PipelineJob job) throws PipelineJobExceptio
                 else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
                 {
                     ensureExplodedZip(job, dataFile);
-                    File dir = getExplodedZipDir(job, dataFile);
-                    File[] results = dir.listFiles((dir1, name) -> RESULTS_NAME.equalsIgnoreCase(FileUtil.getBaseName(name)));
+                    FileLike dir = getExplodedZipDir(job, dataFile);
+                    List results = dir.getChildren((f) -> RESULTS_NAME.equalsIgnoreCase(FileUtil.getBaseName(f)));
 
-                    if (results != null && results.length == 1)
+                    if (results.size() == 1)
                     {
-                        File resultFile = results[0];
+                        FileLike resultFile = results.get(0);
                         job.getLogger().info("Found results file named : " + resultFile + ", loading into results data.");
                         try (DataLoader loader = DataLoaderService.get().createLoader(resultFile, null, true, null, null))
                         {
@@ -310,7 +292,7 @@ else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
         @Override
         @NotNull Map getBatchProperties(PipelineJob job) throws PipelineJobException
         {
-            File dataFile = getDataFile(job);
+            FileLike dataFile = getDataFile(job);
             try
             {
                 if (ExcelLoader.isExcel(dataFile))
@@ -320,11 +302,11 @@ else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
                 else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
                 {
                     ensureExplodedZip(job, dataFile);
-                    File dir = getExplodedZipDir(job, dataFile);
-                    File[] results = dir.listFiles((dir1, name) -> BATCH_PROPS_NAME.equalsIgnoreCase(FileUtil.getBaseName(name)));
-                    if (results != null && results.length == 1)
+                    FileLike dir = getExplodedZipDir(job, dataFile);
+                    List results = dir.getChildren((f) -> BATCH_PROPS_NAME.equalsIgnoreCase(FileUtil.getBaseName(f)));
+                    if (results.size() == 1)
                     {
-                        File resultFile = results[0];
+                        FileLike resultFile = results.get(0);
                         job.getLogger().info("Found batch properties file named : " + resultFile + ", loading into results data.");
                         try (DataLoader loader = DataLoaderService.get().createLoader(resultFile, null, true, null, null))
                         {
@@ -343,7 +325,7 @@ else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
         @Override
         @NotNull Map getRunProperties(PipelineJob job) throws PipelineJobException
         {
-            File dataFile = getDataFile(job);
+            FileLike dataFile = getDataFile(job);
             try
             {
                 if (ExcelLoader.isExcel(dataFile))
@@ -353,11 +335,11 @@ else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
                 else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
                 {
                     ensureExplodedZip(job, dataFile);
-                    File dir = getExplodedZipDir(job, dataFile);
-                    File[] results = dir.listFiles((dir1, name) -> RUN_PROPS_NAME.equalsIgnoreCase(FileUtil.getBaseName(name)));
-                    if (results != null && results.length == 1)
+                    FileLike dir = getExplodedZipDir(job, dataFile);
+                    List results = dir.getChildren((f) -> RUN_PROPS_NAME.equalsIgnoreCase(FileUtil.getBaseName(f)));
+                    if (results.size() == 1)
                     {
-                        File resultFile = results[0];
+                        FileLike resultFile = results.get(0);
                         job.getLogger().info("Found run properties file named : " + resultFile + ", loading into results data.");
                         try (DataLoader loader = DataLoaderService.get().createLoader(resultFile, null, true, null, null))
                         {
@@ -376,17 +358,17 @@ else if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
         @Override
         void cleanUp(PipelineJob job)
         {
-            File dataFile = getDataFile(job);
+            FileLike dataFile = getDataFile(job);
             if ("zip".equalsIgnoreCase(FileUtil.getExtension(dataFile)))
             {
-                File dir = getExplodedZipDir(job, dataFile);
+                FileLike dir = getExplodedZipDir(job, dataFile);
                 FileUtil.deleteDir(dir, job.getLogger());
             }
         }
 
-        private Map loadProperties(File dataFile, String sheetName, Logger log) throws PipelineJobException
+        private Map loadProperties(FileLike dataFile, String sheetName, Logger log) throws PipelineJobException
         {
-            try (ExcelLoader loader = new ExcelLoader(dataFile, true))
+            try (ExcelLoader loader = new ExcelLoader(dataFile.openInputStream(), true, null))
             {
                 if (loader.getSheetNames().contains(sheetName))
                 {
@@ -417,9 +399,9 @@ private Map loadProperties(DataLoader loader) throws PipelineJob
             return properties;
         }
 
-        private void ensureExplodedZip(PipelineJob job, File dataFile) throws PipelineJobException
+        private void ensureExplodedZip(PipelineJob job, FileLike dataFile) throws PipelineJobException
         {
-            File explodedDir = getExplodedZipDir(job, dataFile);
+            FileLike explodedDir = getExplodedZipDir(job, dataFile);
             if (!explodedDir.exists())
             {
                 try
@@ -433,10 +415,10 @@ private void ensureExplodedZip(PipelineJob job, File dataFile) throws PipelineJo
             }
         }
 
-        private File getExplodedZipDir(PipelineJob job, File dataFile)
+        private FileLike getExplodedZipDir(PipelineJob job, FileLike dataFile)
         {
-            File analysisDir = ((AbstractFileAnalysisJob)job).getAnalysisDirectory();
-            return FileUtil.appendName(analysisDir, String.format("%s-expanded", dataFile.getName()));
+            FileLike analysisDir = ((AbstractFileAnalysisJob)job).getAnalysisDirectory();
+            return analysisDir.resolveChild(String.format("%s-expanded", dataFile.getName()));
         }
     }
 
@@ -475,7 +457,7 @@ protected void configure(AssayImportRunTaskFactorySettings settings)
         }
 
         @Override
-        public PipelineJob.Task createTask(PipelineJob job)
+        public AssayImportRunTask createTask(PipelineJob job)
         {
             return new AssayImportRunTask(this, job);
         }
@@ -592,7 +574,7 @@ protected ExpProtocol getProtocol(PipelineJob job, AssayProvider provider) throw
             throw new PipelineJobException("Assay protocol not found: " + protocolName);
         }
 
-        List getOutputs(PipelineJob job) throws PipelineJobException
+        List getOutputs(PipelineJob job)
         {
             return Collections.emptyList();
         }
@@ -615,7 +597,7 @@ Map getRunProperties(PipelineJob job) throws PipelineJobExceptio
             return Collections.emptyMap();
         }
 
-        void cleanUp(PipelineJob job) throws PipelineJobException
+        void cleanUp(PipelineJob job)
         {
         }
     }
@@ -860,7 +842,7 @@ public RecordedActionSet run() throws PipelineJobException
 
             if (getJob() instanceof FileAnalysisJobSupport)
             {
-                File analysisDir = getJob().getJobSupport(FileAnalysisJobSupport.class).getAnalysisDirectory();
+                FileLike analysisDir = getJob().getJobSupport(FileAnalysisJobSupport.class).getAnalysisDirectory();
                 run.setFilePathRoot(analysisDir);
             }
 
diff --git a/core/src/org/labkey/core/admin/sitevalidation/SiteValidationJob.java b/core/src/org/labkey/core/admin/sitevalidation/SiteValidationJob.java
index 81f1aed76c0..1266489b765 100644
--- a/core/src/org/labkey/core/admin/sitevalidation/SiteValidationJob.java
+++ b/core/src/org/labkey/core/admin/sitevalidation/SiteValidationJob.java
@@ -16,8 +16,8 @@
 import org.labkey.api.view.ViewContext;
 import org.labkey.core.admin.AdminController.SiteValidationForm;
 import org.labkey.core.admin.AdminController.ViewValidationResultsAction;
+import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.io.PrintWriter;
 
 public class SiteValidationJob extends PipelineJob
@@ -33,7 +33,7 @@ protected SiteValidationJob(@JsonProperty("_form") SiteValidationForm form)
     public SiteValidationJob(ViewBackgroundInfo info, PipeRoot pipeRoot, SiteValidationForm form)
     {
         super("SiteValidation", info, pipeRoot);
-        setLogFile(FileUtil.appendName(pipeRoot.getLogDirectory(), FileUtil.makeFileNameWithTimestamp("site_validation", "log")).toPath());
+        setLogFile(pipeRoot.getLogDirectory(true).resolveChild(FileUtil.makeFileNameWithTimestamp("site_validation", "log")));
         _form = form;
     }
 
@@ -67,9 +67,9 @@ public void run()
                 getInfo().getURL() == null ? AppProps.getInstance().getHomePageActionURL() : getInfo().getURL());
         ViewContext context = new ViewContext(info);
         template.setViewContext(context);
-        File results = FileUtil.appendName(getPipeRoot().getLogDirectory(), getResultsFileName());
+        FileLike results = getPipeRoot().getLogDirectory(true).resolveChild(getResultsFileName());
 
-        try (PrintWriter out = new PrintWriter(results, StringUtilsLabKey.DEFAULT_CHARSET))
+        try (PrintWriter out = new PrintWriter(results.openOutputStream(), false, StringUtilsLabKey.DEFAULT_CHARSET))
         {
             out.println(template.render());
         }
diff --git a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java
index 09ab704bce0..07903ac9d0c 100644
--- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java
+++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java
@@ -330,7 +330,6 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -2491,7 +2490,7 @@ protected ModelAndView getDataView(DataForm form, BindException errors) throws I
             }
 
             PipeRoot root = PipelineService.get().findPipelineRoot(getContainer());
-            if (root != null && !root.isUnderRoot(_data.getFilePath()))
+            if (root != null && !root.isUnderRoot(_data.getFileLike()))
             {
                 // Issue 35649: ImmPort module "publish" creates exp.data object in this container for paths that originate in a different container
                 FileContentService fileSvc = FileContentService.get();
@@ -6786,7 +6785,7 @@ public boolean handlePost(ImportXarForm form, BindException errors) throws Excep
                     // TODO: Configure module resources with the appropriate log location per container
                     if (form.getModule() != null)
                     {
-                        FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectoryFileLike(true).resolveChild("module-resource-xar.log");
+                        FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectory(true).resolveChild("module-resource-xar.log");
                         job.setLogFile(logFile);
                     }
 
@@ -6826,7 +6825,7 @@ public Object execute(ImportXarForm form, BindException errors) throws Exception
                 // TODO: Configure module resources with the appropriate log location per container
                 if (form.getModule() != null)
                 {
-                    FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectoryFileLike(true).resolveChild("module-resource-xar.log");
+                    FileLike logFile = form.getPipeRoot(getContainer()).getLogDirectory(true).resolveChild("module-resource-xar.log");
                     job.setLogFile(logFile);
                 }
 
diff --git a/experiment/src/org/labkey/experiment/pipeline/ExperimentPipelineProvider.java b/experiment/src/org/labkey/experiment/pipeline/ExperimentPipelineProvider.java
index 307413bbb58..2508967055f 100644
--- a/experiment/src/org/labkey/experiment/pipeline/ExperimentPipelineProvider.java
+++ b/experiment/src/org/labkey/experiment/pipeline/ExperimentPipelineProvider.java
@@ -21,13 +21,12 @@
 import org.labkey.api.pipeline.PipelineDirectory;
 import org.labkey.api.pipeline.PipelineProvider;
 import org.labkey.api.security.permissions.InsertPermission;
-import org.labkey.api.util.FileUtil;
 import org.labkey.api.view.ViewContext;
 import org.labkey.experiment.controllers.exp.ExperimentController;
+import org.labkey.vfs.FileLike;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
 import java.nio.file.Path;
 
 /**
@@ -42,20 +41,14 @@ public class ExperimentPipelineProvider extends PipelineProvider
     private static final String DIR_NAME_UPLOAD = "UploadedXARs";
     private static final String DIR_NAME_MOVE = "moveRunLogs";
 
-    public static Path getMoveDirectory(PipeRoot pr)
+    public static FileLike getMoveDirectory(PipeRoot pr)
     {
-        return getExperimentDirectory(pr, DIR_NAME_MOVE);
+        return getExperimentDirectory(pr.ensureSystemDirectory(), DIR_NAME_MOVE);
     }
 
-    private static Path getExperimentDirectory(PipeRoot pr, String name)
+    private static FileLike getExperimentDirectory(FileLike systemDir, String name)
     {
-        Path systemDir = pr.ensureSystemDirectory().toNioPathForRead();
-        return systemDir.resolve(DIR_NAME_EXPERIMENT).resolve(name);
-    }
-
-    private static Path getExperimentDirectory(Path systemDir, String name)
-    {
-        return systemDir.resolve(DIR_NAME_EXPERIMENT).resolve(name);
+        return systemDir.resolveChild(DIR_NAME_EXPERIMENT).resolveChild(name);
     }
 
     public ExperimentPipelineProvider(Module owningModule)
@@ -65,21 +58,21 @@ public ExperimentPipelineProvider(Module owningModule)
     }
 
     @Override
-    public void initSystemDirectory(Path rootDir, Path systemDir)
+    public void initSystemDirectory(FileLike rootDir, FileLike systemDir)
     {
         locateSystemDir(systemDir, DIR_NAME_MOVE);
         locateSystemDir(systemDir, DIR_NAME_UPLOAD);
     }
 
-    public void locateSystemDir(Path systemDir, String name)
+    public void locateSystemDir(FileLike systemDir, String name)
     {
-        Path path = FileUtil.appendName(systemDir, name);
-        if (Files.exists(path))
+        FileLike path = systemDir.resolveChild(name);
+        if (path.exists())
         {
             try
             {
-                Path dest = FileUtil.appendName(getExperimentDirectory(systemDir, name), FileUtil.getFileName(path));
-                Files.move(path, dest);
+                FileLike dest = getExperimentDirectory(systemDir, name).resolveChild(path.getName());
+                path.move(dest);
             }
             catch (IOException e)
             {
diff --git a/experiment/src/org/labkey/experiment/pipeline/MoveRunsPipelineJob.java b/experiment/src/org/labkey/experiment/pipeline/MoveRunsPipelineJob.java
index 9ede8686a9d..52833951636 100644
--- a/experiment/src/org/labkey/experiment/pipeline/MoveRunsPipelineJob.java
+++ b/experiment/src/org/labkey/experiment/pipeline/MoveRunsPipelineJob.java
@@ -29,9 +29,8 @@
 import org.labkey.api.util.FileUtil;
 import org.labkey.api.view.ActionURL;
 import org.labkey.api.view.ViewBackgroundInfo;
-
-import java.io.File;
-import java.nio.file.Path;
+import org.labkey.vfs.FileLike;
+import org.labkey.vfs.FileSystemLike;
 
 /**
  * User: jeckels
@@ -75,9 +74,9 @@ public MoveRunsPipelineJob(ViewBackgroundInfo info, Container sourceContainer, l
     }
 
     @Override
-    protected Path getWorkingDirectoryString()
+    protected FileLike getWorkingDirectoryString()
     {
-        return getPipeRoot().isCloudRoot() ? FileUtil.getTempDirectory().toPath() : new File(FileUtil.getAbsolutePath(_sourceContainer, ExperimentPipelineProvider.getMoveDirectory(getPipeRoot()))).toPath();
+        return getPipeRoot().isCloudRoot() ? FileUtil.getTempDirectoryFileLike() : FileSystemLike.wrapFile(FileUtil.getAbsoluteCaseSensitivePath(_sourceContainer, ExperimentPipelineProvider.getMoveDirectory(getPipeRoot()).toNioPathForRead()));
     }
 
     @Override
@@ -103,7 +102,7 @@ public Container getSourceContainer()
     }
 
     @Override
-    public TaskPipeline getTaskPipeline()
+    public TaskPipeline getTaskPipeline()
     {
         return PipelineJobService.get().getTaskPipeline(new TaskId(MoveRunsPipelineJob.class));
     }
diff --git a/experiment/src/org/labkey/experiment/pipeline/SampleReloadTask.java b/experiment/src/org/labkey/experiment/pipeline/SampleReloadTask.java
index 74e7159a485..8655106c681 100644
--- a/experiment/src/org/labkey/experiment/pipeline/SampleReloadTask.java
+++ b/experiment/src/org/labkey/experiment/pipeline/SampleReloadTask.java
@@ -34,8 +34,8 @@
 import org.labkey.api.util.FileType;
 import org.labkey.api.util.FileUtil;
 import org.labkey.api.exp.api.SampleTypeDomainKind;
+import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -68,14 +68,14 @@ public RecordedActionSet run()
     {
         PipelineJob job = getJob();
         FileAnalysisJobSupport support = job.getJobSupport(FileAnalysisJobSupport.class);
-        job.setLogFile(support.getDataDirectoryFileLike().resolveChild(FileUtil.makeFileNameWithTimestamp("triggered_sample_reload", "log")));
+        job.setLogFile(support.getDataDirectory().resolveChild(FileUtil.makeFileNameWithTimestamp("triggered_sample_reload", "log")));
         Map params = support.getParameters();
 
         job.setStatus("RELOADING", "Job started at: " + DateUtil.nowISO());
 
         // guaranteed to only have a single file
         assert support.getInputFiles().size() == 1;
-        File dataFile = support.getInputFiles().get(0);
+        FileLike dataFile = support.getInputFiles().get(0);
         Logger log = job.getLogger();
 
         log.info("Loading " + dataFile.getName());
@@ -153,7 +153,7 @@ else if (params.containsKey(SAMPLE_ID_KEY))
 
                 try (DataLoader loader = DataLoader.get().createLoader(dataFile, null, true, job.getContainer(), null))
                 {
-                    DomainKind domainKind = PropertyService.get().getDomainKindByName(SampleTypeDomainKind.NAME);
+                    DomainKind domainKind = PropertyService.get().getDomainKindByName(SampleTypeDomainKind.NAME);
                     Set reservedProps = domainKind.getReservedPropertyNames(null, job.getUser());
                     reservedProps.remove("name");
                     List props = new ArrayList<>();
@@ -188,7 +188,7 @@ else if (params.containsKey(SAMPLE_ID_KEY))
         return new RecordedActionSet();
     }
 
-    private void importSamples(Container c, User user, @Nullable ExpSampleType sampleType, File dataFile, Logger log)
+    private void importSamples(Container c, User user, @Nullable ExpSampleType sampleType, FileLike dataFile, Logger log)
     {
         if (sampleType != null)
         {
@@ -262,7 +262,7 @@ public Factory()
         }
 
         @Override
-        public PipelineJob.Task createTask(PipelineJob job)
+        public SampleReloadTask createTask(PipelineJob job)
         {
             return new SampleReloadTask(this, job);
         }
diff --git a/experiment/src/org/labkey/experiment/pipeline/XarGeneratorTask.java b/experiment/src/org/labkey/experiment/pipeline/XarGeneratorTask.java
index f0e60638a03..e7ac1fddd5f 100644
--- a/experiment/src/org/labkey/experiment/pipeline/XarGeneratorTask.java
+++ b/experiment/src/org/labkey/experiment/pipeline/XarGeneratorTask.java
@@ -107,7 +107,7 @@ public List getProtocolActionNames()
         protected FileLike getXarFile(PipelineJob job)
         {
             FileAnalysisJobSupport jobSupport = job.getJobSupport(FileAnalysisJobSupport.class);
-            return getOutputType().newFile(jobSupport.getAnalysisDirectoryFileLike(), jobSupport.getBaseName());
+            return getOutputType().newFile(jobSupport.getAnalysisDirectory(), jobSupport.getBaseName());
         }
 
         @Override
diff --git a/list/src/org/labkey/list/pipeline/ListReloadJob.java b/list/src/org/labkey/list/pipeline/ListReloadJob.java
index 09007241120..1250c4644cc 100644
--- a/list/src/org/labkey/list/pipeline/ListReloadJob.java
+++ b/list/src/org/labkey/list/pipeline/ListReloadJob.java
@@ -26,24 +26,24 @@
 import org.labkey.api.writer.FileSystemFile;
 import org.labkey.list.model.ListImportContext;
 import org.labkey.list.model.ListImporter;
+import org.labkey.vfs.FileLike;
 
-import java.nio.file.Path;
 import java.util.LinkedList;
 import java.util.List;
 
 public class ListReloadJob extends PipelineJob
 {
-    private final Path _dataFile;
+    private final FileLike _dataFile;
     private final ListImportContext _importContext;
 
     @JsonCreator
-    protected ListReloadJob(@JsonProperty("_dataFile") Path dataFile, @JsonProperty("_importContext") ListImportContext importContext)
+    protected ListReloadJob(@JsonProperty("_dataFile") FileLike dataFile, @JsonProperty("_importContext") ListImportContext importContext)
     {
         _dataFile = dataFile;
         _importContext = importContext;
     }
 
-    public ListReloadJob(ViewBackgroundInfo info, @NotNull PipeRoot root, Path dataFile, Path logFile, @NotNull ListImportContext importContext)
+    public ListReloadJob(ViewBackgroundInfo info, @NotNull PipeRoot root, FileLike dataFile, FileLike logFile, @NotNull ListImportContext importContext)
     {
         super(null, info, root);
         _dataFile = dataFile;
@@ -69,7 +69,7 @@ public void run()
         setStatus("RELOADING", "Job started at: " + DateUtil.nowISO());
         ListImporter importer = new ListImporter(_importContext);
 
-        String fileName = _dataFile.getFileName().toString();
+        String fileName = _dataFile.getName();
 
         getLogger().info("Loading " + fileName);
 
diff --git a/list/src/org/labkey/list/pipeline/ListReloadTask.java b/list/src/org/labkey/list/pipeline/ListReloadTask.java
index aba8dded52c..d395170a651 100644
--- a/list/src/org/labkey/list/pipeline/ListReloadTask.java
+++ b/list/src/org/labkey/list/pipeline/ListReloadTask.java
@@ -27,8 +27,8 @@
 import org.labkey.api.util.FileType;
 import org.labkey.api.util.Pair;
 import org.labkey.list.model.ListImportContext;
+import org.labkey.vfs.FileLike;
 
-import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -59,8 +59,8 @@ public RecordedActionSet run()
         }
 
         // guaranteed to only have a single file
-        assert support.getInputFilePaths().size() == 1;
-        Path dataFile = support.getInputFilePaths().get(0);
+        assert support.getInputFiles().size() == 1;
+        FileLike dataFile = support.getInputFiles().get(0);
         PipeRoot pr = PipelineService.get().findPipelineRoot(job.getContainer());
 
         if (pr != null)
@@ -72,9 +72,9 @@ public RecordedActionSet run()
                 boolean useMerge = false;
 
                 if (params.containsKey(LIST_ID_KEY))
-                    inputDataMap.put(dataFile.getFileName().toString(), new Pair<>(LIST_ID_KEY, params.get(LIST_ID_KEY)));
+                    inputDataMap.put(dataFile.getName(), new Pair<>(LIST_ID_KEY, params.get(LIST_ID_KEY)));
                 else if (params.containsKey(LIST_NAME_KEY))
-                    inputDataMap.put(dataFile.getFileName().toString(), new Pair<>(LIST_NAME_KEY, params.get(LIST_NAME_KEY)));
+                    inputDataMap.put(dataFile.getName(), new Pair<>(LIST_NAME_KEY, params.get(LIST_NAME_KEY)));
 
                 if (params.containsKey(ListImportContext.LIST_MERGE_OPTION))
                     useMerge = Boolean.parseBoolean(params.get(ListImportContext.LIST_MERGE_OPTION));
@@ -83,7 +83,7 @@ else if (params.containsKey(LIST_NAME_KEY))
                     context = new ListImportContext(inputDataMap, useMerge, true);
 
                 context.setProps(params);
-                ListReloadJob reloadJob = new ListReloadJob(job.getInfo(), pr, dataFile, job.getLogFilePath(), context);
+                ListReloadJob reloadJob = new ListReloadJob(job.getInfo(), pr, dataFile, job.getLogFileLike(), context);
                 ListReloadJob unserializedJob = (ListReloadJob) PipelineJob.deserializeJob(reloadJob.serializeJob(false));   // Force round trip to ensure serialization works
                 unserializedJob.run();
 
@@ -126,7 +126,7 @@ public boolean isJobComplete(PipelineJob job)
         }
 
         @Override
-        public PipelineJob.Task createTask(PipelineJob job)
+        public ListReloadTask createTask(PipelineJob job)
         {
             return new ListReloadTask(this, job);
         }
diff --git a/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java b/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java
index 43105dc53c8..d06727af0fb 100644
--- a/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java
+++ b/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java
@@ -54,6 +54,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -234,28 +235,15 @@ else if (tp.isUseFileTypeBaseName())
                     fileName = type.getDefaultName(baseName);
                 }
 
-                File result;
+                FileLike result = switch (tp.getOutputLocation())
+                {
+                    case ANALYSIS_DIR -> support.getAnalysisDirectory().resolveChild(fileName);
+                    case DATA_DIR -> support.getDataDirectory().resolveChild(fileName);
+                    case PATH -> support.findOutputFile(tp.getOutputDir(), fileName);
+                    default -> support.findOutputFile(fileName);
+                };
                 // Check if the output is specifically flagged to go into a special location so we check in the right
                 // place when deciding if the task has already been performed
-                switch (tp.getOutputLocation())
-                {
-                    case ANALYSIS_DIR:
-                        result = new File(support.getAnalysisDirectory(), fileName);
-                        break;
-
-                    case DATA_DIR:
-                        result = new File(support.getDataDirectory(), fileName);
-                        break;
-
-                    case PATH:
-                        result = support.findOutputFile(tp.getOutputDir(), fileName);
-                        break;
-
-                    case DEFAULT:
-                    default:
-                        result = support.findOutputFile(fileName);
-                        break;
-                }
 
                 if (tp.isOptional())
                 {
@@ -447,13 +435,13 @@ public boolean isWriteTaskInfoFile()
      * The task info file will be written into the analysis directory, similar to the .log file.
      * Since it will remain in the pipeline root after the job is complete, it shouldn't contain sensitive information.
      */
-    public File getTaskInfoFile()
+    public FileLike getTaskInfoFile()
     {
         if (!isWriteTaskInfoFile())
             return null;
 
         String infoFileName = getJobSupport().getBaseName() + "-taskInfo.tsv";
-        return new File(getJobSupport().getAnalysisDirectory(), infoFileName);
+        return getJobSupport().getAnalysisDirectory().resolveChild(infoFileName);
     }
 
     /**
@@ -466,10 +454,10 @@ protected String getModuleResourcePath(String path)
             return null;
 
         Resource dir = module.getModuleResource(path);
-        if (dir == null || !(dir instanceof FileResource))
+        if (!(dir instanceof FileResource fr))
             return null;
 
-        File f = ((FileResource)dir).getFile();
+        File f = fr.getFile();
         return f.getPath();
     }
 
@@ -486,27 +474,27 @@ public String[] getProcessPaths(WorkDirectory.Function f, String key) throws IOE
             TaskPath tp = (WorkDirectory.Function.input.equals(f) ?
                     _factory.getInputPaths().get(key) : _factory.getOutputPaths().get(key));
 
-            ArrayList paths = new ArrayList<>();
-            for (File file : _wd.getWorkFiles(f, tp))
+            List paths = new ArrayList<>();
+            for (FileLike file : _wd.getWorkFiles(f, tp))
                 paths.add(_wd.getRelativePath(file));
             return paths.toArray(new String[0]);
         }
     }
 
-    private String[] getOriginalFiles(String key)
+    private List getOriginalFiles(String key)
     {
         TaskPath tp = _factory.getInputPaths().get(key);
 
-        ArrayList paths = new ArrayList<>();
-        for (File file : _wd.getWorkFiles(WorkDirectory.Function.input, tp))
-            paths.add(file.getAbsolutePath());
-        return paths.toArray(new String[0]);
+        List paths = new ArrayList<>();
+        for (FileLike file : _wd.getWorkFiles(WorkDirectory.Function.input, tp))
+            paths.add(file.toNioPathForRead().toFile().getAbsolutePath());
+        return paths;
     }
 
     private void inputFile(TaskPath tp, String role, RecordedAction action) throws IOException
     {
-        List filesInput = _wd.getWorkFiles(WorkDirectory.Function.input, tp);
-        for (File fileInput : filesInput)
+        List filesInput = _wd.getWorkFiles(WorkDirectory.Function.input, tp);
+        for (FileLike fileInput : filesInput)
         {
             // Nothing to do, if this file is optional and does not exist.
             if (tp.isOptional() && !NetworkDrive.exists(fileInput))
@@ -548,10 +536,10 @@ else if (inputPaths.length == 1)
                 // NOTE: The script parser matches ${input1.txt} to the first input file which isn't the same as ${input1[1].txt} which may be the 2nd file in the set of files represented by "input1.txt"
             }
 
-            String[] originalFiles = getOriginalFiles(key);
-            if (originalFiles.length == 1)
+            List originalFiles = getOriginalFiles(key);
+            if (originalFiles.size() == 1)
             {
-                replacements.put(DataTransformService.ORIGINAL_SOURCE_PATH, Matcher.quoteReplacement(originalFiles[0].replaceAll("\\\\", "/")));
+                replacements.put(DataTransformService.ORIGINAL_SOURCE_PATH, Matcher.quoteReplacement(originalFiles.get(0).replaceAll("\\\\", "/")));
             }
         }
 
@@ -587,7 +575,7 @@ else if (outputPaths.length == 1)
         }
 
         // Task info replacement
-        File taskInfoFile = getTaskInfoFile();
+        FileLike taskInfoFile = getTaskInfoFile();
         if (taskInfoFile != null)
         {
             String taskInfoRelativePath = _wd.getRelativePath(taskInfoFile);
@@ -597,7 +585,7 @@ else if (outputPaths.length == 1)
         // Task output parameters file replacement
         if (_wd != null)
         {
-            File taskOutputParamsFile = _wd.newFile(CommandTaskImpl.OUTPUT_PARAMS);
+            FileLike taskOutputParamsFile = _wd.newFile(CommandTaskImpl.OUTPUT_PARAMS);
             String taskOutputParamsRelativePath = _wd.getRelativePath(taskOutputParamsFile);
             replacements.put(PipelineJob.PIPELINE_TASK_OUTPUT_PARAMS_PARAM, taskOutputParamsRelativePath);
         }
@@ -671,7 +659,7 @@ public RecordedActionSet run() throws PipelineJobException
             if (_factory.isRemoveInput())
             {
                 TaskPath tpInput = _factory.getInputPaths().get(WorkDirectory.Function.input.toString());
-                for (File fileInput : _wd.getWorkFiles(WorkDirectory.Function.input, tpInput))
+                for (FileLike fileInput : _wd.getWorkFiles(WorkDirectory.Function.input, tpInput))
                     fileInput.delete();
             }
 
@@ -730,7 +718,7 @@ protected boolean runCommand(RecordedAction action, @Nullable String apiKey, @Nu
 
         // Check if output file is to be generated from the stdout
         // stream of the process.
-        File fileOutput = null;
+        FileLike fileOutput = null;
         int lineInterval = 0;
         if (_factory.isPipeToOutput())
         {
@@ -753,7 +741,7 @@ protected boolean runCommand(RecordedAction action, @Nullable String apiKey, @Nu
 
     public String variableSubstitution(String src, Map map)
     {
-        StringBuffer sb = new StringBuffer();
+        StringBuilder sb = new StringBuilder();
         Matcher matcher = pat.matcher(src);
         while (matcher.find())
         {
@@ -800,33 +788,36 @@ private void applyEnvironment(ProcessBuilder pb)
 
     protected void readOutputParameters(RecordedAction action) throws IOException
     {
-        File file = _wd.newFile(CommandTaskImpl.OUTPUT_PARAMS);
+        FileLike file = _wd.newFile(CommandTaskImpl.OUTPUT_PARAMS);
         if (file.exists())
         {
             getJob().header("Output parameters");
             Map currParams = new HashMap<>(getJob().getParameters());
 
-            TabLoader loader = new TabLoader(file, true, null);
-            loader.setInferTypes(false);
-            for (Map row : loader.load())
+            try (InputStream in = file.openInputStream();
+                 TabLoader loader = new TabLoader(in, true, null))
             {
-                String name = Objects.toString(row.get("Name"), null);
-                String value = Objects.toString(row.get("Value"), null);
-                String type = Objects.toString(row.get("Type"), null);
-                if (name == null || value == null)
-                    continue;
-
-                // Skip null values and parameters that haven't changed
-                String prevValue = currParams.get(name);
-                if (prevValue == null || !prevValue.equals(value))
+                loader.setInferTypes(false);
+                for (Map row : loader.load())
                 {
-                    // Record the new parameter -- it will be merged into the job's parameters automatically
-                    getJob().info(name + ": " + value);
-                    action.addOutputParameter(new RecordedAction.ParameterType(name, PropertyType.STRING), value);
+                    String name = Objects.toString(row.get("Name"), null);
+                    String value = Objects.toString(row.get("Value"), null);
+                    String type = Objects.toString(row.get("Type"), null);
+                    if (name == null || value == null)
+                        continue;
+
+                    // Skip null values and parameters that haven't changed
+                    String prevValue = currParams.get(name);
+                    if (prevValue == null || !prevValue.equals(value))
+                    {
+                        // Record the new parameter -- it will be merged into the job's parameters automatically
+                        getJob().info(name + ": " + value);
+                        action.addOutputParameter(new RecordedAction.ParameterType(name, PropertyType.STRING), value);
+                    }
                 }
-            }
 
-            _wd.discardFile(file);
+                _wd.discardFile(file);
+            }
         }
     }
 }
diff --git a/pipeline/src/org/labkey/pipeline/analysis/ConvertTaskFactory.java b/pipeline/src/org/labkey/pipeline/analysis/ConvertTaskFactory.java
index efe81838a75..78aa5037035 100644
--- a/pipeline/src/org/labkey/pipeline/analysis/ConvertTaskFactory.java
+++ b/pipeline/src/org/labkey/pipeline/analysis/ConvertTaskFactory.java
@@ -27,8 +27,8 @@
 import org.labkey.api.pipeline.cmd.ConvertTaskId;
 import org.labkey.api.pipeline.file.FileAnalysisJobSupport;
 import org.labkey.api.util.FileType;
+import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -85,7 +85,7 @@ else if (_outputType == null)
         types.add(_outputType);
         for (TaskId tid : _commands)
         {
-            TaskFactory factory = PipelineJobService.get().getTaskFactory(tid);
+            TaskFactory factory = PipelineJobService.get().getTaskFactory(tid);
             types.addAll(factory.getInputTypes());
         }
         _initialTypes = types;
@@ -94,31 +94,31 @@ else if (_outputType == null)
     @Override
     public TaskId getActiveId(PipelineJob job)
     {
-        TaskFactory factory = findCommandFactory(job);
+        TaskFactory factory = findCommandFactory(job);
         if (factory != null)
             return factory.getActiveId(job);
 
         return super.getActiveId(job);
     }
 
-    private File getInputFile(PipelineJob job)
+    private FileLike getInputFile(PipelineJob job)
     {
-        List files = job.getJobSupport(FileAnalysisJobSupport.class).getInputFiles();
+        List files = job.getJobSupport(FileAnalysisJobSupport.class).getInputFiles();
         assert files != null && files.size() == 1 : "Conversion job must have one file.";
         return files.get(0);
     }
 
-    private TaskFactory findCommandFactory(PipelineJob job)
+    private TaskFactory findCommandFactory(PipelineJob job)
     {
         // If this job is not actually running a conversion, then no
         // converter command can be determined.
-        List files = job.getJobSupport(FileAnalysisJobSupport.class).getInputFiles();
+        List files = job.getJobSupport(FileAnalysisJobSupport.class).getInputFiles();
         LOG.debug("Checking " + files + " for possible converters");
         if (files == null || files.size() != 1)
             return null;
 
         // Otherwise, find the appropriate converter.
-        File fileInput = getInputFile(job);
+        FileLike fileInput = getInputFile(job);
         LOG.debug("Checking " + fileInput + " against up to " + _commands.length + " possible converters");
         for (TaskId tid : _commands)
         {
@@ -165,7 +165,7 @@ public List getProtocolActionNames()
     }
 
     @Override
-    public PipelineJob.Task createTask(PipelineJob job)
+    public PipelineJob.Task createTask(PipelineJob job)
     {
         throw new UnsupportedOperationException("No task associated with " + getClass() + ".");
     }
@@ -196,7 +196,7 @@ public boolean isParticipant(PipelineJob job) throws IOException
             return false;
         
         // Nothing to do, if the input is already the desired type.
-        File fileInput = getInputFile(job);
+        FileLike fileInput = getInputFile(job);
         if (_outputType.isType(fileInput))
             return false;
         if (findCommandFactory(job) == null)
@@ -207,7 +207,7 @@ public boolean isParticipant(PipelineJob job) throws IOException
     @Override
     public boolean isJobComplete(PipelineJob job)
     {
-        TaskFactory factory = findCommandFactory(job);
+        TaskFactory factory = findCommandFactory(job);
         if (factory == null)
         {
             job.warn("Unexpected missing converter for job. The pipeline configuration may have changed to remove a previously configured converter.");
diff --git a/pipeline/src/org/labkey/pipeline/analysis/FileAnalysisJob.java b/pipeline/src/org/labkey/pipeline/analysis/FileAnalysisJob.java
index 43331da9776..5b69c649c47 100644
--- a/pipeline/src/org/labkey/pipeline/analysis/FileAnalysisJob.java
+++ b/pipeline/src/org/labkey/pipeline/analysis/FileAnalysisJob.java
@@ -28,7 +28,6 @@
 import org.labkey.api.view.ViewBackgroundInfo;
 import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
@@ -117,13 +116,13 @@ public FileAnalysisTaskPipeline getTaskPipeline()
     }
 
     @Override
-    public File findInputFile(String name)
+    public FileLike findInputFile(String name)
     {
         return findFile(name);
     }
 
     @Override
-    public File findOutputFile(String name)
+    public FileLike findOutputFile(String name)
     {
         return findFile(name);
     }
@@ -135,9 +134,9 @@ public File findOutputFile(String name)
      * @param name The name of the file to be located
      * @return The file location outside the analysis directory, or null, if no such match is found.
      */
-    public File findFile(String name)
+    public FileLike findFile(String name)
     {
-        File dirAnalysis = getAnalysisDirectory();
+        FileLike dirAnalysis = getAnalysisDirectory();
 
         for (Map.Entry> entry : getTaskPipeline().getTypeHierarchy().entrySet())
         {
@@ -147,10 +146,10 @@ public File findFile(String name)
                 //       in order to find files.
 
                 // First try to go two directories up
-                File dir = dirAnalysis.getParentFile();
+                FileLike dir = dirAnalysis.getParent();
                 if (dir != null)
                 {
-                    dir = dir.getParentFile();
+                    dir = dir.getParent();
                 }
 
                 List derivedTypes = entry.getValue();
@@ -159,21 +158,21 @@ public File findFile(String name)
                     // Go two directories up for each level of derivation
                     if (dir != null)
                     {
-                        dir = dir.getParentFile();
+                        dir = dir.getParent();
                     }
                     if (dir != null)
                     {
-                        dir = dir.getParentFile();
+                        dir = dir.getParent();
                     }
                 }
 
                 String relativePath = getPipeRoot().relativePath(dir);
-                File expectedFile = getPipeRoot().resolvePath(relativePath + "/" + name);
+                FileLike expectedFile = getPipeRoot().resolvePathToFileLike(relativePath + "/" + name);
 
                 if (!NetworkDrive.exists(expectedFile))
                 {
                     // If the file isn't where we would expect it, check other directories in the same hierarchy
-                    File alternateFile = findFileInAlternateDirectory(expectedFile.getParentFile(), dirAnalysis, name);
+                    FileLike alternateFile = findFileInAlternateDirectory(expectedFile.getParent(), dirAnalysis, name);
                     if (alternateFile != null)
                     {
                         // If we found a file that matches, use it
@@ -185,7 +184,7 @@ public File findFile(String name)
         }
 
         // Path of last resort is always to look in the current directory.
-        return new File(dirAnalysis, name);
+        return dirAnalysis.resolveChild(name);
     }
 
     /**
@@ -196,7 +195,7 @@ public File findFile(String name)
      * @param name name of the file to look for
      * @return matching file, or null if nothing was found
      */
-    private File findFileInAlternateDirectory(File expectedDir, File dir, String name)
+    private FileLike findFileInAlternateDirectory(FileLike expectedDir, FileLike dir, String name)
     {
         // Bail out if we've gotten all the way down to the originally expected file location
         if (dir == null || dir.equals(expectedDir))
@@ -204,14 +203,14 @@ private File findFileInAlternateDirectory(File expectedDir, File dir, String nam
             return null;
         }
         // Recurse through the parent directories to find it in the place closest to the expected directory
-        File result = findFileInAlternateDirectory(expectedDir, dir.getParentFile(), name);
+        FileLike result = findFileInAlternateDirectory(expectedDir, dir.getParent(), name);
         if (result != null)
         {
             // If we found a match, use it
             return result;
         }
 
-        result = new File(dir, name);
+        result = dir.resolveChild(name);
         if (NetworkDrive.exists(result))
         {
             return result;
diff --git a/pipeline/src/org/labkey/pipeline/api/AbstractWorkDirectory.java b/pipeline/src/org/labkey/pipeline/api/AbstractWorkDirectory.java
index c648668e556..42a3b7df228 100644
--- a/pipeline/src/org/labkey/pipeline/api/AbstractWorkDirectory.java
+++ b/pipeline/src/org/labkey/pipeline/api/AbstractWorkDirectory.java
@@ -30,6 +30,7 @@
 import org.labkey.api.util.FileUtil;
 import org.labkey.api.util.NetworkDrive;
 import org.labkey.api.util.URIUtil;
+import org.labkey.vfs.FileLike;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -59,12 +60,12 @@ public abstract class AbstractWorkDirectory implements WorkDirectory
 
     protected FileAnalysisJobSupport _support;
     protected final WorkDirFactory _factory;
-    protected final File _dir;
+    protected final FileLike _dir;
     protected final Logger _jobLog;
-    protected final HashMap _copiedInputs = new HashMap<>();
+    protected final HashMap _copiedInputs = new HashMap<>();
 
     protected CopyingResource _copyingResource;
-    protected File _transferToDirOnFailure = null;
+    protected FileLike _transferToDirOnFailure = null;
 
     public static abstract class AbstractFactory implements WorkDirFactory
     {
@@ -111,7 +112,7 @@ public void setOutputPermissions(String outputPermissions)
         }
     }
 
-    public AbstractWorkDirectory(FileAnalysisJobSupport support, WorkDirFactory factory, File dir, boolean reuseExistingDirectory, Logger log) throws IOException
+    public AbstractWorkDirectory(FileAnalysisJobSupport support, WorkDirFactory factory, FileLike dir, boolean reuseExistingDirectory, Logger log) throws IOException
     {
         _support = support;
         _factory = factory;
@@ -140,13 +141,13 @@ public AbstractWorkDirectory(FileAnalysisJobSupport support, WorkDirFactory fact
     @Override
     public void acceptFilesAsOutputs(Map expectedOutputs, RecordedAction action) throws IOException
     {
-        File[] remainingFiles = getDir().listFiles();
+        List remainingFiles = getDir().getChildren();
 
         if (remainingFiles != null)
         {
             try (WorkDirectory.CopyingResource lock = ensureCopyingLock())
             {
-                Set copiedFiles = new HashSet<>();
+                Set copiedFiles = new HashSet<>();
                 // First handle anything that's been explicitly configured
                 for (Map.Entry entry : expectedOutputs.entrySet())
                 {
@@ -162,13 +163,13 @@ public void acceptFilesAsOutputs(Map expectedOutputs, Recorded
                 _jobLog.debug("Already copied files: " + copiedFiles);
 
                 // Slurp up any other files too
-                File[] additionalFiles = getDir().listFiles();
-                if (additionalFiles != null && additionalFiles.length > 0)
+                List additionalFiles = getDir().getChildren();
+                if (!additionalFiles.isEmpty())
                 {
                     _jobLog.debug("Additional files: " + Arrays.asList(additionalFiles));
                 }
 
-                for (File workFile : remainingFiles)
+                for (FileLike workFile : remainingFiles)
                 {
                     if (copiedFiles.contains(workFile))
                     {
@@ -193,7 +194,7 @@ public void acceptFilesAsOutputs(Map expectedOutputs, Recorded
                     }
                     else
                     {
-                        File f = outputFile(workFile);
+                        FileLike f = outputFile(workFile);
                         String role = "";
                         String baseName = _support.getBaseName();
                         if (f.getName().startsWith(baseName))
@@ -216,7 +217,7 @@ else if (f.getName().contains("."))
                         if (f.isDirectory())
                         {
                             // It's a directory, so add all of the child files instead of the directory itself
-                            Collection contents = FileUtils.listFiles(f, FileFilterUtils.fileFileFilter(), FileFilterUtils.trueFileFilter());
+                            Collection contents = FileUtils.listFiles(f.toNioPathForRead().toFile(), FileFilterUtils.fileFileFilter(), FileFilterUtils.trueFileFilter());
                             for (File content : contents)
                             {
                                 action.addOutput(content, role, false, true);
@@ -234,7 +235,7 @@ else if (f.getName().contains("."))
     }
 
     @Override
-    public List getWorkFiles(WorkDirectory.Function f, TaskPath tp)
+    public List getWorkFiles(Function f, TaskPath tp)
     {
         if (tp == null)
             return Collections.emptyList();
@@ -255,22 +256,22 @@ else if (tp.isUseFileTypeBaseName())
             baseNames = Collections.singletonList(baseName);
         }
 
-        ArrayList files = new ArrayList<>();
+        List files = new ArrayList<>();
         for (String baseName : baseNames)
             files.add(newWorkFile(f, tp, baseName));
         return files;
     }
 
-    private Set outputFile(TaskPath tp, String role, RecordedAction action) throws IOException
+    private Set outputFile(TaskPath tp, String role, RecordedAction action) throws IOException
     {
-        Set result = new HashSet<>();
-        List filesWork = getWorkFiles(WorkDirectory.Function.output, tp);
-        for (File fileWork : filesWork)
+        Set result = new HashSet<>();
+        List filesWork = getWorkFiles(WorkDirectory.Function.output, tp);
+        for (FileLike fileWork : filesWork)
         {
-            File fileOutput = switch (tp.getOutputLocation())
+            FileLike fileOutput = switch (tp.getOutputLocation())
             {
-                case ANALYSIS_DIR -> new File(_support.getAnalysisDirectory(), fileWork.getName());
-                case DATA_DIR -> new File(_support.getDataDirectory(), fileWork.getName());
+                case ANALYSIS_DIR -> _support.getAnalysisDirectory().resolveChild(fileWork.getName());
+                case DATA_DIR -> _support.getDataDirectory().resolveChild(fileWork.getName());
                 case PATH -> _support.findOutputFile(tp.getOutputDir(), fileWork.getName());
                 default -> _support.findOutputFile(fileWork.getName());
             };
@@ -286,7 +287,7 @@ private Set outputFile(TaskPath tp, String role, RecordedAction action) th
                 // CONSIDER: Unfortunately, with a local work directory, this may hide files
                 // that are auto-generated by the command in place.  Such files will not be recorded as output.
                 if (tp.isOptional() ||
-                        !_support.getAnalysisDirectory().equals(fileOutput.getParentFile()))
+                        !_support.getAnalysisDirectory().equals(fileOutput.getParent()))
                 {
                     if (NetworkDrive.exists(fileOutput))
                     {
@@ -299,7 +300,7 @@ private Set outputFile(TaskPath tp, String role, RecordedAction action) th
             if (!tp.isOptional() || fileWork.exists())
             {
                 // Add it as an output if it's non-optional, or if it's optional and the file exists
-                File f = outputFile(fileWork, fileOutput);
+                FileLike f = outputFile(fileWork, fileOutput);
                 action.addOutput(f, role, false, true);
                 result.add(fileWork);
             }
@@ -308,37 +309,37 @@ private Set outputFile(TaskPath tp, String role, RecordedAction action) th
     }
 
     @Override
-    public File getDir()
+    public FileLike getDir()
     {
         return _dir;
     }
 
-    private void copyFile(File source, File target) throws IOException
+    private void copyFile(FileLike source, FileLike target) throws IOException
     {
-        NetworkDrive.ensureDrive(source.getAbsolutePath());
-        NetworkDrive.ensureDrive(target.getAbsolutePath());
+        NetworkDrive.ensureDrive(source);
+        NetworkDrive.ensureDrive(target);
 
         try (WorkDirectory.CopyingResource lock = ensureCopyingLock())
         {
             _jobLog.info("Copying " + source + " to " + target);
             if (source.isDirectory())
             {
-                FileUtils.copyDirectory(source, target);
+                FileUtil.copyDirectory(source.toNioPathForRead(), target.toNioPathForWrite());
             }
             else
             {
-                FileUtils.copyFile(source, target);
+                FileUtil.copyFile(source, target);
             }
         }
     }
 
-    protected File copyInputFile(File fileInput) throws IOException
+    protected FileLike copyInputFile(FileLike fileInput) throws IOException
     {
-        File fileWork = newFile(fileInput.getName());
+        FileLike fileWork = newFile(fileInput.getName());
         return copyInputFile(fileInput, fileWork);
     }
 
-    protected File copyInputFile(File fileInput, File fileWork) throws IOException
+    protected FileLike copyInputFile(FileLike fileInput, FileLike fileWork) throws IOException
     {
         //ensure fileWork is a descendent of workDir
         if (getRelativePath(fileWork) == null)
@@ -351,7 +352,7 @@ protected File copyInputFile(File fileInput, File fileWork) throws IOException
         return fileWork;
     }
 
-    private File getDir(Function f, String name)
+    private FileLike getDir(Function f, String name)
     {
         if (Function.output.equals(f))
         {
@@ -361,34 +362,34 @@ private File getDir(Function f, String name)
         }
         else
         {
-            File file = _support.findInputFile(name);
-            return file.getParentFile();
+            FileLike file = _support.findInputFile(name);
+            return file.getParent();
         }
     }
 
     @Override
-    public File newFile(FileType type)
+    public FileLike newFile(FileType type)
     {
         // TODO: Issue 20143: pipeline: Custom output directory for task outputs
         return newFile(Function.output, type.getName(_dir, _support.getBaseName()));
     }
 
     @Override
-    public File newFile(String name)
+    public FileLike newFile(String name)
     {
         return newFile(Function.output, name);
     }
 
     @Override
-    public File newFile(Function f, String name)
+    public FileLike newFile(Function f, String name)
     {
-        File file = new File(getDir(f, name), name);
+        FileLike file = getDir(f, name).resolveChild(name);
 
         if (Function.input.equals(f))
         {
             // See if the file has already been copied into the working directory.
             // In which case, the copied version should be used.
-            File fileWork = _copiedInputs.get(file);
+            FileLike fileWork = _copiedInputs.get(file);
             if (fileWork != null)
                 return fileWork;
         }
@@ -397,32 +398,32 @@ public File newFile(Function f, String name)
     }
 
     @Override
-    public String getRelativePath(File fileWork) throws IOException
+    public String getRelativePath(FileLike fileWork) throws IOException
     {
         return FileUtil.relativize(_dir, fileWork, true);
     }
 
     @Override
-    public File outputFile(File fileWork) throws IOException
+    public FileLike outputFile(FileLike fileWork) throws IOException
     {
         return outputFile(fileWork, fileWork.getName());
     }
 
     @Override
-    public File outputFile(File fileWork, String nameDest) throws IOException
+    public FileLike outputFile(FileLike fileWork, String nameDest) throws IOException
     {
         return outputFile(fileWork, _support.findOutputFile(nameDest));
     }
 
     @Override
-    public File outputFile(File fileWork, File fileDest) throws IOException
+    public FileLike outputFile(FileLike fileWork, FileLike fileDest) throws IOException
     {
-        NetworkDrive.ensureDrive(fileDest.getAbsolutePath());
+        NetworkDrive.ensureDrive(fileDest);
 
         // TPP treats .xml.gz as a native format, follow suit
         if (fileWork.getName().endsWith(".gz") && !fileDest.getName().endsWith(".gz"))
         {
-            fileDest = new File(fileDest.getPath()+".gz");
+            fileDest = fileDest.getParent().resolveChild(fileDest.getName() + ".gz");
         }
 
         if (!fileWork.exists())
@@ -434,8 +435,8 @@ public File outputFile(File fileWork, File fileDest) throws IOException
             throw new FileNotFoundException("Failed to find expected output " + fileWork);
         }
         ensureDescendant(fileWork);
-        File fileReplace = null;
-        File fileCopy = null;
+        FileLike fileReplace = null;
+        FileLike fileCopy = null;
 
         try (WorkDirectory.CopyingResource lock = ensureCopyingLock())
         {
@@ -443,7 +444,7 @@ public File outputFile(File fileWork, File fileDest) throws IOException
             {
                 // If the destination exists, rename it out of the way while we try to
                 // replace it. Rename within the same directory is always an atomic action.
-                fileReplace = FT_MOVE.newFile(fileDest.getParentFile(), fileDest.getName());
+                fileReplace = FT_MOVE.newFile(fileDest.getParent(), fileDest.getName());
                 _jobLog.info("Moving " + fileDest + " to " + fileReplace);
                 if (!fileDest.renameTo(fileReplace))
                 {
@@ -459,14 +460,14 @@ public File outputFile(File fileWork, File fileDest) throws IOException
                 // File.renameTo() is the most efficient way to move a file, but it annoyingly doesn't necessarily
                 // work across different file systems.  Use a copy to a .copy file, and then an
                 // atomic rename within the same directory to the destination.
-                fileCopy = FT_COPY.newFile(fileDest.getParentFile(), fileDest.getName());
+                fileCopy = FT_COPY.newFile(fileDest.getParent(), fileDest.getName());
                 if (directory)
                 {
-                    FileUtils.copyDirectory(fileWork, fileCopy);
+                    FileUtil.copyDirectory(fileWork, fileCopy);
                 }
                 else
                 {
-                    FileUtils.copyFile(fileWork, fileCopy);
+                    FileUtil.copyFile(fileWork, fileCopy);
                 }
                 if (!fileCopy.renameTo(fileDest))
                 {
@@ -492,7 +493,7 @@ public File outputFile(File fileWork, File fileDest) throws IOException
             }
             if (fileReplace != null)
             {
-                File fileRemove = fileReplace;
+                FileLike fileRemove = fileReplace;
                 fileReplace = null;    // Output file is successfully in place.
 
                 _jobLog.info("Removing " + fileRemove);
@@ -502,7 +503,7 @@ public File outputFile(File fileWork, File fileDest) throws IOException
             {
                 if (directory)
                 {
-                    FileUtils.deleteDirectory(fileWork);
+                    FileUtil.deleteDir(fileWork);
                 }
                 else if (!fileWork.delete())
                 {
@@ -524,13 +525,13 @@ else if (!fileWork.delete())
             }
         }
 
-        _factory.setPermissions(fileDest);
+        _factory.setPermissions(fileDest.toNioPathForWrite().toFile());
 
         return fileDest;
     }
 
     @Override
-    public void discardFile(File fileWork) throws IOException
+    public void discardFile(FileLike fileWork) throws IOException
     {
         _jobLog.debug("discarding file: " + fileWork.getPath());
         ensureDescendant(fileWork);
@@ -543,7 +544,7 @@ public void discardFile(File fileWork) throws IOException
 
             if (fileWork.isDirectory())
             {
-                FileUtils.deleteDirectory(fileWork);
+                FileUtil.deleteDir(fileWork);
             }
 
             if (fileWork.exists())
@@ -565,7 +566,7 @@ public void discardCopiedInputs() throws IOException
     {
         if (NetworkDrive.exists(_dir))
         {
-            for (File input : _copiedInputs.values())
+            for (FileLike input : _copiedInputs.values())
                 discardFile(input);
             _copiedInputs.clear();
         }
@@ -580,12 +581,12 @@ public void remove(boolean success) throws IOException
         {
             if (!success && _transferToDirOnFailure != null)
             {
-                File dest = FileUtil.findUniqueFileName(_dir.getName(), _transferToDirOnFailure);
+                FileLike dest = FileUtil.findUniqueFileName(_dir.getName(), _transferToDirOnFailure);
                 _jobLog.debug("after failure, moving working directory to: " + dest.getPath());
 
                 try
                 {
-                    FileUtils.moveDirectory(_dir, dest);
+                    FileUtils.moveDirectory(_dir.toNioPathForRead().toFile(), dest.toNioPathForRead().toFile());
                 }
                 catch (IOException e)
                 {
@@ -599,11 +600,11 @@ else if (!_dir.delete() && success)
             {
                 StringBuilder message = new StringBuilder();
                 message.append("Failed to remove work directory ").append(_dir);
-                File[] files = _dir.listFiles();
-                if (files != null && files.length > 0)
+                List files = _dir.getChildren();
+                if (!files.isEmpty())
                 {
                     message.append(" unexpected files found:");
-                    for (File f : files)
+                    for (FileLike f : files)
                         message.append("\n").append(f.getName());
                 }
 
@@ -612,7 +613,7 @@ else if (!_dir.delete() && success)
         }
     }
 
-    private void ensureDescendant(File fileWork) throws IOException
+    private void ensureDescendant(FileLike fileWork) throws IOException
     {
         if (!URIUtil.isDescendant(_dir.toURI(), fileWork.toURI()))
             throw new IOException("The file " + fileWork + " is not a descendant of " + _dir);
@@ -634,7 +635,7 @@ public CopyingResource ensureCopyingLock() throws IOException
     }
 
     @Override
-    public File newWorkFile(WorkDirectory.Function f, TaskPath tp, String baseName)
+    public FileLike newWorkFile(Function f, TaskPath tp, String baseName)
     {
         if (tp == null)
             return null;
@@ -665,7 +666,7 @@ public void close()
     }
 
     @Override
-    public File getWorkingCopyForInput(File f)
+    public FileLike getWorkingCopyForInput(FileLike f)
     {
         return _copiedInputs.get(f);
     }
diff --git a/pipeline/src/org/labkey/pipeline/api/ParamParserImpl.java b/pipeline/src/org/labkey/pipeline/api/ParamParserImpl.java
index 196580e0f2a..8109cb019f7 100644
--- a/pipeline/src/org/labkey/pipeline/api/ParamParserImpl.java
+++ b/pipeline/src/org/labkey/pipeline/api/ParamParserImpl.java
@@ -20,6 +20,8 @@
 import org.labkey.api.pipeline.ParamParser;
 import org.labkey.api.util.StringUtilsLabKey;
 import org.labkey.api.util.XmlBeansUtil;
+import org.labkey.api.writer.PrintWriters;
+import org.labkey.vfs.FileLike;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
@@ -36,8 +38,6 @@
 import javax.xml.transform.stream.StreamResult;
 import java.io.BufferedWriter;
 import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -93,11 +93,6 @@ public ErrorImpl(String message, int line, int column)
             _column = column;
         }
 
-        public ErrorImpl(SAXParseException spe)
-        {
-            this(spe.getLocalizedMessage(), spe.getLineNumber(), spe.getColumnNumber());
-        }
-
         @Override
         public String getMessage()
         {
@@ -230,7 +225,7 @@ protected void validateDocument()
             }
 
             String type = elNote.getAttribute(ATTR_TYPE);
-            if (type == null || type.isEmpty() || "description".equals(type))
+            if (type.isEmpty() || "description".equals(type))
                 continue;
 
             if (!VAL_INPUT.equals(type))
@@ -367,9 +362,10 @@ private boolean isInputParameterElement(String name, Element elNote)
     @Override
     public String getXMLFromMap(Map params)
     {
-        String xmlEmpty = "\n" +
-                "\n" +
-                "";
+        String xmlEmpty = """
+                
+                
+                """;
         parse(new ByteArrayInputStream(xmlEmpty.getBytes(StringUtilsLabKey.DEFAULT_CHARSET)));
         String[] keys = params.keySet().toArray(new String[0]);
         Arrays.sort(keys);
@@ -380,9 +376,9 @@ public String getXMLFromMap(Map params)
     }
 
     @Override
-    public void writeFromMap(Map params, File fileDest) throws IOException
+    public void writeFromMap(Map params, FileLike fileDest) throws IOException
     {
-        try (BufferedWriter inputWriter = new BufferedWriter(new FileWriter(fileDest)))
+        try (BufferedWriter inputWriter = new BufferedWriter(PrintWriters.getPrintWriter(fileDest.openOutputStream())))
         {
             String xml = getXMLFromMap(params);
             _log.debug("Writing " + params.size() + " parameters (" + fileDest + "):");
diff --git a/pipeline/src/org/labkey/pipeline/api/PipeRootImpl.java b/pipeline/src/org/labkey/pipeline/api/PipeRootImpl.java
index df8f896ca74..007412a728e 100644
--- a/pipeline/src/org/labkey/pipeline/api/PipeRootImpl.java
+++ b/pipeline/src/org/labkey/pipeline/api/PipeRootImpl.java
@@ -167,7 +167,7 @@ public FileLike ensureSystemDirectory()
                 }
 
                 for (PipelineProvider provider : PipelineService.get().getPipelineProviders())
-                    provider.initSystemDirectory(root.toNioPathForWrite(), systemDir.toNioPathForWrite());
+                    provider.initSystemDirectory(root, systemDir);
             }
             catch (IOException e)
             {
@@ -226,20 +226,11 @@ public Path getRootNioPath()
 
     @Override
     @NotNull
-    public File getLogDirectory()
+    public FileLike getLogDirectory(boolean forWrite)
     {
         // If pipeline root is in File system, return that; otherwise return temp directory
-        if (isCloudRoot())
-            return FileUtil.getTempDirectory();
-        else
-            return getRootPath();
-    }
-
-    @Override
-    @NotNull
-    public FileLike getLogDirectoryFileLike(boolean forWrite)
-    {
-        var b = new FileSystemLike.Builder(getLogDirectory()).readonly();
+        File dir = isCloudRoot() ? FileUtil.getTempDirectory() : getRootPath();
+        var b = new FileSystemLike.Builder(dir).readonly();
         if (forWrite)
             b.readwrite();
         return b.root();
@@ -458,6 +449,17 @@ public Path resolveToNioPath(String pathStr)
         }
     }
 
+    @Override
+    public FileLike resolveToFileLikeFromUrl(String url) throws IOException
+    {
+        Path path = resolveToNioPathFromUrl(url);
+        if (path != null)
+        {
+            return FileSystemLike.wrapFile(getRootNioPath(), path);
+        }
+        return null;
+    }
+
     @Override
     @Nullable
     public Path resolveToNioPathFromUrl(String url)
diff --git a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
index 4d5128496c8..d0026940400 100644
--- a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
+++ b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
@@ -51,6 +51,7 @@
 import javax.script.ScriptException;
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -210,26 +211,26 @@ else if (factory._scriptPath != null)
             {
                 TaskPath tpOut = _factory.getOutputPaths().get(WorkDirectory.Function.output.toString());
                 assert !tpOut.isSplitFiles() : "Invalid attempt to pipe output to split files.";
-                File fileOutput = _wd.newWorkFile(WorkDirectory.Function.output,
+                FileLike fileOutput = _wd.newWorkFile(WorkDirectory.Function.output,
                         tpOut, getJobSupport().getBaseName());
-                FileUtils.write(fileOutput, String.valueOf(o), StringUtilsLabKey.DEFAULT_CHARSET);
+                FileUtils.write(fileOutput.toNioPathForWrite().toFile(), String.valueOf(o), StringUtilsLabKey.DEFAULT_CHARSET);
             }
 
             // If we got this far, we were successful in running the script.
             // Delete the rewritten script and output files from the work directory
             // so they won't be attached as related outputs of the task.
-            if (bindings.get(ExternalScriptEngine.REWRITTEN_SCRIPT_FILE) instanceof File rewrittenScriptFile)
+            if (bindings.get(ExternalScriptEngine.REWRITTEN_SCRIPT_FILE) instanceof FileLike rewrittenScriptFile)
             {
                 _wd.discardFile(rewrittenScriptFile);
             }
 
             // Delete the console out file (e.g., "script.Rout") from the work directory
-            if (_engine.getFactory() instanceof ExternalScriptEngineFactory)
+            if (_engine.getFactory() instanceof ExternalScriptEngineFactory esee)
             {
-                ExternalScriptEngineDefinition externalEngineDef = ((ExternalScriptEngineFactory)_engine.getFactory()).getDefinition();
+                ExternalScriptEngineDefinition externalEngineDef = esee.getDefinition();
                 if (externalEngineDef.getOutputFileName() != null)
                 {
-                    File consoleOutputFile = ((ExternalScriptEngine) _engine).getConsoleOutputFile(_engine.getContext());
+                    FileLike consoleOutputFile = ((ExternalScriptEngine) _engine).getConsoleOutputFile(_engine.getContext());
                     if (consoleOutputFile != null)
                         _wd.discardFile(consoleOutputFile);
                 }
@@ -247,7 +248,7 @@ else if (factory._scriptPath != null)
         }
     }
 
-    protected void writeTaskInfo(File file, RecordedAction action) throws IOException
+    protected void writeTaskInfo(FileLike file, RecordedAction action) throws IOException
     {
         List columns = Arrays.asList("Name", "Value");
         RowMapFactory factory = new RowMapFactory<>(columns);
@@ -290,7 +291,7 @@ protected void writeTaskInfo(File file, RecordedAction action) throws IOExceptio
             File f = new File(uri);
             if (f.exists())
             {
-                String inputPath = _wd.getRelativePath(f);
+                String inputPath = _wd.getRelativePath(FileSystemLike.wrapFile(f));
                 rows.add(factory.getRowMap(role, inputPath));
             }
         }
@@ -309,10 +310,11 @@ protected void writeTaskInfo(File file, RecordedAction action) throws IOExceptio
             }
         }
 
-        try (TSVMapWriter tsvWriter = new TSVMapWriter(columns, rows))
+        try (TSVMapWriter tsvWriter = new TSVMapWriter(columns, rows);
+            OutputStream out = file.openOutputStream())
         {
             tsvWriter.setHeaderRowVisible(false);
-            tsvWriter.write(file);
+            tsvWriter.write(out);
         }
     }
 
@@ -325,7 +327,7 @@ protected String rewritePath(String path)
             File f = new File(path);
             if (!f.isAbsolute())
             {
-                f = new File(_wd.getDir(), path);
+                f = new File(_wd.getDir().toNioPathForRead().toFile(), path);
                 path = f.getAbsolutePath();
             }
 
diff --git a/pipeline/src/org/labkey/pipeline/api/WorkDirectoryLocal.java b/pipeline/src/org/labkey/pipeline/api/WorkDirectoryLocal.java
index 33909993812..0c64533c5b3 100644
--- a/pipeline/src/org/labkey/pipeline/api/WorkDirectoryLocal.java
+++ b/pipeline/src/org/labkey/pipeline/api/WorkDirectoryLocal.java
@@ -19,8 +19,8 @@
 import org.labkey.api.pipeline.WorkDirFactory;
 import org.labkey.api.pipeline.WorkDirectory;
 import org.labkey.api.pipeline.file.FileAnalysisJobSupport;
+import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.io.IOException;
 
 /**
@@ -36,20 +36,20 @@ public static class Factory extends AbstractFactory
         @Override
         public WorkDirectory createWorkDirectory(String jobId, FileAnalysisJobSupport support, boolean useDeterministicFolderPath, Logger log) throws IOException
         {
-            File dir = FT_WORK_DIR.newFile(support.getAnalysisDirectory(),
+            FileLike dir = FT_WORK_DIR.newFile(support.getAnalysisDirectory(),
                     support.getBaseName());
 
             return new WorkDirectoryLocal(support, this, dir, useDeterministicFolderPath, log);
         }
     }
 
-    public WorkDirectoryLocal(FileAnalysisJobSupport support, WorkDirFactory factory, File dir, boolean reuseExistingDirectory, Logger log) throws IOException
+    public WorkDirectoryLocal(FileAnalysisJobSupport support, WorkDirFactory factory, FileLike dir, boolean reuseExistingDirectory, Logger log) throws IOException
     {
         super(support, factory, dir, reuseExistingDirectory, log);
     }
 
     @Override
-    public File inputFile(File fileInput, boolean forceCopy) throws IOException
+    public FileLike inputFile(FileLike fileInput, boolean forceCopy) throws IOException
     {
         if (!forceCopy)
             return fileInput;
@@ -57,7 +57,7 @@ public File inputFile(File fileInput, boolean forceCopy) throws IOException
     }
 
     @Override
-    public File inputFile(File fileInput, File fileWork, boolean forceCopy) throws IOException
+    public FileLike inputFile(FileLike fileInput, FileLike fileWork, boolean forceCopy) throws IOException
     {
         if (!forceCopy)
             return fileInput;
diff --git a/pipeline/src/org/labkey/pipeline/api/WorkDirectoryRemote.java b/pipeline/src/org/labkey/pipeline/api/WorkDirectoryRemote.java
index a8b49ecaa20..0b55e2d4c3d 100644
--- a/pipeline/src/org/labkey/pipeline/api/WorkDirectoryRemote.java
+++ b/pipeline/src/org/labkey/pipeline/api/WorkDirectoryRemote.java
@@ -15,7 +15,6 @@
  */
 package org.labkey.pipeline.api;
 
-import org.apache.commons.io.FileUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.labkey.api.pipeline.WorkDirFactory;
@@ -25,6 +24,8 @@
 import org.labkey.api.util.NetworkDrive;
 import org.labkey.api.util.StringUtilsLabKey;
 import org.labkey.api.util.URIUtil;
+import org.labkey.vfs.FileLike;
+import org.labkey.vfs.FileSystemLike;
 import org.springframework.beans.factory.InitializingBean;
 
 import java.io.ByteArrayOutputStream;
@@ -35,6 +36,7 @@
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -51,19 +53,19 @@ public class WorkDirectoryRemote extends AbstractWorkDirectory
 
     private static final int FILE_LOCKS_DEFAULT = 5;
 
-    private final File _lockDirectory;
-    private final File _folderToClean;
+    private final FileLike _lockDirectory;
+    private final FileLike _folderToClean;
 
     private static final Map _locks = new HashMap<>();
 
     @Override
-    public File inputFile(File fileInput, boolean forceCopy) throws IOException
+    public FileLike inputFile(FileLike fileInput, boolean forceCopy) throws IOException
     {
         return inputFile(fileInput, newFile(fileInput.getName()), forceCopy);
     }
 
     @Override
-    public File inputFile(File fileInput, File fileWork, boolean forceCopy) throws IOException
+    public FileLike inputFile(FileLike fileInput, FileLike fileWork, boolean forceCopy) throws IOException
     {
         //can be used to prevent duplicate copy attempts
         if (fileWork.exists() && !forceCopy)
@@ -107,14 +109,14 @@ public WorkDirectory createWorkDirectory(String jobId, FileAnalysisJobSupport su
                 _deterministicWorkingDirName = true;
             }
 
-            File tempDir;
-            File tempDirBase = null;
+            FileLike tempDir;
+            FileLike tempDirBase = null;
             int attempt = 0;
             do
             {
                 // We've seen very intermittent problems failing to create temp files in the past during the DRTs,
                 // so try a few times before failing
-                File dirParent = (_tempDirectory == null ? null : new File(_tempDirectory));
+                FileLike dirParent = (_tempDirectory == null ? null : FileSystemLike.wrapFile(new File(_tempDirectory)));
 
                 // If the temp directory is shared, then create a jobId directory to be sure the
                 // work directory path is unique.
@@ -124,7 +126,7 @@ public WorkDirectory createWorkDirectory(String jobId, FileAnalysisJobSupport su
                     {
                         if (_deterministicWorkingDirName)
                         {
-                            dirParent = new File(dirParent, jobId);
+                            dirParent = dirParent.resolveChild(jobId);
                             tempDirBase = dirParent;
                         }
                         else
@@ -160,7 +162,7 @@ else if (name.length() < 3)
 
                     if (_deterministicWorkingDirName)
                     {
-                        tempDir = new File(dirParent, name + WORK_DIR_SUFFIX);
+                        tempDir = dirParent.resolveChild(name + WORK_DIR_SUFFIX);
                     }
                     else
                     {
@@ -193,8 +195,8 @@ else if (name.length() < 3)
                 throw new IOException("Failed to create local working directory " + tempDir);
             }
 
-            File lockDir = (_lockDirectory == null ? null : new File(_lockDirectory));
-            File transferToDirOnFailure = (_transferToDirOnFailure == null ? null : new File(_transferToDirOnFailure));
+            FileLike lockDir = (_lockDirectory == null ? null : FileSystemLike.wrapFile(new File(_lockDirectory)));
+            FileLike transferToDirOnFailure = (_transferToDirOnFailure == null ? null : FileSystemLike.wrapFile(new File(_transferToDirOnFailure)));
             return new WorkDirectoryRemote(support, this, log, lockDir, tempDir, transferToDirOnFailure, _allowReuseExistingTempDirectory, tempDirBase);
         }
 
@@ -327,7 +329,7 @@ public void setDeterministicWorkingDirName(boolean deterministicWorkingDirName)
         }
     }
 
-    public WorkDirectoryRemote(FileAnalysisJobSupport support, WorkDirFactory factory, Logger log, File lockDir, File tempDir, File transferToDirOnFailure, boolean reuseExistingDirectory, File folderToClean) throws IOException
+    public WorkDirectoryRemote(FileAnalysisJobSupport support, WorkDirFactory factory, Logger log, FileLike lockDir, FileLike tempDir, FileLike transferToDirOnFailure, boolean reuseExistingDirectory, FileLike folderToClean) throws IOException
     {
         super(support, factory, tempDir, reuseExistingDirectory, log);
 
@@ -414,7 +416,7 @@ public void remove(boolean success) throws IOException
         {
             _jobLog.debug("removing entire work dir through: " + _folderToClean.getPath());
             _jobLog.debug("starting with: " + _dir.getPath());
-            File toCheck = _dir;
+            FileLike toCheck = _dir;
 
             //debugging only:
             if (!URIUtil.isDescendant(_folderToClean.toURI(), toCheck.toURI()))
@@ -427,27 +429,22 @@ public void remove(boolean success) throws IOException
                 if (!toCheck.exists())
                 {
                     _jobLog.debug("directory does not exist: " + toCheck.getPath());
-                    toCheck = toCheck.getParentFile();
+                    toCheck = toCheck.getParent();
                     continue;
                 }
 
-                String[] children = toCheck.list();
-                if (children != null && children.length == 0)
+                List children = toCheck.getChildren();
+                if (children.isEmpty())
                 {
                     _jobLog.debug("removing directory: " + toCheck.getPath());
-                    FileUtils.deleteDirectory(toCheck);
-                    toCheck = toCheck.getParentFile();
-                }
-                else if (children == null)
-                {
-                    _jobLog.debug("unable to list children, will not delete: " + toCheck.getPath());
-                    continue;
+                    FileUtil.deleteDir(toCheck);
+                    toCheck = toCheck.getParent();
                 }
                 else
                 {
                     _jobLog.debug("work directory has children, will not delete: " + toCheck.getPath());
                     _jobLog.debug("files:");
-                    for (String fn : children)
+                    for (FileLike fn : children)
                     {
                         _jobLog.debug(fn);
                     }
@@ -477,7 +474,7 @@ protected CopyingResource createCopyingLock() throws IOException
 
             try
             {
-                File masterLockFile = new File(_lockDirectory, "counter");
+                File masterLockFile = _lockDirectory.resolveChild("counter").toNioPathForWrite().toFile();
                 randomAccessFile = new RandomAccessFile(masterLockFile, "rw");
                 FileChannel masterChannel = randomAccessFile.getChannel();
                 masterLock = masterChannel.lock();
@@ -494,7 +491,7 @@ protected CopyingResource createCopyingLock() throws IOException
         }
 
         _jobLog.debug("Acquiring lock #" + lockInfo.getCurrentLock());
-        File f = new File(_lockDirectory, "lock" + lockInfo.getCurrentLock());
+        File f = _lockDirectory.resolveChild("lock" + lockInfo.getCurrentLock()).toNioPathForWrite().toFile();
         FileChannel lockChannel = new FileOutputStream(f, true).getChannel();
         FileLockCopyingResource result = new FileLockCopyingResource(lockChannel, lockInfo.getCurrentLock(), f);
         _jobLog.debug("Lock #" + lockInfo.getCurrentLock() + " acquired");
@@ -509,7 +506,7 @@ private void rewriteMasterLock(RandomAccessFile masterFile, MasterLockInfo lockI
 
         String output = Integer.toString(lockInfo.getCurrentLock());
         if (lockInfo.getTotalLocks() != FILE_LOCKS_DEFAULT)
-            output += " " + Integer.toString(lockInfo.getTotalLocks());
+            output += " " + lockInfo.getTotalLocks();
         byte[] outputBytes = output.getBytes(StringUtilsLabKey.DEFAULT_CHARSET);
         masterFile.write(outputBytes);
         masterFile.setLength(outputBytes.length);
diff --git a/pipeline/src/org/labkey/pipeline/importer/FolderImportTask.java b/pipeline/src/org/labkey/pipeline/importer/FolderImportTask.java
index 2d703a034ef..550bfdb30ea 100644
--- a/pipeline/src/org/labkey/pipeline/importer/FolderImportTask.java
+++ b/pipeline/src/org/labkey/pipeline/importer/FolderImportTask.java
@@ -70,18 +70,18 @@ public RecordedActionSet run() throws PipelineJobException
             job.getJobSupport(CloudArchiveImporterSupport.class).updateWorkingRoot(importRoot);
         }
 
-        boolean isFileAnalysisJob = FileAnalysisJobSupport.class.isInstance(job); // File watcher triggered job
+        boolean isFileAnalysisJob = job instanceof FileAnalysisJobSupport; // File watcher triggered job
         if (isFileAnalysisJob)
         {
             FileAnalysisJobSupport support = job.getJobSupport(FileAnalysisJobSupport.class);
             ImportOptions options = new ImportOptions(job.getContainerId(), job.getUser().getUserId());
-            options.setAnalysisDir(support.getDataDirectoryFileLike());
+            options.setAnalysisDir(support.getDataDirectory());
 
-            job = new FolderImportJob(job.getContainer(), job.getUser(), null, support.findInputFileLike(FOLDER_XML), FOLDER_XML, job.getPipeRoot(), options);
+            job = new FolderImportJob(job.getContainer(), job.getUser(), null, support.findInputFile(FOLDER_XML), FOLDER_XML, job.getPipeRoot(), options);
             job.setStatus(PipelineJob.TaskStatus.running.toString(), "Starting folder import job", true);
 
             importContext = ((FolderImportJob) job).getImportContext();
-            vf = new FileSystemFile(support.getDataDirectoryFileLike());
+            vf = new FileSystemFile(support.getDataDirectory());
         }
         /* Standard Pipeline triggered job */
         else
diff --git a/pipeline/src/org/labkey/pipeline/mule/test/DummyPipelineJob.java b/pipeline/src/org/labkey/pipeline/mule/test/DummyPipelineJob.java
index 1765440d137..d6d440bd2ee 100644
--- a/pipeline/src/org/labkey/pipeline/mule/test/DummyPipelineJob.java
+++ b/pipeline/src/org/labkey/pipeline/mule/test/DummyPipelineJob.java
@@ -74,7 +74,7 @@ public DummyPipelineJob(Container c, User user, Worker worker)
         _worker = worker;
         try
         {
-            setLogFile(FileUtil.createTempFile("DummyPipelineJob", ".tmp"));
+            setLogFile(FileUtil.createTempFileLike("DummyPipelineJob", ".tmp"));
         }
         catch (IOException e)
         {
diff --git a/query/src/org/labkey/query/reports/ReportsController.java b/query/src/org/labkey/query/reports/ReportsController.java
index de1862784f1..106678c9231 100644
--- a/query/src/org/labkey/query/reports/ReportsController.java
+++ b/query/src/org/labkey/query/reports/ReportsController.java
@@ -1478,10 +1478,10 @@ public ModelAndView getView(ScriptReportBean form, BindException errors) throws
             {
                 try
                 {
-                    File file = rReport.createInputDataFile(getViewContext());
+                    FileLike file = rReport.createInputDataFile(getViewContext());
                     if (file.exists())
                     {
-                        PageFlowUtil.streamFile(getViewContext().getResponse(), file.toPath(), true);
+                        PageFlowUtil.streamFile(getViewContext().getResponse(), file.toNioPathForRead(), true);
                     }
                 }
                 catch (SQLException e)
diff --git a/specimen/src/org/labkey/specimen/SpecimenModule.java b/specimen/src/org/labkey/specimen/SpecimenModule.java
index 02ad4f3b1b0..745a9a16c57 100644
--- a/specimen/src/org/labkey/specimen/SpecimenModule.java
+++ b/specimen/src/org/labkey/specimen/SpecimenModule.java
@@ -90,8 +90,8 @@
 import org.labkey.specimen.writer.SpecimenArchiveWriter;
 import org.labkey.specimen.writer.SpecimenSettingsWriter;
 import org.labkey.specimen.writer.SpecimenWriter;
+import org.labkey.vfs.FileLike;
 
-import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -171,7 +171,7 @@ public ActionURL getSpecimensURL(Container c)
             }
 
             @Override
-            public void importSpecimenArchive(@Nullable Path inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge, boolean syncParticipantVisit) throws PipelineJobException, ValidationException
+            public void importSpecimenArchive(@Nullable FileLike inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge, boolean syncParticipantVisit) throws PipelineJobException, ValidationException
             {
                 AbstractSpecimenTask.doImport(inputFile, job, ctx, merge, syncParticipantVisit);
             }
diff --git a/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java b/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java
index ff74eb52a86..3acf4e95dda 100644
--- a/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java
+++ b/specimen/src/org/labkey/specimen/importer/AbstractSpecimenTask.java
@@ -16,7 +16,6 @@
 
 package org.labkey.specimen.importer;
 
-import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.labkey.api.admin.ImportException;
@@ -25,7 +24,6 @@
 import org.labkey.api.pipeline.PipelineJobException;
 import org.labkey.api.pipeline.RecordedActionSet;
 import org.labkey.api.pipeline.TaskFactory;
-import org.labkey.api.query.ValidationException;
 import org.labkey.api.study.SpecimenService;
 import org.labkey.api.study.SpecimenTransform;
 import org.labkey.api.study.importer.SimpleStudyImportContext;
@@ -37,11 +35,9 @@
 import org.labkey.api.writer.ZipUtil;
 import org.labkey.specimen.pipeline.SpecimenJobSupport;
 import org.labkey.specimen.writer.SpecimenArchiveDataTypes;
+import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.sql.BatchUpdateException;
 import java.util.Date;
 
@@ -66,7 +62,7 @@ public RecordedActionSet run() throws PipelineJobException
         try
         {
             PipelineJob job = getJob();
-            Path specimenArchive = getSpecimenFile(job);
+            FileLike specimenArchive = getSpecimenFile(job);
             SimpleStudyImportContext ctx = getImportContext(job);
 
             doImport(specimenArchive, job, ctx, isMerge(), true, getImportHelper());
@@ -83,7 +79,7 @@ public RecordedActionSet run() throws PipelineJobException
         return new RecordedActionSet();
     }
 
-    protected Path getSpecimenFile(PipelineJob job) throws Exception
+    protected FileLike getSpecimenFile(PipelineJob job) throws Exception
     {
         SpecimenJobSupport support = job.getJobSupport(SpecimenJobSupport.class);
         return support.getSpecimenArchivePath();
@@ -94,25 +90,30 @@ protected SimpleStudyImportContext getImportContext(PipelineJob job)
         return job.getJobSupport(SpecimenJobSupport.class).getImportContext();
     }
 
-    public static void doImport(@Nullable Path inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge,
-            boolean syncParticipantVisit) throws PipelineJobException, ValidationException
+    public static void doImport(@Nullable FileLike inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge,
+                                boolean syncParticipantVisit) throws PipelineJobException
     {
         doImport(inputFile, job, ctx, merge, syncParticipantVisit, new DefaultImportHelper());
     }
 
-    private static void doPostTransform(SpecimenTransform transformer, Path inputFile, PipelineJob job) throws PipelineJobException
+    private static void doPostTransform(SpecimenTransform transformer, FileLike inputFile, PipelineJob job) throws PipelineJobException
     {
         if (transformer.getFileType().isType(inputFile))
         {
             if (job != null)
                 job.setStatus("OPTIONAL POST TRANSFORMING STEP " + transformer.getName() + " DATA");
-            File specimenArchive = ARCHIVE_FILE_TYPE.getFile(inputFile.getParent().toFile(), transformer.getFileType().getBaseName(inputFile));
-            transformer.postTransform(job, inputFile.toFile(), specimenArchive);
+            FileLike specimenArchive = ARCHIVE_FILE_TYPE.getFile(inputFile.getParent(), transformer.getFileType().getBaseName(inputFile));
+            transformer.postTransform(job, inputFile, specimenArchive);
         }
     }
 
-    public static void doImport(@Nullable Path inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge,
-                                boolean syncParticipantVisit, ImportHelper importHelper) throws PipelineJobException, ValidationException
+    public AbstractSpecimenTask(TaskFactory factory, PipelineJob job)
+    {
+        super(factory, job);
+    }
+
+    public static void doImport(@Nullable FileLike inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge,
+                                boolean syncParticipantVisit, ImportHelper importHelper) throws PipelineJobException
     {
         // do nothing if we've specified data types and specimen is not one of them
         if (!ctx.isDataTypeSelected(SpecimenArchiveDataTypes.SPECIMENS))
@@ -157,8 +158,8 @@ public static void doImport(@Nullable Path inputFile, PipelineJob job, SimpleStu
         }
         catch (Exception e)
         {
-            if (e instanceof BatchUpdateException && null != ((BatchUpdateException)e).getNextException())
-                e = ((BatchUpdateException)e).getNextException();
+            if (e instanceof BatchUpdateException bue && null != bue.getNextException())
+                e = bue.getNextException();
             throw new PipelineJobException(e);
         }
         finally
@@ -180,30 +181,30 @@ public ImportHelper getImportHelper()
 
     public interface ImportHelper
     {
-        VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx, @Nullable Path inputFile) throws IOException, ImportException, PipelineJobException;
+        VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx, @Nullable FileLike inputFile) throws IOException, ImportException, PipelineJobException;
         void afterImport(SimpleStudyImportContext ctx);
     }
 
     protected static class DefaultImportHelper implements ImportHelper
     {
-        private Path _unzipDir;
+        private FileLike _unzipDir;
 
         @Override
-        public VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx, @Nullable Path inputFile) throws IOException, ImportException, PipelineJobException
+        public VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx, @Nullable FileLike inputFile) throws IOException, ImportException, PipelineJobException
         {
             // backwards compatibility, if we are given a specimen archive as a zip file, we need to extract it
             if (inputFile != null)
             {
                 // Might need to transform to a file type that we know how to import
-                Path specimenArchive = inputFile;
+                FileLike specimenArchive = inputFile;
 
                 for (SpecimenTransform transformer : SpecimenService.get().getSpecimenTransforms(ctx.getContainer()))
                 {
-                    if (transformer.getFileType().isType(inputFile.getFileName().toString()))
+                    if (transformer.getFileType().isType(inputFile.getName()))
                     {
                         if (job != null)
                             job.setStatus("TRANSFORMING " + transformer.getName() + " DATA");
-                        specimenArchive = ARCHIVE_FILE_TYPE.getPath(inputFile.getParent(), transformer.getFileType().getBaseName(inputFile));
+                        specimenArchive = ARCHIVE_FILE_TYPE.getFile(inputFile.getParent(), transformer.getFileType().getBaseName(inputFile));
                         transformer.transform(job, inputFile, specimenArchive);
                         break;
                     }
@@ -221,7 +222,7 @@ public VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx,
 
                     ctx.getLogger().info("Unzipping specimen archive " + specimenArchive);
                     String tempDirName = DateUtil.formatDateTime(new Date(), "yyMMddHHmmssSSS");
-                    _unzipDir = specimenArchive.getParent().resolve(tempDirName);
+                    _unzipDir = specimenArchive.getParent().resolveChild(tempDirName);
                     ZipUtil.unzipToDirectory(specimenArchive, _unzipDir, ctx.getLogger());
 
                     ctx.getLogger().info("Archive unzipped to " + _unzipDir);
@@ -242,19 +243,11 @@ public void afterImport(SimpleStudyImportContext ctx)
                 delete(_unzipDir, ctx);
         }
 
-        protected void delete(Path path, SimpleStudyImportContext ctx)
+        protected void delete(FileLike path, SimpleStudyImportContext ctx)
         {
-            Logger log = ctx.getLogger();
-            if (Files.isDirectory(path))
+            if (path.isDirectory())
             {
-                try
-                {
-                    FileUtil.deleteDir(path);
-                }
-                catch (IOException e)
-                {
-                    log.error("Error deleting files from " + path, e);
-                }
+                FileUtil.deleteDir(path);
             }
         }
     }
diff --git a/specimen/src/org/labkey/specimen/pipeline/FileAnalysisSpecimenTask.java b/specimen/src/org/labkey/specimen/pipeline/FileAnalysisSpecimenTask.java
index 05b37f3d1b5..546b5e0d37d 100644
--- a/specimen/src/org/labkey/specimen/pipeline/FileAnalysisSpecimenTask.java
+++ b/specimen/src/org/labkey/specimen/pipeline/FileAnalysisSpecimenTask.java
@@ -18,10 +18,9 @@
 import org.labkey.api.util.DateUtil;
 import org.labkey.api.writer.FileSystemFile;
 import org.labkey.api.writer.VirtualFile;
+import org.labkey.vfs.FileLike;
 
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.Date;
 import java.util.List;
 
@@ -36,10 +35,10 @@ public FileAnalysisSpecimenTask(FileAnalysisSpecimenTask.Factory factory, Pipeli
     }
 
     @Override
-    protected Path getSpecimenFile(PipelineJob job)
+    protected FileLike getSpecimenFile(PipelineJob job)
     {
         FileAnalysisJobSupport support = job.getJobSupport(FileAnalysisJobSupport.class);
-        List paths = support.getInputFilePaths();
+        List paths = support.getInputFiles();
         // there should only be a single file associated with this task
         assert paths.size() == 1;
         return paths.get(0);
@@ -60,7 +59,7 @@ protected boolean isMerge()
         if (support.getParameters().containsKey(MERGE_SPECIMEN))
             isMerge = BooleanUtils.toBoolean(support.getParameters().get(MERGE_SPECIMEN));
 
-        getJob().getLogger().info("Specimen merge option is set to : " + isMerge);
+        getJob().getLogger().info("Specimen merge option is set to: {}", isMerge);
         return isMerge;
     }
 
@@ -72,14 +71,14 @@ public AbstractSpecimenTask.ImportHelper getImportHelper()
 
     private static class ImportHelper extends DefaultImportHelper
     {
-        private Path _tempDir;
+        private FileLike _tempDir;
 
         @Override
-        public VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx, @Nullable Path inputFile) throws IOException, ImportException, PipelineJobException
+        public VirtualFile getSpecimenDir(PipelineJob job, SimpleStudyImportContext ctx, @Nullable FileLike inputFile) throws ImportException, PipelineJobException
         {
             if (inputFile != null)
             {
-                try (TikaInputStream is = TikaInputStream.get(Files.newInputStream(inputFile)))
+                try (TikaInputStream is = TikaInputStream.get(inputFile.openInputStream()))
                 {
                     // determine the type of file being imported
                     DefaultDetector detector = new DefaultDetector();
@@ -97,9 +96,9 @@ else if (MediaType.TEXT_PLAIN.equals(type))
                         //
                         ctx.getLogger().info("Single specimen file detected, moving to a temp folder for processing.");
                         String tempDirName = DateUtil.formatDateTime(new Date(), "yyMMddHHmmssSSS");
-                        _tempDir = inputFile.getParent().resolve(tempDirName);
+                        _tempDir = inputFile.getParent().resolveChild(tempDirName);
                         FileUtil.createDirectory(_tempDir);
-                        Files.copy(inputFile, _tempDir.resolve(inputFile.getFileName()));
+                        FileUtil.copyFile(inputFile, _tempDir.resolveChild(inputFile.getName()));
 
                         return new FileSystemFile(_tempDir);
                     }
@@ -132,7 +131,7 @@ public Factory()
         }
 
         @Override
-        public PipelineJob.Task createTask(PipelineJob job)
+        public FileAnalysisSpecimenTask createTask(PipelineJob job)
         {
             return new FileAnalysisSpecimenTask(this, job);
         }
diff --git a/specimen/src/org/labkey/specimen/pipeline/SpecimenBatch.java b/specimen/src/org/labkey/specimen/pipeline/SpecimenBatch.java
index b9c6a2acbcd..2cd5d28860d 100644
--- a/specimen/src/org/labkey/specimen/pipeline/SpecimenBatch.java
+++ b/specimen/src/org/labkey/specimen/pipeline/SpecimenBatch.java
@@ -31,9 +31,7 @@
 import org.labkey.api.view.ViewBackgroundInfo;
 import org.labkey.vfs.FileLike;
 
-import java.io.File;
 import java.io.Serializable;
-import java.nio.file.Path;
 
 /**
  * User: brittp
@@ -56,9 +54,9 @@ public SpecimenBatch(ViewBackgroundInfo info, FileLike definitionFile, PipeRoot
     }
 
     @Override
-    protected File createLogFile()
+    protected FileLike createLogFile()
     {
-        return new File(getPipeRoot().getLogDirectory(), FileUtil.makeFileNameWithTimestamp(_definitionFile.getName(), "log"));
+        return getPipeRoot().getLogDirectory(true).resolveChild(FileUtil.makeFileNameWithTimestamp(_definitionFile.getName(), "log"));
     }
 
     @Override
@@ -77,9 +75,9 @@ public ActionURL getStatusHref()
     }
 
     @Override
-    public Path getSpecimenArchivePath()
+    public FileLike getSpecimenArchivePath()
     {
-        return _definitionFile.toNioPathForRead();
+        return _definitionFile;
     }
 
     @Override
diff --git a/specimen/src/org/labkey/specimen/pipeline/SpecimenJobSupport.java b/specimen/src/org/labkey/specimen/pipeline/SpecimenJobSupport.java
index fb9b983defb..11043224c5e 100644
--- a/specimen/src/org/labkey/specimen/pipeline/SpecimenJobSupport.java
+++ b/specimen/src/org/labkey/specimen/pipeline/SpecimenJobSupport.java
@@ -18,8 +18,7 @@
 
 import org.labkey.api.admin.ImportException;
 import org.labkey.api.study.importer.SimpleStudyImportContext;
-
-import java.nio.file.Path;
+import org.labkey.vfs.FileLike;
 
 /*
 * User: adam
@@ -28,7 +27,7 @@
 */
 public interface SpecimenJobSupport
 {
-    Path getSpecimenArchivePath() throws ImportException;
+    FileLike getSpecimenArchivePath() throws ImportException;
     boolean isMerge();
     SimpleStudyImportContext getImportContext();
 }
diff --git a/study/api-src/org/labkey/api/specimen/SpecimenMigrationService.java b/study/api-src/org/labkey/api/specimen/SpecimenMigrationService.java
index 669d5700e3a..c5feb8ec739 100644
--- a/study/api-src/org/labkey/api/specimen/SpecimenMigrationService.java
+++ b/study/api-src/org/labkey/api/specimen/SpecimenMigrationService.java
@@ -17,8 +17,8 @@
 import org.labkey.api.study.importer.SimpleStudyImportContext;
 import org.labkey.api.view.ActionURL;
 import org.labkey.api.view.ViewContext;
+import org.labkey.vfs.FileLike;
 
-import java.nio.file.Path;
 import java.util.Set;
 
 // Temporary service that provides entry points to ease migration of code from study module to specimen module
@@ -41,7 +41,7 @@ static void setInstance(SpecimenMigrationService impl)
     ActionURL getSelectedSpecimensURL(Container c);
     ActionURL getSpecimensURL(Container c);
 
-    void importSpecimenArchive(@Nullable Path inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge,
+    void importSpecimenArchive(@Nullable FileLike inputFile, PipelineJob job, SimpleStudyImportContext ctx, boolean merge,
                                boolean syncParticipantVisit) throws PipelineJobException, ValidationException;
 
     void clearRequestCaches(Container c);
diff --git a/study/api-src/org/labkey/api/study/pipeline/AbstractStudyPipelineJob.java b/study/api-src/org/labkey/api/study/pipeline/AbstractStudyPipelineJob.java
index 2555366289e..ed811951ced 100644
--- a/study/api-src/org/labkey/api/study/pipeline/AbstractStudyPipelineJob.java
+++ b/study/api-src/org/labkey/api/study/pipeline/AbstractStudyPipelineJob.java
@@ -36,9 +36,7 @@
 import org.labkey.api.view.ViewContext;
 import org.labkey.api.writer.VirtualFile;
 import org.labkey.study.xml.StudyDocument;
-
-import java.io.File;
-import java.nio.file.Path;
+import org.labkey.vfs.FileLike;
 
 // Allows some sharing of code between study publication and specimen refresh
 public abstract class AbstractStudyPipelineJob extends PipelineJob
@@ -55,7 +53,7 @@ public AbstractStudyPipelineJob(String provider, Container source, Container des
 
         setDstContainer(destination);
 
-        Path tempLogFile = root.getLogDirectoryFileLike(true).resolveChild(FileUtil.makeFileNameWithTimestamp(getLogName(), "log")).toNioPathForWrite();
+        FileLike tempLogFile = root.getLogDirectory(true).resolveChild(FileUtil.makeFileNameWithTimestamp(getLogName(), "log"));
         setLogFile(tempLogFile);
     }
 
diff --git a/study/api-src/org/labkey/api/study/pipeline/StudyBatch.java b/study/api-src/org/labkey/api/study/pipeline/StudyBatch.java
index fe8382aee8c..a3711ad82a7 100644
--- a/study/api-src/org/labkey/api/study/pipeline/StudyBatch.java
+++ b/study/api-src/org/labkey/api/study/pipeline/StudyBatch.java
@@ -61,7 +61,7 @@ public String getDescription()
         return "Import files";
     }
 
-    protected abstract File createLogFile();
+    protected abstract FileLike createLogFile();
 
     public void submit() throws IOException
     {
diff --git a/study/src/org/labkey/study/importer/StudyImportContext.java b/study/src/org/labkey/study/importer/StudyImportContext.java
index 1adf4a587f5..4341e57d0e2 100644
--- a/study/src/org/labkey/study/importer/StudyImportContext.java
+++ b/study/src/org/labkey/study/importer/StudyImportContext.java
@@ -32,6 +32,8 @@
 import org.labkey.study.model.StudyImpl;
 import org.labkey.study.model.StudyManager;
 import org.labkey.study.xml.StudyDocument;
+import org.labkey.vfs.FileLike;
+import org.labkey.vfs.FileSystemLike;
 
 import java.io.File;
 import java.io.IOException;
@@ -127,7 +129,7 @@ public synchronized StudyDocument getDocument() throws ImportException
 
     // TODO: this should go away once study import fully supports using VirtualFile -  HMMM.  Why doesn't it?
     @Deprecated
-    private Path getStudyFile(VirtualFile root, VirtualFile dir, String name) throws ImportException
+    private FileLike getStudyFile(VirtualFile root, VirtualFile dir, String name) throws ImportException
     {
         Path rootFile = FileUtil.stringToPath(getContainer(), root.getLocation());
         Path dirFile = FileUtil.stringToPath(getContainer(), dir.getLocation());
@@ -140,7 +142,7 @@ private Path getStudyFile(VirtualFile root, VirtualFile dir, String name) throws
         if (!Files.isRegularFile(file))
             throw new ImportException(source + " refers to " + ImportException.getRelativePath(rootFile, file) + ": expected a file but found a directory");
 
-        return file;
+        return FileSystemLike.wrapFile(file);
     }
 
     private StudyDocument readStudyDocument(Path studyXml) throws ImportException, IOException
@@ -163,7 +165,7 @@ private StudyDocument readStudyDocument(Path studyXml) throws ImportException, I
         return studyDoc;
     }
 
-    public Path getSpecimenArchive(VirtualFile root) throws ImportException
+    public FileLike getSpecimenArchive(VirtualFile root) throws ImportException
     {
         StudyDocument.Study.Specimens specimens = getXml().getSpecimens();
 
diff --git a/study/src/org/labkey/study/importer/StudyImporterFactory.java b/study/src/org/labkey/study/importer/StudyImporterFactory.java
index 2692e0d40ad..7f297fe49d8 100644
--- a/study/src/org/labkey/study/importer/StudyImporterFactory.java
+++ b/study/src/org/labkey/study/importer/StudyImporterFactory.java
@@ -44,6 +44,7 @@
 import org.labkey.study.writer.StudyArchiveDataTypes;
 import org.labkey.study.writer.StudySerializationRegistry;
 import org.labkey.study.xml.StudyDocument;
+import org.labkey.vfs.FileLike;
 import org.springframework.validation.BindException;
 
 import java.io.IOException;
@@ -151,11 +152,11 @@ public void process(@Nullable PipelineJob job, FolderImportContext ctx, VirtualF
                 // import specimens, if the module is present
                 if (null != SpecimenService.get())
                 {
-                    Path specimenFile = studyImportContext.getSpecimenArchive(studyDir);
+                    FileLike specimenFile = studyImportContext.getSpecimenArchive(studyDir);
                     if (useLocalImportDir)
                     {   //TODO this should be done from the import context getSpecimenArchive
-                        specimenFile = job.getPipeRoot().getRootNioPath().relativize(specimenFile);
-                        specimenFile = job.getPipeRoot().getImportDirectory().toNioPathForRead().resolve(specimenFile);
+                        String path = job.getPipeRoot().relativePath(specimenFile);
+                        specimenFile = job.getPipeRoot().getImportDirectory().resolveFile(org.labkey.api.util.Path.parse(path));
                     }
 
                     SpecimenMigrationService.get().importSpecimenArchive(specimenFile, job, studyImportContext, false, false);
diff --git a/study/src/org/labkey/study/pipeline/DatasetBatch.java b/study/src/org/labkey/study/pipeline/DatasetBatch.java
index 98c8216ebe3..33af58c5d1f 100644
--- a/study/src/org/labkey/study/pipeline/DatasetBatch.java
+++ b/study/src/org/labkey/study/pipeline/DatasetBatch.java
@@ -25,6 +25,8 @@
 import org.labkey.api.util.Path;
 import org.labkey.api.view.ViewBackgroundInfo;
 import org.labkey.api.writer.VirtualFile;
+import org.labkey.vfs.FileLike;
+import org.labkey.vfs.FileSystemLike;
 
 import java.io.File;
 import java.io.Serializable;
@@ -52,10 +54,10 @@ public DatasetBatch(ViewBackgroundInfo info, VirtualFile datasetsDirectory, Stri
     }
 
     @Override
-    protected File createLogFile()
+    protected FileLike createLogFile()
     {
-        Path logFilePath = Path.parse(_datasetsDirectory.getLocation()).append(_datasetsFileName);
-        return new File(logFilePath.getParent().toString(), FileUtil.makeFileNameWithTimestamp(logFilePath.getName(), "log"));
+        FileLike datasetDir = FileSystemLike.wrapFile(new File(Path.parse(_datasetsDirectory.getLocation()).toString()));
+        return datasetDir.resolveChild(FileUtil.makeFileNameWithTimestamp(_datasetsFileName, "log"));
     }
 
     @Override
@@ -80,7 +82,7 @@ public String getDatasetsFileName()
     }
 
     @Override
-    public TaskPipeline getTaskPipeline()
+    public TaskPipeline getTaskPipeline()
     {
         return PipelineJobService.get().getTaskPipeline(new TaskId(DatasetBatch.class));
     }
diff --git a/study/src/org/labkey/study/pipeline/DatasetInferSchemaReader.java b/study/src/org/labkey/study/pipeline/DatasetInferSchemaReader.java
index a08d517cc1b..aec62c765c7 100644
--- a/study/src/org/labkey/study/pipeline/DatasetInferSchemaReader.java
+++ b/study/src/org/labkey/study/pipeline/DatasetInferSchemaReader.java
@@ -28,6 +28,7 @@
 import org.labkey.study.importer.StudyImportContext;
 import org.labkey.study.model.DatasetDefinition;
 import org.labkey.study.model.StudyImpl;
+import org.labkey.vfs.FileLike;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -50,14 +51,14 @@ public class DatasetInferSchemaReader extends DatasetFileReader implements Schem
 
     private final Map _datasetInfoMap = new LinkedHashMap<>();
     private final List _builders = new ArrayList<>();
-    private Map> _inputDataMap;
+    private Map> _inputDataMap;
 
     public DatasetInferSchemaReader(VirtualFile datasetsDirectory, String datasetsFileName, StudyImpl study, StudyImportContext studyImportContext)
     {
         super(datasetsDirectory, datasetsFileName, study, studyImportContext);
     }
 
-    public DatasetInferSchemaReader(VirtualFile datasetsDirectory, StudyImpl study, StudyImportContext studyImportContext, Map> inputDataMap)
+    public DatasetInferSchemaReader(VirtualFile datasetsDirectory, StudyImpl study, StudyImportContext studyImportContext, Map> inputDataMap)
     {
         super(datasetsDirectory, null, study, studyImportContext);
         _inputDataMap = inputDataMap;
@@ -105,7 +106,7 @@ private void initialize()
             List datasetIds = _study.getDatasets().stream()
                 .map(DatasetDefinition::getDatasetId)
                 .sorted(Comparator.comparingInt(o -> o))
-                .collect(Collectors.toList());
+                .toList();
 
             // next available dataset ID
             int nextId = datasetIds.isEmpty() ? 1000 : datasetIds.get(datasetIds.size()-1) + 1;
@@ -176,7 +177,7 @@ protected String getKeyFromDatasetName(String name)
         // data instead of just the legacy regex method
         if (_inputDataMap != null)
         {
-            for (Map.Entry> entry : _inputDataMap.entrySet())
+            for (Map.Entry> entry : _inputDataMap.entrySet())
             {
                 if (entry.getKey().getName().equalsIgnoreCase(name))
                 {
@@ -196,9 +197,7 @@ protected String getKeyFromDatasetName(String name)
         Matcher m = _filePattern.matcher(name);
         if (m.matches())
         {
-            String key = m.group(1);
-            if (key != null)
-                return key;
+            return m.group(1);
         }
         return null;
     }
@@ -207,7 +206,7 @@ protected String getKeyFromDatasetName(String name)
     protected List getDatasetFileNames()
     {
         if (_inputDataMap != null)
-            return _inputDataMap.keySet().stream().map(File::getName).collect(Collectors.toList());
+            return _inputDataMap.keySet().stream().map(FileLike::getName).collect(Collectors.toList());
         else
             return super.getDatasetFileNames();
     }
diff --git a/study/src/org/labkey/study/pipeline/FileAnalysisDatasetTask.java b/study/src/org/labkey/study/pipeline/FileAnalysisDatasetTask.java
index 7a37571dda9..136b69eacc1 100644
--- a/study/src/org/labkey/study/pipeline/FileAnalysisDatasetTask.java
+++ b/study/src/org/labkey/study/pipeline/FileAnalysisDatasetTask.java
@@ -30,6 +30,7 @@
 import org.labkey.study.importer.StudyImportContext;
 import org.labkey.study.model.StudyImpl;
 import org.labkey.study.model.StudyManager;
+import org.labkey.vfs.FileLike;
 import org.springframework.validation.BindException;
 
 import java.io.File;
@@ -67,7 +68,7 @@ protected String getDatasetsFileName()
     protected VirtualFile getDatasetsDirectory()
     {
         FileAnalysisJobSupport jobSupport = getJob().getJobSupport(FileAnalysisJobSupport.class);
-        File dataDir = jobSupport.getDataDirectory();
+        FileLike dataDir = jobSupport.getDataDirectory();
         if (dataDir.exists())
         {
             return new FileSystemFile(dataDir);
@@ -92,11 +93,11 @@ public RecordedActionSet run() throws PipelineJobException
                 return new RecordedActionSet();
             }
 
-            Map> inputDataMap = new HashMap<>();
+            Map> inputDataMap = new HashMap<>();
 
             // guaranteed to only have a single file
             assert jobSupport.getInputFiles().size() == 1;
-            for (File file : jobSupport.getInputFiles())
+            for (FileLike file : jobSupport.getInputFiles())
             {
                 if (params.containsKey(DATASET_ID_KEY))
                     inputDataMap.put(file, new Pair<>(DATASET_ID_KEY, params.get(DATASET_ID_KEY)));
diff --git a/study/src/org/labkey/study/pipeline/MasterPatientIndexUpdateTask.java b/study/src/org/labkey/study/pipeline/MasterPatientIndexUpdateTask.java
index d4111545f27..648aab81505 100644
--- a/study/src/org/labkey/study/pipeline/MasterPatientIndexUpdateTask.java
+++ b/study/src/org/labkey/study/pipeline/MasterPatientIndexUpdateTask.java
@@ -23,7 +23,6 @@
 import org.labkey.api.util.URLHelper;
 import org.labkey.api.view.ViewBackgroundInfo;
 
-import java.io.File;
 import java.io.IOException;
 
 public class MasterPatientIndexUpdateTask extends PipelineJob
@@ -39,8 +38,7 @@ public MasterPatientIndexUpdateTask(ViewBackgroundInfo info, @NotNull PipeRoot r
         super(PIPELINE_PROVIDER, info, root);
 
         _svc = service;
-        File logFile = FileUtil.createTempFile("patientIndexUpdateJob", ".log", root.getRootPath());
-        setLogFile(logFile);
+        setLogFile(FileUtil.createTempFile("patientIndexUpdateJob", ".log", root.getRootFileLike()));
     }
 
     @Override
diff --git a/study/src/org/labkey/study/visitmanager/PurgeParticipantsJob.java b/study/src/org/labkey/study/visitmanager/PurgeParticipantsJob.java
index 59a7d774e41..24de99611c5 100644
--- a/study/src/org/labkey/study/visitmanager/PurgeParticipantsJob.java
+++ b/study/src/org/labkey/study/visitmanager/PurgeParticipantsJob.java
@@ -29,7 +29,7 @@ public PurgeParticipantsJob()
     PurgeParticipantsJob(ViewBackgroundInfo info, PipeRoot pipeRoot)
     {
         super("StudyParticipantPurge", info, pipeRoot);
-        setLogFile(pipeRoot.getLogDirectoryFileLike(true).resolveChild(FileUtil.makeFileNameWithTimestamp("purge_participants", "log")));
+        setLogFile(pipeRoot.getLogDirectory(true).resolveChild(FileUtil.makeFileNameWithTimestamp("purge_participants", "log")));
     }
 
     @Override

From 4923b9993adb8e3e40135624c3f1e5d9481d3af1 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Mon, 17 Nov 2025 16:14:53 -0800
Subject: [PATCH 02/18] Test fixes

---
 .../assay/transform/DataTransformService.java | 10 +----
 .../api/reports/ExternalScriptEngine.java     | 41 ++++++++-----------
 2 files changed, 20 insertions(+), 31 deletions(-)

diff --git a/api/src/org/labkey/api/assay/transform/DataTransformService.java b/api/src/org/labkey/api/assay/transform/DataTransformService.java
index 0b46f8387c8..e597f04d57b 100644
--- a/api/src/org/labkey/api/assay/transform/DataTransformService.java
+++ b/api/src/org/labkey/api/assay/transform/DataTransformService.java
@@ -23,13 +23,11 @@
 import org.labkey.api.util.Pair;
 import org.labkey.api.util.UnexpectedException;
 import org.labkey.vfs.FileLike;
-import org.labkey.vfs.FileSystemLike;
 
 import javax.script.Bindings;
 import javax.script.ScriptContext;
 import javax.script.ScriptEngine;
 import java.io.BufferedReader;
-import java.io.File;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
@@ -158,13 +156,9 @@ public TransformResult transformAndValidate(
                         Object output = engine.eval(script);
 
                         FileLike rewrittenScriptFile;
-                        if (bindings.get(ExternalScriptEngine.REWRITTEN_SCRIPT_FILE) instanceof File)
+                        if (bindings.get(ExternalScriptEngine.REWRITTEN_SCRIPT_FILE) instanceof FileLike file)
                         {
-                            var rewrittenScriptFileObject = bindings.get(ExternalScriptEngine.REWRITTEN_SCRIPT_FILE);
-                            if (rewrittenScriptFileObject instanceof FileLike fo)
-                                rewrittenScriptFile = fo;
-                            else
-                                rewrittenScriptFile = FileSystemLike.wrapFile((File)rewrittenScriptFileObject);
+                                rewrittenScriptFile = file;
                         }
                         else
                         {
diff --git a/api/src/org/labkey/api/reports/ExternalScriptEngine.java b/api/src/org/labkey/api/reports/ExternalScriptEngine.java
index 158611084b0..d1ec588c890 100644
--- a/api/src/org/labkey/api/reports/ExternalScriptEngine.java
+++ b/api/src/org/labkey/api/reports/ExternalScriptEngine.java
@@ -40,7 +40,6 @@
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
@@ -106,11 +105,7 @@ public boolean isBinary(FileLike file)
     {
         String ext = FileUtil.getExtension(file);
 
-        if ("jar".equalsIgnoreCase(ext)) return true;
-        if ("class".equalsIgnoreCase(ext)) return true;
-        if ("exe".equalsIgnoreCase(ext)) return true;
-
-        return false;
+        return "jar".equalsIgnoreCase(ext) || "class".equalsIgnoreCase(ext) || "exe".equalsIgnoreCase(ext);
     }
 
     @Override
@@ -153,26 +148,26 @@ protected Object eval(FileLike scriptFile, ScriptContext context) throws ScriptE
     @Override
     public Object eval(Reader reader, ScriptContext context) throws ScriptException
     {
-        BufferedReader br = new BufferedReader(reader);
 
-        try {
-            String l;
-            StringBuilder sb = new StringBuilder();
-            while ((l = br.readLine()) != null)
+        try (BufferedReader br = new BufferedReader(reader))
+        {
+            try
             {
-                sb.append(l);
-                sb.append('\n');
+                String l;
+                StringBuilder sb = new StringBuilder();
+                while ((l = br.readLine()) != null)
+                {
+                    sb.append(l);
+                    sb.append('\n');
+                }
+                return eval(sb.toString(), context);
+            }
+            catch (IOException ioe)
+            {
+                ExceptionUtil.logExceptionToMothership(null, ioe);
             }
-            return eval(sb.toString(), context);
-        }
-        catch (IOException ioe)
-        {
-            ExceptionUtil.logExceptionToMothership(null, ioe);
-        }
-        finally
-        {
-            try {br.close();} catch(IOException ignored) {}
         }
+        catch (IOException ignored) {}
         return null;
     }
 
@@ -532,7 +527,7 @@ public FileLike getConsoleOutputFile(ScriptContext context)
         String fileName = _def.getOutputFileName();
         if (fileName != null)
         {
-            if (context.getAttribute(REWRITTEN_SCRIPT_FILE) instanceof File scriptFile)
+            if (context.getAttribute(REWRITTEN_SCRIPT_FILE) instanceof FileLike scriptFile)
             {
                 // Replace the ${scriptName} substitution with the actual name of the script file (minus extension)
                 // E.g., if "script.R" is the filename and "${scriptName}.Rout" is the replacement, try "script.Rout"

From 157fe603fef204f1b446be15c6d3744d1ce1e4c4 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Tue, 18 Nov 2025 17:56:14 -0800
Subject: [PATCH 03/18] Fix assorted transform script tests

---
 pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
index d0026940400..6e79d7a1cc3 100644
--- a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
+++ b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
@@ -170,7 +170,7 @@ else if (factory._scriptPath != null)
             if (scriptFile != null)
                 bindings.put(ExternalScriptEngine.SCRIPT_PATH, scriptFile.toString());
 
-            bindings.put(ExternalScriptEngine.WORKING_DIRECTORY, _wd.getDir().getPath());
+            bindings.put(ExternalScriptEngine.WORKING_DIRECTORY, _wd.getDir().toNioPathForRead().toString());
 
             // Thread the timeout option through to the external script engine
             if (_factory.getTimeout() != null && _factory.getTimeout() > 0)

From 844d65f07f72d1d8e95e3bf7233ccf4a15ab17ac Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Wed, 19 Nov 2025 16:00:54 -0800
Subject: [PATCH 04/18] Fix assorted tests

---
 .../api/reports/report/r/RReportJob.java      |  4 +-
 .../study/actions/TransformResultsAction.java |  4 +-
 api/src/org/labkey/api/util/ImageUtil.java    | 20 ++++------
 api/src/org/labkey/api/util/PageFlowUtil.java | 39 +++----------------
 api/src/org/labkey/api/writer/ZipUtil.java    |  2 +-
 .../controllers/exp/ExperimentController.java |  4 +-
 .../labkey/pipeline/PipelineController.java   |  2 +-
 .../labkey/pipeline/api/ScriptTaskImpl.java   |  5 +--
 .../query/reports/ReportsController.java      | 12 +++---
 .../reports/ReportsController.java            |  5 ++-
 10 files changed, 33 insertions(+), 64 deletions(-)

diff --git a/api/src/org/labkey/api/reports/report/r/RReportJob.java b/api/src/org/labkey/api/reports/report/r/RReportJob.java
index e9f984a6672..a7c5ded89ce 100644
--- a/api/src/org/labkey/api/reports/report/r/RReportJob.java
+++ b/api/src/org/labkey/api/reports/report/r/RReportJob.java
@@ -42,6 +42,8 @@
 
 import java.io.File;
 import java.io.Serializable;
+import java.nio.file.CopyOption;
+import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -315,7 +317,7 @@ protected void processOutputs(RReport report, List outputSubst
                             {
                                 newFile = FileUtil.createTempFile(LOG_FILE_PREFIX, ".log", parentDir);
                                 getJob().setLogFile(newFile);
-                                FileUtil.copyFile(file, newFile);
+                                FileUtil.copyFile(file, newFile, StandardCopyOption.REPLACE_EXISTING);
                             }
                             // report.log != getLogFile(), just regular file
                             else
diff --git a/api/src/org/labkey/api/study/actions/TransformResultsAction.java b/api/src/org/labkey/api/study/actions/TransformResultsAction.java
index bb9112306ac..72ecff2a916 100644
--- a/api/src/org/labkey/api/study/actions/TransformResultsAction.java
+++ b/api/src/org/labkey/api/study/actions/TransformResultsAction.java
@@ -43,10 +43,10 @@ public ModelAndView getView(TransformResultsForm form, BindException errors) thr
         {
             FileLike downloadFile = transformDir.resolveChild(form.getName());
             // isn't this always true?
-            if(URIUtil.isDescendant(transformDir.toURI(), downloadFile.toURI()))
+            if (URIUtil.isDescendant(transformDir.toURI(), downloadFile.toURI()))
             {
                 HttpServletResponse response = getViewContext().getResponse();
-                PageFlowUtil.streamFile(response, FileSystemLike.toFile(downloadFile), true);
+                PageFlowUtil.streamFile(response, downloadFile, true);
             }
         }
 
diff --git a/api/src/org/labkey/api/util/ImageUtil.java b/api/src/org/labkey/api/util/ImageUtil.java
index 778d0ffe583..26df16deb5d 100644
--- a/api/src/org/labkey/api/util/ImageUtil.java
+++ b/api/src/org/labkey/api/util/ImageUtil.java
@@ -37,8 +37,8 @@
 
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
-import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -258,10 +258,10 @@ public void setBaseURL(String url)
      * Retrieves a file cached in the session
      */
     @Nullable
-    public static File getFileFromSession(HttpServletRequest request, String key)
+    public static FileLike getFileFromSession(HttpServletRequest request, String key)
     {
         Object o = request.getSession().getAttribute(key);
-        if (o instanceof File file && file.exists())
+        if (o instanceof FileLike file && file.exists())
             return file;
 
         return null;
@@ -314,26 +314,22 @@ public ImageResource getImageResource(String uri)
                     String sessionKey = helper.getParameter(FILE_SESSION_PARAM);
                     String deleteFile = helper.getParameter(DELETE_FILE_PARAM);
 
-                    File file = getFileFromSession(_context.getRequest(), sessionKey);
+                    FileLike file = getFileFromSession(_context.getRequest(), sessionKey);
                     if (file != null)
                     {
-                        try
+                        try (InputStream in = file.openInputStream())
                         {
-                            BufferedImage img = ImageIO.read(file);
+                            BufferedImage img = ImageIO.read(in);
                             ir = createImageResource(uri, img);
                             _imageCache.put(uri, ir);
 
                             if (BooleanUtils.toBoolean(deleteFile))
                                 file.delete();
                         }
-                        catch(IOException e)
-                        {
-                        }
+                        catch (IOException ignored) {}
                     }
                 }
-                catch(URISyntaxException e)
-                {
-                }
+                catch (URISyntaxException ignored) {}
             }
 
             if (ir != null)
diff --git a/api/src/org/labkey/api/util/PageFlowUtil.java b/api/src/org/labkey/api/util/PageFlowUtil.java
index feaf2515c73..2868ead9087 100644
--- a/api/src/org/labkey/api/util/PageFlowUtil.java
+++ b/api/src/org/labkey/api/util/PageFlowUtil.java
@@ -80,6 +80,7 @@
 import org.labkey.api.view.template.ClientDependency;
 import org.labkey.api.view.template.PageConfig;
 import org.labkey.api.writer.ContainerUser;
+import org.labkey.vfs.FileLike;
 import org.springframework.beans.PropertyValue;
 import org.springframework.beans.PropertyValues;
 import org.springframework.web.util.WebUtils;
@@ -986,29 +987,6 @@ private static void _prepareResponseForFile(HttpServletResponse response, Map responseHeaders, File file, boolean asAttachment) throws IOException
+    public static void streamFile(@NotNull HttpServletResponse response, @NotNull Map responseHeaders, FileLike file, boolean asAttachment) throws IOException
     {
-        streamFile(response,responseHeaders, file.toPath(), asAttachment);
+        streamFile(response, responseHeaders, file.toNioPathForRead(), asAttachment);
     }
 
     public static void streamFile(@NotNull HttpServletResponse response, @NotNull Map responseHeaders, Path file, boolean asAttachment) throws IOException
diff --git a/api/src/org/labkey/api/writer/ZipUtil.java b/api/src/org/labkey/api/writer/ZipUtil.java
index 3f5d43e7e71..92f3efa0f23 100644
--- a/api/src/org/labkey/api/writer/ZipUtil.java
+++ b/api/src/org/labkey/api/writer/ZipUtil.java
@@ -175,7 +175,7 @@ public static void zipToStream(HttpServletResponse response, File file, boolean
 
         if (preZipped)
         {
-            PageFlowUtil.streamFile(response, file, true);
+            PageFlowUtil.streamFile(response, file.toPath(), true);
             return;
         }
 
diff --git a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java
index 07903ac9d0c..2c3c44a438a 100644
--- a/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java
+++ b/experiment/src/org/labkey/experiment/controllers/exp/ExperimentController.java
@@ -1883,7 +1883,7 @@ public ModelAndView getView(ExperimentRunForm form, BindException errors) throws
 
             try
             {
-                PageFlowUtil.streamFile(getViewContext().getResponse(), new File(files.getImageFile().getAbsolutePath()), false);
+                PageFlowUtil.streamFile(getViewContext().getResponse(), files.getImageFile().toPath(), false);
             }
             catch (FileNotFoundException e)
             {
@@ -6639,7 +6639,7 @@ public ModelAndView getView(ShowExternalDocsForm form, BindException errors) thr
                 throw new NotFoundException();
             }
 
-            PageFlowUtil.streamFile(getViewContext().getResponse(), new File(f.getAbsolutePath()), false);
+            PageFlowUtil.streamFile(getViewContext().getResponse(), f.toPath(), false);
             return null;
         }
 
diff --git a/pipeline/src/org/labkey/pipeline/PipelineController.java b/pipeline/src/org/labkey/pipeline/PipelineController.java
index d8636fee02f..733bcbd29ef 100644
--- a/pipeline/src/org/labkey/pipeline/PipelineController.java
+++ b/pipeline/src/org/labkey/pipeline/PipelineController.java
@@ -1115,7 +1115,7 @@ public ModelAndView getView(PathForm form, BindException errors) throws Exceptio
             if (!pipeRoot.hasPermission(getUser(), ReadPermission.class))
                 throw new UnauthorizedException();
 
-            File file = pipeRoot.resolvePath(form.getPath());
+            FileLike file = pipeRoot.resolvePathToFileLike(form.getPath());
             if (!file.exists() || !file.isFile())
                 throw new NotFoundException();
 
diff --git a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
index 6e79d7a1cc3..2abacfa8c32 100644
--- a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
+++ b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
@@ -310,11 +310,10 @@ protected void writeTaskInfo(FileLike file, RecordedAction action) throws IOExce
             }
         }
 
-        try (TSVMapWriter tsvWriter = new TSVMapWriter(columns, rows);
-            OutputStream out = file.openOutputStream())
+        try (TSVMapWriter tsvWriter = new TSVMapWriter(columns, rows))
         {
             tsvWriter.setHeaderRowVisible(false);
-            tsvWriter.write(out);
+            tsvWriter.write(file.openOutputStream());
         }
     }
 
diff --git a/query/src/org/labkey/query/reports/ReportsController.java b/query/src/org/labkey/query/reports/ReportsController.java
index 106678c9231..fd4ba6b3c39 100644
--- a/query/src/org/labkey/query/reports/ReportsController.java
+++ b/query/src/org/labkey/query/reports/ReportsController.java
@@ -1511,7 +1511,7 @@ public ModelAndView getView(Object o, BindException errors) throws Exception
             String cacheFile = (String) getViewContext().get(ImageUtil.CACHE_FILE_PARAM);
             if (sessionKey != null)
             {
-                File file = ImageUtil.getFileFromSession(getViewContext().getRequest(), sessionKey);
+                FileLike file = ImageUtil.getFileFromSession(getViewContext().getRequest(), sessionKey);
                 if (file != null)
                 {
                     Map responseHeaders = Collections.emptyMap();
@@ -1521,18 +1521,18 @@ public ModelAndView getView(Object o, BindException errors) throws Exception
 
                         responseHeaders.put("Pragma", "private");
                         responseHeaders.put("Cache-Control", "private, max-age=3600");
-                        _log.debug("Caching file: " + file.getAbsolutePath());
+                        _log.debug("Caching file: " + file);
                     }
-                    PageFlowUtil.streamFile(getViewContext().getResponse(), responseHeaders, file.toPath(), BooleanUtils.toBoolean(attachment));
+                    PageFlowUtil.streamFile(getViewContext().getResponse(), responseHeaders, file, BooleanUtils.toBoolean(attachment));
                     if (BooleanUtils.toBoolean(deleteFile))
                     {
                         file.delete();
-                        _log.debug("Deleting file: " + file.getAbsolutePath());
+                        _log.debug("Deleting file: " + file);
                     }
                     return null;
                 }
             }
-            return new HtmlView(HtmlString.of("Requested Resource not found"));
+            return new HtmlView(HtmlString.of("Requested resource not found"));
         }
 
         @Override
@@ -2008,7 +2008,7 @@ public ModelAndView getView(ModuleReportForm form, BindException errors) throws
             Resource imageResource = ReportUtil.getModuleImageFile(report, imageFilePrefix);
 
             if (null != imageResource)
-                PageFlowUtil.streamFile(getViewContext().getResponse(), ((FileResource) imageResource).getFile(), true);
+                PageFlowUtil.streamFile(getViewContext().getResponse(), ((FileResource) imageResource).getFile().toPath(), true);
 
             return null;
         }
diff --git a/study/src/org/labkey/study/controllers/reports/ReportsController.java b/study/src/org/labkey/study/controllers/reports/ReportsController.java
index e09c6265b59..d7b6545c1cb 100644
--- a/study/src/org/labkey/study/controllers/reports/ReportsController.java
+++ b/study/src/org/labkey/study/controllers/reports/ReportsController.java
@@ -87,6 +87,7 @@
 import org.labkey.study.query.StudyQuerySchema;
 import org.labkey.study.reports.ParticipantReport;
 import org.labkey.study.reports.ReportManager;
+import org.labkey.vfs.FileLike;
 import org.springframework.validation.BindException;
 import org.springframework.validation.Errors;
 import org.springframework.web.servlet.ModelAndView;
@@ -149,10 +150,10 @@ public ModelAndView getView(Object o, BindException errors) throws Exception
                 return null;
             }
 
-            File file = ImageUtil.getFileFromSession(getViewContext().getRequest(), sessionKey);
+            FileLike file = ImageUtil.getFileFromSession(getViewContext().getRequest(), sessionKey);
             if (file != null)
             {
-                PageFlowUtil.streamFile(getViewContext().getResponse(), file.toPath(), false);
+                PageFlowUtil.streamFile(getViewContext().getResponse(), file, false);
                 file.delete();
             }
             return null;

From 9fafc8ad1cb50d5cd4c583ad240eccba4527520d Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sat, 22 Nov 2025 18:23:50 -0800
Subject: [PATCH 05/18] Fix tests

---
 api/src/org/labkey/api/reports/Report.java                    | 4 ++--
 api/src/org/labkey/api/reports/report/AbstractReport.java     | 4 ++--
 api/src/org/labkey/api/reports/report/ScriptReport.java       | 3 +--
 api/src/org/labkey/api/reports/report/r/RReport.java          | 2 +-
 api/src/org/labkey/api/reports/report/r/RReportJob.java       | 2 +-
 api/src/org/labkey/api/reports/report/view/RunReportView.java | 2 +-
 pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java      | 2 +-
 7 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/api/src/org/labkey/api/reports/Report.java b/api/src/org/labkey/api/reports/Report.java
index e16f3af3e60..731a651d601 100644
--- a/api/src/org/labkey/api/reports/Report.java
+++ b/api/src/org/labkey/api/reports/Report.java
@@ -61,13 +61,13 @@ public interface Report extends AttachmentParent, ThumbnailProvider
      * Return the data view (if any) for this report. Many reports are created from a source
      * data grid (or query view), this view can be displayed on another tab with the rendered results.
      */
-    HttpView renderDataView(ViewContext context) throws Exception;
+    HttpView renderDataView(ViewContext context) throws Exception;
 
     /**
      * Returns a view appropriate for displaying report results, may be a simple view, or
      * it may aggregate more than one of the rendered views a report can display.
      */
-    HttpView getRunReportView(ViewContext context) throws Exception;
+    HttpView getRunReportView(ViewContext context) throws Exception;
 
     ReportDescriptor getDescriptor();
     void setDescriptor(ReportDescriptor descriptor);
diff --git a/api/src/org/labkey/api/reports/report/AbstractReport.java b/api/src/org/labkey/api/reports/report/AbstractReport.java
index ab5fa28a6e1..47ff97eeda2 100644
--- a/api/src/org/labkey/api/reports/report/AbstractReport.java
+++ b/api/src/org/labkey/api/reports/report/AbstractReport.java
@@ -205,13 +205,13 @@ public ActionURL getEditReportURL(ViewContext context)
     }
 
     @Override
-    public HttpView renderDataView(ViewContext context) throws Exception
+    public HttpView renderDataView(ViewContext context) throws Exception
     {
         return HtmlView.of("No Data view available for this report");
     }
 
     @Override
-    public HttpView getRunReportView(ViewContext context) throws Exception
+    public HttpView getRunReportView(ViewContext context) throws Exception
     {
         return renderReport(context);
     }
diff --git a/api/src/org/labkey/api/reports/report/ScriptReport.java b/api/src/org/labkey/api/reports/report/ScriptReport.java
index 863180ecda3..ea99b57dffa 100644
--- a/api/src/org/labkey/api/reports/report/ScriptReport.java
+++ b/api/src/org/labkey/api/reports/report/ScriptReport.java
@@ -25,7 +25,6 @@
 import org.labkey.api.data.BaseColumnInfo;
 import org.labkey.api.data.BooleanFormat;
 import org.labkey.api.data.ColumnHeaderType;
-import org.labkey.api.data.ColumnInfo;
 import org.labkey.api.data.CompareType;
 import org.labkey.api.data.Container;
 import org.labkey.api.data.ContainerManager;
@@ -364,7 +363,7 @@ public ActionURL getEditReportURL(ViewContext context)
     }
 
     @Override
-    public HttpView getRunReportView(ViewContext context) throws Exception
+    public HttpView getRunReportView(ViewContext context) throws Exception
     {
         String tabId = (String) context.get("tabId");
 
diff --git a/api/src/org/labkey/api/reports/report/r/RReport.java b/api/src/org/labkey/api/reports/report/r/RReport.java
index 9bf523eb35a..be6e61a388a 100644
--- a/api/src/org/labkey/api/reports/report/r/RReport.java
+++ b/api/src/org/labkey/api/reports/report/r/RReport.java
@@ -674,7 +674,7 @@ public String getScriptFileExtension()
      * If this R Report is using knitr then put all the results into the cache directory instead of a temp
      * directory.  This enables knitr to do its own caching.  If this is a non-Knitr report or we don't have a cache
      * directory because the report was not saved yet, then fall back to the non-cache case.  Note that knitr caching
-     * is not the same as report caching.  Report caching saves off the output parameters of the report and then serves
+     * is different from report caching.  Report caching saves off the output parameters of the report and then serves
      * them up without executing the script again if the incoming URL is the same.
      * For Knitr caching, we always run the R script and let the knitr library manage the caching options.
      * @param executingContainerId id of the container in which the report is running
diff --git a/api/src/org/labkey/api/reports/report/r/RReportJob.java b/api/src/org/labkey/api/reports/report/r/RReportJob.java
index a7c5ded89ce..2413636089c 100644
--- a/api/src/org/labkey/api/reports/report/r/RReportJob.java
+++ b/api/src/org/labkey/api/reports/report/r/RReportJob.java
@@ -337,7 +337,7 @@ protected void processOutputs(RReport report, List outputSubst
                     substitutionMap = reportDir.getParent().resolveChild(RReport.SUBSTITUTION_MAP);
                 }
                 else
-                    substitutionMap = reportDir.getParent().resolveChild(RReport.SUBSTITUTION_MAP);
+                    substitutionMap = reportDir.resolveChild(RReport.SUBSTITUTION_MAP);
                 ParamReplacementSvc.get().toFile(outputSubst, substitutionMap);
             }
         }
diff --git a/api/src/org/labkey/api/reports/report/view/RunReportView.java b/api/src/org/labkey/api/reports/report/view/RunReportView.java
index bb070aa00f7..0aa53d57c2b 100644
--- a/api/src/org/labkey/api/reports/report/view/RunReportView.java
+++ b/api/src/org/labkey/api/reports/report/view/RunReportView.java
@@ -96,7 +96,7 @@ protected void renderTitle(Object model, PrintWriter out)
                 sb.append("");
             }
             sb.append("");
-            include(new HttpView() {
+            include(new HttpView<>() {
 
                 @Override
                 protected void renderInternal(Object model, PrintWriter out)
diff --git a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
index 2abacfa8c32..44ec80cef57 100644
--- a/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
+++ b/pipeline/src/org/labkey/pipeline/api/ScriptTaskImpl.java
@@ -168,7 +168,7 @@ else if (factory._scriptPath != null)
 
             // NOTE: Local path to the script file doesn't need to be rewritten as a remote path
             if (scriptFile != null)
-                bindings.put(ExternalScriptEngine.SCRIPT_PATH, scriptFile.toString());
+                bindings.put(ExternalScriptEngine.SCRIPT_PATH, scriptFile.toNioPathForRead().toFile().toString());
 
             bindings.put(ExternalScriptEngine.WORKING_DIRECTORY, _wd.getDir().toNioPathForRead().toString());
 

From 297ecf5cf03a125a76d6326a30706d5c68727120 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 08:46:05 -0800
Subject: [PATCH 06/18] Fix tests

---
 pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java b/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java
index d06727af0fb..35c7772c1cb 100644
--- a/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java
+++ b/pipeline/src/org/labkey/pipeline/analysis/CommandTaskImpl.java
@@ -815,9 +815,8 @@ protected void readOutputParameters(RecordedAction action) throws IOException
                         action.addOutputParameter(new RecordedAction.ParameterType(name, PropertyType.STRING), value);
                     }
                 }
-
-                _wd.discardFile(file);
             }
+            _wd.discardFile(file);
         }
     }
 }

From 7801ddb247e6f670d6e823d3cb1cf89ad1470692 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 12:48:57 -0800
Subject: [PATCH 07/18] Troubleshoot

---
 .../api/reports/report/DbReportIdentifier.java     |  5 ++---
 .../api/reports/report/ReportDescriptor.java       | 10 +++++-----
 api/src/org/labkey/api/writer/ZipUtil.java         |  2 +-
 api/src/org/labkey/vfs/FileSystemLike.java         | 14 ++++++++++++--
 4 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/api/src/org/labkey/api/reports/report/DbReportIdentifier.java b/api/src/org/labkey/api/reports/report/DbReportIdentifier.java
index af40ca91d33..cd3f1e009f2 100644
--- a/api/src/org/labkey/api/reports/report/DbReportIdentifier.java
+++ b/api/src/org/labkey/api/reports/report/DbReportIdentifier.java
@@ -27,7 +27,6 @@
 import org.labkey.api.writer.ContainerUser;
 
 import java.util.List;
-import java.util.stream.Collectors;
 
 /*
 * User: Dave
@@ -85,7 +84,7 @@ public DbReportIdentifier(String id, @Nullable User user, @Nullable Container co
             if (user != null && container != null)
             {
                 // Filter all available reports by name to see if we get a single match
-                List matchingReports = ReportService.get().getReports(user, container).stream().filter((r) -> suffix.equalsIgnoreCase(r.getDescriptor().getReportName())).collect(Collectors.toList());
+                List matchingReports = ReportService.get().getReports(user, container).stream().filter((r) -> suffix.equalsIgnoreCase(r.getDescriptor().getReportName())).toList();
 
                 LOG.debug("Found " + matchingReports.size() + " matching DB-based reports for id '" + id + "' for user " + user.getEmail() + " in " + container.getPath());
 
@@ -110,7 +109,7 @@ public DbReportIdentifier(String id, @Nullable User user, @Nullable Container co
     @Override
     public String toString()
     {
-        return PREFIX + String.valueOf(getRowId());
+        return PREFIX + getRowId();
     }
 
     @Override
diff --git a/api/src/org/labkey/api/reports/report/ReportDescriptor.java b/api/src/org/labkey/api/reports/report/ReportDescriptor.java
index e721f196bd5..65be58b2ae9 100644
--- a/api/src/org/labkey/api/reports/report/ReportDescriptor.java
+++ b/api/src/org/labkey/api/reports/report/ReportDescriptor.java
@@ -403,7 +403,7 @@ public String toQueryString()
     {
         final StringBuffer sb = new StringBuffer();
         String strAnd = "";
-        for (Map.Entry entry : _props.entrySet())
+        for (Map.Entry entry : _props.entrySet())
         {
             sb.append(strAnd);
             if (null == entry.getKey())
@@ -416,12 +416,12 @@ public String toQueryString()
                 for (String value : ((List)entry.getValue()))
                 {
                     sb.append(delim);
-                    encode(sb, (String)entry.getKey(), value);
+                    encode(sb, entry.getKey(), value);
                     delim = "&";
                 }
             }
             else
-                encode(sb, (String)entry.getKey(), String.valueOf(v));
+                encode(sb, entry.getKey(), String.valueOf(v));
 
             strAnd = "&";
         }
@@ -532,9 +532,9 @@ protected final ReportDescriptorDocument getDescriptorDocument(Container c, @Nul
                 continue;
 
             final Object value = entry.getValue();
-            if (value instanceof List)
+            if (value instanceof List l)
             {
-                for (Object item : ((List)value))
+                for (Object item : l)
                 {
                     addProperty(context, props, entry.getKey(), item);
                 }
diff --git a/api/src/org/labkey/api/writer/ZipUtil.java b/api/src/org/labkey/api/writer/ZipUtil.java
index 92f3efa0f23..864c68305c1 100644
--- a/api/src/org/labkey/api/writer/ZipUtil.java
+++ b/api/src/org/labkey/api/writer/ZipUtil.java
@@ -59,7 +59,7 @@ public static List unzipToDirectory(FileLike zipFile, FileLike unzipDi
         return unzipToDirectory(zipFile, unzipDir, null);
     }
 
-        public static List unzipToDirectory(FileLike zipFile, FileLike unzipDir, @Nullable Logger logger) throws IOException
+    public static List unzipToDirectory(FileLike zipFile, FileLike unzipDir, @Nullable Logger logger) throws IOException
     {
         List paths = unzipToDirectory(zipFile.toNioPathForRead(), unzipDir.toNioPathForWrite(), logger);
         File rootFile = unzipDir.toNioPathForRead().toFile();
diff --git a/api/src/org/labkey/vfs/FileSystemLike.java b/api/src/org/labkey/vfs/FileSystemLike.java
index ef5f63a2d6a..be0484d80e6 100644
--- a/api/src/org/labkey/vfs/FileSystemLike.java
+++ b/api/src/org/labkey/vfs/FileSystemLike.java
@@ -1,6 +1,8 @@
 package org.labkey.vfs;
 
 import com.fasterxml.jackson.databind.DeserializationContext;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.labkey.api.cloud.CloudStoreService;
 import org.labkey.api.collections.CaseInsensitiveHashMap;
 import org.labkey.api.data.Container;
@@ -267,7 +269,11 @@ static FileLike wrapFile(java.nio.file.Path p)
     static FileLike wrapFile(File root, File f) throws IOException
     {
         if (!root.isDirectory())
-            throw new FileNotFoundException(root.getPath());
+        {
+            var e = new FileNotFoundException(root.getPath());
+            LogManager.getLogger(FileSystemLike.class).error("Failed to wrap file", e);
+            throw e;
+        }
         FileSystemLike fs = new Builder(root.toURI()).build();
         String rel = FileUtil.relativize(root, f, true);
         return fs.getRoot().resolveFile(Path.parse(rel));
@@ -277,7 +283,11 @@ static FileLike wrapFile(File root, File f) throws IOException
     static FileLike wrapFile(java.nio.file.Path root, java.nio.file.Path f) throws IOException
     {
         if (!Files.isDirectory(root))
-            throw new FileNotFoundException(root.toString());
+        {
+            var e = new FileNotFoundException(root.toString());
+            LogManager.getLogger(FileSystemLike.class).error("Failed to wrap path", e);
+            throw e;
+        }
         FileSystemLike fs = new Builder(root.toUri()).build();
         URI relative = URIUtil.relativize(root.toUri(), f.toUri());
         if (null == relative)

From 194b5736e64cae9d4f0b201569bc1c83579c3895 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 13:56:51 -0800
Subject: [PATCH 08/18] Troubleshoot

---
 .../reports/report/InternalScriptEngineReport.java   |  2 ++
 api/src/org/labkey/vfs/FileSystemLike.java           | 12 ++----------
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java b/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java
index fce5d6f5fc3..b4b09cd4918 100644
--- a/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java
+++ b/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java
@@ -15,6 +15,7 @@
  */
 package org.labkey.api.reports.report;
 
+import org.apache.logging.log4j.LogManager;
 import org.labkey.api.ApiModule;
 import org.labkey.api.module.ModuleLoader;
 import org.labkey.api.reports.ExternalScriptEngine;
@@ -80,6 +81,7 @@ public HttpView renderReport(ViewContext context) throws Exception
         }
         catch (ScriptException e)
         {
+            LogManager.getLogger(getClass()).error("Error executing script", e);
             final String error1 = "Error executing command";
             final String error2 = PageFlowUtil.filter(e.getMessage());
 
diff --git a/api/src/org/labkey/vfs/FileSystemLike.java b/api/src/org/labkey/vfs/FileSystemLike.java
index be0484d80e6..ac81a2b131d 100644
--- a/api/src/org/labkey/vfs/FileSystemLike.java
+++ b/api/src/org/labkey/vfs/FileSystemLike.java
@@ -269,11 +269,7 @@ static FileLike wrapFile(java.nio.file.Path p)
     static FileLike wrapFile(File root, File f) throws IOException
     {
         if (!root.isDirectory())
-        {
-            var e = new FileNotFoundException(root.getPath());
-            LogManager.getLogger(FileSystemLike.class).error("Failed to wrap file", e);
-            throw e;
-        }
+            throw new FileNotFoundException(root.getPath());
         FileSystemLike fs = new Builder(root.toURI()).build();
         String rel = FileUtil.relativize(root, f, true);
         return fs.getRoot().resolveFile(Path.parse(rel));
@@ -283,11 +279,7 @@ static FileLike wrapFile(File root, File f) throws IOException
     static FileLike wrapFile(java.nio.file.Path root, java.nio.file.Path f) throws IOException
     {
         if (!Files.isDirectory(root))
-        {
-            var e = new FileNotFoundException(root.toString());
-            LogManager.getLogger(FileSystemLike.class).error("Failed to wrap path", e);
-            throw e;
-        }
+            throw new FileNotFoundException(root.toString());
         FileSystemLike fs = new Builder(root.toUri()).build();
         URI relative = URIUtil.relativize(root.toUri(), f.toUri());
         if (null == relative)

From 02a1177d4d118154a2203584026b15a1b1685c31 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 17:11:19 -0800
Subject: [PATCH 09/18] Troubleshoot

---
 api/src/org/labkey/api/reports/report/view/ReportUtil.java | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/api/src/org/labkey/api/reports/report/view/ReportUtil.java b/api/src/org/labkey/api/reports/report/view/ReportUtil.java
index d97bb090ebe..0dae9f755c9 100644
--- a/api/src/org/labkey/api/reports/report/view/ReportUtil.java
+++ b/api/src/org/labkey/api/reports/report/view/ReportUtil.java
@@ -19,6 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Strings;
 import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.logging.log4j.LogManager;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.json.JSONArray;
@@ -608,6 +609,7 @@ public static String createReportSessionId()
      */
     public static String makeExceptionString(Exception e, String formatString)
     {
+        LogManager.getLogger(ReportUtil.class).error("Error executing script - makeExceptionString", e);
         final String error1 = "Error executing command";
         final String error2 = PageFlowUtil.filter(e.getMessage());
         return String.format(formatString, error1, error2);

From fc744be637276fbc9c0b920fcea4b8634ec6fbe1 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 17:39:47 -0800
Subject: [PATCH 10/18] Troubleshoot

---
 .../org/labkey/api/reports/report/r/RserveScriptEngine.java   | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java
index 5a39018d7dd..9cf0c9f0a61 100644
--- a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java
+++ b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java
@@ -271,6 +271,10 @@ protected void copyWorkingDirectoryFromRemote(RConnection rconn) throws IOExcept
                 if ("script.R".equalsIgnoreCase(remotePath))
                     continue;
                 FileLike file = wd.resolveFile(org.labkey.api.util.Path.parse(remotePath));
+                if (!file.getParent().exists())
+                {
+                    FileUtil.mkdir(file.getParent());
+                }
                 try (InputStream is = rconn.openFile(remotePath);
                      OutputStream fos = file.openOutputStream())
                 {

From 251daeb7eabf2aba150304498226940b08e50219 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 18:01:40 -0800
Subject: [PATCH 11/18] Troubleshoot

---
 api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java
index 9cf0c9f0a61..2ca7b759924 100644
--- a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java
+++ b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java
@@ -273,7 +273,7 @@ protected void copyWorkingDirectoryFromRemote(RConnection rconn) throws IOExcept
                 FileLike file = wd.resolveFile(org.labkey.api.util.Path.parse(remotePath));
                 if (!file.getParent().exists())
                 {
-                    FileUtil.mkdir(file.getParent());
+                    FileUtil.mkdirs(file.getParent());
                 }
                 try (InputStream is = rconn.openFile(remotePath);
                      OutputStream fos = file.openOutputStream())

From 07fa390802ac90081ff93aa52afad256b0488673 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Sun, 23 Nov 2025 21:53:22 -0800
Subject: [PATCH 12/18] Cleanup

---
 .../report/InternalScriptEngineReport.java    | 21 +++++++-----
 .../api/reports/report/view/ReportUtil.java   | 34 ++++---------------
 2 files changed, 18 insertions(+), 37 deletions(-)

diff --git a/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java b/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java
index b4b09cd4918..39413a2aeb8 100644
--- a/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java
+++ b/api/src/org/labkey/api/reports/report/InternalScriptEngineReport.java
@@ -15,15 +15,17 @@
  */
 package org.labkey.api.reports.report;
 
-import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.labkey.api.ApiModule;
 import org.labkey.api.module.ModuleLoader;
+import org.labkey.api.query.ValidationException;
 import org.labkey.api.reports.ExternalScriptEngine;
 import org.labkey.api.reports.report.r.ParamReplacement;
 import org.labkey.api.reports.report.r.ParamReplacementSvc;
 import org.labkey.api.reports.report.r.view.ConsoleOutput;
 import org.labkey.api.usageMetrics.SimpleMetricsService;
 import org.labkey.api.util.PageFlowUtil;
+import org.labkey.api.util.logging.LogHelper;
 import org.labkey.api.view.HtmlView;
 import org.labkey.api.view.HttpView;
 import org.labkey.api.view.VBox;
@@ -36,20 +38,21 @@
 import javax.script.ScriptContext;
 import javax.script.ScriptEngine;
 import javax.script.ScriptException;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
-/*
-* User: Karl Lum
-* Date: Jan 19, 2009
-* Time: 4:11:54 PM
-*/
+/**
+ * Executes scripts, such as R, and renders the results.
+ */
 public class InternalScriptEngineReport extends ScriptEngineReport
 {
     public static final String TYPE = "ReportService.internalScriptEngineReport";
+    private static final Logger LOG = LogHelper.getLogger(InternalScriptEngineReport.class, "Executes scripts, such as R, and renders the results.");
 
     @Override
     public String getType()
@@ -58,7 +61,7 @@ public String getType()
     }
 
     @Override
-    public HttpView renderReport(ViewContext context) throws Exception
+    public HttpView renderReport(ViewContext context) throws ValidationException, SQLException, IOException
     {
         VBox view = new VBox();
         String script = getDescriptor().getProperty(ScriptReportDescriptor.Prop.script);
@@ -81,7 +84,7 @@ public HttpView renderReport(ViewContext context) throws Exception
         }
         catch (ScriptException e)
         {
-            LogManager.getLogger(getClass()).error("Error executing script", e);
+            LOG.error("Error executing script", e);
             final String error1 = "Error executing command";
             final String error2 = PageFlowUtil.filter(e.getMessage());
 
@@ -146,7 +149,7 @@ public String runScript(ViewContext context, List outputSubst,
                 }
                 return output != null ? output.toString() : "";
             }
-            catch(Exception e)
+            catch (Exception e)
             {
                 if (!errors.getBuffer().isEmpty())
                     throw new ScriptException(e.getMessage() + errors.getBuffer());
diff --git a/api/src/org/labkey/api/reports/report/view/ReportUtil.java b/api/src/org/labkey/api/reports/report/view/ReportUtil.java
index 0dae9f755c9..8eb5b205699 100644
--- a/api/src/org/labkey/api/reports/report/view/ReportUtil.java
+++ b/api/src/org/labkey/api/reports/report/view/ReportUtil.java
@@ -19,7 +19,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Strings;
 import org.apache.commons.lang3.math.NumberUtils;
-import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.json.JSONArray;
@@ -76,6 +76,7 @@
 import org.labkey.api.util.ThumbnailUtil;
 import org.labkey.api.util.URLHelper;
 import org.labkey.api.util.UniqueID;
+import org.labkey.api.util.logging.LogHelper;
 import org.labkey.api.view.ActionURL;
 import org.labkey.api.view.TabStripView;
 import org.labkey.api.view.ViewContext;
@@ -90,6 +91,8 @@
 
 public class ReportUtil
 {
+    private static final Logger LOG = LogHelper.getLogger(ReportUtil.class, "Utilities to help with report rendering and management");
+
     public static ActionURL getScriptReportDesignerURL(ViewContext context, ScriptReportBean bean)
     {
         ActionURL url = PageFlowUtil.urlProvider(ReportUrls.class).urlCreateScriptReport(context.getContainer());
@@ -395,7 +398,7 @@ public static void addErrors(List reportErrors, Errors errors)
 
     public static String getErrors(List reportErrors)
     {
-        StringBuffer sb = new StringBuffer();
+        StringBuilder sb = new StringBuilder();
         for (ValidationError error : reportErrors)
             sb.append(error.getMessage()).append("\n");
 
@@ -527,31 +530,6 @@ public static boolean isInherited(CustomViewInfo view, Container container)
         return false;
     }
 
-    public static JSONArray getCreateReportButtons(ViewContext context)
-    {
-        List designers = new ArrayList<>();
-        for (ReportService.UIProvider provider : ReportService.get().getUIProviders())
-            designers.addAll(provider.getDesignerInfo(context));
-
-        designers.sort(Comparator.comparing(ReportService.DesignerInfo::getLabel));
-
-        JSONArray json = new JSONArray();
-
-        for (ReportService.DesignerInfo info : designers)
-        {
-            JSONObject o = new JSONObject();
-
-            o.put("text", info.getLabel());
-            o.put("id", info.getId());
-            o.put("disabled", info.isDisabled());
-            o.put("icon", info.getIconURL().getLocalURIString());
-            o.put("redirectUrl", info.getDesignerURL().getLocalURIString());
-
-            json.put(o);
-        }
-        return json;
-    }
-
     public static Report getReportById(ViewContext viewContext, String reportIdString)
     {
         ReportIdentifier reportId = ReportService.get().getReportIdentifier(reportIdString, viewContext.getUser(), viewContext.getContainer());
@@ -609,7 +587,7 @@ public static String createReportSessionId()
      */
     public static String makeExceptionString(Exception e, String formatString)
     {
-        LogManager.getLogger(ReportUtil.class).error("Error executing script - makeExceptionString", e);
+        LOG.warn("Error executing script", e);
         final String error1 = "Error executing command";
         final String error2 = PageFlowUtil.filter(e.getMessage());
         return String.format(formatString, error1, error2);

From 4a7a52c1504fe73845350625c5a815c4b9dd2871 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Mon, 24 Nov 2025 09:32:01 -0800
Subject: [PATCH 13/18] Cleanup

---
 .../api/reports/report/view/ReportUtil.java   |  4 +--
 api/src/org/labkey/api/util/FileType.java     | 26 ++++++-------------
 .../query/reports/ReportViewProvider.java     |  2 +-
 3 files changed, 10 insertions(+), 22 deletions(-)

diff --git a/api/src/org/labkey/api/reports/report/view/ReportUtil.java b/api/src/org/labkey/api/reports/report/view/ReportUtil.java
index 8eb5b205699..2d7991ece6d 100644
--- a/api/src/org/labkey/api/reports/report/view/ReportUtil.java
+++ b/api/src/org/labkey/api/reports/report/view/ReportUtil.java
@@ -22,7 +22,6 @@
 import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.json.JSONArray;
 import org.json.JSONObject;
 import org.labkey.api.action.ApiJsonForm;
 import org.labkey.api.action.ReturnUrlForm;
@@ -84,7 +83,6 @@
 
 import javax.script.ScriptEngine;
 import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -405,7 +403,7 @@ public static String getErrors(List reportErrors)
         return sb.toString();
     }
 
-    public static URLHelper getDefaultThumbnailUrl(Container c, Report r)
+    public static URLHelper getDefaultThumbnailUrl(Report r)
     {
         return ThumbnailUtil.getStaticThumbnailURL(r, ImageType.Large);
     }
diff --git a/api/src/org/labkey/api/util/FileType.java b/api/src/org/labkey/api/util/FileType.java
index b968ccf776d..25d895a6fff 100644
--- a/api/src/org/labkey/api/util/FileType.java
+++ b/api/src/org/labkey/api/util/FileType.java
@@ -38,6 +38,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * FileType
@@ -323,11 +324,6 @@ private boolean isAntiFileType(String name, byte[] header)
      * Looks for a file in the parentDir that matches, in priority order. If one is found, returns its file name.
      * If nothing matches, uses the defaultSuffix to build a file name.
      */
-    public String getName(File parentDir, String basename)
-    {
-        return getName(parentDir.toPath(), basename);
-    }
-
     public String getName(FileLike parentDir, String basename)
     {
         return getName(parentDir.toNioPathForRead(), basename);
@@ -357,8 +353,7 @@ public String getName(Path parentDir, String basename)
 
     public String getName(String parentDirName, String basename)
     {
-        File parentDir = new File(parentDirName);
-        return getName(parentDir,basename);
+        return getName(new File(parentDirName).toPath(), basename);
     }
 
     /**
@@ -462,11 +457,6 @@ else if (_supportGZ.booleanValue()) // TPP treats .xml.gz as a native read forma
         return fileName.substring(0, fileName.length() - suffix.length());
     }
 
-    public File newFile(File parent, String basename)
-    {
-        return FileUtil.appendName(parent, getName(parent, basename));
-    }
-
     public FileLike newFile(FileLike parent, String basename)
     {
         return parent.resolveChild(getName(parent, basename));
@@ -624,13 +614,13 @@ public boolean equals(Object o)
 
         FileType fileType = (FileType) o;
 
-        if (_supportGZ != null ? !_supportGZ.equals(fileType._supportGZ) : fileType._supportGZ != null) return false;
-        if (_preferGZ != null ? !_preferGZ.equals(fileType._preferGZ) : fileType._preferGZ != null) return false;
-        if (_dir != null ? !_dir.equals(fileType._dir) : fileType._dir != null) return false;
-        if (_defaultSuffix != null ? !_defaultSuffix.equals(fileType._defaultSuffix) : fileType._defaultSuffix != null)
+        if (!Objects.equals(_supportGZ, fileType._supportGZ)) return false;
+        if (!Objects.equals(_preferGZ, fileType._preferGZ)) return false;
+        if (!Objects.equals(_dir, fileType._dir)) return false;
+        if (!Objects.equals(_defaultSuffix, fileType._defaultSuffix))
             return false;
-        if (_antiTypes != null ? !_antiTypes.equals(fileType._antiTypes) : fileType._antiTypes != null) return false;
-        return !(_suffixes != null ? !_suffixes.equals(fileType._suffixes) : fileType._suffixes != null);
+        if (!Objects.equals(_antiTypes, fileType._antiTypes)) return false;
+        return !(!Objects.equals(_suffixes, fileType._suffixes));
     }
 
     public String getDefaultSuffix()
diff --git a/query/src/org/labkey/query/reports/ReportViewProvider.java b/query/src/org/labkey/query/reports/ReportViewProvider.java
index 1f56a77c749..bb57b9231d6 100644
--- a/query/src/org/labkey/query/reports/ReportViewProvider.java
+++ b/query/src/org/labkey/query/reports/ReportViewProvider.java
@@ -262,7 +262,7 @@ private List getViews(ViewContext context, @Nullable String schema
                 // This is the thumbnail
                 info.setAllowCustomThumbnail(true);
                 info.setThumbnailUrl(ReportUtil.getThumbnailUrl(c, r));
-                info.setDefaultThumbnailUrl(ReportUtil.getDefaultThumbnailUrl(c, r));
+                info.setDefaultThumbnailUrl(ReportUtil.getDefaultThumbnailUrl(r));
 
                 info.setTags(ReportPropsManager.get().getProperties(descriptor.getEntityId(), c));
 

From f306c99ae860d0b0c987af3dba630a03c8193ac6 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Mon, 24 Nov 2025 15:42:15 -0800
Subject: [PATCH 14/18] Code review suggestion

---
 .../org/labkey/api/assay/AbstractAssayProvider.java    | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/api/src/org/labkey/api/assay/AbstractAssayProvider.java b/api/src/org/labkey/api/assay/AbstractAssayProvider.java
index 973df2aa79e..00fc23f2bd3 100644
--- a/api/src/org/labkey/api/assay/AbstractAssayProvider.java
+++ b/api/src/org/labkey/api/assay/AbstractAssayProvider.java
@@ -18,6 +18,7 @@
 
 import jakarta.servlet.http.HttpServletRequest;
 import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -125,7 +126,6 @@
 import java.io.IOException;
 import java.net.URI;
 import java.net.URL;
-import java.nio.file.Files;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.ArrayList;
@@ -798,11 +798,7 @@ public Set getAssociatedStudyContainers(ExpProtocol protocol, Collect
                 {
                     // Ignore Results domain TargetStudy for now.
                     // The participant resolver will find the TargetStudy on the row.
-                    ExpObject source = switch (pair.first)
-                    {
-                        case Run -> run;
-                        default -> cache.getBatch(run);
-                    };
+                    ExpObject source = pair.first == ExpProtocol.AssayDomainTypes.Run ? run : cache.getBatch(run);
 
                     if (source != null)
                     {
@@ -1280,7 +1276,7 @@ public Pair> setValidationAndAnalysisS
                         String scriptText;
                         try
                         {
-                            scriptText = Files.readString(scriptFile.toNioPathForRead(), StringUtilsLabKey.DEFAULT_CHARSET);
+                            scriptText = IOUtils.toString(scriptFile.openInputStream(), StringUtilsLabKey.DEFAULT_CHARSET);
                         }
                         catch (IOException e)
                         {

From dcee9541d6c0c26bd873c906900ad4da8c82f7ab Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Mon, 24 Nov 2025 15:50:00 -0800
Subject: [PATCH 15/18] Code review suggestion

---
 api/src/org/labkey/api/reports/report/python/IpynbReport.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/api/src/org/labkey/api/reports/report/python/IpynbReport.java b/api/src/org/labkey/api/reports/report/python/IpynbReport.java
index 5dd1b9fe894..5a9258f05f0 100644
--- a/api/src/org/labkey/api/reports/report/python/IpynbReport.java
+++ b/api/src/org/labkey/api/reports/report/python/IpynbReport.java
@@ -183,7 +183,6 @@ public HttpView renderReport(ViewContext context) throws Exception
         String apikey = SessionApiKeyManager.get().getApiKey(context.getRequest(), "ipynb report");
         FileLike workingDirectory = getReportDir(context.getContainer().getId());
 
-        assert workingDirectory.toNioPathForRead().isAbsolute();
         if (!workingDirectory.isDirectory())
             throw new IOException("Could not create working directory");
         FileUtil.deleteDirectoryContents(workingDirectory);

From ee180b1479250c219a5389c7b89f6f63bc8c731a Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Mon, 24 Nov 2025 16:44:22 -0800
Subject: [PATCH 16/18] Few more migrations

---
 api/src/org/labkey/api/assay/AssayService.java     |  4 ++--
 .../labkey/api/assay/DefaultAssayRunCreator.java   |  2 +-
 .../api/assay/pipeline/AssayUploadPipelineJob.java |  2 +-
 .../api/exp/api/DefaultExperimentSaveHandler.java  |  2 +-
 .../org/labkey/api/util/SystemMaintenanceJob.java  |  2 +-
 assay/src/org/labkey/assay/AssayManager.java       | 10 +++++-----
 .../labkey/experiment/api/ExpDataTableImpl.java    | 14 ++++++++------
 .../experiment/api/ExperimentServiceImpl.java      |  4 ++--
 .../labkey/study/assay/StudyPublishManager.java    |  2 +-
 .../controllers/publish/PublishController.java     |  2 +-
 .../study/visitmanager/PurgeParticipantsTask.java  |  2 +-
 11 files changed, 24 insertions(+), 22 deletions(-)

diff --git a/api/src/org/labkey/api/assay/AssayService.java b/api/src/org/labkey/api/assay/AssayService.java
index c41726c1103..4d2a6e05441 100644
--- a/api/src/org/labkey/api/assay/AssayService.java
+++ b/api/src/org/labkey/api/assay/AssayService.java
@@ -41,9 +41,9 @@
 import org.labkey.api.view.ViewContext;
 import org.labkey.api.view.WebPartView;
 import org.labkey.api.view.template.ClientDependency;
+import org.labkey.vfs.FileLike;
 import org.springframework.validation.BindException;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
@@ -144,7 +144,7 @@ static void setInstance(AssayService impl)
     /**
      * Creates a run, but does not persist it to the database. Creates the run only, no protocol applications, etc.
      */
-    ExpRun createExperimentRun(@Nullable String name, Container container, ExpProtocol protocol, @Nullable File file);
+    ExpRun createExperimentRun(@Nullable String name, Container container, ExpProtocol protocol, @Nullable FileLike file);
 
     /**
      * Returns the list of valid locations an assay design can be created in.
diff --git a/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java b/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java
index ad03fc510ae..1ba68d835cd 100644
--- a/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java
+++ b/api/src/org/labkey/api/assay/DefaultAssayRunCreator.java
@@ -169,7 +169,7 @@ public Pair saveExperimentRun(
                 FileLike primaryFile = context.getUploadedData().get(AssayDataCollector.PRIMARY_FILE);
                 if (primaryFile != null)
                     auditEvent.addDetail(TransactionAuditProvider.TransactionDetail.ImportFileName, primaryFile.getName());
-                run = AssayService.get().createExperimentRun(context.getName(), context.getContainer(), protocol, null == primaryFile ? null : primaryFile.toNioPathForRead().toFile());
+                run = AssayService.get().createExperimentRun(context.getName(), context.getContainer(), protocol, primaryFile);
                 run.setComments(context.getComments());
                 run.setWorkflowTaskId(context.getWorkflowTask());
 
diff --git a/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java b/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java
index c9ccf8c94c2..0899f208001 100644
--- a/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java
+++ b/api/src/org/labkey/api/assay/pipeline/AssayUploadPipelineJob.java
@@ -138,7 +138,7 @@ public void doWork()
             }
 
             // Create the basic run
-            _run = AssayService.get().createExperimentRun(_context.getName(), getContainer(), _context.getProtocol(), _primaryFile.toNioPathForRead().toFile());
+            _run = AssayService.get().createExperimentRun(_context.getName(), getContainer(), _context.getProtocol(), _primaryFile);
             _run.setComments(_context.getComments());
             _run.setWorkflowTaskId(_context.getWorkflowTask());
             // remember which job created the run so we can show this run on the job details page
diff --git a/api/src/org/labkey/api/exp/api/DefaultExperimentSaveHandler.java b/api/src/org/labkey/api/exp/api/DefaultExperimentSaveHandler.java
index 374f5576a6c..cbcfc13361e 100644
--- a/api/src/org/labkey/api/exp/api/DefaultExperimentSaveHandler.java
+++ b/api/src/org/labkey/api/exp/api/DefaultExperimentSaveHandler.java
@@ -103,7 +103,7 @@ protected ExpRun createRun(String name, Container container, ExpProtocol protoco
         {
             throw new NotFoundException("Pipeline root is not configured for folder " + container);
         }
-        run.setFilePathRoot(pipeRoot.getRootPath());
+        run.setFilePathRoot(pipeRoot.getRootFileLike());
 
         return run;
     }
diff --git a/api/src/org/labkey/api/util/SystemMaintenanceJob.java b/api/src/org/labkey/api/util/SystemMaintenanceJob.java
index acc7c75d1b3..f19fc72e3a7 100644
--- a/api/src/org/labkey/api/util/SystemMaintenanceJob.java
+++ b/api/src/org/labkey/api/util/SystemMaintenanceJob.java
@@ -108,7 +108,7 @@ public String call()
             throw new ConfigurationException("Invalid pipeline configuration at the root container");
 
         if (!root.isValid())
-            throw new ConfigurationException("Invalid pipeline configuration at the root container: " + root.getRootPath().getPath());
+            throw new ConfigurationException("Invalid pipeline configuration at the root container: " + root.getRootFileLike());
 
         final String jobGuid;
 
diff --git a/assay/src/org/labkey/assay/AssayManager.java b/assay/src/org/labkey/assay/AssayManager.java
index fd388acf34e..1fa842571d8 100644
--- a/assay/src/org/labkey/assay/AssayManager.java
+++ b/assay/src/org/labkey/assay/AssayManager.java
@@ -98,9 +98,9 @@
 import org.labkey.api.webdav.WebdavResource;
 import org.labkey.assay.ModuleAssayCache.ModuleAssayCollections;
 import org.labkey.assay.query.AssaySchemaImpl;
+import org.labkey.vfs.FileLike;
 import org.springframework.validation.BindException;
 
-import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -802,7 +802,7 @@ public void deindexAssayBatches(Collection batches)
     }
 
     @Override
-    public ExpRun createExperimentRun(@Nullable String name, Container container, ExpProtocol protocol, @Nullable File file)
+    public ExpRun createExperimentRun(@Nullable String name, Container container, ExpProtocol protocol, @Nullable FileLike file)
     {
         if (name == null)
         {
@@ -824,7 +824,7 @@ public ExpRun createExperimentRun(@Nullable String name, Container container, Ex
         run.setProtocol(ExperimentService.get().getExpProtocol(protocol.getRowId()));
         run.setEntityId(entityId);
 
-        File runRoot;
+        FileLike runRoot;
         if (file == null)
         {
             PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(container);
@@ -832,11 +832,11 @@ public ExpRun createExperimentRun(@Nullable String name, Container container, Ex
             {
                 throw new NotFoundException("Pipeline root is not configured for folder " + container);
             }
-            runRoot = pipeRoot.getRootPath();
+            runRoot = pipeRoot.getRootFileLike();
         }
         else if (file.isFile())
         {
-            runRoot = file.getParentFile();
+            runRoot = file.getParent();
         }
         else
         {
diff --git a/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java b/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java
index 1896b1eb5b6..c9955ec5f5c 100644
--- a/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java
+++ b/experiment/src/org/labkey/experiment/api/ExpDataTableImpl.java
@@ -86,6 +86,8 @@
 import org.labkey.api.writer.VirtualFile;
 import org.labkey.experiment.controllers.exp.ExperimentController;
 import org.labkey.experiment.lineage.LineageMethod;
+import org.labkey.vfs.FileLike;
+import org.labkey.vfs.FileSystemLike;
 import org.springframework.beans.MutablePropertyValues;
 import org.springframework.validation.BindException;
 
@@ -816,7 +818,7 @@ private class WebDavTestCase
             private final ExpData _data;
             private final String _relativePath;
 
-            public WebDavTestCase(String name, File file, String relativePath)
+            public WebDavTestCase(String name, FileLike file, String relativePath)
             {
                 _data = ExperimentService.get().createData(getProject(), new DataType("TestData"));
                 _data.setDataFileURI(file.toURI());
@@ -857,14 +859,14 @@ public void testWebDavColumns() throws Exception
             List testCases = new ArrayList<>();
 
             final String PATH1 = "webDavTest.txt";
-            testCases.add(new WebDavTestCase("TestData1", FileUtil.appendName(pr.getRootPath(), PATH1), PATH1));
-            testCases.add(new WebDavTestCase("TestData2", FileUtil.appendPath(pr.getRootPath(), org.labkey.api.util.Path.parse("subfolder/webDavTest.txt")), "subfolder/webDavTest.txt"));
-            testCases.add(new WebDavTestCase("TestData3", FileUtil.appendPath(pr.getRootPath(), org.labkey.api.util.Path.parse("subfolder/webD  avT est.txt")), "subfolder/webD%20%20avT%20est.txt"));
+            testCases.add(new WebDavTestCase("TestData1", pr.getRootFileLike().resolveChild(PATH1), PATH1));
+            testCases.add(new WebDavTestCase("TestData2", pr.getRootFileLike().resolveFile(org.labkey.api.util.Path.parse("subfolder/webDavTest.txt")), "subfolder/webDavTest.txt"));
+            testCases.add(new WebDavTestCase("TestData3", pr.getRootFileLike().resolveFile(org.labkey.api.util.Path.parse("subfolder/webD  avT est.txt")), "subfolder/webD%20%20avT%20est.txt"));
 
             PipeRoot prHome = PipelineService.get().getPipelineRootSetting(ContainerManager.getHomeContainer());
-            testCases.add(new WebDavTestCase("NotUnderFolderRoot", FileUtil.appendName(prHome.getRootPath(), PATH1), null));
+            testCases.add(new WebDavTestCase("NotUnderFolderRoot", prHome.getRootFileLike().resolveChild(PATH1), null));
 
-            testCases.add(new WebDavTestCase("NotUnderFileRoot", new File("/", PATH1), null));
+            testCases.add(new WebDavTestCase("NotUnderFileRoot", FileSystemLike.wrapFile(new File("/", PATH1)), null));
 
             UserSchema us = QueryService.get().getUserSchema(getUser(), getProject(), ExpSchema.SCHEMA_NAME);
 
diff --git a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java
index e3bc1bbb24b..f5e6644ee39 100644
--- a/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java
+++ b/experiment/src/org/labkey/experiment/api/ExperimentServiceImpl.java
@@ -7550,7 +7550,7 @@ private ExpRunImpl createRun(Map inputMaterials,
         ExpProtocol protocol = ensureSampleDerivationProtocol(info.getUser());
         ExpRunImpl run = createExperimentRun(info.getContainer(), getDerivationRunName(inputMaterials, inputDatas, outputMaterials.size(), outputDatas.size()));
         run.setProtocol(protocol);
-        run.setFilePathRoot(pipeRoot.getRootPath());
+        run.setFilePathRoot(pipeRoot.getRootFileLike());
 
         return run;
     }
@@ -7610,7 +7610,7 @@ public ExpRunImpl createAliquotRun(ExpMaterial parent, Collection a
         ExpProtocol protocol = ensureSampleAliquotProtocol(info.getUser());
         ExpRunImpl run = createExperimentRun(info.getContainer(), getAliquotRunName(parent, aliquots.size()));
         run.setProtocol(protocol);
-        run.setFilePathRoot(pipeRoot.getRootPath());
+        run.setFilePathRoot(pipeRoot.getRootFileLike());
 
         return run;
     }
diff --git a/study/src/org/labkey/study/assay/StudyPublishManager.java b/study/src/org/labkey/study/assay/StudyPublishManager.java
index d4a2cc2c894..48ee81a8210 100644
--- a/study/src/org/labkey/study/assay/StudyPublishManager.java
+++ b/study/src/org/labkey/study/assay/StudyPublishManager.java
@@ -579,7 +579,7 @@ private void createProvenanceRun(User user, @NotNull Container targetContainer,
             run.setProtocol(studyPublishProtocol);
             ViewBackgroundInfo info = new ViewBackgroundInfo(targetContainer, user, null);
             PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(info.getContainer());
-            run.setFilePathRoot(pipeRoot.getRootPath());
+            run.setFilePathRoot(pipeRoot.getRootFileLike());
 
             run = ExperimentService.get().saveSimpleExperimentRun(run,
                     Collections.emptyMap(),
diff --git a/study/src/org/labkey/study/controllers/publish/PublishController.java b/study/src/org/labkey/study/controllers/publish/PublishController.java
index c675c1e3aaa..10af3f7d354 100644
--- a/study/src/org/labkey/study/controllers/publish/PublishController.java
+++ b/study/src/org/labkey/study/controllers/publish/PublishController.java
@@ -242,7 +242,7 @@ private String startAutoLinkPipelineJob(AutoLinkRunForm form)
                 throw new ConfigurationException("Invalid pipeline configuration for " + c);
 
             if (!root.isValid())
-                throw new ConfigurationException("Invalid pipeline configuration for " + c + ", " + root.getRootPath().getPath());
+                throw new ConfigurationException("Invalid pipeline configuration for " + c + ", " + root.getRootFileLike());
 
             try
             {
diff --git a/study/src/org/labkey/study/visitmanager/PurgeParticipantsTask.java b/study/src/org/labkey/study/visitmanager/PurgeParticipantsTask.java
index 444a1037b2d..7ee4b849702 100644
--- a/study/src/org/labkey/study/visitmanager/PurgeParticipantsTask.java
+++ b/study/src/org/labkey/study/visitmanager/PurgeParticipantsTask.java
@@ -60,7 +60,7 @@ public void run()
             throw new ConfigurationException("Invalid pipeline configuration at the root container");
 
         if (!root.isValid())
-            throw new ConfigurationException("Invalid pipeline configuration at the root container: " + root.getRootPath().getPath());
+            throw new ConfigurationException("Invalid pipeline configuration at the root container: " + root.getRootFileLike());
 
         try
         {

From fdd1384f22b25e0c7b472bb4b69d489c11fc40bf Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Mon, 1 Dec 2025 10:53:13 -0800
Subject: [PATCH 17/18] More cleanup

---
 .../api/action/AbstractFileUploadAction.java  | 25 ++++++++++---------
 .../org/labkey/api/reader/FastaLoader.java    |  2 +-
 .../api/util/SessionTempFileHolder.java       | 23 ++++++++++++-----
 .../src/org/labkey/assay/AssayController.java | 12 ++++-----
 .../property/PropertyController.java          | 15 ++++-------
 5 files changed, 42 insertions(+), 35 deletions(-)

diff --git a/api/src/org/labkey/api/action/AbstractFileUploadAction.java b/api/src/org/labkey/api/action/AbstractFileUploadAction.java
index e08c56782e8..fc2f2fd3159 100644
--- a/api/src/org/labkey/api/action/AbstractFileUploadAction.java
+++ b/api/src/org/labkey/api/action/AbstractFileUploadAction.java
@@ -25,6 +25,7 @@
 import org.labkey.api.util.URLHelper;
 import org.labkey.api.view.NavTree;
 import org.labkey.api.writer.PrintWriters;
+import org.labkey.vfs.FileLike;
 import org.springframework.beans.MutablePropertyValues;
 import org.springframework.beans.PropertyValues;
 import org.springframework.validation.BindException;
@@ -34,8 +35,6 @@
 import org.springframework.web.servlet.ModelAndView;
 
 import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -187,7 +186,7 @@ private void export(FORM form, HttpServletResponse response) throws Exception
                 return;
             }
 
-            if (form.getFileName().length != form.getFileContent().length)
+            if (form.getFileName().length == form.getFileContent().length)  // TODO - restore before commiting
             {
                 error(writer, "Must include the same number of fileName and fileContent parameter values", HttpServletResponse.SC_BAD_REQUEST);
                 return;
@@ -196,7 +195,7 @@ private void export(FORM form, HttpServletResponse response) throws Exception
             HttpServletRequest basicRequest = getViewContext().getRequest();
 
             // Parameter name (String) -> File on disk/original file name Pair
-            Map> savedFiles = new HashMap<>();
+            Map> savedFiles = new HashMap<>();
 
             if (basicRequest instanceof MultipartHttpServletRequest request)
             {
@@ -212,7 +211,7 @@ private void export(FORM form, HttpServletResponse response) throws Exception
                     {
                         if (!file.isEmpty())
                         {
-                            File f = handleFile(filename, input, writer);
+                            FileLike f = handleFile(filename, input, writer);
                             if (f == null)
                             {
                                 return;
@@ -229,7 +228,7 @@ private void export(FORM form, HttpServletResponse response) throws Exception
                 String content = form.getFileContent()[i];
                 if (content != null)
                 {
-                    File f = handleFile(filename, new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), writer);
+                    FileLike f = handleFile(filename, new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), writer);
                     if (f != null)
                     {
                         savedFiles.put("FileContent" + (i == 0 ? "" : (i + 1)), new Pair<>(f, filename));
@@ -249,7 +248,7 @@ private void export(FORM form, HttpServletResponse response) throws Exception
         }
     }
 
-    protected File handleFile(String filename, InputStream input, Writer writer) throws IOException
+    protected FileLike handleFile(String filename, InputStream input, Writer writer) throws IOException
     {
         if (filename == null || input == null)
         {
@@ -261,9 +260,9 @@ protected File handleFile(String filename, InputStream input, Writer writer) thr
         String legalName = FileUtil.makeLegalName(filename);
         try
         {
-            File targetFile = getTargetFile(legalName);
+            FileLike targetFile = getTargetFile(legalName);
 
-            try (OutputStream output = new FileOutputStream(targetFile))
+            try (OutputStream output = targetFile.openOutputStream())
             {
                 byte[] buffer = new byte[1024];
                 int len;
@@ -305,15 +304,17 @@ public int getStatusCode()
         }
     }
 
-    /** Figures out where to write the uploaded file */
-    protected abstract File getTargetFile(String filename) throws IOException;
+    /**
+     * Figures out where to write the uploaded file
+     */
+    protected abstract FileLike getTargetFile(String filename) throws IOException;
 
     /**
      * Callback once the file has been written to the server's file system.
      * @param files HTTP parameter name -> [File as saved on disk (potentially renamed to be unique, Original file name in POST]
      * @return a meaningful handle that the client can use to refer to the file
      */
-    public abstract String getResponse(FORM form, Map> files) throws UploadException;
+    public abstract String getResponse(FORM form, Map> files) throws IOException;
 
     private void error(Writer writer, String message, int statusCode) throws IOException
     {
diff --git a/api/src/org/labkey/api/reader/FastaLoader.java b/api/src/org/labkey/api/reader/FastaLoader.java
index eeb38831846..34846511613 100644
--- a/api/src/org/labkey/api/reader/FastaLoader.java
+++ b/api/src/org/labkey/api/reader/FastaLoader.java
@@ -98,7 +98,7 @@ private void init()
                     {
                         _reader.close();
                     }
-                    catch (IOException x2) {}
+                    catch (IOException ignored) {}
                 }
             }
 
diff --git a/api/src/org/labkey/api/util/SessionTempFileHolder.java b/api/src/org/labkey/api/util/SessionTempFileHolder.java
index 78b1756cf09..823c811a8c3 100644
--- a/api/src/org/labkey/api/util/SessionTempFileHolder.java
+++ b/api/src/org/labkey/api/util/SessionTempFileHolder.java
@@ -17,7 +17,9 @@
 
 import jakarta.servlet.http.HttpSessionBindingEvent;
 import jakarta.servlet.http.HttpSessionBindingListener;
-import java.io.File;
+import org.labkey.vfs.FileLike;
+
+import java.io.IOException;
 
 /**
  * A session holder for temporary files. This will hold a file in the session,
@@ -28,20 +30,20 @@
  */
 public class SessionTempFileHolder implements HttpSessionBindingListener
 {
-    private final File file;
+    private final FileLike file;
     private boolean deleted = false;
 
-    public SessionTempFileHolder(File file)
+    public SessionTempFileHolder(FileLike file)
     {
         this.file = file;
     }
 
-    public File getFile()
+    public FileLike getFile()
     {
         return file;
     }
 
-    public boolean delete()
+    public boolean delete() throws IOException
     {
         deleted = true;
         return this.file.delete();
@@ -57,6 +59,15 @@ public void valueBound(HttpSessionBindingEvent event)
     public void valueUnbound(HttpSessionBindingEvent event)
     {
         if (!deleted)
-            file.delete();
+        {
+            try
+            {
+                file.delete();
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
     }
 }
diff --git a/assay/src/org/labkey/assay/AssayController.java b/assay/src/org/labkey/assay/AssayController.java
index 9aabf71c746..9afe9e03eda 100644
--- a/assay/src/org/labkey/assay/AssayController.java
+++ b/assay/src/org/labkey/assay/AssayController.java
@@ -803,7 +803,7 @@ public void setProtocolId(Long protocolId)
     public static class AssayFileUploadAction extends AbstractFileUploadAction
     {
         @Override
-        protected File getTargetFile(String filename) throws IOException
+        protected FileLike getTargetFile(String filename) throws IOException
         {
             if (!PipelineService.get().hasValidPipelineRoot(getContainer()))
                 throw new UploadException("Pipeline root must be configured before uploading assay files", HttpServletResponse.SC_NOT_FOUND);
@@ -811,7 +811,7 @@ protected File getTargetFile(String filename) throws IOException
             try
             {
                 FileLike targetDirectory = AssayFileWriter.ensureUploadDirectory(getContainer());
-                return FileUtil.findUniqueFileName(filename, targetDirectory).toNioPathForWrite().toFile();
+                return FileUtil.findUniqueFileName(filename, targetDirectory);
             }
             catch (ExperimentException e)
             {
@@ -820,13 +820,13 @@ protected File getTargetFile(String filename) throws IOException
         }
 
         @Override
-        public String getResponse(AssayFileUploadForm form, Map> files)
+        public String getResponse(AssayFileUploadForm form, Map> files)
         {
             JSONObject fullMap = new JSONObject();
-            for (Map.Entry> entry : files.entrySet())
+            for (Map.Entry> entry : files.entrySet())
             {
                 String paramName = entry.getKey();
-                File file = entry.getValue().getKey();
+                FileLike file = entry.getValue().getKey();
                 String originalName = entry.getValue().getValue();
 
                 DataType dataType = getDataType(form, originalName);
@@ -840,7 +840,7 @@ public String getResponse(AssayFileUploadForm form, Map> files) throws UploadException
+        public String getResponse(FileUploadForm form, Map> files) throws IOException
         {
             if (files.isEmpty())
             {
@@ -1371,7 +1366,7 @@ public String getResponse(FileUploadForm form, Map> f
             {
                 StringBuilder message = new StringBuilder();
                 String separator = "";
-                for (Pair fileStringPair : files.values())
+                for (Pair fileStringPair : files.values())
                 {
                     message.append(separator);
                     separator = ", ";

From 8cbe809c4fa784c236635f0eaeea883806bb3938 Mon Sep 17 00:00:00 2001
From: labkey-jeckels 
Date: Tue, 2 Dec 2025 14:45:16 -0800
Subject: [PATCH 18/18] Prep for merge

---
 api/src/org/labkey/api/action/AbstractFileUploadAction.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/api/src/org/labkey/api/action/AbstractFileUploadAction.java b/api/src/org/labkey/api/action/AbstractFileUploadAction.java
index fc2f2fd3159..902cfa8a4d3 100644
--- a/api/src/org/labkey/api/action/AbstractFileUploadAction.java
+++ b/api/src/org/labkey/api/action/AbstractFileUploadAction.java
@@ -186,7 +186,7 @@ private void export(FORM form, HttpServletResponse response) throws Exception
                 return;
             }
 
-            if (form.getFileName().length == form.getFileContent().length)  // TODO - restore before commiting
+            if (form.getFileName().length != form.getFileContent().length)
             {
                 error(writer, "Must include the same number of fileName and fileContent parameter values", HttpServletResponse.SC_BAD_REQUEST);
                 return;