218 lines
6.3 KiB
JavaScript
218 lines
6.3 KiB
JavaScript
const AssetsDBApi = require('../db/api/assets');
|
|
const Asset_variantsDBApi = require('../db/api/asset_variants');
|
|
const { createEntityService } = require('../factories/service.factory');
|
|
const ValidationError = require('./notifications/errors/validation');
|
|
const { downloadToBuffer, uploadBuffer } = require('./file');
|
|
const videoProcessing = require('./videoProcessing');
|
|
const { logger } = require('../utils/logger');
|
|
|
|
/**
|
|
* Valid MIME type patterns for each asset type
|
|
*/
|
|
const VALID_MIME_PATTERNS = {
|
|
image: {
|
|
prefixes: ['image/'],
|
|
description: 'image (jpeg, png, gif, webp, svg, etc.)',
|
|
},
|
|
video: {
|
|
prefixes: ['video/'],
|
|
description: 'video (mp4, webm, mov, etc.)',
|
|
},
|
|
audio: {
|
|
prefixes: ['audio/'],
|
|
description: 'audio (mp3, wav, ogg, etc.)',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Validate that mime_type matches asset_type
|
|
* @param {string} assetType - Expected asset type (image, video, audio)
|
|
* @param {string} mimeType - Actual MIME type of the file
|
|
* @returns {{ valid: boolean, error?: string }}
|
|
*/
|
|
function validateAssetMimeType(assetType, mimeType) {
|
|
// If no asset_type specified, skip validation
|
|
if (!assetType) {
|
|
return { valid: true };
|
|
}
|
|
|
|
const patterns = VALID_MIME_PATTERNS[assetType];
|
|
|
|
// If asset_type is not one we validate (e.g., 'file'), skip validation
|
|
if (!patterns) {
|
|
return { valid: true };
|
|
}
|
|
|
|
// If no mime_type provided, we can't validate but allow it
|
|
// (browser may not always send mime type)
|
|
if (!mimeType) {
|
|
return { valid: true };
|
|
}
|
|
|
|
const normalizedMime = String(mimeType).toLowerCase().trim();
|
|
|
|
// Check if mime_type matches any of the valid prefixes
|
|
const isValid = patterns.prefixes.some((prefix) =>
|
|
normalizedMime.startsWith(prefix),
|
|
);
|
|
|
|
if (!isValid) {
|
|
return {
|
|
valid: false,
|
|
error: `Invalid file type for ${assetType}. Expected ${patterns.description}, got "${mimeType}"`,
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
// Create base service from factory
|
|
const BaseService = createEntityService(AssetsDBApi, {
|
|
entityName: 'assets',
|
|
});
|
|
|
|
/**
|
|
* Assets Service with validation and video pre-processing
|
|
*/
|
|
class AssetsService extends BaseService {
|
|
/**
|
|
* Create asset with MIME type validation and video pre-processing
|
|
*/
|
|
static async create(data, currentUser) {
|
|
// Validate asset_type and mime_type match
|
|
const assetType = data.asset_type;
|
|
const mimeType = data.mime_type;
|
|
|
|
const validation = validateAssetMimeType(assetType, mimeType);
|
|
if (!validation.valid) {
|
|
throw new ValidationError(validation.error);
|
|
}
|
|
|
|
// Call parent create
|
|
const asset = await super.create(data, currentUser);
|
|
|
|
// Pre-generate reversed video for video assets (async, doesn't block response)
|
|
if (assetType === 'video' && data.storage_key) {
|
|
AssetsService.preGenerateReversedVideo(asset, currentUser).catch(
|
|
(err) => {
|
|
logger.error(
|
|
{ err, assetId: asset.id },
|
|
'Failed to pre-generate reversed video (non-blocking)',
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
return asset;
|
|
}
|
|
|
|
/**
|
|
* Pre-generate reversed video variant for a video asset.
|
|
* Runs asynchronously after asset creation - doesn't block the upload response.
|
|
* This ensures reversed videos are ready for instant use in transitions.
|
|
*
|
|
* @param {Object} asset - Created asset record
|
|
* @param {Object} currentUser - Current user context
|
|
*/
|
|
static async preGenerateReversedVideo(asset, currentUser) {
|
|
const log = logger.child({
|
|
assetId: asset.id,
|
|
operation: 'preGenerateReversed',
|
|
});
|
|
|
|
log.info('Starting pre-generation of reversed video');
|
|
|
|
try {
|
|
// Check if FFmpeg is available
|
|
const ffmpegAvailable = await videoProcessing.isFFmpegAvailable();
|
|
if (!ffmpegAvailable) {
|
|
log.warn('FFmpeg not available, skipping pre-generation');
|
|
return null;
|
|
}
|
|
|
|
// Check if reversed variant already exists (shouldn't happen on create, but safety check)
|
|
const existingAsset = await AssetsDBApi.findBy({ id: asset.id });
|
|
const variants = existingAsset?.asset_variants_asset || [];
|
|
const existingReversed = variants.find(
|
|
(v) => v.variant_type === 'reversed',
|
|
);
|
|
|
|
if (existingReversed) {
|
|
log.debug('Reversed variant already exists');
|
|
return existingReversed.storage_key;
|
|
}
|
|
|
|
// Download original video to buffer
|
|
const storageKey = asset.storage_key;
|
|
log.info({ storageKey }, 'Downloading original video');
|
|
|
|
const originalBuffer = await downloadToBuffer(storageKey);
|
|
|
|
// Generate reversed video
|
|
log.info('Generating reversed video with FFmpeg');
|
|
const reversedBuffer = await videoProcessing.reverseVideo(
|
|
originalBuffer,
|
|
asset.original_file_name || 'video.mp4',
|
|
);
|
|
|
|
// Upload reversed video to storage
|
|
const reversedKey = `assets/${asset.id}/reversed.mp4`;
|
|
log.info({ reversedKey }, 'Uploading reversed video');
|
|
|
|
const result = await uploadBuffer(reversedKey, reversedBuffer, {
|
|
contentType: 'video/mp4',
|
|
});
|
|
|
|
// Create variant record
|
|
await Asset_variantsDBApi.create(
|
|
{
|
|
assetId: asset.id,
|
|
variant_type: 'reversed',
|
|
cdn_url: result.url,
|
|
storage_key: reversedKey,
|
|
size_mb: reversedBuffer.length / (1024 * 1024),
|
|
},
|
|
{ currentUser },
|
|
);
|
|
|
|
log.info(
|
|
{
|
|
reversedKey,
|
|
sizeMb: (reversedBuffer.length / (1024 * 1024)).toFixed(2),
|
|
},
|
|
'Pre-generated reversed video successfully',
|
|
);
|
|
|
|
return reversedKey;
|
|
} catch (err) {
|
|
log.error({ err }, 'Failed to pre-generate reversed video');
|
|
// Don't throw - this is a background operation
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update asset with MIME type validation
|
|
*/
|
|
static async update(data, id, currentUser) {
|
|
// If updating asset_type or mime_type, validate they match
|
|
if (data.asset_type || data.mime_type) {
|
|
const assetType = data.asset_type;
|
|
const mimeType = data.mime_type;
|
|
|
|
// Only validate if both are provided in the update
|
|
if (assetType && mimeType) {
|
|
const validation = validateAssetMimeType(assetType, mimeType);
|
|
if (!validation.valid) {
|
|
throw new ValidationError(validation.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call parent update
|
|
return super.update(data, id, currentUser);
|
|
}
|
|
}
|
|
|
|
module.exports = AssetsService;
|