Back to home page

EIC code displayed by LXR

 
 

    


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 }