backend: streamline storage initialization process and enhance API key handling
This commit is contained in:
parent
981c7e0d96
commit
8f7462c39a
@ -1,5 +1,4 @@
|
|||||||
USE_HTTPS=false
|
USE_HTTPS=false
|
||||||
MEET_LOG_LEVEL=debug
|
MEET_LOG_LEVEL=debug
|
||||||
SERVER_CORS_ORIGIN=*
|
SERVER_CORS_ORIGIN=*
|
||||||
MEET_INITIAL_API_KEY=meet-api-key
|
MEET_INITIAL_API_KEY=meet-api-key
|
||||||
MEET_INITIAL_WEBHOOK_ENABLED=false
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
USE_HTTPS=false
|
USE_HTTPS=false
|
||||||
MEET_LOG_LEVEL=verbose
|
MEET_LOG_LEVEL=verbose
|
||||||
SERVER_CORS_ORIGIN=*
|
SERVER_CORS_ORIGIN=*
|
||||||
MEET_INITIAL_API_KEY=meet-api-key
|
MEET_INITIAL_API_KEY=meet-api-key
|
||||||
MEET_INITIAL_WEBHOOK_ENABLED=false
|
|
||||||
@ -97,6 +97,6 @@ export const initializeEagerServices = async () => {
|
|||||||
const storageService = container.get(MeetStorageService);
|
const storageService = container.get(MeetStorageService);
|
||||||
await storageService.checkStartupHealth();
|
await storageService.checkStartupHealth();
|
||||||
|
|
||||||
// Initialize global preferences after health checks pass
|
// Initialize storage after health checks pass
|
||||||
await storageService.initializeGlobalPreferences();
|
await storageService.initializeStorage();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const {
|
|||||||
* - To change them after the initial start, use the OpenVidu Meet API instead of modifying these environment variables.
|
* - To change them after the initial start, use the OpenVidu Meet API instead of modifying these environment variables.
|
||||||
*/
|
*/
|
||||||
MEET_INITIAL_WEBHOOK_ENABLED = 'false',
|
MEET_INITIAL_WEBHOOK_ENABLED = 'false',
|
||||||
MEET_INITIAL_WEBHOOK_URL = 'http://localhost:5080/webhook',
|
MEET_INITIAL_WEBHOOK_URL = '',
|
||||||
|
|
||||||
// LiveKit configuration
|
// LiveKit configuration
|
||||||
LIVEKIT_URL = 'ws://localhost:7880',
|
LIVEKIT_URL = 'ws://localhost:7880',
|
||||||
|
|||||||
@ -14,7 +14,8 @@ export class PasswordHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a secure API key using uid with a length of 32 characters
|
// Generate a secure API key using uid with a length of 32 characters
|
||||||
static generateApiKey(): MeetApiKey {
|
// If a key is provided, it will be used instead of generating a new one
|
||||||
return { key: `ovmeet-${uid(32)}`, creationDate: new Date().getTime() };
|
static generateApiKey(key?: string): MeetApiKey {
|
||||||
|
return { key: key || `ovmeet-${uid(32)}`, creationDate: new Date().getTime() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,8 +34,8 @@ export class MeetLock {
|
|||||||
return `${RedisLockPrefix.BASE}${RedisLockName.ROOM_GARBAGE_COLLECTOR}`;
|
return `${RedisLockPrefix.BASE}${RedisLockName.ROOM_GARBAGE_COLLECTOR}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getGlobalPreferencesLock(): string {
|
static getStorageInitializationLock(): string {
|
||||||
return `${RedisLockPrefix.BASE}${RedisLockName.GLOBAL_PREFERENCES}`;
|
return `${RedisLockPrefix.BASE}${RedisLockName.STORAGE_INITIALIZATION}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getWebhookLock(webhookEvent: WebhookEvent) {
|
static getWebhookLock(webhookEvent: WebhookEvent) {
|
||||||
|
|||||||
@ -26,6 +26,6 @@ export const enum RedisLockName {
|
|||||||
ROOM_GARBAGE_COLLECTOR = 'room_garbage_collector',
|
ROOM_GARBAGE_COLLECTOR = 'room_garbage_collector',
|
||||||
RECORDING_ACTIVE = 'recording_active',
|
RECORDING_ACTIVE = 'recording_active',
|
||||||
SCHEDULED_TASK = 'scheduled_task',
|
SCHEDULED_TASK = 'scheduled_task',
|
||||||
GLOBAL_PREFERENCES = 'global_preferences',
|
STORAGE_INITIALIZATION = 'storage_initialization',
|
||||||
WEBHOOK = 'webhook'
|
WEBHOOK = 'webhook'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { User } from '@typings-ce';
|
import { MeetApiKey, User } from '@typings-ce';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { MEET_INITIAL_API_KEY } from '../environment.js';
|
|
||||||
import { PasswordHelper } from '../helpers/index.js';
|
import { PasswordHelper } from '../helpers/index.js';
|
||||||
import { errorApiKeyNotConfigured } from '../models/error.model.js';
|
import { errorApiKeyNotConfigured } from '../models/error.model.js';
|
||||||
import { MeetStorageService, UserService } from './index.js';
|
import { MeetStorageService, UserService } from './index.js';
|
||||||
@ -22,24 +21,23 @@ export class AuthService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApiKey() {
|
async createApiKey(): Promise<MeetApiKey> {
|
||||||
const apiKey = PasswordHelper.generateApiKey();
|
const apiKey = PasswordHelper.generateApiKey();
|
||||||
await this.storageService.saveApiKey(apiKey);
|
await this.storageService.saveApiKey(apiKey);
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApiKeys() {
|
async getApiKeys(): Promise<MeetApiKey[]> {
|
||||||
const apiKeys = await this.storageService.getApiKeys();
|
const apiKeys = await this.storageService.getApiKeys();
|
||||||
return apiKeys;
|
return apiKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteApiKeys() {
|
async deleteApiKeys() {
|
||||||
await this.storageService.deleteApiKeys();
|
await this.storageService.deleteApiKeys();
|
||||||
return { message: 'API keys deleted successfully' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateApiKey(apiKey: string): Promise<boolean> {
|
async validateApiKey(apiKey: string): Promise<boolean> {
|
||||||
let storedApiKeys: { key: string; creationDate: number }[];
|
let storedApiKeys: MeetApiKey[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
storedApiKeys = await this.getApiKeys();
|
storedApiKeys = await this.getApiKeys();
|
||||||
@ -48,11 +46,11 @@ export class AuthService {
|
|||||||
storedApiKeys = [];
|
storedApiKeys = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storedApiKeys.length === 0 && !MEET_INITIAL_API_KEY) {
|
if (storedApiKeys.length === 0) {
|
||||||
throw errorApiKeyNotConfigured();
|
throw errorApiKeyNotConfigured();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the provided API key matches any stored API key or the MEET_API_KEY
|
// Check if the provided API key matches any stored API key
|
||||||
return storedApiKeys.some((key) => key.key === apiKey) || apiKey === MEET_INITIAL_API_KEY;
|
return storedApiKeys.some((key) => key.key === apiKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
MeetApiKey,
|
||||||
MeetRecordingInfo,
|
MeetRecordingInfo,
|
||||||
MeetRoom,
|
MeetRoom,
|
||||||
MeetWebhookEvent,
|
MeetWebhookEvent,
|
||||||
@ -8,9 +9,8 @@ import {
|
|||||||
} from '@typings-ce';
|
} from '@typings-ce';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { MEET_INITIAL_API_KEY } from '../environment.js';
|
|
||||||
import { AuthService, LoggerService, MeetStorageService } from './index.js';
|
|
||||||
import { errorWebhookUrlUnreachable } from '../models/error.model.js';
|
import { errorWebhookUrlUnreachable } from '../models/error.model.js';
|
||||||
|
import { AuthService, LoggerService, MeetStorageService } from './index.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenViduWebhookService {
|
export class OpenViduWebhookService {
|
||||||
@ -218,14 +218,16 @@ export class OpenViduWebhookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getApiKey(): Promise<string> {
|
protected async getApiKey(): Promise<string> {
|
||||||
const apiKeys = await this.authService.getApiKeys();
|
let apiKeys: MeetApiKey[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
apiKeys = await this.authService.getApiKeys();
|
||||||
|
} catch (error) {
|
||||||
|
// If there is an error retrieving API keys, we assume they are not configured
|
||||||
|
apiKeys = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (apiKeys.length === 0) {
|
if (apiKeys.length === 0) {
|
||||||
// If no API keys are configured, check if the MEET_API_KEY environment variable is set
|
|
||||||
if (MEET_INITIAL_API_KEY) {
|
|
||||||
return MEET_INITIAL_API_KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('There are no API keys configured yet. Please, create one to use webhooks.');
|
throw new Error('There are no API keys configured yet. Please, create one to use webhooks.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,10 @@ export class MeetStorageService<
|
|||||||
this.keyBuilder = keyBuilder;
|
this.keyBuilder = keyBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// INITIALIZATION LOGIC
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a health check on the storage system.
|
* Performs a health check on the storage system.
|
||||||
* Verifies both service connectivity and container/bucket existence.
|
* Verifies both service connectivity and container/bucket existence.
|
||||||
@ -97,61 +101,46 @@ export class MeetStorageService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// GLOBAL PREFERENCES DOMAIN LOGIC
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes default preferences if not already initialized.
|
* Initializes the storage with default data and initial environment variables if not already initialized.
|
||||||
* @returns {Promise<GPrefs>} Default global preferences.
|
* This includes global preferences, admin user and API key.
|
||||||
*/
|
*/
|
||||||
async initializeGlobalPreferences(): Promise<void> {
|
async initializeStorage(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Acquire a global lock to prevent multiple initializations at the same time when running in HA mode
|
// Acquire a global lock to prevent multiple initializations at the same time when running in HA mode
|
||||||
const lock = await this.mutexService.acquire(MeetLock.getGlobalPreferencesLock(), ms('30s'));
|
const lock = await this.mutexService.acquire(MeetLock.getStorageInitializationLock(), ms('30s'));
|
||||||
|
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'Unable to acquire lock for global preferences initialization. May be already initialized by another instance.'
|
'Unable to acquire lock for storage initialization. May be already initialized by another instance.'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose('Initializing global preferences with default values');
|
const isInitialized = await this.checkStorageInitialization();
|
||||||
const redisKey = RedisKeyName.GLOBAL_PREFERENCES;
|
|
||||||
const storageKey = this.keyBuilder.buildGlobalPreferencesKey();
|
|
||||||
|
|
||||||
const preferences = this.getDefaultPreferences();
|
if (isInitialized) {
|
||||||
this.logger.verbose('Initializing global preferences with default values');
|
this.logger.verbose('Storage already initialized for this project, skipping initialization');
|
||||||
const existing = await this.getFromCacheAndStorage<GPrefs>(redisKey, storageKey);
|
return;
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
await this.saveCacheAndStorage(redisKey, storageKey, preferences);
|
|
||||||
this.logger.info('Global preferences initialized with default values');
|
|
||||||
} else {
|
|
||||||
// Check if it's from a different project
|
|
||||||
const existingProjectId = (existing as GlobalPreferences)?.projectId;
|
|
||||||
const newProjectId = (preferences as GlobalPreferences)?.projectId;
|
|
||||||
|
|
||||||
if (existingProjectId !== newProjectId) {
|
|
||||||
this.logger.info('Different project detected, overwriting global preferences');
|
|
||||||
await this.saveCacheAndStorage(redisKey, storageKey, preferences);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the default admin user
|
this.logger.info('Storage not initialized or different project detected, proceeding with initialization');
|
||||||
const admin = {
|
|
||||||
username: MEET_INITIAL_ADMIN_USER,
|
await this.initializeGlobalPreferences();
|
||||||
passwordHash: await PasswordHelper.hashPassword(MEET_INITIAL_ADMIN_PASSWORD),
|
await this.initializeAdminUser();
|
||||||
roles: [UserRole.ADMIN, UserRole.USER]
|
await this.initializeApiKey();
|
||||||
} as MUser;
|
|
||||||
await this.saveUser(admin);
|
this.logger.info('Storage initialization completed successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, 'Error initializing default preferences');
|
this.handleError(error, 'Error initializing storage with default data');
|
||||||
throw internalError('Failed to initialize global preferences');
|
throw internalError('Failed to initialize storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// GLOBAL PREFERENCES DOMAIN LOGIC
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
async getGlobalPreferences(): Promise<GPrefs> {
|
async getGlobalPreferences(): Promise<GPrefs> {
|
||||||
const redisKey = RedisKeyName.GLOBAL_PREFERENCES;
|
const redisKey = RedisKeyName.GLOBAL_PREFERENCES;
|
||||||
const storageKey = this.keyBuilder.buildGlobalPreferencesKey();
|
const storageKey = this.keyBuilder.buildGlobalPreferencesKey();
|
||||||
@ -162,7 +151,6 @@ export class MeetStorageService<
|
|||||||
|
|
||||||
// Build and save default preferences if not found in cache or storage
|
// Build and save default preferences if not found in cache or storage
|
||||||
await this.initializeGlobalPreferences();
|
await this.initializeGlobalPreferences();
|
||||||
|
|
||||||
return this.getDefaultPreferences();
|
return this.getDefaultPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,8 +641,158 @@ export class MeetStorageService<
|
|||||||
// PRIVATE HELPER METHODS
|
// PRIVATE HELPER METHODS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if storage is already initialized by verifying that global preferences exist
|
||||||
|
* and belong to the current project.
|
||||||
|
* @returns {Promise<boolean>} True if storage is already initialized for this project
|
||||||
|
*/
|
||||||
|
protected async checkStorageInitialization(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const redisKey = RedisKeyName.GLOBAL_PREFERENCES;
|
||||||
|
const storageKey = this.keyBuilder.buildGlobalPreferencesKey();
|
||||||
|
|
||||||
|
const existing = await this.getFromCacheAndStorage<GPrefs>(redisKey, storageKey);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
this.logger.verbose('No global preferences found, storage needs initialization');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's from the same project
|
||||||
|
const existingProjectId = (existing as GlobalPreferences)?.projectId;
|
||||||
|
const currentProjectId = MEET_NAME_ID;
|
||||||
|
|
||||||
|
if (existingProjectId !== currentProjectId) {
|
||||||
|
this.logger.info(
|
||||||
|
`Different project detected: existing='${existingProjectId}', current='${currentProjectId}'. Re-initialization required.`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.verbose(`Storage already initialized for project '${currentProjectId}'`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Error checking storage initialization status:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes default global preferences if not already present.
|
||||||
|
*/
|
||||||
|
protected async initializeGlobalPreferences(): Promise<void> {
|
||||||
|
const preferences = this.getDefaultPreferences();
|
||||||
|
await this.saveGlobalPreferences(preferences);
|
||||||
|
this.logger.info('Global preferences initialized with default values');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default global preferences.
|
||||||
|
* @returns {GPrefs}
|
||||||
|
*/
|
||||||
|
protected getDefaultPreferences(): GPrefs {
|
||||||
|
return {
|
||||||
|
projectId: MEET_NAME_ID,
|
||||||
|
webhooksPreferences: {
|
||||||
|
enabled: MEET_INITIAL_WEBHOOK_ENABLED === 'true',
|
||||||
|
url: MEET_INITIAL_WEBHOOK_URL
|
||||||
|
},
|
||||||
|
securityPreferences: {
|
||||||
|
authentication: {
|
||||||
|
authMethod: {
|
||||||
|
type: AuthType.SINGLE_USER
|
||||||
|
},
|
||||||
|
authModeToAccessRoom: AuthMode.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as GPrefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the default admin user
|
||||||
|
*/
|
||||||
|
protected async initializeAdminUser(): Promise<void> {
|
||||||
|
const admin = {
|
||||||
|
username: MEET_INITIAL_ADMIN_USER,
|
||||||
|
passwordHash: await PasswordHelper.hashPassword(MEET_INITIAL_ADMIN_PASSWORD),
|
||||||
|
roles: [UserRole.ADMIN, UserRole.USER]
|
||||||
|
} as MUser;
|
||||||
|
|
||||||
|
await this.saveUser(admin);
|
||||||
|
this.logger.info(`Admin user initialized with default credentials`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the API key if configured
|
||||||
|
*/
|
||||||
|
protected async initializeApiKey(): Promise<void> {
|
||||||
|
// Check if initial API key is configured
|
||||||
|
const initialApiKey = process.env.MEET_INITIAL_API_KEY;
|
||||||
|
|
||||||
|
if (!initialApiKey) {
|
||||||
|
this.logger.verbose('No initial API key configured, skipping API key initialization');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyData: MeetApiKey = PasswordHelper.generateApiKey(initialApiKey);
|
||||||
|
await this.saveApiKey(apiKeyData);
|
||||||
|
this.logger.info('API key initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getRecordingFileSize(key: string, recordingId: string): Promise<number> {
|
||||||
|
const { contentLength: fileSize } = await this.storageProvider.getObjectHeaders(key);
|
||||||
|
|
||||||
|
if (!fileSize) {
|
||||||
|
this.logger.warn(`Recording media not found for recording ${recordingId}`);
|
||||||
|
throw errorRecordingNotFound(recordingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validateAndAdjustRange(
|
||||||
|
range: { end: number; start: number } | undefined,
|
||||||
|
fileSize: number,
|
||||||
|
recordingId: string
|
||||||
|
): { start: number; end: number } | undefined {
|
||||||
|
if (!range) return undefined;
|
||||||
|
|
||||||
|
const { start, end: originalEnd } = range;
|
||||||
|
|
||||||
|
// Validate input values
|
||||||
|
if (isNaN(start) || isNaN(originalEnd) || start < 0) {
|
||||||
|
this.logger.warn(`Invalid range values for recording ${recordingId}: start=${start}, end=${originalEnd}`);
|
||||||
|
this.logger.warn(`Returning full stream for recording ${recordingId}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if start is beyond file size
|
||||||
|
if (start >= fileSize) {
|
||||||
|
this.logger.error(
|
||||||
|
`Invalid range: start=${start} exceeds fileSize=${fileSize} for recording ${recordingId}`
|
||||||
|
);
|
||||||
|
throw errorRecordingRangeNotSatisfiable(recordingId, fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust end to not exceed file bounds
|
||||||
|
const adjustedEnd = Math.min(originalEnd, fileSize - 1);
|
||||||
|
|
||||||
|
// Validate final range
|
||||||
|
if (start > adjustedEnd) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Invalid range after adjustment: start=${start}, end=${adjustedEnd} for recording ${recordingId}`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Valid range for recording ${recordingId}: start=${start}, end=${adjustedEnd}, fileSize=${fileSize}`
|
||||||
|
);
|
||||||
|
return { start, end: adjustedEnd };
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// HYBRID CACHE METHODS (Redis + Storage)
|
// PRIVATE HYBRID CACHE METHODS (Redis + Storage)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -877,80 +1015,6 @@ export class MeetStorageService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the default global preferences.
|
|
||||||
* @returns {GPrefs}
|
|
||||||
*/
|
|
||||||
protected getDefaultPreferences(): GPrefs {
|
|
||||||
return {
|
|
||||||
projectId: MEET_NAME_ID,
|
|
||||||
webhooksPreferences: {
|
|
||||||
enabled: MEET_INITIAL_WEBHOOK_ENABLED === 'true',
|
|
||||||
url: MEET_INITIAL_WEBHOOK_URL
|
|
||||||
},
|
|
||||||
securityPreferences: {
|
|
||||||
authentication: {
|
|
||||||
authMethod: {
|
|
||||||
type: AuthType.SINGLE_USER
|
|
||||||
},
|
|
||||||
authModeToAccessRoom: AuthMode.NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as GPrefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getRecordingFileSize(key: string, recordingId: string): Promise<number> {
|
|
||||||
const { contentLength: fileSize } = await this.storageProvider.getObjectHeaders(key);
|
|
||||||
|
|
||||||
if (!fileSize) {
|
|
||||||
this.logger.warn(`Recording media not found for recording ${recordingId}`);
|
|
||||||
throw errorRecordingNotFound(recordingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected validateAndAdjustRange(
|
|
||||||
range: { end: number; start: number } | undefined,
|
|
||||||
fileSize: number,
|
|
||||||
recordingId: string
|
|
||||||
): { start: number; end: number } | undefined {
|
|
||||||
if (!range) return undefined;
|
|
||||||
|
|
||||||
const { start, end: originalEnd } = range;
|
|
||||||
|
|
||||||
// Validate input values
|
|
||||||
if (isNaN(start) || isNaN(originalEnd) || start < 0) {
|
|
||||||
this.logger.warn(`Invalid range values for recording ${recordingId}: start=${start}, end=${originalEnd}`);
|
|
||||||
this.logger.warn(`Returning full stream for recording ${recordingId}`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if start is beyond file size
|
|
||||||
if (start >= fileSize) {
|
|
||||||
this.logger.error(
|
|
||||||
`Invalid range: start=${start} exceeds fileSize=${fileSize} for recording ${recordingId}`
|
|
||||||
);
|
|
||||||
throw errorRecordingRangeNotSatisfiable(recordingId, fileSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust end to not exceed file bounds
|
|
||||||
const adjustedEnd = Math.min(originalEnd, fileSize - 1);
|
|
||||||
|
|
||||||
// Validate final range
|
|
||||||
if (start > adjustedEnd) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Invalid range after adjustment: start=${start}, end=${adjustedEnd} for recording ${recordingId}`
|
|
||||||
);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Valid range for recording ${recordingId}: start=${start}, end=${adjustedEnd}, fileSize=${fileSize}`
|
|
||||||
);
|
|
||||||
return { start, end: adjustedEnd };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected handleError(error: unknown, context: string): void {
|
protected handleError(error: unknown, context: string): void {
|
||||||
if (error instanceof OpenViduMeetError) {
|
if (error instanceof OpenViduMeetError) {
|
||||||
this.logger.error(`${context}: ${error.message}`);
|
this.logger.error(`${context}: ${error.message}`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user