diff --git a/firebird-ng/src/app/pages/input-config/input-config.component.html b/firebird-ng/src/app/pages/input-config/input-config.component.html
index dda39de..8cd1962 100644
--- a/firebird-ng/src/app/pages/input-config/input-config.component.html
+++ b/firebird-ng/src/app/pages/input-config/input-config.component.html
@@ -147,14 +147,6 @@
API URL:
-
- Server Port |
- {{ firebirdConfig.serverPort }} |
-
-
- Server Host |
- {{ firebirdConfig.serverHost }} |
-
Served by Pyrobird |
{{ firebirdConfig.servedByPyrobird ? 'Yes' : 'No' }} |
@@ -164,8 +156,8 @@ API URL:
{{ firebirdConfig.apiAvailable ? 'Yes' : 'No' }} |
- Use Authentication |
- {{ firebirdConfig.useAuthentication ? 'Yes' : 'No' }} |
+ API Base URL |
+ {{ firebirdConfig.apiBaseUrl }} |
Log Level |
diff --git a/firebird-ng/src/app/services/server-config.service.spec.ts b/firebird-ng/src/app/services/server-config.service.spec.ts
index d7c002e..7fd3172 100644
--- a/firebird-ng/src/app/services/server-config.service.spec.ts
+++ b/firebird-ng/src/app/services/server-config.service.spec.ts
@@ -21,46 +21,19 @@ describe('FirebirdConfigService', () => {
});
//
it('should fetch and parse JSONC data correctly', () => {
- const dummyConfig = { serverPort: 5001 };
+ const dummyConfig = { apiBaseUrl: "http://localhost:5454" };
const jsoncData = '{ "key": "value" // comment }';
- service.loadConfig().then(()=>{
- expect(service.config.serverPort).toBe(5001);
- });
-
// Set up the HttpTestingController
const req = httpMock.expectOne(service['configUrl']);
expect(req.request.method).toBe('GET');
- req.flush('{ "serverPort": 5001, "useAuthentication": true, "logLevel": "info" }'); // Mock the HTTP response
+ req.flush('{ "apiBaseUrl": "http://localhost:5454", "logLevel": "info" }'); // Mock the HTTP response
+
+ service.loadConfig().then(()=>{
+ expect(service.config.apiBaseUrl).toBe("http://localhost:5454");
+ });
});
- //
- // it('should handle HTTP errors gracefully', () => {
- // const errorResponse = new ErrorEvent('Network error');
- //
- // service.getConfig().subscribe({
- // next: config => fail('should have failed with the network error'),
- // error: error => expect(error).toBeTruthy(),
- // complete: () => fail('The request should not complete successfully')
- // });
- //
- // });
});
-// import { TestBed } from '@angular/core/testing';
-//
-// import { ServerConfigService } from './firebird-config.service';
-//
-// describe('ServerConfigService', () => {
-// let service: ServerConfigService;
-//
-// beforeEach(() => {
-// TestBed.configureTestingModule({});
-// service = TestBed.inject(ServerConfigService);
-// });
-//
-// it('should be created', () => {
-// expect(service).toBeTruthy();
-// });
-// });
diff --git a/firebird-ng/src/app/services/server-config.service.ts b/firebird-ng/src/app/services/server-config.service.ts
index 8fa5fbc..8f2fb86 100644
--- a/firebird-ng/src/app/services/server-config.service.ts
+++ b/firebird-ng/src/app/services/server-config.service.ts
@@ -6,20 +6,16 @@ import {BehaviorSubject, Observable, catchError, map, of, firstValueFrom} from "
export interface ServerConfig {
- serverPort: number;
- serverHost: string;
servedByPyrobird: boolean;
apiAvailable: boolean;
- useAuthentication: boolean;
+ apiBaseUrl: string;
logLevel: string;
}
export const defaultFirebirdConfig: ServerConfig = {
- serverPort: 5454,
- serverHost: "localhost",
apiAvailable: false,
+ apiBaseUrl: "",
servedByPyrobird: false,
- useAuthentication: true,
logLevel: 'info'
};
diff --git a/firebird-ng/src/app/services/url.service.spec.ts b/firebird-ng/src/app/services/url.service.spec.ts
index b6dd7ea..67601b0 100644
--- a/firebird-ng/src/app/services/url.service.spec.ts
+++ b/firebird-ng/src/app/services/url.service.spec.ts
@@ -32,8 +32,8 @@ describe('UrlService', () => {
serverConfigService = {
config: {
servedByPyrobird: false,
- serverHost: 'localhost',
- serverPort: 5000
+ apiAvailable: true,
+ apiBaseUrl: 'http://localhost:5454',
}
};
@@ -58,6 +58,7 @@ describe('UrlService', () => {
const expectedUrl = 'http://localhost:5454/api/v1/download?f=%2Fpath%2Fto%2Ffile.root';
const resolvedUrl = service.resolveDownloadUrl(inputUrl);
+
expect(resolvedUrl).toBe(expectedUrl);
}));
diff --git a/firebird-ng/src/app/services/url.service.ts b/firebird-ng/src/app/services/url.service.ts
index 669936e..439ea66 100644
--- a/firebird-ng/src/app/services/url.service.ts
+++ b/firebird-ng/src/app/services/url.service.ts
@@ -20,7 +20,6 @@ import { ServerConfigService } from "./server-config.service";
* - Standalone static site without backend API.
* - Served by a Flask application (e.g., PyroBird) with a backend API.
* - Served by another service with or without a backend API.
- * - Users may configure a custom API endpoint via `UserConfigService`.
* - The service needs to resolve URLs for files that may:
* - Be accessible over HTTP/HTTPS.
* - Be local files requiring backend API to serve them.
@@ -28,7 +27,7 @@ import { ServerConfigService } from "./server-config.service";
*
* ### Use Cases:
* - **Case 1: Downloading Files**
- * - **1.1**: Input URL starts with `http://` or `https://`. The URL is used as is.
+ * - **1.1**: Input URL starts with protocol like `root://`, `http://` or `https://`. The URL is used as is.
* - **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.
* - **Case 2: Converting Files**
* - **2.1 & 2.2**: Input URL is converted using the backend 'convert' endpoint, regardless of the original protocol.
@@ -96,8 +95,7 @@ export class UrlService {
this.isBackendAvailable = servedByPyrobird || userUseApi;
if (servedByPyrobird) {
- const protocol = window.location.protocol; // 'http:' or 'https:'
- this.serverAddress = `${protocol}//${this.serverConfigService.config.serverHost}:${this.serverConfigService.config.serverPort}`;
+ this.serverAddress = this.serverConfigService.config.apiBaseUrl;
} else if (userUseApi) {
this.serverAddress = userServerUrl;
} else {
diff --git a/firebird-ng/src/app/services/user-config.service.spec.ts b/firebird-ng/src/app/services/user-config.service.spec.ts
index bd06b9b..cc80a0f 100644
--- a/firebird-ng/src/app/services/user-config.service.spec.ts
+++ b/firebird-ng/src/app/services/user-config.service.spec.ts
@@ -27,8 +27,7 @@ describe('UrlService', () => {
const mockServerConfigService = {
config: {
servedByPyrobird: false,
- serverHost: 'localhost',
- serverPort: 5000
+ apiBaseUrl: 'http:/localhost:4545'
}
};
@@ -162,47 +161,4 @@ describe('UrlService', () => {
expect(resolvedUrl).toBe(expectedUrl);
});
});
-
- describe('backend availability and server address', () => {
- it('should use PyroBird server address if servedByPyrobird is true', () => {
- // Simulate PyroBird server
- serverConfigService.config.servedByPyrobird = true;
- serverConfigService.config.serverHost = 'pyrobirdhost';
- serverConfigService.config.serverPort = 8080;
-
- const expectedServerAddress = `${window.location.protocol}//pyrobirdhost:8080`;
-
- // Reinitialize the service to pick up new config
- service = new UrlService(userConfigService, serverConfigService);
-
- // Access private property via getter (assuming you add getters)
- // expect(service.getServerAddress()).toBe(expectedServerAddress);
-
- // Or access private property directly for testing purposes
- expect((service as any).serverAddress).toBe(expectedServerAddress);
- });
-
- it('should use user-configured server address if localServerUseApi is true', () => {
- userConfigService.localServerUseApi.subject.next(true);
- userConfigService.localServerUrl.subject.next('http://customserver:1234');
-
- const expectedServerAddress = 'http://customserver:1234';
-
- // Reinitialize the service to pick up new config
- service = new UrlService(userConfigService, serverConfigService);
-
- expect((service as any).serverAddress).toBe(expectedServerAddress);
- });
-
- it('should have backend unavailable when neither PyroBird nor user API is configured', () => {
- userConfigService.localServerUseApi.subject.next(false);
- serverConfigService.config.servedByPyrobird = false;
-
- // Reinitialize the service to pick up new config
- service = new UrlService(userConfigService, serverConfigService);
-
- expect((service as any).isBackendAvailable).toBe(false);
- expect((service as any).serverAddress).toBe('');
- });
- });
});
diff --git a/pyrobird/README.md b/pyrobird/README.md
index ef3bdf7..ae93c8f 100644
--- a/pyrobird/README.md
+++ b/pyrobird/README.md
@@ -134,9 +134,9 @@ This is technical explanation of what is under the hood of the server part
### Configuration Options
- **DOWNLOAD_PATH**: `str[getcwd()]`, Specifies the directory from which files can be downloaded when using relative paths.
-- **DOWNLOAD_DISABLE**: `bool[False]` If set to `True`, all download functionalities are disabled.
+- **DOWNLOAD_IS_DISABLED**: `bool[False]` If set to `True`, all download functionalities are disabled.
- **DOWNLOAD_IS_UNRESTRICTED**: `bool[False]`, allows unrestricted access to download any file, including sensitive ones.
-- **DOWNLOAD_ALLOW_CORS**: `bool[False]`, If set to `True`, enables Cross-Origin Resource Sharing (CORS) for download routes.
+- **CORS_IS_ALLOWED**: `bool[False]`, If set to `True`, enables Cross-Origin Resource Sharing (CORS) for download routes.
diff --git a/pyrobird/example_wsgi.py b/pyrobird/example_wsgi.py
index 6e2645d..6d8b6ea 100644
--- a/pyrobird/example_wsgi.py
+++ b/pyrobird/example_wsgi.py
@@ -1,7 +1,7 @@
config = {
- "DOWNLOAD_DISABLE": True,
+ "DOWNLOAD_IS_DISABLED": True,
"DOWNLOAD_IS_UNRESTRICTED": False,
- "DOWNLOAD_ALLOW_CORS": True
+ "CORS_IS_ALLOWED": True
}
from pyrobird.server import flask_app as application
diff --git a/pyrobird/src/pyrobird/cli/serve.py b/pyrobird/src/pyrobird/cli/serve.py
index c1fa9b1..1181d3e 100644
--- a/pyrobird/src/pyrobird/cli/serve.py
+++ b/pyrobird/src/pyrobird/cli/serve.py
@@ -1,6 +1,8 @@
import logging
import click
import pyrobird.server
+from pyrobird.server import CFG_DOWNLOAD_IS_UNRESTRICTED, CFG_DOWNLOAD_IS_DISABLED, CFG_DOWNLOAD_PATH, \
+ CFG_CORS_IS_ALLOWED, CFG_API_BASE_URL, CFG_FIREBIRD_CONFIG_PATH
# Configure logging
logger = logging.getLogger(__name__)
@@ -26,8 +28,10 @@
@click.option("--work-path", "work_path", show_default=True, default="", help="Set the base directory path for file downloads. Defaults to the current working directory.")
@click.option("--host", "host", default="", help="Set the host for development server to listen to")
@click.option("--port", "port", default="", help="Set the port for development server to listen to")
+@click.option("--api-url", "api_url", default="", help="Force to use this address as backend API base URL. E.g. https://my-server:1234/")
+@click.option("--config", "config_path", default="", help="Path to firebird config.jsonc if used a custom")
@click.pass_context
-def serve(ctx, unsecure_files, allow_cors, disable_download, work_path, host, port):
+def serve(ctx, unsecure_files, allow_cors, disable_download, work_path, host, port, api_url, config_path):
"""
Start the server that serves Firebird frontend and can communicate with it.
@@ -61,10 +65,12 @@ def serve(ctx, unsecure_files, allow_cors, disable_download, work_path, host, po
port = 5454
pyrobird.server.run(debug=True, host=host, port=port, config={
- "DOWNLOAD_IS_UNRESTRICTED": unsecure_files,
- "DOWNLOAD_DISABLE": disable_download,
- "DOWNLOAD_PATH": work_path,
- "DOWNLOAD_ALLOW_CORS": allow_cors})
+ CFG_DOWNLOAD_IS_UNRESTRICTED: unsecure_files,
+ CFG_DOWNLOAD_IS_DISABLED: disable_download,
+ CFG_DOWNLOAD_PATH: work_path,
+ CFG_CORS_IS_ALLOWED: allow_cors,
+ CFG_API_BASE_URL: api_url,
+ CFG_FIREBIRD_CONFIG_PATH: config_path})
if __name__ == '__main__':
diff --git a/pyrobird/src/pyrobird/server/__init__.py b/pyrobird/src/pyrobird/server/__init__.py
index d156067..078489d 100644
--- a/pyrobird/src/pyrobird/server/__init__.py
+++ b/pyrobird/src/pyrobird/server/__init__.py
@@ -30,11 +30,21 @@
flask_app.config.update()
# Compression config
-# We want to use compression only transfering JSON for now...
+# We want to use compression only transferring JSON for now...
flask_app.config["COMPRESS_REGISTER"] = False # disable default compression of all requests
compress = Compress()
compress.init_app(flask_app)
+# Config KEYS
+
+CFG_DOWNLOAD_IS_UNRESTRICTED = "DOWNLOAD_IS_UNRESTRICTED"
+CFG_DOWNLOAD_IS_DISABLED = "DOWNLOAD_IS_DISABLED"
+CFG_DOWNLOAD_PATH = "DOWNLOAD_PATH"
+CFG_CORS_IS_ALLOWED = "DOWNLOAD_ALLOW_CORS"
+CFG_API_BASE_URL = "API_BASE_URL"
+CFG_FIREBIRD_CONFIG_PATH = "FIREBIRD_CONFIG_PATH"
+
+
class ExcludeAPIConverter(BaseConverter):
"""
@@ -80,7 +90,7 @@ def _can_user_download_file(filename):
- bool: True if the file can be downloaded, False otherwise.
Process:
- - If downloading is globally disabled (DOWNLOAD_DISABLE=True), returns False.
+ - If downloading is globally disabled (DOWNLOAD_IS_DISABLED=True), returns False.
- If unrestricted downloads are allowed (DOWNLOAD_IS_UNRESTRICTED=True), returns True.
- For relative paths, assumes that the download is allowable.
- For absolute paths, checks that the file resides within the configured DOWNLOAD_PATH.
@@ -90,12 +100,12 @@ def _can_user_download_file(filename):
app = flask.current_app
# If any downloads are disabled
- if app.config.get("DOWNLOAD_DISABLE") is True:
- logger.warning("Can't download file. DOWNLOAD_DISABLE=True")
+ if app.config.get(CFG_DOWNLOAD_IS_DISABLED) is True:
+ logger.warning("Can't download file. DOWNLOAD_IS_DISABLED=True")
return False
# If we allow any download
- unrestricted_download = app.config.get("DOWNLOAD_IS_UNRESTRICTED") is True
+ unrestricted_download = app.config.get(CFG_DOWNLOAD_IS_UNRESTRICTED) is True
if unrestricted_download:
return True
@@ -106,7 +116,7 @@ def _can_user_download_file(filename):
# HERE we have and absolute path! Check if it is safe to download
- allowed_path = app.config.get("DOWNLOAD_PATH")
+ allowed_path = app.config.get(CFG_DOWNLOAD_PATH)
if not allowed_path:
allowed_path = os.getcwd()
@@ -138,7 +148,7 @@ def download_file(filename=None):
# If it is relative, combine it with DOWNLOAD_PATH
if not os.path.isabs(filename):
- download_path = flask.current_app.config.get("DOWNLOAD_PATH")
+ download_path = flask.current_app.config.get(CFG_DOWNLOAD_PATH)
if not download_path:
download_path = os.getcwd()
@@ -202,7 +212,7 @@ def open_edm4eic_file(filename=None, file_type="edm4eic", entries="0"):
if not is_remote:
# If it is relative, combine it with DOWNLOAD_PATH
if not os.path.isabs(filename):
- download_path = flask.current_app.config.get("DOWNLOAD_PATH")
+ download_path = flask.current_app.config.get(CFG_DOWNLOAD_PATH)
if not download_path:
download_path = os.getcwd()
@@ -272,7 +282,9 @@ def asset_config():
It reads out existing file adding server info
"""
- config_path = 'assets/config.jsonc'
+ config_path = flask_app.config.get(CFG_FIREBIRD_CONFIG_PATH)
+ if not config_path:
+ config_path = 'assets/config.jsonc'
config_dict = {}
os_config_path = os.path.join(flask_app.static_folder, 'assets', 'config.jsonc')
@@ -286,7 +298,7 @@ def asset_config():
except Exception as ex:
logger.error(f"error opening {config_path}: {ex}")
- host = 'unknown'
+ host = 'localhost'
port = 80
tokens = request.host.split(':')
@@ -309,6 +321,8 @@ def asset_config():
config_dict['serverHost'] = host
config_dict['servedByPyrobird'] = True
config_dict['apiAvailable'] = True
+ config_api_url = flask_app.config.get(CFG_API_BASE_URL)
+ config_dict['apiBaseUrl'] = config_api_url if config_api_url else f"{request.scheme}://{host}:{port}"
# Convert the updated dictionary to JSON
return jsonify(config_dict)
@@ -344,18 +358,19 @@ def shutdown():
def run(config=None, host=None, port=5454, debug=False, load_dotenv=False):
"""Runs flask server"""
if config:
- if isinstance(config, Config) or isinstance(config, map):
+ if isinstance(config, flask.Config) or isinstance(config, map):
flask_app.config.from_mapping(config)
else:
flask_app.config.from_object(config)
- if flask_app.config and flask_app.config.get("DOWNLOAD_ALLOW_CORS") is True:
+ if flask_app.config and flask_app.config.get(CFG_CORS_IS_ALLOWED) is True:
from flask_cors import CORS
# Enable CORS for all routes and specify the domains and settings
CORS(flask_app, resources={
r"/download/*": {"origins": "*"},
r"/api/v1/*": {"origins": "*"},
+ r"/assets/config.jsonc": {"origins": "*"},
})
logger.debug("Serve path:")
diff --git a/pyrobird/tests/test_server.py b/pyrobird/tests/test_server.py
index 6d87fc0..71a7123 100644
--- a/pyrobird/tests/test_server.py
+++ b/pyrobird/tests/test_server.py
@@ -22,7 +22,7 @@ def client():
# Set the DOWNLOAD_PATH to the 'data' directory where test files are located
flask_app.config['DOWNLOAD_PATH'] = os.path.abspath(TEST_ROOT_DATA_DIR)
# Ensure downloads are allowed
- flask_app.config['DOWNLOAD_DISABLE'] = False
+ flask_app.config['DOWNLOAD_IS_DISABLED'] = False
flask_app.config['DOWNLOAD_IS_UNRESTRICTED'] = False
return flask_app.test_client()
@@ -58,7 +58,7 @@ def test_open_dangerous(client):
filename = '/etc/passwd' # A file outside the allowed path
event_number = 0
flask_app.config['DOWNLOAD_IS_UNRESTRICTED'] = True
- flask_app.config['DOWNLOAD_DISABLE'] = False
+ flask_app.config['DOWNLOAD_IS_DISABLED'] = False
response = client.get(f'/api/v1/download?filename={filename}')
assert response.status_code == 200 # OK
@@ -81,9 +81,9 @@ def test_open_edm4eic_file_nonexistent_file(client):
assert response.status_code == 404 # Not Found
-def test_open_edm4eic_file_download_disabled(client):
+def test_open_edm4eic_file_DOWNLOAD_IS_DISABLEDd(client):
# Test accessing a file when downloads are disabled
- flask_app.config['DOWNLOAD_DISABLE'] = True
+ flask_app.config['DOWNLOAD_IS_DISABLED'] = True
filename = 'reco_2024-09_craterlake_2evt.edm4eic.root'
event_number = 0
@@ -92,7 +92,7 @@ def test_open_edm4eic_file_download_disabled(client):
assert response.status_code == 403 # Forbidden
# Re-enable downloads for other tests
- flask_app.config['DOWNLOAD_DISABLE'] = False
+ flask_app.config['DOWNLOAD_IS_DISABLED'] = False
def test_open_edm4eic_file_invalid_file(client):