backend: Implement room metadata archiving and retrieval in storage service

This commit is contained in:
Carlos Santos 2025-04-22 13:42:32 +02:00
parent c33ee7218b
commit 32c0c9d242
4 changed files with 105 additions and 41 deletions

View File

@ -14,6 +14,7 @@ import { SystemEventService } from './system-event.service.js';
import { SystemEventType } from '../models/system-event.model.js';
import { MeetRoomHelper } from '../helpers/room.helper.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { MeetStorageService } from './storage/storage.service.js';
@injectable()
export class LivekitWebhookService {
@ -23,6 +24,7 @@ export class LivekitWebhookService {
@inject(RecordingService) protected recordingService: RecordingService,
@inject(LiveKitService) protected livekitService: LiveKitService,
@inject(RoomService) protected roomService: RoomService,
@inject(MeetStorageService) protected storageService: MeetStorageService,
@inject(OpenViduWebhookService) protected openViduWebhookService: OpenViduWebhookService,
@inject(MutexService) protected mutexService: MutexService,
@inject(SystemEventService) protected systemEventService: SystemEventService,
@ -200,7 +202,7 @@ export class LivekitWebhookService {
switch (webhookAction) {
case 'started':
tasks.push(
this.saveRoomMetadataFileIfNeeded(roomId),
this.storageService.archiveRoomMetadata(roomId),
this.openViduWebhookService.sendRecordingStartedWebhook(recordingInfo)
);
break;
@ -235,43 +237,4 @@ export class LivekitWebhookService {
);
}
}
/**
* Saves room metadata to a JSON file in the S3 bucket if it doesn't already exist.
*
* This method checks if the metadata file for the given room already exists in the
* S3 bucket. If not, it retrieves the room information, extracts the necessary
* secrets and preferences, and saves them to a metadata JSON file in the
* .metadata/{roomId}/ directory of the S3 bucket.
*
* @param roomId - The unique identifier of the room
*/
protected async saveRoomMetadataFileIfNeeded(roomId: string): Promise<void> {
try {
const filePath = `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.metadata/${roomId}/room_metadata.json`;
const fileExists = await this.s3Service.exists(filePath);
if (fileExists) {
this.logger.debug(`Room metadata already saved for room ${roomId} in recordings bucket`);
return;
}
const room = await this.roomService.getMeetRoom(roomId);
if (room) {
const { publisherSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room);
const roomMetadata = {
publisherSecret,
moderatorSecret,
preferences: {
recordingPreferences: room.preferences?.recordingPreferences
}
};
await this.s3Service.saveObject(filePath, roomMetadata);
this.logger.debug(`Room metadata saved for room ${roomId} in recordings bucket`);
}
} catch (error) {
this.logger.error(`Error saving room metadata for room ${roomId} in recordings bucket: ${error}`);
}
}
}

View File

@ -245,13 +245,74 @@ export class S3StorageProvider<G extends GlobalPreferences = GlobalPreferences,
const redisKeysToDelete = roomIds.map((id) => RedisKeyName.ROOM + id);
try {
await Promise.all([this.s3Service.deleteObjects(roomsToDelete), this.redisService.delete(redisKeysToDelete)]);
await Promise.all([
this.s3Service.deleteObjects(roomsToDelete),
this.redisService.delete(redisKeysToDelete)
]);
this.logger.verbose(`Rooms deleted successfully: ${roomIds.join(', ')}`);
} catch (error) {
this.handleError(error, `Error deleting rooms: ${roomIds.join(', ')}`);
}
}
async getArchivedRoomMetadata(roomId: string): Promise<Partial<R> | null> {
try {
const filePath = `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.metadata/${roomId}/room_metadata.json`;
const roomMetadata = await this.getFromS3<Partial<R>>(filePath);
if (!roomMetadata) {
this.logger.warn(`Room metadata not found for room ${roomId} in recordings bucket`);
return null;
}
return roomMetadata;
} catch (error) {
this.handleError(error, `Error fetching archived room metadata for room ${roomId}`);
return null;
}
}
/**
* Saves room metadata to a JSON file in the S3 bucket if it doesn't already exist.
*
* This method checks if the metadata file for the given room already exists in the
* S3 bucket. If not, it retrieves the room information, extracts the necessary
* secrets and preferences, and saves them to a metadata JSON file in the
* .metadata/{roomId}/ directory of the S3 bucket.
*
* @param roomId - The unique identifier of the room
*/
async archiveRoomMetadata(roomId: string): Promise<void> {
try {
const filePath = `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.metadata/${roomId}/room_metadata.json`;
const fileExists = await this.s3Service.exists(filePath);
if (fileExists) {
this.logger.debug(`Room metadata already saved for room ${roomId} in recordings bucket`);
return;
}
const room = await this.getMeetRoom(roomId);
if (room) {
const roomMetadata = {
moderatorRoomUrl: room.moderatorRoomUrl,
publisherRoomUrl: room.publisherRoomUrl,
preferences: {
recordingPreferences: room.preferences?.recordingPreferences
}
};
await this.s3Service.saveObject(filePath, roomMetadata);
this.logger.debug(`Room metadata saved for room ${roomId} in recordings bucket`);
return;
}
this.logger.error(`Error saving room metadata for room ${roomId} in recordings bucket`);
} catch (error) {
this.logger.error(`Error saving room metadata for room ${roomId} in recordings bucket: ${error}`);
}
}
/**
* Retrieves an object of type U from Redis by the given key.
* Returns null if the key is not found or an error occurs.

View File

@ -78,4 +78,35 @@ export interface StorageProvider<T extends GlobalPreferences = GlobalPreferences
* @returns A promise that resolves when the room have been deleted.
**/
deleteMeetRooms(roomIds: string[]): Promise<void>;
/**
* Gets the archived metadata for a specific room.
*
* The archived metadata is necessary for checking the permissions of the recording viewer when the room is deleted.
*
* @param roomId - The name of the room to retrieve.
*/
getArchivedRoomMetadata(roomId: string): Promise<Partial<R> | null>;
/**
* Archives the metadata for a specific room.
*
* This is necessary for persisting the metadata of a room although it is deleted.
* The metadata will be used to check the permissions of the recording viewer.
*
* @param roomId: The room ID to archive.
*/
archiveRoomMetadata(roomId: string): Promise<void>;
//TODO:
// deleteArchivedRoomMetadata(roomId: string): Promise<void>;
//TODO:
// saveRecordingMetadata;
//TODO:
// getRecordingMetadata;
//TODO:
// deleteRecordingMetadata;
}

View File

@ -121,6 +121,15 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
return this.storageProvider.deleteMeetRooms(roomIds);
}
async getArchivedRoomMetadata(roomId: string): Promise<Partial<R> | null> {
return this.storageProvider.getArchivedRoomMetadata(roomId) as Promise<Partial<R> | null>;
}
async archiveRoomMetadata(roomId: string): Promise<void> {
return this.storageProvider.archiveRoomMetadata(roomId);
}
/**
* Returns the default global preferences.
* @returns {G}