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
This commit is contained in:
Eric Dubé
2026-03-04 19:10:32 -05:00
committed by GitHub
parent d802d12a05
commit 0864b9ece1
3 changed files with 24 additions and 23 deletions
+18 -20
View File
@@ -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
@@ -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 ) {
@@ -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,