From 0864b9ece1ac206194cfd3d8ee8518cf91f5de43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Dub=C3=A9?= <7225168+KernelDeimos@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:10:32 -0500 Subject: [PATCH] dev(apps): add configuration to not use app icon subdomain (#2603) * dev(apps): add configuration to not use app icon subdomain For some local configurations of Puter, such as using ngrok or a hosting service that has limited subdomains or limited subdomain depth, the static hosting subdomain for app icons is difficult to configure and may not be viable. Since this is only done so an external geo-replicated cache can be used, and it's always possible for Puter's backend to generate an app icon, it should be possible to configure Puter's backend to use icons from a URL hosted on the default subdomain. Add a configuration parameter to the service "app-icon" called "no_subdomain" which prevents Puter's backend from sending puter-icons subdomain URLs or redirecting to them. Also perform some small cleanup changes to make the existing code easier to understand, including: - rename `buildAppIconUrl` to `buildAppIconSubdomainUrl` to make it clear which of the two types of icon URLs are being generated. - replace `withAppIconUrl` with `get_app_icon_url` so it is possible to get an icon URL without mutating an app object. - make the `get_taskbar_items` helper use the same code for getting app icons as the `get_apps` helper. * fix(apps): test initializes a service with no config --- src/backend/src/helpers.js | 38 +++++++++---------- .../src/modules/apps/AppIconService.js | 4 +- .../src/modules/apps/AppIconService.test.js | 5 ++- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index 7c4a1cc64..991e1b56a 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -23,8 +23,8 @@ import { contentType as _contentType } from 'mime-types'; import { resolve as _resolve, extname } from 'path'; import { v4 } from 'uuid'; import APIError from './api/APIError.js'; -import { redisClient } from './clients/redis/redisSingleton.js'; import { setRedisCacheValue } from './clients/redis/cacheUpdate.js'; +import { redisClient } from './clients/redis/redisSingleton.js'; import config from './config.js'; import { APP_ICONS_SUBDOMAIN } from './consts/app-icons.js'; import { NodeUIDSelector } from './filesystem/node/selectors.js'; @@ -145,7 +145,7 @@ const isBase64AppIcon = (app) => { return isRawBase64ImageString(trimmed); }; -const buildAppIconUrl = (app_uid, size = DEFAULT_APP_ICON_SIZE) => { +const buildAppIconSubdomainUrl = (app_uid, size = DEFAULT_APP_ICON_SIZE) => { if ( ! app_uid ) return null; const normalized_uid = normalizeAppUid(app_uid); const iconSize = Number.isFinite(Number(size)) ? Number(size) : DEFAULT_APP_ICON_SIZE; @@ -176,16 +176,6 @@ const buildAppIconEndpointUrl = (app_uid, size = DEFAULT_APP_ICON_SIZE) => { return `${apiBaseUrl}/app-icon/${normalized_uid}/${iconSize}`; }; -const withAppIconUrl = (app) => { - if ( ! app ) return app; - const iconIsBase64 = isBase64AppIcon(app); - const icon_url = iconIsBase64 - ? buildAppIconEndpointUrl(app.uid ?? app.uuid) - : buildAppIconUrl(app.uid ?? app.uuid); - if ( ! icon_url ) return { ...app }; - return { ...app, icon: icon_url, icon_is_base64: iconIsBase64 }; -}; - export async function is_empty (dir_uuid) { /** @type BaseDatabaseAccessService */ const db = servicesContainer.services.get('database').get(DB_READ, 'filesystem'); @@ -521,6 +511,15 @@ export async function get_app (options) { return app; } +const get_app_icon_url = (app, size) => { + const iconIsBase64 = isBase64AppIcon(app); + const svc_appIcon = servicesContainer.services.get('app-icon'); + console.log('THIS SHOULD BE TRUE', svc_appIcon.config.no_subdomain); + return (iconIsBase64 || svc_appIcon.config.no_subdomain) + ? buildAppIconEndpointUrl(app.uid ?? app.uuid, size) + : buildAppIconSubdomainUrl(app.uid ?? app.uuid, size); +}; + /** * Get multiple apps by uid/name/id, aligned to the input order. * @@ -533,7 +532,12 @@ export const get_apps = spanify('get_apps', async (specifiers, options = {}) => specifiers = [specifiers]; } - const decorateApp = (app) => (withAppIconUrl(app)); + const decorateApp = (app) => { + if ( ! app ) return app; + const icon_url = get_app_icon_url(app.uid ?? app.uuid); + if ( ! icon_url ) return { ...app }; + return { ...app, icon: icon_url }; + }; const normalizeAppForCache = (app) => { if ( ! app ) return app; const normalized = { ...app }; @@ -2113,13 +2117,7 @@ export async function get_taskbar_items (user, { if ( no_icons ) { delete item.icon; } else { - const svc_appIcon = servicesContainer.services.get('app-icon'); - const iconUrl = svc_appIcon.getSizedIconUrl({ - appUid: item.uid, - size: iconSize, - }); - - item.icon = iconUrl;; + item.icon = get_app_icon_url(item, iconSize); } // add to final object diff --git a/src/backend/src/modules/apps/AppIconService.js b/src/backend/src/modules/apps/AppIconService.js index 02ed4fc1e..09ecfb40d 100644 --- a/src/backend/src/modules/apps/AppIconService.js +++ b/src/backend/src/modules/apps/AppIconService.js @@ -25,11 +25,11 @@ import { LLMkdir } from '../../filesystem/ll_operations/ll_mkdir.js'; import { LLRead } from '../../filesystem/ll_operations/ll_read.js'; import { NodePathSelector } from '../../filesystem/node/selectors.js'; import { get_app, get_user } from '../../helpers.js'; -import { AppRedisCacheSpace } from './AppRedisCacheSpace.js'; import BaseService from '../../services/BaseService.js'; import { DB_READ, DB_WRITE } from '../../services/database/consts.js'; import { Endpoint } from '../../util/expressutil.js'; import { buffer_to_stream, stream_to_buffer } from '../../util/streamutil.js'; +import { AppRedisCacheSpace } from './AppRedisCacheSpace.js'; import DEFAULT_APP_ICON from './default-app-icon.js'; const require = createRequire(import.meta.url); @@ -91,7 +91,7 @@ export class AppIconService extends BaseService { } = await this.#getIconStream({ appUid, size: resolvedSize, - allowRedirect: true, + allowRedirect: !this.config.no_subdomain, }); if ( redirectUrl ) { diff --git a/src/backend/src/modules/apps/AppIconService.test.js b/src/backend/src/modules/apps/AppIconService.test.js index 0bbc058c6..0e927cf36 100644 --- a/src/backend/src/modules/apps/AppIconService.test.js +++ b/src/backend/src/modules/apps/AppIconService.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import config from '../../config.js'; import { AppIconService } from './AppIconService.js'; @@ -15,6 +15,9 @@ describe('AppIconService', () => { describe('URL helpers', () => { it('extracts a puter subdomain from a static hosting URL', () => { const service = Object.create(AppIconService.prototype); + // TODO: We might need a better way to do this. A service with no + // initialization is difficult to test. + service.config = {}; const domain = 'site.puter.localhost:4100'; config.load_config({ static_hosting_domain: domain,