From 2487251bb94cd6fbfe58a271c58c50df92243eba Mon Sep 17 00:00:00 2001 From: "Hariz S." Date: Mon, 27 Oct 2025 18:35:11 -0400 Subject: [PATCH] Improved public folder file links (#1687) * Don't update URL when opening public files via direct URL * Alert user when attempting to open a nonexistent file * Fix toolbar autohide in public file urls * Remove extraneous debug logs * Properly define stat var in public folder URL handling * Update window URL when focusing an app opened through file URL * Localize error messages --- package-lock.json | 31 +---- src/gui/src/UI/UIDesktop.js | 207 +++++++++++++++------------- src/gui/src/UI/UIWindow.js | 17 ++- src/gui/src/helpers/launch_app.js | 11 ++ src/gui/src/i18n/translations/en.js | 3 + 5 files changed, 140 insertions(+), 129 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9479d2d4..606d68bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -891,7 +891,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -3033,7 +3032,6 @@ "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/core": "^0.22.12" } @@ -3070,7 +3068,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3083,7 +3080,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3108,7 +3104,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12", "tinycolor2": "^1.6.0" @@ -3152,7 +3147,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3276,7 +3270,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3289,7 +3282,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3305,7 +3297,6 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", "license": "MIT", - "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3613,7 +3604,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3623,7 +3613,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.49.1.tgz", "integrity": "sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api": "^1.0.0" }, @@ -7323,7 +7312,6 @@ "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", @@ -7866,8 +7854,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", @@ -7917,7 +7904,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8598,7 +8584,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -8831,7 +8816,6 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -10549,7 +10533,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -15981,7 +15964,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -16182,7 +16164,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17772,7 +17753,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17993,7 +17973,6 @@ "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -18259,7 +18238,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -18309,7 +18287,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -18541,7 +18518,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -18746,7 +18722,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -19080,7 +19055,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -19288,7 +19262,6 @@ "version": "3.29.5", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -20016,7 +19989,6 @@ "version": "3.29.5", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -20179,7 +20151,6 @@ "version": "3.29.5", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, diff --git a/src/gui/src/UI/UIDesktop.js b/src/gui/src/UI/UIDesktop.js index 394e5645..1dab0d5a 100644 --- a/src/gui/src/UI/UIDesktop.js +++ b/src/gui/src/UI/UIDesktop.js @@ -1391,104 +1391,6 @@ async function UIDesktop(options) { }) } - //-------------------------------------------------------------------------------------- - // Trying to view a user's public folder? - // i.e. https://puter.com/@ - //-------------------------------------------------------------------------------------- - const url_paths = window.location.pathname.split('/').filter(element => element); - if (url_paths[0]?.startsWith('@')) { - const username = url_paths[0].substring(1); - let item_path = '/' + username + '/Public'; - if ( url_paths.length > 1 ) { - item_path += '/' + url_paths.slice(1).join('/'); - } - - // GUARD: avoid invalid user directories - { - if (!username.match(/^[a-z0-9_]+$/i)) { - UIAlert({ - message: 'Invalid username.' - }); - return; - } - } - - const stat = await puter.fs.stat({path: item_path, consistency: 'eventual'}); - - // TODO: DRY everything here with open_item. Unfortunately we can't - // use open_item here because it's coupled with UI logic; - // it requires a UIItem element and cannot operate on a - // file path on its own. - if ( ! stat.is_dir ) { - if ( stat.associated_app ) { - launch_app({ name: stat.associated_app.name }); - return; - } - - const ext_pref = - window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`]; - - if ( ext_pref ) { - launch_app({ - name: ext_pref, - file_path: item_path, - }); - return; - } - - - const open_item_meta = await $.ajax({ - url: window.api_origin + "/open_item", - type: 'POST', - contentType: "application/json", - data: JSON.stringify({ - path: item_path, - }), - headers: { - "Authorization": "Bearer "+window.auth_token - }, - statusCode: { - 401: function () { - window.logout(); - }, - }, - }); - const suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({ - path: item_path - }); - - // Note: I'm not adding unzipping logic here. We'll wait until - // we've refactored open_item so that Puter can have a - // properly-reusable open function. - if ( suggested_apps.length !== 0 ) { - launch_app({ - name: suggested_apps[0].name, - token: open_item_meta.token, - file_path: item_path, - app_obj: suggested_apps[0], - window_title: path.basename(item_path), - maximized: options.maximized, - file_signature: open_item_meta.signature, - }); - return; - } - - await UIAlert({ - message: 'Cannot find an app to open this file; ' + - 'opening directory instead.' - }); - item_path = item_path.split('/').slice(0, -1).join('/') - } - - UIWindow({ - path: item_path, - title: path.basename(item_path), - icon: await item_icon({ is_dir: true, path: item_path }), - is_dir: true, - app: 'explorer', - }); - } - window.hide_toolbar = (animate = true) => { // Always show toolbar on mobile and tablet devices if (isMobile.phone || isMobile.tablet) { @@ -1770,6 +1672,115 @@ async function UIDesktop(options) { } } }); + + //-------------------------------------------------------------------------------------- + // Trying to view a user's public folder? + // i.e. https://puter.com/@ + //-------------------------------------------------------------------------------------- + const url_paths = window.location.pathname.split('/').filter(element => element); + if (url_paths[0]?.startsWith('@')) { + const username = url_paths[0].substring(1); + let item_path = '/' + username + '/Public'; + if ( url_paths.length > 1 ) { + item_path += '/' + url_paths.slice(1).join('/'); + } + + // GUARD: avoid invalid user directories + { + if (!username.match(/^[a-z0-9_]+$/i)) { + UIAlert({ + message: i18n('error_invalid_username') + }); + return; + } + } + + let stat; + try { + stat = await puter.fs.stat({path: item_path, consistency: 'eventual'}); + } catch ( e ) { + window.history.replaceState(null, document.title, '/'); + UIAlert({ + message: i18n('error_user_or_path_not_found'), + type: 'error' + }); + return; + } + + // TODO: DRY everything here with open_item. Unfortunately we can't + // use open_item here because it's coupled with UI logic; + // it requires a UIItem element and cannot operate on a + // file path on its own. + if ( ! stat.is_dir ) { + if ( stat.associated_app ) { + launch_app({ name: stat.associated_app.name }); + return; + } + + const ext_pref = + window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`]; + + if ( ext_pref ) { + launch_app({ + name: ext_pref, + file_path: item_path, + }); + return; + } + + + const open_item_meta = await $.ajax({ + url: window.api_origin + "/open_item", + type: 'POST', + contentType: "application/json", + data: JSON.stringify({ + path: item_path, + }), + headers: { + "Authorization": "Bearer "+window.auth_token + }, + statusCode: { + 401: function () { + window.logout(); + }, + }, + }); + const suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({ + path: item_path + }); + + // Note: I'm not adding unzipping logic here. We'll wait until + // we've refactored open_item so that Puter can have a + // properly-reusable open function. + if ( suggested_apps.length !== 0 ) { + launch_app({ + name: suggested_apps[0].name, + token: open_item_meta.token, + file_path: item_path, + app_obj: suggested_apps[0], + window_title: path.basename(item_path), + maximized: options.maximized, + file_signature: open_item_meta.signature, + custom_path: window.location.pathname, + }); + return; + } + + await UIAlert({ + message: 'Cannot find an app to open this file; ' + + 'opening directory instead.' + }); + item_path = item_path.split('/').slice(0, -1).join('/') + } + + UIWindow({ + path: item_path, + title: path.basename(item_path), + icon: await item_icon({ is_dir: true, path: item_path }), + is_dir: true, + app: 'explorer', + }); + } } $(document).on('contextmenu taphold', '.taskbar', function (event) { diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 641909fa..9d96b4e4 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -121,6 +121,9 @@ async function UIWindow(options) { options.is_visible = options.is_visible ?? true; + // used for files opened via direct url + options.custom_path = options.custom_path ?? null; + // if only one instance is allowed, bring focus to the window that is already open if(options.single_instance && options.app !== ''){ let $already_open_window = $(`.window[data-app="${html_encode(options.app)}"]`); @@ -252,6 +255,7 @@ async function UIWindow(options) { data-sort_order ="${options.sort_order ?? 'asc'}" data-multiselectable = "${options.selectable_body}" data-update_window_url = "${options.update_window_url && options.is_visible}" + data-custom_path = "${html_encode(options.custom_path)}" data-user_set_url_params = "${html_encode(user_set_url_params)}" data-initial_zindex = "${zindex}" data-is_panel ="${options.is_panel ? 1 : 0}" @@ -3745,7 +3749,18 @@ $.fn.focusWindow = function(event) { //change window URL const update_window_url = $(this).attr('data-update_window_url'); const url_app_name = $(this).attr('data-app_pseudonym') || $(this).attr('data-app'); - if(update_window_url === 'true' || update_window_url === null){ + let custom_path = $(this).attr('data-custom_path'); + + if (custom_path && custom_path !== ''){ + if(update_window_url === 'true' || update_window_url === null){ + if (!custom_path.startsWith('/')) { + custom_path = '/' + custom_path; + } + window.history.replaceState({window_id: $(this).attr('data-id')}, '', custom_path); + document.title = $(this).attr('data-name'); + } + } + else if(update_window_url === 'true' || update_window_url === null){ window.history.replaceState({window_id: $(this).attr('data-id')}, '', '/app/'+url_app_name+$(this).attr('data-user_set_url_params')); document.title = $(this).attr('data-name'); } diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js index d6349804..5ed74459 100644 --- a/src/gui/src/helpers/launch_app.js +++ b/src/gui/src/helpers/launch_app.js @@ -368,6 +368,15 @@ const launch_app = async (options)=>{ if(window_options?.has_head !== undefined) has_head = window_options.has_head; + // update_window_url + let update_window_url = true; + if (options.update_window_url !== undefined && typeof options.update_window_url === 'boolean') + update_window_url = options.update_window_url; + + let custom_path; + if(options.custom_path !== undefined) + custom_path = options.custom_path; + // open window el_win = UIWindow({ element_uuid: uuid, @@ -392,6 +401,8 @@ const launch_app = async (options)=>{ is_resizable: window_resizable, has_head: has_head, show_in_taskbar: show_in_taskbar, + update_window_url: update_window_url, + custom_path: custom_path, }); // If the app is not in the background, show the window diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index 1ea26271..fb3f81c9 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -509,6 +509,9 @@ const en = { 'open_containing_folder': 'Open Containing Folder', 'set_as_background': 'Set as Desktop Background', + + 'error_user_or_path_not_found': 'User or path not found.', + 'error_invalid_username': 'Invalid username.', } };