backend: Rename S3 storage references and update method names for consistency
This commit is contained in:
parent
2c65ec1da8
commit
92ef26f58c
@ -12,7 +12,7 @@ import {
|
|||||||
RecordingService,
|
RecordingService,
|
||||||
RedisService,
|
RedisService,
|
||||||
RoomService,
|
RoomService,
|
||||||
S3Storage,
|
S3StorageProvider,
|
||||||
S3Service,
|
S3Service,
|
||||||
SystemEventService,
|
SystemEventService,
|
||||||
TaskSchedulerService,
|
TaskSchedulerService,
|
||||||
@ -50,7 +50,7 @@ export const registerDependencies = () => {
|
|||||||
container.bind(MeetStorageService).toSelf().inSingletonScope();
|
container.bind(MeetStorageService).toSelf().inSingletonScope();
|
||||||
container.bind(ParticipantService).toSelf().inSingletonScope();
|
container.bind(ParticipantService).toSelf().inSingletonScope();
|
||||||
|
|
||||||
container.bind(S3Storage).toSelf().inSingletonScope();
|
container.bind(S3StorageProvider).toSelf().inSingletonScope();
|
||||||
container.bind(StorageFactory).toSelf().inSingletonScope();
|
container.bind(StorageFactory).toSelf().inSingletonScope();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,6 @@ export * from './mutex.service.js';
|
|||||||
export * from './storage/index.js';
|
export * from './storage/index.js';
|
||||||
export * from './redis.service.js';
|
export * from './redis.service.js';
|
||||||
export * from './s3.service.js';
|
export * from './s3.service.js';
|
||||||
export * from './storage/providers/s3-storage.js';
|
export * from './storage/providers/s3-storage.provider.js';
|
||||||
export * from './token.service.js';
|
export * from './token.service.js';
|
||||||
export * from './user.service.js';
|
export * from './user.service.js';
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { inject, injectable } from '../config/dependency-injector.config.js';
|
|||||||
import { CreateOptions, Room, SendDataOptions } from 'livekit-server-sdk';
|
import { CreateOptions, Room, SendDataOptions } from 'livekit-server-sdk';
|
||||||
import { LoggerService } from './logger.service.js';
|
import { LoggerService } from './logger.service.js';
|
||||||
import { LiveKitService } from './livekit.service.js';
|
import { LiveKitService } from './livekit.service.js';
|
||||||
import { GlobalPreferencesService } from './preferences/global-preferences.service.js';
|
import { MeetStorageService } from './storage/storage.service.js';
|
||||||
import { MeetRoom, MeetRoomOptions, ParticipantRole } from '@typings-ce';
|
import { MeetRoom, MeetRoomFilters, MeetRoomOptions, MeetRoomPreferences, ParticipantRole } from '@typings-ce';
|
||||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||||
import { SystemEventService } from './system-event.service.js';
|
import { SystemEventService } from './system-event.service.js';
|
||||||
import { TaskSchedulerService } from './task-scheduler.service.js';
|
import { TaskSchedulerService } from './task-scheduler.service.js';
|
||||||
@ -13,6 +13,7 @@ import { OpenViduComponentsAdapterHelper } from '../helpers/index.js';
|
|||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
import { MEET_NAME_ID } from '../environment.js';
|
import { MEET_NAME_ID } from '../environment.js';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
import { UtilsHelper } from '../helpers/utils.helper.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing OpenVidu Meet rooms.
|
* Service for managing OpenVidu Meet rooms.
|
||||||
@ -24,7 +25,7 @@ import ms from 'ms';
|
|||||||
export class RoomService {
|
export class RoomService {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(LoggerService) protected logger: LoggerService,
|
@inject(LoggerService) protected logger: LoggerService,
|
||||||
@inject(GlobalPreferencesService) protected globalPrefService: GlobalPreferencesService,
|
@inject(MeetStorageService) protected storageService: MeetStorageService,
|
||||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||||
@inject(SystemEventService) protected systemEventService: SystemEventService,
|
@inject(SystemEventService) protected systemEventService: SystemEventService,
|
||||||
@inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService
|
@inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService
|
||||||
@ -66,7 +67,7 @@ export class RoomService {
|
|||||||
const { preferences, expirationDate, roomIdPrefix } = roomOptions;
|
const { preferences, expirationDate, roomIdPrefix } = roomOptions;
|
||||||
const roomId = roomIdPrefix ? `${roomIdPrefix}-${uid(15)}` : uid(15);
|
const roomId = roomIdPrefix ? `${roomIdPrefix}-${uid(15)}` : uid(15);
|
||||||
|
|
||||||
const openviduRoom: MeetRoom = {
|
const meetRoom: MeetRoom = {
|
||||||
roomId,
|
roomId,
|
||||||
roomIdPrefix,
|
roomIdPrefix,
|
||||||
creationDate: Date.now(),
|
creationDate: Date.now(),
|
||||||
@ -77,9 +78,9 @@ export class RoomService {
|
|||||||
publisherRoomUrl: `${baseUrl}/room/${roomId}?secret=${secureUid(10)}`
|
publisherRoomUrl: `${baseUrl}/room/${roomId}?secret=${secureUid(10)}`
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.globalPrefService.saveOpenViduRoom(openviduRoom);
|
await this.storageService.saveMeetRoom(meetRoom);
|
||||||
|
|
||||||
return openviduRoom;
|
return meetRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,13 +114,41 @@ export class RoomService {
|
|||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the preferences of a specific meeting room.
|
||||||
|
*
|
||||||
|
* @param roomId - The unique identifier of the meeting room to update
|
||||||
|
* @param preferences - The new preferences to apply to the meeting room
|
||||||
|
* @returns A Promise that resolves to the updated MeetRoom object
|
||||||
|
*/
|
||||||
|
async updateMeetRoomPreferences(roomId: string, preferences: MeetRoomPreferences): Promise<MeetRoom> {
|
||||||
|
const room = await this.getMeetRoom(roomId);
|
||||||
|
room.preferences = preferences;
|
||||||
|
|
||||||
|
return await this.storageService.saveMeetRoom(room);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a list of rooms.
|
* Retrieves a list of rooms.
|
||||||
* @returns A Promise that resolves to an array of {@link MeetRoom} objects.
|
* @returns A Promise that resolves to an array of {@link MeetRoom} objects.
|
||||||
* @throws If there was an error retrieving the rooms.
|
* @throws If there was an error retrieving the rooms.
|
||||||
*/
|
*/
|
||||||
async listOpenViduRooms(): Promise<MeetRoom[]> {
|
async getAllMeetRooms({ maxItems, nextPageToken, fields }: MeetRoomFilters): Promise<{
|
||||||
return await this.globalPrefService.getOpenViduRooms();
|
rooms: MeetRoom[];
|
||||||
|
isTruncated: boolean;
|
||||||
|
nextPageToken?: string;
|
||||||
|
}> {
|
||||||
|
const response = await this.storageService.getMeetRooms(maxItems, nextPageToken);
|
||||||
|
|
||||||
|
if (fields && fields.length > 0) {
|
||||||
|
const fieldsArray = Array.isArray(fields) ? fields : fields.split(',').map((f) => f.trim());
|
||||||
|
const filteredRooms = response.rooms.map((room) =>
|
||||||
|
UtilsHelper.filterObjectFields(room as unknown as Record<string, unknown>, fieldsArray)
|
||||||
|
);
|
||||||
|
response.rooms = filteredRooms as MeetRoom[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,8 +157,19 @@ export class RoomService {
|
|||||||
* @param roomId - The name of the room to retrieve.
|
* @param roomId - The name of the room to retrieve.
|
||||||
* @returns A promise that resolves to an {@link MeetRoom} object.
|
* @returns A promise that resolves to an {@link MeetRoom} object.
|
||||||
*/
|
*/
|
||||||
async getMeetRoom(roomId: string): Promise<MeetRoom> {
|
async getMeetRoom(roomId: string, fields?: string): Promise<MeetRoom> {
|
||||||
return await this.globalPrefService.getOpenViduRoom(roomId);
|
const meetRoom = await this.storageService.getMeetRoom(roomId);
|
||||||
|
|
||||||
|
if (fields && fields.length > 0) {
|
||||||
|
const fieldsArray = Array.isArray(fields) ? fields : fields.split(',').map((f) => f.trim());
|
||||||
|
const filteredRoom = UtilsHelper.filterObjectFields(
|
||||||
|
meetRoom as unknown as Record<string, unknown>,
|
||||||
|
fieldsArray
|
||||||
|
);
|
||||||
|
return filteredRoom as MeetRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return meetRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,7 +180,7 @@ export class RoomService {
|
|||||||
* @param roomIds - An array of room names to be deleted.
|
* @param roomIds - An array of room names to be deleted.
|
||||||
* @returns A promise that resolves with an array of successfully deleted room names.
|
* @returns A promise that resolves with an array of successfully deleted room names.
|
||||||
*/
|
*/
|
||||||
async deleteRooms(roomIds: string[]): Promise<string[]> {
|
async bulkDeleteRooms(roomIds: string[]): Promise<string[]> {
|
||||||
const [openViduResults, livekitResults] = await Promise.all([
|
const [openViduResults, livekitResults] = await Promise.all([
|
||||||
this.deleteOpenViduRooms(roomIds),
|
this.deleteOpenViduRooms(roomIds),
|
||||||
Promise.allSettled(roomIds.map((roomId) => this.livekitService.deleteRoom(roomId)))
|
Promise.allSettled(roomIds.map((roomId) => this.livekitService.deleteRoom(roomId)))
|
||||||
@ -171,10 +211,8 @@ export class RoomService {
|
|||||||
* @param roomIds - List of room names to delete.
|
* @param roomIds - List of room names to delete.
|
||||||
* @returns A promise that resolves with an array of successfully deleted room names.
|
* @returns A promise that resolves with an array of successfully deleted room names.
|
||||||
*/
|
*/
|
||||||
async deleteOpenViduRooms(roomIds: string[]): Promise<string[]> {
|
protected async deleteOpenViduRooms(roomIds: string[]): Promise<string[]> {
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(roomIds.map((roomId) => this.storageService.deleteMeetRoom(roomId)));
|
||||||
roomIds.map((roomId) => this.globalPrefService.deleteOpenViduRoom(roomId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const successfulRooms: string[] = [];
|
const successfulRooms: string[] = [];
|
||||||
|
|
||||||
@ -307,23 +345,24 @@ export class RoomService {
|
|||||||
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
||||||
*/
|
*/
|
||||||
protected async deleteOpenViduExpiredRooms(): Promise<string[]> {
|
protected async deleteOpenViduExpiredRooms(): Promise<string[]> {
|
||||||
const now = Date.now();
|
// const now = Date.now();
|
||||||
this.logger.verbose(`Checking OpenVidu expired rooms at ${new Date(now).toISOString()}`);
|
// this.logger.verbose(`Checking OpenVidu expired rooms at ${new Date(now).toISOString()}`);
|
||||||
const rooms = await this.listOpenViduRooms();
|
// const rooms = await this.getAllMeetRooms();
|
||||||
const expiredRooms = rooms
|
// const expiredRooms = rooms
|
||||||
.filter((room) => room.expirationDate && room.expirationDate < now)
|
// .filter((room) => room.expirationDate && room.expirationDate < now)
|
||||||
.map((room) => room.roomId);
|
// .map((room) => room.roomId);
|
||||||
|
|
||||||
if (expiredRooms.length === 0) {
|
// if (expiredRooms.length === 0) {
|
||||||
this.logger.verbose('No OpenVidu expired rooms to delete.');
|
// this.logger.verbose('No OpenVidu expired rooms to delete.');
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.logger.info(`Deleting ${expiredRooms.length} OpenVidu expired rooms: ${expiredRooms.join(', ')}`);
|
||||||
|
|
||||||
|
// return await this.deleteOpenViduRooms(expiredRooms);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Deleting ${expiredRooms.length} OpenVidu expired rooms: ${expiredRooms.join(', ')}`);
|
|
||||||
|
|
||||||
return await this.deleteOpenViduRooms(expiredRooms);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores missing Livekit rooms by comparing the list of rooms from Livekit and OpenVidu.
|
* Restores missing Livekit rooms by comparing the list of rooms from Livekit and OpenVidu.
|
||||||
* If any rooms are missing in Livekit, they will be created.
|
* If any rooms are missing in Livekit, they will be created.
|
||||||
@ -333,52 +372,43 @@ export class RoomService {
|
|||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected async restoreMissingLivekitRooms(): Promise<void> {
|
protected async restoreMissingLivekitRooms(): Promise<void> {
|
||||||
this.logger.verbose(`Checking missing Livekit rooms ...`);
|
// this.logger.verbose(`Checking missing Livekit rooms ...`);
|
||||||
|
// const [lkResult, ovResult] = await Promise.allSettled([
|
||||||
const [lkResult, ovResult] = await Promise.allSettled([
|
// this.livekitService.listRooms(),
|
||||||
this.livekitService.listRooms(),
|
// this.getAllMeetRooms()
|
||||||
this.listOpenViduRooms()
|
// ]);
|
||||||
]);
|
// let lkRooms: Room[] = [];
|
||||||
|
// let ovRooms: MeetRoom[] = [];
|
||||||
let lkRooms: Room[] = [];
|
// if (lkResult.status === 'fulfilled') {
|
||||||
let ovRooms: MeetRoom[] = [];
|
// lkRooms = lkResult.value;
|
||||||
|
// } else {
|
||||||
if (lkResult.status === 'fulfilled') {
|
// this.logger.error('Failed to list Livekit rooms:', lkResult.reason);
|
||||||
lkRooms = lkResult.value;
|
// }
|
||||||
} else {
|
// if (ovResult.status === 'fulfilled') {
|
||||||
this.logger.error('Failed to list Livekit rooms:', lkResult.reason);
|
// ovRooms = ovResult.value;
|
||||||
}
|
// } else {
|
||||||
|
// this.logger.error('Failed to list OpenVidu rooms:', ovResult.reason);
|
||||||
if (ovResult.status === 'fulfilled') {
|
// }
|
||||||
ovRooms = ovResult.value;
|
// const missingRooms: MeetRoom[] = ovRooms.filter(
|
||||||
} else {
|
// (ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomId)
|
||||||
this.logger.error('Failed to list OpenVidu rooms:', ovResult.reason);
|
// );
|
||||||
}
|
// if (missingRooms.length === 0) {
|
||||||
|
// this.logger.verbose('All OpenVidu rooms are present in Livekit. No missing rooms to restore. ');
|
||||||
const missingRooms: MeetRoom[] = ovRooms.filter(
|
// return;
|
||||||
(ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomId)
|
// }
|
||||||
);
|
// this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
||||||
|
// const creationResults = await Promise.allSettled(
|
||||||
if (missingRooms.length === 0) {
|
// missingRooms.map(({ roomId }: MeetRoom) => {
|
||||||
this.logger.verbose('All OpenVidu rooms are present in Livekit. No missing rooms to restore. ');
|
// this.logger.debug(`Restoring room: ${roomId}`);
|
||||||
return;
|
// this.createLivekitRoom(roomId);
|
||||||
}
|
// })
|
||||||
|
// );
|
||||||
this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
// creationResults.forEach((result, index) => {
|
||||||
|
// if (result.status === 'rejected') {
|
||||||
const creationResults = await Promise.allSettled(
|
// this.logger.error(`Failed to restore room "${missingRooms[index].roomId}": ${result.reason}`);
|
||||||
missingRooms.map(({ roomId }: MeetRoom) => {
|
// } else {
|
||||||
this.logger.debug(`Restoring room: ${roomId}`);
|
// this.logger.info(`Restored room "${missingRooms[index].roomId}"`);
|
||||||
this.createLivekitRoom(roomId);
|
// }
|
||||||
})
|
// });
|
||||||
);
|
|
||||||
|
|
||||||
creationResults.forEach((result, index) => {
|
|
||||||
if (result.status === 'rejected') {
|
|
||||||
this.logger.error(`Failed to restore room "${missingRooms[index].roomId}": ${result.reason}`);
|
|
||||||
} else {
|
|
||||||
this.logger.info(`Restored room "${missingRooms[index].roomId}"`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export * from './storage.service.js';
|
export * from './storage.service.js';
|
||||||
export * from './storage.interface.js';
|
export * from './storage.interface.js';
|
||||||
export * from './storage.factory.js';
|
export * from './storage.factory.js';
|
||||||
export * from './providers/s3-storage.js';
|
export * from './providers/s3-storage.provider.js';
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import { LoggerService } from '../../logger.service.js';
|
|||||||
import { RedisService } from '../../redis.service.js';
|
import { RedisService } from '../../redis.service.js';
|
||||||
import { OpenViduMeetError } from '../../../models/error.model.js';
|
import { OpenViduMeetError } from '../../../models/error.model.js';
|
||||||
import { inject, injectable } from '../../../config/dependency-injector.config.js';
|
import { inject, injectable } from '../../../config/dependency-injector.config.js';
|
||||||
import { MEET_S3_ROOMS_PREFIX, MEET_S3_SUBBUCKET } from '../../../environment.js';
|
import { MEET_S3_ROOMS_PREFIX } from '../../../environment.js';
|
||||||
import { RedisKeyName } from '../../../models/redis.model.js';
|
import { RedisKeyName } from '../../../models/redis.model.js';
|
||||||
|
import { PutObjectCommandOutput } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the StorageProvider interface using AWS S3 for persistent storage
|
* Implementation of the StorageProvider interface using AWS S3 for persistent storage
|
||||||
@ -26,10 +27,10 @@ import { RedisKeyName } from '../../../models/redis.model.js';
|
|||||||
* @implements {StorageProvider}
|
* @implements {StorageProvider}
|
||||||
*/
|
*/
|
||||||
@injectable()
|
@injectable()
|
||||||
export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extends MeetRoom = MeetRoom>
|
export class S3StorageProvider<G extends GlobalPreferences = GlobalPreferences, R extends MeetRoom = MeetRoom>
|
||||||
implements StorageProvider
|
implements StorageProvider
|
||||||
{
|
{
|
||||||
protected readonly S3_GLOBAL_PREFERENCES_KEY = `${MEET_S3_SUBBUCKET}/global-preferences.json`;
|
protected readonly S3_GLOBAL_PREFERENCES_KEY = `global-preferences.json`;
|
||||||
constructor(
|
constructor(
|
||||||
@inject(LoggerService) protected logger: LoggerService,
|
@inject(LoggerService) protected logger: LoggerService,
|
||||||
@inject(S3Service) protected s3Service: S3Service,
|
@inject(S3Service) protected s3Service: S3Service,
|
||||||
@ -131,49 +132,47 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a room object to S3 and Redis concurrently.
|
||||||
|
* If at least one operation fails, performs a rollback by deleting the successfully saved object.
|
||||||
|
*
|
||||||
|
* @param ovRoom - The room object to save.
|
||||||
|
* @returns The saved room if both operations succeed.
|
||||||
|
* @throws The error from the first failed operation.
|
||||||
|
*/
|
||||||
async saveMeetRoom(ovRoom: R): Promise<R> {
|
async saveMeetRoom(ovRoom: R): Promise<R> {
|
||||||
const { roomId } = ovRoom;
|
const { roomId } = ovRoom;
|
||||||
const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
||||||
const roomStr = JSON.stringify(ovRoom);
|
const redisPayload = JSON.stringify(ovRoom);
|
||||||
|
const redisKey = RedisKeyName.ROOM + roomId;
|
||||||
|
|
||||||
const results = await Promise.allSettled([
|
const [s3Result, redisResult] = await Promise.allSettled([
|
||||||
this.s3Service.saveObject(s3Path, ovRoom),
|
this.s3Service.saveObject(s3Path, ovRoom),
|
||||||
// TODO: Use a key prefix for Redis
|
this.redisService.set(redisKey, redisPayload, false)
|
||||||
this.redisService.set(roomId, roomStr, false)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const s3Result = results[0];
|
|
||||||
const redisResult = results[1];
|
|
||||||
|
|
||||||
if (s3Result.status === 'fulfilled' && redisResult.status === 'fulfilled') {
|
if (s3Result.status === 'fulfilled' && redisResult.status === 'fulfilled') {
|
||||||
return ovRoom;
|
return ovRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollback changes if one of the operations failed
|
// Rollback any changes made by the successful operation
|
||||||
if (s3Result.status === 'fulfilled') {
|
await this.rollbackRoomSave(roomId, s3Result, redisResult, s3Path, redisKey);
|
||||||
try {
|
|
||||||
await this.s3Service.deleteObject(s3Path);
|
|
||||||
} catch (rollbackError) {
|
|
||||||
this.logger.error(`Error rolling back S3 save for room ${roomId}: ${rollbackError}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (redisResult.status === 'fulfilled') {
|
|
||||||
try {
|
|
||||||
await this.redisService.delete(roomId);
|
|
||||||
} catch (rollbackError) {
|
|
||||||
this.logger.error(`Error rolling back Redis set for room ${roomId}: ${rollbackError}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the error that occurred first
|
// Return the error that occurred first
|
||||||
const rejectedResult: PromiseRejectedResult =
|
const failedOperation: PromiseRejectedResult =
|
||||||
s3Result.status === 'rejected' ? s3Result : (redisResult as PromiseRejectedResult);
|
s3Result.status === 'rejected' ? s3Result : (redisResult as PromiseRejectedResult);
|
||||||
const error = rejectedResult.reason;
|
const error = failedOperation.reason;
|
||||||
this.handleError(error, `Error saving Room preferences for room ${roomId}`);
|
this.handleError(error, `Error saving Room preferences for room ${roomId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of Meet rooms from S3.
|
||||||
|
*
|
||||||
|
* @param maxItems - Maximum number of items to retrieve.
|
||||||
|
* @param nextPageToken - Continuation token for pagination.
|
||||||
|
* @returns An object containing the list of rooms, a flag indicating whether the list is truncated, and, if available, the next page token.
|
||||||
|
*/
|
||||||
async getMeetRooms(
|
async getMeetRooms(
|
||||||
maxItems: number,
|
maxItems: number,
|
||||||
nextPageToken?: string
|
nextPageToken?: string
|
||||||
@ -189,71 +188,48 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
|||||||
NextContinuationToken
|
NextContinuationToken
|
||||||
} = await this.s3Service.listObjectsPaginated(MEET_S3_ROOMS_PREFIX, maxItems, nextPageToken);
|
} = await this.s3Service.listObjectsPaginated(MEET_S3_ROOMS_PREFIX, maxItems, nextPageToken);
|
||||||
|
|
||||||
if (!roomFiles) {
|
if (!roomFiles || roomFiles.length === 0) {
|
||||||
this.logger.verbose('No rooms found. Returning an empty array.');
|
this.logger.verbose('No room files found in S3.');
|
||||||
return { rooms: [], isTruncated: false };
|
return { rooms: [], isTruncated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// const promises: Promise<R>[] = [];
|
// Extract room IDs directly and filter out invalid values
|
||||||
// // Retrieve the data for each room
|
const roomIds = roomFiles
|
||||||
// roomFiles.forEach((item) => {
|
.map((file) => this.extractRoomId(file.Key))
|
||||||
// if (item?.Key && item.Key.endsWith('.json')) {
|
.filter((id): id is string => Boolean(id));
|
||||||
// promises.push(getOpenViduRoom(item.Key) as Promise<R>);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Extract room names from file paths
|
// Fetch and log any room lookup errors individually
|
||||||
const roomIds = roomFiles.map((file) => this.extractRoomId(file.Key)).filter(Boolean) as string[];
|
|
||||||
// Fetch room preferences in parallel
|
// Fetch room preferences in parallel
|
||||||
const rooms = await Promise.all(
|
const rooms = await Promise.all(
|
||||||
roomIds.map(async (roomId: string) => {
|
roomIds.map(async (roomId) => {
|
||||||
if (!roomId) return null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.getMeetRoom(roomId);
|
return await this.getMeetRoom(roomId);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn(`Failed to fetch room "${roomId}": ${error.message}`);
|
this.logger.warn(`Failed to fetch room "${roomId}": ${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter out null values
|
// Filter out null values
|
||||||
const roomsResponse = rooms.filter(Boolean) as R[];
|
const validRooms = rooms.filter((room) => room !== null) as R[];
|
||||||
return { rooms: roomsResponse, isTruncated: !!IsTruncated, nextPageToken: NextContinuationToken };
|
return { rooms: validRooms, isTruncated: !!IsTruncated, nextPageToken: NextContinuationToken };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, 'Error fetching Room preferences');
|
this.handleError(error, 'Error fetching Room preferences');
|
||||||
return { rooms: [], isTruncated: false };
|
return { rooms: [], isTruncated: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the room id from the given file path.
|
|
||||||
* Assumes the room name is located one directory before the file name.
|
|
||||||
* Example: 'path/to/roomId/file.json' -> 'roomId'
|
|
||||||
* @param filePath - The S3 object key representing the file path.
|
|
||||||
* @returns The extracted room name or null if extraction fails.
|
|
||||||
*/
|
|
||||||
private extractRoomId(filePath?: string): string | null {
|
|
||||||
if (!filePath) return null;
|
|
||||||
|
|
||||||
const parts = filePath.split('/');
|
|
||||||
|
|
||||||
if (parts.length < 2) {
|
|
||||||
this.logger.warn(`Invalid room file path: ${filePath}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts[parts.length - 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMeetRoom(roomId: string): Promise<R | null> {
|
async getMeetRoom(roomId: string): Promise<R | null> {
|
||||||
try {
|
try {
|
||||||
|
// Try to get room preferences from Redis cache
|
||||||
const room: R | null = await this.getFromRedis<R>(roomId);
|
const room: R | null = await this.getFromRedis<R>(roomId);
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
this.logger.debug(`Room ${roomId} not found in Redis. Fetching from S3...`);
|
const s3RoomPath = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
||||||
return await this.getFromS3<R>(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`);
|
this.logger.debug(`Room ${roomId} not found in Redis. Fetching from S3 at ${s3RoomPath}...`);
|
||||||
|
|
||||||
|
return await this.getFromS3<R>(s3RoomPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Room ${roomId} verified in Redis`);
|
this.logger.debug(`Room ${roomId} verified in Redis`);
|
||||||
@ -265,11 +241,12 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteMeetRoom(roomId: string): Promise<void> {
|
async deleteMeetRoom(roomId: string): Promise<void> {
|
||||||
|
const s3RoomPath = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
||||||
|
const redisKey = RedisKeyName.ROOM + roomId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([this.s3Service.deleteObject(s3RoomPath), this.redisService.delete(redisKey)]);
|
||||||
this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`),
|
this.logger.verbose(`Room ${roomId} deleted successfully from S3 and Redis`);
|
||||||
this.redisService.delete(roomId)
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, `Error deleting Room preferences for room ${roomId}`);
|
this.handleError(error, `Error deleting Room preferences for room ${roomId}`);
|
||||||
}
|
}
|
||||||
@ -320,6 +297,61 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the room ID from the given S3 file path.
|
||||||
|
* Assumes the room ID is the directory name immediately preceding the file name.
|
||||||
|
* Example: 'path/to/roomId/file.json' -> 'roomId'
|
||||||
|
*
|
||||||
|
* @param filePath - The S3 object key representing the file path.
|
||||||
|
* @returns The extracted room ID or null if extraction fails.
|
||||||
|
*/
|
||||||
|
protected extractRoomId(filePath?: string): string | null {
|
||||||
|
if (!filePath) return null;
|
||||||
|
|
||||||
|
const parts = filePath.split('/');
|
||||||
|
const roomId = parts.slice(-2, -1)[0];
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
this.logger.warn(`Invalid room file path: ${filePath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs rollback of saved room data.
|
||||||
|
*
|
||||||
|
* @param roomId - The room identifier.
|
||||||
|
* @param s3Result - The result of the S3 save operation.
|
||||||
|
* @param redisResult - The result of the Redis set operation.
|
||||||
|
* @param s3Path - The S3 key used to save the room data.
|
||||||
|
* @param redisKey - The Redis key used to cache the room data.
|
||||||
|
*/
|
||||||
|
protected async rollbackRoomSave(
|
||||||
|
roomId: string,
|
||||||
|
s3Result: PromiseSettledResult<PutObjectCommandOutput>,
|
||||||
|
redisResult: PromiseSettledResult<string>,
|
||||||
|
s3Path: string,
|
||||||
|
redisKey: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (s3Result.status === 'fulfilled') {
|
||||||
|
try {
|
||||||
|
await this.s3Service.deleteObject(s3Path);
|
||||||
|
} catch (rollbackError) {
|
||||||
|
this.logger.error(`Error rolling back S3 save for room ${roomId}: ${rollbackError}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redisResult.status === 'fulfilled') {
|
||||||
|
try {
|
||||||
|
await this.redisService.delete(redisKey);
|
||||||
|
} catch (rollbackError) {
|
||||||
|
this.logger.error(`Error rolling back Redis set for room ${roomId}: ${rollbackError}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected handleError(error: any, message: string) {
|
protected handleError(error: any, message: string) {
|
||||||
if (error instanceof OpenViduMeetError) {
|
if (error instanceof OpenViduMeetError) {
|
||||||
this.logger.error(`${message}: ${error.message}`);
|
this.logger.error(`${message}: ${error.message}`);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { StorageProvider } from './storage.interface.js';
|
import { StorageProvider } from './storage.interface.js';
|
||||||
import { S3Storage } from './providers/s3-storage.js';
|
import { S3StorageProvider } from './providers/s3-storage.provider.js';
|
||||||
import { MEET_PREFERENCES_STORAGE_MODE } from '../../environment.js';
|
import { MEET_PREFERENCES_STORAGE_MODE } from '../../environment.js';
|
||||||
import { inject, injectable } from '../../config/dependency-injector.config.js';
|
import { inject, injectable } from '../../config/dependency-injector.config.js';
|
||||||
import { LoggerService } from '../logger.service.js';
|
import { LoggerService } from '../logger.service.js';
|
||||||
@ -13,7 +13,7 @@ import { LoggerService } from '../logger.service.js';
|
|||||||
@injectable()
|
@injectable()
|
||||||
export class StorageFactory {
|
export class StorageFactory {
|
||||||
constructor(
|
constructor(
|
||||||
@inject(S3Storage) protected s3Storage: S3Storage,
|
@inject(S3StorageProvider) protected s3StorageProvider: S3StorageProvider,
|
||||||
@inject(LoggerService) protected logger: LoggerService
|
@inject(LoggerService) protected logger: LoggerService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -22,11 +22,11 @@ export class StorageFactory {
|
|||||||
|
|
||||||
switch (storageMode) {
|
switch (storageMode) {
|
||||||
case 's3':
|
case 's3':
|
||||||
return this.s3Storage;
|
return this.s3StorageProvider;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.logger.info('No preferences storage mode specified. Defaulting to S3.');
|
this.logger.info('No preferences storage mode specified. Defaulting to S3.');
|
||||||
return this.s3Storage;
|
return this.s3StorageProvider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,17 @@ export interface StorageProvider<T extends GlobalPreferences = GlobalPreferences
|
|||||||
*/
|
*/
|
||||||
saveGlobalPreferences(preferences: T): Promise<T>;
|
saveGlobalPreferences(preferences: T): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Retrieves the OpenVidu Meet Rooms.
|
||||||
|
*
|
||||||
|
* @param maxItems - The maximum number of items to retrieve. If not provided, all items will be retrieved.
|
||||||
|
* @param nextPageToken - The token for the next page of results. If not provided, the first page will be retrieved.
|
||||||
|
* @returns A promise that resolves to an object containing ç
|
||||||
|
* - the retrieved rooms,
|
||||||
|
* - a boolean indicating if there are more items to retrieve
|
||||||
|
* - an optional next page token.
|
||||||
|
*/
|
||||||
getMeetRooms(
|
getMeetRooms(
|
||||||
maxItems?: number,
|
maxItems?: number,
|
||||||
nextPageToken?: string
|
nextPageToken?: string
|
||||||
|
|||||||
@ -65,12 +65,12 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
|||||||
return this.storageProvider.saveGlobalPreferences(preferences) as Promise<G>;
|
return this.storageProvider.saveGlobalPreferences(preferences) as Promise<G>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveOpenViduRoom(ovRoom: R): Promise<R> {
|
async saveMeetRoom(meetRoom: R): Promise<R> {
|
||||||
this.logger.info(`Saving OpenVidu room ${ovRoom.roomId}`);
|
this.logger.info(`Saving OpenVidu room ${meetRoom.roomId}`);
|
||||||
return this.storageProvider.saveMeetRoom(ovRoom) as Promise<R>;
|
return this.storageProvider.saveMeetRoom(meetRoom) as Promise<R>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenViduRooms(
|
async getMeetRooms(
|
||||||
maxItems?: number,
|
maxItems?: number,
|
||||||
nextPageToken?: string
|
nextPageToken?: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@ -92,23 +92,24 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
|||||||
* @returns A promise that resolves to the room's preferences.
|
* @returns A promise that resolves to the room's preferences.
|
||||||
* @throws Error if the room preferences are not found.
|
* @throws Error if the room preferences are not found.
|
||||||
*/
|
*/
|
||||||
async getOpenViduRoom(roomId: string): Promise<R> {
|
async getMeetRoom(roomId: string): Promise<R> {
|
||||||
const openviduRoom = await this.storageProvider.getMeetRoom(roomId);
|
const meetRoom = await this.storageProvider.getMeetRoom(roomId);
|
||||||
|
|
||||||
if (!openviduRoom) {
|
if (!meetRoom) {
|
||||||
this.logger.error(`Room not found for room ${roomId}`);
|
this.logger.error(`Room not found for room ${roomId}`);
|
||||||
throw errorRoomNotFound(roomId);
|
throw errorRoomNotFound(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return openviduRoom as R;
|
return meetRoom as R;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOpenViduRoom(roomId: string): Promise<void> {
|
async deleteMeetRoom(roomId: string): Promise<void> {
|
||||||
return this.storageProvider.deleteMeetRoom(roomId);
|
return this.storageProvider.deleteMeetRoom(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: REMOVE THIS METHOD
|
||||||
async getOpenViduRoomPreferences(roomId: string): Promise<MeetRoomPreferences> {
|
async getOpenViduRoomPreferences(roomId: string): Promise<MeetRoomPreferences> {
|
||||||
const openviduRoom = await this.getOpenViduRoom(roomId);
|
const openviduRoom = await this.getMeetRoom(roomId);
|
||||||
|
|
||||||
if (!openviduRoom.preferences) {
|
if (!openviduRoom.preferences) {
|
||||||
throw new Error('Room preferences not found');
|
throw new Error('Room preferences not found');
|
||||||
@ -126,9 +127,9 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
|||||||
async updateOpenViduRoomPreferences(roomId: string, roomPreferences: MeetRoomPreferences): Promise<R> {
|
async updateOpenViduRoomPreferences(roomId: string, roomPreferences: MeetRoomPreferences): Promise<R> {
|
||||||
this.validateRoomPreferences(roomPreferences);
|
this.validateRoomPreferences(roomPreferences);
|
||||||
|
|
||||||
const openviduRoom = await this.getOpenViduRoom(roomId);
|
const openviduRoom = await this.getMeetRoom(roomId);
|
||||||
openviduRoom.preferences = roomPreferences;
|
openviduRoom.preferences = roomPreferences;
|
||||||
return this.saveOpenViduRoom(openviduRoom);
|
return this.saveMeetRoom(openviduRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user