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,
|
||||
RedisService,
|
||||
RoomService,
|
||||
S3Storage,
|
||||
S3StorageProvider,
|
||||
S3Service,
|
||||
SystemEventService,
|
||||
TaskSchedulerService,
|
||||
@ -50,7 +50,7 @@ export const registerDependencies = () => {
|
||||
container.bind(MeetStorageService).toSelf().inSingletonScope();
|
||||
container.bind(ParticipantService).toSelf().inSingletonScope();
|
||||
|
||||
container.bind(S3Storage).toSelf().inSingletonScope();
|
||||
container.bind(S3StorageProvider).toSelf().inSingletonScope();
|
||||
container.bind(StorageFactory).toSelf().inSingletonScope();
|
||||
};
|
||||
|
||||
|
||||
@ -13,6 +13,6 @@ export * from './mutex.service.js';
|
||||
export * from './storage/index.js';
|
||||
export * from './redis.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 './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 { LoggerService } from './logger.service.js';
|
||||
import { LiveKitService } from './livekit.service.js';
|
||||
import { GlobalPreferencesService } from './preferences/global-preferences.service.js';
|
||||
import { MeetRoom, MeetRoomOptions, ParticipantRole } from '@typings-ce';
|
||||
import { MeetStorageService } from './storage/storage.service.js';
|
||||
import { MeetRoom, MeetRoomFilters, MeetRoomOptions, MeetRoomPreferences, ParticipantRole } from '@typings-ce';
|
||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||
import { SystemEventService } from './system-event.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 { MEET_NAME_ID } from '../environment.js';
|
||||
import ms from 'ms';
|
||||
import { UtilsHelper } from '../helpers/utils.helper.js';
|
||||
|
||||
/**
|
||||
* Service for managing OpenVidu Meet rooms.
|
||||
@ -24,7 +25,7 @@ import ms from 'ms';
|
||||
export class RoomService {
|
||||
constructor(
|
||||
@inject(LoggerService) protected logger: LoggerService,
|
||||
@inject(GlobalPreferencesService) protected globalPrefService: GlobalPreferencesService,
|
||||
@inject(MeetStorageService) protected storageService: MeetStorageService,
|
||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||
@inject(SystemEventService) protected systemEventService: SystemEventService,
|
||||
@inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService
|
||||
@ -66,7 +67,7 @@ export class RoomService {
|
||||
const { preferences, expirationDate, roomIdPrefix } = roomOptions;
|
||||
const roomId = roomIdPrefix ? `${roomIdPrefix}-${uid(15)}` : uid(15);
|
||||
|
||||
const openviduRoom: MeetRoom = {
|
||||
const meetRoom: MeetRoom = {
|
||||
roomId,
|
||||
roomIdPrefix,
|
||||
creationDate: Date.now(),
|
||||
@ -77,9 +78,9 @@ export class RoomService {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns A Promise that resolves to an array of {@link MeetRoom} objects.
|
||||
* @throws If there was an error retrieving the rooms.
|
||||
*/
|
||||
async listOpenViduRooms(): Promise<MeetRoom[]> {
|
||||
return await this.globalPrefService.getOpenViduRooms();
|
||||
async getAllMeetRooms({ maxItems, nextPageToken, fields }: MeetRoomFilters): Promise<{
|
||||
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.
|
||||
* @returns A promise that resolves to an {@link MeetRoom} object.
|
||||
*/
|
||||
async getMeetRoom(roomId: string): Promise<MeetRoom> {
|
||||
return await this.globalPrefService.getOpenViduRoom(roomId);
|
||||
async getMeetRoom(roomId: string, fields?: string): Promise<MeetRoom> {
|
||||
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.
|
||||
* @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([
|
||||
this.deleteOpenViduRooms(roomIds),
|
||||
Promise.allSettled(roomIds.map((roomId) => this.livekitService.deleteRoom(roomId)))
|
||||
@ -171,10 +211,8 @@ export class RoomService {
|
||||
* @param roomIds - List of room names to delete.
|
||||
* @returns A promise that resolves with an array of successfully deleted room names.
|
||||
*/
|
||||
async deleteOpenViduRooms(roomIds: string[]): Promise<string[]> {
|
||||
const results = await Promise.allSettled(
|
||||
roomIds.map((roomId) => this.globalPrefService.deleteOpenViduRoom(roomId))
|
||||
);
|
||||
protected async deleteOpenViduRooms(roomIds: string[]): Promise<string[]> {
|
||||
const results = await Promise.allSettled(roomIds.map((roomId) => this.storageService.deleteMeetRoom(roomId)));
|
||||
|
||||
const successfulRooms: string[] = [];
|
||||
|
||||
@ -307,21 +345,22 @@ export class RoomService {
|
||||
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
||||
*/
|
||||
protected async deleteOpenViduExpiredRooms(): Promise<string[]> {
|
||||
const now = Date.now();
|
||||
this.logger.verbose(`Checking OpenVidu expired rooms at ${new Date(now).toISOString()}`);
|
||||
const rooms = await this.listOpenViduRooms();
|
||||
const expiredRooms = rooms
|
||||
.filter((room) => room.expirationDate && room.expirationDate < now)
|
||||
.map((room) => room.roomId);
|
||||
// const now = Date.now();
|
||||
// this.logger.verbose(`Checking OpenVidu expired rooms at ${new Date(now).toISOString()}`);
|
||||
// const rooms = await this.getAllMeetRooms();
|
||||
// const expiredRooms = rooms
|
||||
// .filter((room) => room.expirationDate && room.expirationDate < now)
|
||||
// .map((room) => room.roomId);
|
||||
|
||||
if (expiredRooms.length === 0) {
|
||||
this.logger.verbose('No OpenVidu expired rooms to delete.');
|
||||
return [];
|
||||
}
|
||||
// if (expiredRooms.length === 0) {
|
||||
// this.logger.verbose('No OpenVidu expired rooms to delete.');
|
||||
// return [];
|
||||
// }
|
||||
|
||||
this.logger.info(`Deleting ${expiredRooms.length} OpenVidu expired rooms: ${expiredRooms.join(', ')}`);
|
||||
// this.logger.info(`Deleting ${expiredRooms.length} OpenVidu expired rooms: ${expiredRooms.join(', ')}`);
|
||||
|
||||
return await this.deleteOpenViduRooms(expiredRooms);
|
||||
// return await this.deleteOpenViduRooms(expiredRooms);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -333,52 +372,43 @@ export class RoomService {
|
||||
* @protected
|
||||
*/
|
||||
protected async restoreMissingLivekitRooms(): Promise<void> {
|
||||
this.logger.verbose(`Checking missing Livekit rooms ...`);
|
||||
|
||||
const [lkResult, ovResult] = await Promise.allSettled([
|
||||
this.livekitService.listRooms(),
|
||||
this.listOpenViduRooms()
|
||||
]);
|
||||
|
||||
let lkRooms: Room[] = [];
|
||||
let ovRooms: MeetRoom[] = [];
|
||||
|
||||
if (lkResult.status === 'fulfilled') {
|
||||
lkRooms = lkResult.value;
|
||||
} else {
|
||||
this.logger.error('Failed to list Livekit rooms:', lkResult.reason);
|
||||
}
|
||||
|
||||
if (ovResult.status === 'fulfilled') {
|
||||
ovRooms = ovResult.value;
|
||||
} else {
|
||||
this.logger.error('Failed to list OpenVidu rooms:', ovResult.reason);
|
||||
}
|
||||
|
||||
const missingRooms: MeetRoom[] = ovRooms.filter(
|
||||
(ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomId)
|
||||
);
|
||||
|
||||
if (missingRooms.length === 0) {
|
||||
this.logger.verbose('All OpenVidu rooms are present in Livekit. No missing rooms to restore. ');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
||||
|
||||
const creationResults = await Promise.allSettled(
|
||||
missingRooms.map(({ roomId }: MeetRoom) => {
|
||||
this.logger.debug(`Restoring room: ${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}"`);
|
||||
}
|
||||
});
|
||||
// this.logger.verbose(`Checking missing Livekit rooms ...`);
|
||||
// const [lkResult, ovResult] = await Promise.allSettled([
|
||||
// this.livekitService.listRooms(),
|
||||
// this.getAllMeetRooms()
|
||||
// ]);
|
||||
// let lkRooms: Room[] = [];
|
||||
// let ovRooms: MeetRoom[] = [];
|
||||
// if (lkResult.status === 'fulfilled') {
|
||||
// lkRooms = lkResult.value;
|
||||
// } else {
|
||||
// this.logger.error('Failed to list Livekit rooms:', lkResult.reason);
|
||||
// }
|
||||
// if (ovResult.status === 'fulfilled') {
|
||||
// ovRooms = ovResult.value;
|
||||
// } else {
|
||||
// this.logger.error('Failed to list OpenVidu rooms:', ovResult.reason);
|
||||
// }
|
||||
// const missingRooms: MeetRoom[] = ovRooms.filter(
|
||||
// (ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomId)
|
||||
// );
|
||||
// if (missingRooms.length === 0) {
|
||||
// this.logger.verbose('All OpenVidu rooms are present in Livekit. No missing rooms to restore. ');
|
||||
// return;
|
||||
// }
|
||||
// this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
||||
// const creationResults = await Promise.allSettled(
|
||||
// missingRooms.map(({ roomId }: MeetRoom) => {
|
||||
// this.logger.debug(`Restoring room: ${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.interface.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 { OpenViduMeetError } from '../../../models/error.model.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 { PutObjectCommandOutput } from '@aws-sdk/client-s3';
|
||||
|
||||
/**
|
||||
* Implementation of the StorageProvider interface using AWS S3 for persistent storage
|
||||
@ -26,10 +27,10 @@ import { RedisKeyName } from '../../../models/redis.model.js';
|
||||
* @implements {StorageProvider}
|
||||
*/
|
||||
@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
|
||||
{
|
||||
protected readonly S3_GLOBAL_PREFERENCES_KEY = `${MEET_S3_SUBBUCKET}/global-preferences.json`;
|
||||
protected readonly S3_GLOBAL_PREFERENCES_KEY = `global-preferences.json`;
|
||||
constructor(
|
||||
@inject(LoggerService) protected logger: LoggerService,
|
||||
@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> {
|
||||
const { roomId } = ovRoom;
|
||||
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),
|
||||
// TODO: Use a key prefix for Redis
|
||||
this.redisService.set(roomId, roomStr, false)
|
||||
this.redisService.set(redisKey, redisPayload, false)
|
||||
]);
|
||||
|
||||
const s3Result = results[0];
|
||||
const redisResult = results[1];
|
||||
|
||||
if (s3Result.status === 'fulfilled' && redisResult.status === 'fulfilled') {
|
||||
return ovRoom;
|
||||
}
|
||||
|
||||
// Rollback changes if one of the operations failed
|
||||
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(roomId);
|
||||
} catch (rollbackError) {
|
||||
this.logger.error(`Error rolling back Redis set for room ${roomId}: ${rollbackError}`);
|
||||
}
|
||||
}
|
||||
// Rollback any changes made by the successful operation
|
||||
await this.rollbackRoomSave(roomId, s3Result, redisResult, s3Path, redisKey);
|
||||
|
||||
// Return the error that occurred first
|
||||
const rejectedResult: PromiseRejectedResult =
|
||||
const failedOperation: 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}`);
|
||||
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(
|
||||
maxItems: number,
|
||||
nextPageToken?: string
|
||||
@ -189,71 +188,48 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
||||
NextContinuationToken
|
||||
} = await this.s3Service.listObjectsPaginated(MEET_S3_ROOMS_PREFIX, maxItems, nextPageToken);
|
||||
|
||||
if (!roomFiles) {
|
||||
this.logger.verbose('No rooms found. Returning an empty array.');
|
||||
if (!roomFiles || roomFiles.length === 0) {
|
||||
this.logger.verbose('No room files found in S3.');
|
||||
return { rooms: [], isTruncated: false };
|
||||
}
|
||||
|
||||
// const promises: Promise<R>[] = [];
|
||||
// // Retrieve the data for each room
|
||||
// roomFiles.forEach((item) => {
|
||||
// if (item?.Key && item.Key.endsWith('.json')) {
|
||||
// promises.push(getOpenViduRoom(item.Key) as Promise<R>);
|
||||
// }
|
||||
// });
|
||||
// Extract room IDs directly and filter out invalid values
|
||||
const roomIds = roomFiles
|
||||
.map((file) => this.extractRoomId(file.Key))
|
||||
.filter((id): id is string => Boolean(id));
|
||||
|
||||
// Extract room names from file paths
|
||||
const roomIds = roomFiles.map((file) => this.extractRoomId(file.Key)).filter(Boolean) as string[];
|
||||
// Fetch and log any room lookup errors individually
|
||||
// Fetch room preferences in parallel
|
||||
const rooms = await Promise.all(
|
||||
roomIds.map(async (roomId: string) => {
|
||||
if (!roomId) return null;
|
||||
|
||||
roomIds.map(async (roomId) => {
|
||||
try {
|
||||
return await this.getMeetRoom(roomId);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Failed to fetch room "${roomId}": ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
this.logger.warn(`Failed to fetch room "${roomId}": ${error}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Filter out null values
|
||||
const roomsResponse = rooms.filter(Boolean) as R[];
|
||||
return { rooms: roomsResponse, isTruncated: !!IsTruncated, nextPageToken: NextContinuationToken };
|
||||
const validRooms = rooms.filter((room) => room !== null) as R[];
|
||||
return { rooms: validRooms, isTruncated: !!IsTruncated, nextPageToken: NextContinuationToken };
|
||||
} catch (error) {
|
||||
this.handleError(error, 'Error fetching Room preferences');
|
||||
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> {
|
||||
try {
|
||||
// Try to get room preferences from Redis cache
|
||||
const room: R | null = await this.getFromRedis<R>(roomId);
|
||||
|
||||
if (!room) {
|
||||
this.logger.debug(`Room ${roomId} not found in Redis. Fetching from S3...`);
|
||||
return await this.getFromS3<R>(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`);
|
||||
const s3RoomPath = `${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`);
|
||||
@ -265,11 +241,12 @@ export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extend
|
||||
}
|
||||
|
||||
async deleteMeetRoom(roomId: string): Promise<void> {
|
||||
const s3RoomPath = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
||||
const redisKey = RedisKeyName.ROOM + roomId;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`),
|
||||
this.redisService.delete(roomId)
|
||||
]);
|
||||
await Promise.all([this.s3Service.deleteObject(s3RoomPath), this.redisService.delete(redisKey)]);
|
||||
this.logger.verbose(`Room ${roomId} deleted successfully from S3 and Redis`);
|
||||
} catch (error) {
|
||||
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) {
|
||||
if (error instanceof OpenViduMeetError) {
|
||||
this.logger.error(`${message}: ${error.message}`);
|
||||
@ -1,5 +1,5 @@
|
||||
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 { inject, injectable } from '../../config/dependency-injector.config.js';
|
||||
import { LoggerService } from '../logger.service.js';
|
||||
@ -13,7 +13,7 @@ import { LoggerService } from '../logger.service.js';
|
||||
@injectable()
|
||||
export class StorageFactory {
|
||||
constructor(
|
||||
@inject(S3Storage) protected s3Storage: S3Storage,
|
||||
@inject(S3StorageProvider) protected s3StorageProvider: S3StorageProvider,
|
||||
@inject(LoggerService) protected logger: LoggerService
|
||||
) {}
|
||||
|
||||
@ -22,11 +22,11 @@ export class StorageFactory {
|
||||
|
||||
switch (storageMode) {
|
||||
case 's3':
|
||||
return this.s3Storage;
|
||||
return this.s3StorageProvider;
|
||||
|
||||
default:
|
||||
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>;
|
||||
|
||||
/**
|
||||
*
|
||||
* 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(
|
||||
maxItems?: number,
|
||||
nextPageToken?: string
|
||||
|
||||
@ -65,12 +65,12 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
||||
return this.storageProvider.saveGlobalPreferences(preferences) as Promise<G>;
|
||||
}
|
||||
|
||||
async saveOpenViduRoom(ovRoom: R): Promise<R> {
|
||||
this.logger.info(`Saving OpenVidu room ${ovRoom.roomId}`);
|
||||
return this.storageProvider.saveMeetRoom(ovRoom) as Promise<R>;
|
||||
async saveMeetRoom(meetRoom: R): Promise<R> {
|
||||
this.logger.info(`Saving OpenVidu room ${meetRoom.roomId}`);
|
||||
return this.storageProvider.saveMeetRoom(meetRoom) as Promise<R>;
|
||||
}
|
||||
|
||||
async getOpenViduRooms(
|
||||
async getMeetRooms(
|
||||
maxItems?: number,
|
||||
nextPageToken?: string
|
||||
): Promise<{
|
||||
@ -92,23 +92,24 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
||||
* @returns A promise that resolves to the room's preferences.
|
||||
* @throws Error if the room preferences are not found.
|
||||
*/
|
||||
async getOpenViduRoom(roomId: string): Promise<R> {
|
||||
const openviduRoom = await this.storageProvider.getMeetRoom(roomId);
|
||||
async getMeetRoom(roomId: string): Promise<R> {
|
||||
const meetRoom = await this.storageProvider.getMeetRoom(roomId);
|
||||
|
||||
if (!openviduRoom) {
|
||||
if (!meetRoom) {
|
||||
this.logger.error(`Room not found for room ${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);
|
||||
}
|
||||
|
||||
//TODO: REMOVE THIS METHOD
|
||||
async getOpenViduRoomPreferences(roomId: string): Promise<MeetRoomPreferences> {
|
||||
const openviduRoom = await this.getOpenViduRoom(roomId);
|
||||
const openviduRoom = await this.getMeetRoom(roomId);
|
||||
|
||||
if (!openviduRoom.preferences) {
|
||||
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> {
|
||||
this.validateRoomPreferences(roomPreferences);
|
||||
|
||||
const openviduRoom = await this.getOpenViduRoom(roomId);
|
||||
const openviduRoom = await this.getMeetRoom(roomId);
|
||||
openviduRoom.preferences = roomPreferences;
|
||||
return this.saveOpenViduRoom(openviduRoom);
|
||||
return this.saveMeetRoom(openviduRoom);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user