Warning, /firebird/packages/firebird-ng/api/src/lib/services/url.service.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * @file url.service.ts
0003 * @description Provides URL resolution services for handling file downloads and conversions in an Angular application.
0004 * The service supports different configurations depending on how the application is served and includes
0005 * protocol aliasing for custom URL schemes.
0006 */
0007
0008 import { Injectable, computed, inject } from '@angular/core';
0009 import { ConfigService, ServerConfigService, URL_ALIASES } from "@dexvis/app-features";
0010 import type { ServerConfig } from "./server-config";
0011 import { BACKEND_URL_CONFIG, BACKEND_USE_API_CONFIG } from "../config-keys";
0012
0013 /** Marks a path as relative to the backend's working directory. */
0014 const LOCAL_PREFIX = 'local://';
0015
0016 /**
0017 * @class UrlService
0018 * @description This service resolves URLs for downloading and converting files. It handles different scenarios based
0019 * on whether the application is served as a standalone static site, served by a backend like PyroBird, or
0020 * configured to use a specific API endpoint. It also supports protocol aliases and special URL schemes.
0021 *
0022 * ### Assumptions:
0023 * - The application may be served in various configurations:
0024 * - Standalone static site without backend API.
0025 * - Served by a Flask application (e.g., PyroBird) with a backend API.
0026 * - Served by another service with or without a backend API.
0027 * - The service needs to resolve URLs for files that may:
0028 * - Be accessible over HTTP/HTTPS.
0029 * - Be local files requiring backend API to serve them.
0030 * - Use custom protocols (e.g., `asset://`, `local://`, or an alias a pack contributes).
0031 *
0032 * ### Use Cases:
0033 * - **Case 1: Downloading Files**
0034 * - **1.1**: Input URL starts with protocol like `root://`, `http://` or `https://`. The URL is used as is.
0035 * - **1.2**: Input URL has no protocol or is a local file. The service checks if a backend is available and constructs the download endpoint URL.
0036 * - **Case 2: Converting Files**
0037 * - **2.1 & 2.2**: Input URL is converted using the backend 'convert' endpoint, regardless of the original protocol.
0038 *
0039 * ### Protocol Aliases:
0040 * - `asset://`: Points to assets served from the frontend server.
0041 * - `local://`: A path relative to the backend's working directory
0042 * (`pyrobird serve --work-path`). Resolves to a relative path, which the
0043 * download/convert endpoints below turn into a backend request. Needs a
0044 * backend — on a static deployment there is nothing to serve the file.
0045 * - Aliases contributed through `withUrlAlias()`: prefix replaced with a base URL.
0046 *
0047 * ### Examples:
0048 * - Download URL:
0049 * - Input: `https://example.com/file.root`
0050 * - Output: `https://example.com/file.root` (Case 1.1)
0051 * - Download URL:
0052 * - Input: `/path/to/file.root`
0053 * - Output: `<serverAddress>/api/v1/download?f=%2Fpath%2Fto%2Ffile.root` (if backend is available) (Case 1.2)
0054 * - Download URL:
0055 * - Input: `local://file.root`
0056 * - Output: `<serverAddress>/api/v1/download?f=file.root` (if backend is available) (Case 1.2)
0057 * - Convert URL:
0058 * - Input: `https://example.com/file.root`, `fileType`, `entries`
0059 * - Output: `<serverAddress>/api/v1/convert/fileType/entries?f=https%3A%2F%2Fexample.com%2Ffile.root`
0060 */
0061
0062 @Injectable({
0063 providedIn: 'root'
0064 })
0065 export class UrlService {
0066 private readonly serverConfigService = inject<ServerConfigService<ServerConfig>>(ServerConfigService);
0067 private readonly config = inject(ConfigService);
0068
0069 // The backend chosen on the config page ("Use specific backend", "Base API
0070 // URL"). Declared here, by the code that reads them; the page binds to the
0071 // same schemas.
0072 private readonly useApi = this.config.declare(BACKEND_USE_API_CONFIG);
0073 private readonly apiUrl = this.config.declare(BACKEND_URL_CONFIG);
0074
0075 /**
0076 * True when a backend can serve files and conversions: pyrobird serves this
0077 * page, or the user configured one (`server.useApi`).
0078 */
0079 readonly isBackendAvailable = computed(() =>
0080 this.serverConfigService.configSignal().servedByPyrobird || this.useApi.valueSignal());
0081
0082 /**
0083 * Base URL of that backend, without a trailing slash; '' when there is none.
0084 * The serving pyrobird wins over a configured one.
0085 */
0086 readonly serverAddress = computed(() => {
0087 const server = this.serverConfigService.configSignal();
0088 if (server.servedByPyrobird) {
0089 return server.apiBaseUrl;
0090 }
0091 if (this.useApi.valueSignal()) {
0092 return this.apiUrl.valueSignal().trim().replace(/\/+$/, '');
0093 }
0094 return '';
0095 });
0096
0097 // Protocol aliases come from DI (`withUrlAlias()` features); the flagship's
0098 // epic:// alias comes from the ePIC pack (`withEpic()` in packages/epic).
0099 // One per prefix: a later alias for a prefix replaces the earlier one.
0100 private readonly protocolAliases: { [key: string]: string } =
0101 Object.fromEntries((inject(URL_ALIASES, { optional: true }) ?? []).map(a => [a.prefix, a.base]));
0102
0103 /**
0104 * Resolves protocol aliases in the URL.
0105 *
0106 * - Replaces 'asset://' with the base URI of the application and the 'assets' path.
0107 * - Replaces any custom protocol aliases defined in the `protocolAliases` map.
0108 *
0109 * @param url The URL to resolve.
0110 * @returns The URL with protocol aliases resolved.
0111 */
0112 private resolveProtocolAliases(url: string): string {
0113 if (url.startsWith('asset://')) {
0114 const assetPath = url.substring('asset://'.length);
0115 // Assets are served from where the frontend is served.
0116 // Ensure there's no double slash
0117 const baseUri = document.baseURI.endsWith('/') ? document.baseURI : `${document.baseURI}/`;
0118 return `${baseUri}assets/${assetPath}`;
0119 }
0120
0121 if (url.startsWith(LOCAL_PREFIX)) {
0122 // Files under the backend's work path are not web-served, so the only
0123 // way to reach them is the download/convert endpoints. Stripping the
0124 // prefix leaves a path with no protocol, which is exactly the input
0125 // those endpoints take (Case 1.2 below).
0126 return url.substring(LOCAL_PREFIX.length);
0127 }
0128
0129 for (const alias in this.protocolAliases) {
0130 if (url.startsWith(alias)) {
0131 return url.replace(alias, this.protocolAliases[alias]);
0132 }
0133 }
0134 return url;
0135 }
0136
0137 /**
0138 * Checks if a URL is absolute (i.e., starts with a protocol like 'http://').
0139 *
0140 * @param url The URL to check.
0141 * @returns True if the URL is absolute, false otherwise.
0142 */
0143 private isAbsoluteUrl(url: string): boolean {
0144 return /^[a-z][a-z0-9+.-]*:/.test(url);
0145 }
0146
0147 /**
0148 * Resolves the URL for downloading a file.
0149 *
0150 * **Case 1.1**: If the URL is absolute (starts with 'http://' or 'https://'), it is returned as is.
0151 *
0152 * **Case 1.2**: If the URL has no protocol, it uses the backend download endpoint if available.
0153 * Constructs the URL: `<serverAddress>/api/v1/download?f=<encoded inputUrl>`
0154 *
0155 * @param inputUrl The input URL to resolve.
0156 * @returns The resolved URL for downloading.
0157 */
0158 public resolveDownloadUrl(inputUrl: string): string {
0159 inputUrl = this.resolveProtocolAliases(inputUrl);
0160
0161 if (this.isAbsoluteUrl(inputUrl)) {
0162 // Case 1.1: Leave the URL as is
0163 return inputUrl;
0164 } else {
0165 // Case 1.2: Use the download endpoint if available
0166 const serverAddress = this.serverAddress();
0167 if (this.isBackendAvailable() && serverAddress) {
0168 return `${serverAddress}/api/v1/download?f=${encodeURIComponent(inputUrl)}`;
0169 } else {
0170 console.warn(
0171 `Backend is not available to fetch '${inputUrl}'. Server-side files ` +
0172 `(local:// or a plain path) need 'pyrobird serve' or a configured API server.`
0173 );
0174 return inputUrl;
0175 }
0176 }
0177 }
0178
0179 /**
0180 * Resolves the URL for converting a file using the 'convert' endpoint.
0181 *
0182 * **Case 2.1 & 2.2**: Constructs the URL using the backend 'convert' endpoint.
0183 * Constructs the URL: `<serverAddress>/api/v1/convert/<fileType>/<entries>?f=<encoded inputUrl>`
0184 *
0185 * @param inputUrl The input URL to resolve.
0186 * @param fileType The file type for conversion.
0187 * @param entries Additional entries for conversion.
0188 * @returns The resolved URL for conversion.
0189 */
0190 public resolveConvertUrl(inputUrl: string, fileType: string, entries: string, collections?: string[]): string {
0191 inputUrl = this.resolveProtocolAliases(inputUrl);
0192
0193 const serverAddress = this.serverAddress();
0194 if (this.isBackendAvailable() && serverAddress) {
0195 // `collections` matches `pyrobird convert --collections`; absent means all groups
0196 const collectionsParam = collections?.length
0197 ? `&collections=${encodeURIComponent(collections.join(','))}`
0198 : '';
0199 return `${serverAddress}/api/v1/convert/${fileType}/${entries}?f=${encodeURIComponent(inputUrl)}${collectionsParam}`;
0200 } else {
0201 const message = "Backend is not available to convert the file";
0202 console.warn(message);
0203 throw Error(message);
0204 }
0205 }
0206 }