完成新页面开发
This commit is contained in:
132
src/server.ts
132
src/server.ts
@@ -27,10 +27,23 @@ type RecordingMetadata = {
|
||||
mimetype?: string;
|
||||
size?: number;
|
||||
userId?: string;
|
||||
host?: RecordingPerson;
|
||||
participants?: RecordingPerson[];
|
||||
uploadedAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
type RecordingPerson = {
|
||||
participantId?: string;
|
||||
userId?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
role?: string;
|
||||
status?: string;
|
||||
mediaState?: any;
|
||||
};
|
||||
|
||||
function safeAvatarExtension(file: any): string {
|
||||
const originalExt = path.extname(file.originalname || '').toLowerCase();
|
||||
if (ALLOWED_AVATAR_EXTENSIONS.has(originalExt)) {
|
||||
@@ -101,6 +114,88 @@ function isAllowedRecording(file: any): boolean {
|
||||
return ext.length > 0 && isCompatibleMime;
|
||||
}
|
||||
|
||||
function getRecordingMimeTypeFromExtension(ext: string): string {
|
||||
return ext.toLowerCase() === '.mp4' ? 'video/mp4' : 'video/webm';
|
||||
}
|
||||
|
||||
function parseJsonField(value: any): any {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (_error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeMetadataString(value: any, maxLength = 200): string {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value).replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function sanitizeRecordingPerson(value: any, fallbackRole: string): RecordingPerson | undefined {
|
||||
const parsed = parseJsonField(value);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const person: RecordingPerson = {
|
||||
participantId: sanitizeMetadataString(parsed.participantId || parsed.connectionId, 120),
|
||||
userId: sanitizeMetadataString(parsed.userId || parsed.id, 120),
|
||||
id: sanitizeMetadataString(parsed.id || parsed.userId, 120),
|
||||
name: sanitizeMetadataString(parsed.name, 120),
|
||||
avatar: sanitizeMetadataString(parsed.avatar, 400),
|
||||
role: sanitizeMetadataString(parsed.role || fallbackRole, 40),
|
||||
status: sanitizeMetadataString(parsed.status, 40)
|
||||
};
|
||||
|
||||
if (parsed.mediaState && typeof parsed.mediaState === 'object') {
|
||||
person.mediaState = {
|
||||
audio: Boolean(parsed.mediaState.audio),
|
||||
video: Boolean(parsed.mediaState.video),
|
||||
screenShare: Boolean(parsed.mediaState.screenShare),
|
||||
recording: Boolean(parsed.mediaState.recording),
|
||||
isSpeaking: Boolean(parsed.mediaState.isSpeaking)
|
||||
};
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
function sanitizeRecordingParticipants(value: any): RecordingPerson[] {
|
||||
const parsed = parseJsonField(value);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parsed
|
||||
.slice(0, 100)
|
||||
.map((participant) => sanitizeRecordingPerson(participant, 'participant'))
|
||||
.filter((participant) => Boolean(participant));
|
||||
}
|
||||
|
||||
function buildFallbackRecordingHost(userId: string | undefined): RecordingPerson | undefined {
|
||||
const safeUserId = sanitizeMetadataString(userId, 120);
|
||||
if (!safeUserId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: safeUserId,
|
||||
id: safeUserId,
|
||||
role: 'host'
|
||||
};
|
||||
}
|
||||
|
||||
function readRecordingMetadata(metadataPath: string): RecordingMetadata {
|
||||
try {
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
@@ -115,11 +210,13 @@ function readRecordingMetadata(metadataPath: string): RecordingMetadata {
|
||||
}
|
||||
|
||||
function getRecordingMimeType(filename: string, metadata: RecordingMetadata): string {
|
||||
if (metadata.mimetype) {
|
||||
return metadata.mimetype;
|
||||
const ext = path.extname(filename);
|
||||
const mimetype = normalizeMimeType(metadata.mimetype);
|
||||
if (mimetype && mimetype !== 'text/plain' && mimetype !== 'application/octet-stream') {
|
||||
return mimetype;
|
||||
}
|
||||
|
||||
return path.extname(filename).toLowerCase() === '.mp4' ? 'video/mp4' : 'video/webm';
|
||||
return getRecordingMimeTypeFromExtension(ext);
|
||||
}
|
||||
|
||||
function buildRecordingInfo(recordingRoot: string, meetingId: string, filename: string) {
|
||||
@@ -129,6 +226,7 @@ function buildRecordingInfo(recordingRoot: string, meetingId: string, filename:
|
||||
const metadata = readRecordingMetadata(metadataPath);
|
||||
const resolvedMeetingId = metadata.meetingId || meetingId;
|
||||
const resolvedFilename = metadata.filename || filename;
|
||||
const participants = Array.isArray(metadata.participants) ? metadata.participants : [];
|
||||
|
||||
return {
|
||||
id: metadata.id || path.basename(filename, path.extname(filename)),
|
||||
@@ -138,6 +236,9 @@ function buildRecordingInfo(recordingRoot: string, meetingId: string, filename:
|
||||
mimetype: getRecordingMimeType(filename, metadata),
|
||||
size: stat.size,
|
||||
userId: metadata.userId || '',
|
||||
host: metadata.host || buildFallbackRecordingHost(metadata.userId),
|
||||
participants,
|
||||
participantCount: participants.length,
|
||||
uploadedAt: metadata.uploadedAt || stat.birthtime.toISOString(),
|
||||
updatedAt: metadata.updatedAt || stat.mtime.toISOString(),
|
||||
modifiedAt: stat.mtime.toISOString(),
|
||||
@@ -394,6 +495,9 @@ export const createServer = (config: Options): express.Express => {
|
||||
const recordingId = uuid();
|
||||
const meetingId = sanitizePathSegment(request.body.meetingId, 'unknown');
|
||||
const originalFilename = path.basename(request.body.filename || request.file.originalname || `recording${ext}`);
|
||||
const userId = sanitizeMetadataString(request.body.userId, 120);
|
||||
const host = sanitizeRecordingPerson(request.body.host, 'host') || buildFallbackRecordingHost(userId);
|
||||
const participants = sanitizeRecordingParticipants(request.body.participants);
|
||||
const finalFilename = `${new Date().toISOString().replace(/[:.]/g, '-')}-${recordingId}${ext}`;
|
||||
const meetingDir = path.join(recordingRoot, meetingId);
|
||||
const finalPath = path.join(meetingDir, finalFilename);
|
||||
@@ -425,9 +529,11 @@ export const createServer = (config: Options): express.Express => {
|
||||
meetingId,
|
||||
filename: finalFilename,
|
||||
originalFilename,
|
||||
mimetype: normalizeMimeType(request.file.mimetype),
|
||||
mimetype: getRecordingMimeTypeFromExtension(ext),
|
||||
size: request.file.size,
|
||||
userId: request.body.userId || '',
|
||||
userId,
|
||||
host,
|
||||
participants,
|
||||
uploadedAt: new Date().toISOString()
|
||||
};
|
||||
fs.writeFile(path.join(meetingDir, `${finalFilename}.json`), JSON.stringify(metadata, null, 2), () => undefined);
|
||||
@@ -516,6 +622,16 @@ export const createServer = (config: Options): express.Express => {
|
||||
}
|
||||
|
||||
const metadata = readRecordingMetadata(targetMetadataPath);
|
||||
const nextUserId = typeof req.body.userId === 'string' ? sanitizeMetadataString(req.body.userId, 120) : metadata.userId || '';
|
||||
const shouldSyncHostFromUserId = !metadata.host
|
||||
|| metadata.host.userId === metadata.userId
|
||||
|| metadata.host.id === metadata.userId;
|
||||
const nextHost = req.body.host !== undefined
|
||||
? sanitizeRecordingPerson(req.body.host, 'host') || buildFallbackRecordingHost(nextUserId)
|
||||
: shouldSyncHostFromUserId ? buildFallbackRecordingHost(nextUserId) : metadata.host;
|
||||
const nextParticipants = req.body.participants !== undefined
|
||||
? sanitizeRecordingParticipants(req.body.participants)
|
||||
: Array.isArray(metadata.participants) ? metadata.participants : [];
|
||||
const nextMetadata = {
|
||||
...metadata,
|
||||
meetingId: nextMeetingId,
|
||||
@@ -523,8 +639,10 @@ export const createServer = (config: Options): express.Express => {
|
||||
originalFilename: typeof req.body.originalFilename === 'string' && req.body.originalFilename.trim()
|
||||
? path.basename(req.body.originalFilename.trim())
|
||||
: metadata.originalFilename || filename,
|
||||
userId: typeof req.body.userId === 'string' ? req.body.userId.trim() : metadata.userId || '',
|
||||
mimetype: metadata.mimetype || (ext === '.mp4' ? 'video/mp4' : 'video/webm'),
|
||||
userId: nextUserId,
|
||||
host: nextHost,
|
||||
participants: nextParticipants,
|
||||
mimetype: metadata.mimetype || getRecordingMimeTypeFromExtension(ext),
|
||||
size: fs.statSync(targetPath).size,
|
||||
uploadedAt: metadata.uploadedAt || fs.statSync(targetPath).birthtime.toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
|
||||
Reference in New Issue
Block a user