Skip to content

Add browser-native file operations to Hop Web - #8276

Merged
mattcasters merged 3 commits into
apache:mainfrom
michaaels:feature/web-file-browser-transfers
Sep 9, 2026
Merged

Add browser-native file operations to Hop Web#8276
mattcasters merged 3 commits into
apache:mainfrom
michaaels:feature/web-file-browser-transfers

Conversation

@michaaels

@michaaels michaaels commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds browser-native file operations to Hop Web while preserving the existing server-side File workflow.

  • Adds a separate File Browser category for creating, uploading, downloading, and exporting pipelines/workflows and projects.
  • Keeps Close and Close All only in File, and shows SVG export only while a pipeline or workflow is active.
  • Adds session-isolated RAP upload/download handling with bounded sizes, one-time download tokens, expiry, cleanup, and safe response headers.
  • Validates Kettle/PDI ZIP uploads against traversal, duplicate paths, excessive depth/count/size, and suspicious compression ratios.
  • Reuses the existing Projects and Kettle import GUI plugins rather than duplicating their implementation.
  • Includes hop-misc-import once in the standard plugin assembly so the action is available in the standard web distribution, and removes it from the optional marketplace catalog.
  • Adds English/Spanish messages and focused unit tests.

Compatibility

  • Java 21
  • Desktop SWT/RCP behavior remains server-file based.
  • Hop Web/RAP receives the browser-native File Browser actions.
  • RAP/FileUpload dependencies in the web assembly are aligned at 4.7.0; Commons FileUpload is M5 and Commons IO is 2.22.0.

Security notes

Uploads are scoped to a RAP UI session. Download handlers require both the owning HTTP session and UI session, use one-time expiring tokens, and delete temporary files after use or session disposal. Kettle ZIP extraction rejects absolute/traversal paths, duplicates, overlong/deep paths, excessive entry counts, per-entry and aggregate expansion limits, and high compression ratios.

Review follow-up

  • Browser uploads are parsed directly into untitled pipeline/workflow models before opening an editor. No session-temporary filename reaches recent files, tab restoration, file watchers, or the server Save action.
  • Browser Save is enabled for any serializable active pipeline/workflow with FILE_SAVE permission, including unchanged documents. Save As prompts for a download filename; Save uses the existing suggestion.
  • After the HTTP response is streamed and flushed, the owning UI session acknowledges only the downloaded revision of an untitled document. Edits made during a transfer remain dirty. Downloading a copy of a server-backed file deliberately does not acknowledge pending server changes.
  • Desktop Kettle import failures retain the original exception and localized details; only web dialogs use cause-stripped messages referring to the server log.
  • Uploads must be regular, non-symlink, direct children of the normalized per-request directory. Listener filenames use sanitized basenames. Rejected external paths are never deleted.
  • Removed the process-wide scheme-less VFS URI rewrite. Added regression coverage for existing percent-escape semantics and explicit local file URIs, including Windows execution. ExplorerCreateUtilsTest now passes.
  • Added transfer-service tests for token consumption, HTTP/UI session ownership, expiry, the 16-download bound, response headers, callback timing, upload containment, failure cleanup, and disposal races.
  • Project ZIP export now uses the shared ui/images/zipfile.svg icon.
  • The Spanish error title is translated. The menu intentionally remains exactly File Browser in both languages, matching the requested product name.
  • Kept the RAP-aligned FileUpload milestone: the RAP 4.7.0 File Upload POM declares FileUpload 2.0.0-M5 and Commons IO 2.22.0. No independent dependency downgrade or duplicate import plugin was introduced.
  • Documented server/browser Save semantics and session lifetime in the Hop Web user manual.

Validation

All commands below used Java 21.0.10.

  • Focused tests: 84 executed, 83 passed, 1 skipped, 0 failures/errors. The skipped symlink test requires Windows symlink privileges. Suites: HopVfsTest (11), HopHomeTest (5), HopGuiWebUserFileMenuTest (4), ExplorerCreateUtilsTest (15), KettleImportDialogTest (7), HopWebUserFilePluginTest (14), UserFileTransferTest (28).
  • -pl assemblies/web -am -Dspotless.skip=true -DskipTests -DskipITs package: passed (10 modules).
  • -pl assemblies/client -am -Pskip-uitest -Dspotless.skip=true -DskipTests -DskipITs package: passed (284 modules). This broader check compiled production/test sources and packaged the distribution; it did not execute the broad test suite.
  • Spotless check for core/import/marketplace/projects/ui/rap/web and their dependencies: passed (21 modules).
  • git diff --check: passed.
  • Direct RAT checks for rap and plugins/misc/import: passed with zero unapproved files. A full root clean/install/RAT validation is not claimed.
  • Local Tomcat 10.1.59 with the updated core/UI/RAP/import artifacts returns HTTP 200 on /ui, bound to loopback. An earlier Chrome session confirmed GUI startup and captured screenshots without console or HTTP errors.

Remaining manual/browser validation

The current browser MCP reports both Chrome and the integrated browser unavailable. End-to-end SVG download/content validation, menu screenshots in all open/close/project/reload states, the browser upload widget, and the wider RAP dialog/JEE-vs-SWT smoke test are not yet certified. Unit/service tests do not replace those checks. The review threads remain for reviewer confirmation.


Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Run mvn clean install apache-rat:check to make sure basic checks pass. A more thorough check will be performed on your pull request automatically.
  • If you have a group of commits related to the same change, please squash your commits into one and force push your branch using git rebase -i.
  • Mention the appropriate issue in your description (for example: addresses #123), if applicable.

To make clear that you license your contribution under the Apache License Version 2.0, January 2004
you have to acknowledge this by using the following check-box.

@mattcasters

Copy link
Copy Markdown
Contributor

@michaaels : thanks a lot for the pull request and bringing forward what is really a great idea. I ran a code review on the PR and found a few minor issues. Let us know if you want any help fixing these, the broken unit test or anything else!

@mattcasters mattcasters left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Let's make sure the file a user is working on is properly saved on the server at all times and properly flagged as changed.

String safeName = safeFilename(filename, "");
Path file = getSessionTempDirectory().resolve(UUID.randomUUID().toString() + "-" + safeName);
Files.copy(uploadedFile, file);
IHopFileTypeHandler handler = HopGui.getInstance().fileDelegate.fileOpen(file.toString());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] File Browser → Open copies the upload into a RAP session temp directory and then fileOpen()s that path. Hop treats it as a normal server file: toolbar/File → Save writes back to /tmp/hop-web-user-files-…, recent-file audit stores that path, and getSessionTempDirectory() deletes the tree when the UI session ends. A user who opens a pipeline from their laptop and hits the familiar Save button will think the work is stored, then lose it on logoff/timeout. File Browser → Save only downloads a copy and does not clear the dirty flag or rebind the filename, so Close still prompts and the default Save path remains the doomed temp file.

Suggestion: After a browser open, treat the handler as untitled (empty filename) so File/toolbar Save goes through Save As to a real project/VFS path, or intercept Save when the handler is in userFileNames and download instead. Do not persist session-temp paths in last-opened/recent files. After a successful File Browser download, clearChanged() so Close does not send the user back into the temp-file Save path.

String title = BaseMessages.getString(PKG, "HopGuiImport.Error.Title");
String message = BaseMessages.getString(PKG, "HopGuiImport.Error.Message");
hopGui.getLog().logError(message, e);
new ErrorDialog(hopGui.getShell(), title, message, new HopException(message));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] Failures now log the real exception, then open ErrorDialog with new HopException(message) (no cause). The English text is “Unable to open the Kettle/PDI import. See the server log for details.” This plugin is used from File → Import on desktop as well as from File Browser on web. Desktop users no longer see the underlying error in the dialog, and there is no server log.

Suggestion: Keep the generic, cause-stripped dialog for Hop Web only (as ProjectsGuiPlugin.exportProject already does with showConfirmation ? e : new HopException(...)). On desktop pass e through and use a message that does not say “server log”.

String title = BaseMessages.getString(PKG, "KettleImportDialog.Error.Title");
String message = BaseMessages.getString(PKG, "KettleImportDialog.Error.Message");
LogChannel.UI.logError(message, e);
new ErrorDialog(shell, title, message, new HopException(message));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] The same pattern is applied in doImport(): the real failure is logged, then the dialog is given a fresh HopException and KettleImportDialog.Error.Message (“The Kettle/PDI import failed. See the server log for details.”). Import from the File menu on desktop is now opaque; the Details pane only shows the wrapper.

Suggestion: Same split as the HopImportGuiPlugin dialog change. Pass the original exception into ErrorDialog unless EnvironmentUtils.isWeb().

return;
}

Path uploadedFile = Path.of(uploadedPath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] After RAP FileDialog.open(), the code accepts whatever absolute path RAP reports (Path.of(uploadedPath)) as long as it is a regular file under the size limit. It never checks that the file is inside requestDirectory. RAP FileUploadProcessor does FilenameUtils.getName() before DiskFileUploadReceiver writes new File(uploadDir, fileName), so ../ in the client filename is usually stripped, but names like .. and a future RAP/FileUpload change would write/read outside the per-request directory. deleteTree(requestDirectory) would also miss a file that landed elsewhere.

Suggestion: Resolve the uploaded path with NOFOLLOW_LINKS, require uploadedFile.startsWith(requestDirectory.toAbsolutePath().normalize()) (and that it is a direct child), and use only a sanitized basename for the listener’s filename. Reject/delete anything outside that directory.

fileType, handler, ID_MAIN_TOOLBAR_SAVE_AS, IHopFileType.CAPABILITY_SAVE_AS);

mainMenuWidgets.enableMenuItem(
fileType, handler, ID_MAIN_MENU_FILE_USER_SAVE, IHopFileType.CAPABILITY_SAVE, changed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] File Browser → Save is enabled with the same changed flag as server File → Save. For a download action that is the wrong extra condition: after File Browser → Open of an unchanged pipeline, Save is greyed out and only Save As (an EnterStringDialog, then download) works. The error copy already calls this a download (HopGui.FileBrowser.Error.Save=Unable to download the active file).

Suggestion: Enable File Browser Save whenever the active handler can serialize a pipeline/workflow (and FILE_SAVE is allowed), not only when the tab is dirty. Keep the dirty flag for the server File menu.

* File#toURI()} emits {@code file:/C:/...}; normalize that form while leaving UNC and non-file
* URIs untouched.
*/
private static String toFileUri(File file) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Every scheme-less path now goes through File.toURI() plus a Windows file:/C:file:///C: rewrite. That is a process-wide VFS behavior change, not web-only. The new test covers a Linux path with spaces and #; the drive-letter rewrite (the reason for toFileUri) is untested.

Suggestion: Add a unit test that feeds toFileUri/getFileObject a file:/C:/… style URI (can be string-level if the job isn’t on Windows) and confirm HopVfs.getFilename round-trips spaces, #, and drive letters. Watch any callers that compared native paths to FileObject URIs.

}

@Test
void sanitizesDownloadHeaders() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] ZIP traversal/bomb tests and contentDisposition sanitization are useful, but nothing asserts the download invariants the PR claims: one-time tokens, HTTP+UI session match → 404 otherwise, TTL eviction, max 16 pending, or upload path containment. sanitizesDownloadHeaders only checks the header helper.

Suggestion: Add tests around UserFileTransfer (package-private) for safeHeaderFilename/contentDisposition edge cases already there, plus token consume-once, foreign-session 404, and “uploaded path must stay under the request directory”.

Comment thread assemblies/web/pom.xml

<properties>
<rap.version>4.4.0</rap.version>
<commons.fileupload2.version>2.0.0-M5</commons.fileupload2.version>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The web WAR now ships commons-fileupload2-*-2.0.0-M5 because RAP 4.7’s fileupload bundle depends on that milestone. Aligning assemblies/web RAP from 4.4.0 to 4.7.0 with hop-ui-rap is necessary for FileDialog upload APIs, but M5 is still a milestone on an ASF release train, and the RWT runtime bump is three minors.

Suggestion: Call out in the PR that FileUpload 2.0.0-M5 is RAP-required, and smoke-test Hop Web beyond the new menu (existing RAP dialogs, file upload widget, JEE vs SWT compatibility mode). Prefer a non-milestone FileUpload if RAP will take it.

root = HopGui.ID_MAIN_MENU,
id = HopGui.ID_MAIN_MENU_FILE_USER_EXPORT_PROJECT,
label = "i18n::HopGui.Menu.File.ExportProjectZip",
image = "export.svg",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] File Browser → Export Project ZIP uses image = "export.svg". That file only exists in the projects plugin (plugins/misc/projects/src/main/resources/export.svg). The RAP classloader will miss it and fall back to no_image.svg. Other new items correctly use ui/images/….

Suggestion: Point at an icon that lives in ui/images (for example ui/images/zipfile.svg or ui/images/download.svg), matching importFromKettleZip.

HopGui.Menu.File.ExportToSVG=Exportar a SVG
HopGui.Menu.File.ExportProjectZip=Exportar proyecto ZIP
HopGui.Menu.File.ImportKettleZip=Importar ZIP de Kettle/PDI
HopGui.Menu.File.User=File Browser

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] HopGui.Menu.File.User and HopGui.FileBrowser.Error.Title are left as English “File Browser” in the Spanish bundle. Neighboring File Browser strings were translated.

Suggestion: Translate both keys (e.g. “Navegador de archivos”) so the new top-level menu is not mixed-language.

@michaaels

Copy link
Copy Markdown
Contributor Author

Thank you for your comments; I'm currently working on the corrections.

File Browser ZIP import extracts into a hop-web-user-files session temp folder that is deleted when the dialog closes. Do not persist that path, ignore an already-stored temp last-used value, and remember a real folder only if the user browsed away from the upload.
@mattcasters
mattcasters merged commit 7e6a26e into apache:main Sep 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants