| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- /**
- * 云函数:fileAssetManager
- *
- * 记录七牛文件的归属和生命周期元数据。文件本体仍存放在七牛,
- * 元数据主存储为 Parse 项目命名空间 Class:VideoWorkflowFileAsset。
- */
- const { requireSession, requireAdmin, assertRequestedUserMatchesSession } = require('./_session');
- const {
- VIDEO_WORKFLOW_CLASSES,
- createParseClassStore,
- parseUserFields,
- parseUserWhere,
- } = require('./_parseClassStore');
- async function handler(request, response) {
- try {
- const action = pickParam(request, 'action') || 'list';
- if (action === 'adminInspect') {
- const adminSession = await requireAdmin(request, Psql);
- const store = createParseClassStore({ sessionToken: adminSession.sessionToken });
- return adminInspect(request, response, adminSession, store);
- }
- const session = await requireSession(request, Psql);
- assertRequestedUserMatchesSession(request, session);
- const store = createParseClassStore({ sessionToken: session.sessionToken });
- const userId = session.userId;
- if (action === 'register') return registerAsset(request, response, userId, store);
- if (action === 'get') return getAsset(request, response, userId, store);
- if (action === 'list') return listAssets(request, response, userId, store);
- if (action === 'stats') return getStats(request, response, userId, store);
- if (action === 'bind') return bindAsset(request, response, userId, store);
- if (action === 'delete') return deleteAsset(request, response, userId, store);
- if (action === 'purge') return purgeAsset(request, response, userId, store);
- return response.json({ code: 400, success: false, error: `Unknown action: ${action}` });
- } catch (error) {
- console.error('fileAssetManager failed:', error && error.message);
- return response.json({ code: error.status || 500, success: false, error: error.message || 'fileAssetManager failed' });
- }
- }
- async function registerAsset(request, response, userId, store) {
- const qiniuKey = requiredText(request, 'qiniuKey', 'key');
- assertUserScopedKey(qiniuKey, userId);
- const data = readAssetInput(request);
- const where = parseUserWhere(userId, { qiniuKey });
- const row = await store.upsertByQuery(VIDEO_WORKFLOW_CLASSES.fileAsset, where, {
- ...parseUserFields(userId),
- spaceId: data.spaceId,
- qiniuKey,
- url: data.url,
- bucket: data.bucket,
- mimeType: data.mimeType,
- kind: data.kind,
- sizeBytes: data.sizeBytes,
- sha256: data.sha256,
- sourceModule: data.sourceModule,
- bizType: data.bizType,
- bizId: data.bizId,
- status: data.status,
- metadata: data.metadata,
- });
- return response.json({ code: 200, success: true, data: rowToAsset(row) });
- }
- async function getAsset(request, response, userId, store) {
- const assetId = requiredText(request, 'assetId', 'id');
- const row = await store.findFirst(VIDEO_WORKFLOW_CLASSES.fileAsset, parseUserWhere(userId, { objectId: assetId }), { limit: 1 });
- return response.json({ code: 200, success: true, data: row ? rowToAsset(row) : null });
- }
- async function listAssets(request, response, userId, store) {
- const kind = String(pickParam(request, 'kind') || '').trim();
- const sourceModule = String(pickParam(request, 'sourceModule') || '').trim();
- const bizType = String(pickParam(request, 'bizType') || '').trim();
- const bizId = String(pickParam(request, 'bizId') || '').trim();
- const status = String(pickParam(request, 'status') || 'active').trim();
- const limit = safeLimit(pickParam(request, 'limit'), 50, 200);
- const where = parseUserWhere(userId, {
- ...(kind ? { kind } : {}),
- ...(sourceModule ? { sourceModule } : {}),
- ...(bizType ? { bizType } : {}),
- ...(bizId ? { bizId } : {}),
- ...(status ? { status } : {}),
- });
- const rows = await store.list(VIDEO_WORKFLOW_CLASSES.fileAsset, where, { order: '-createdAt', limit });
- return response.json({ code: 200, success: true, data: rows.map(rowToAsset) });
- }
- async function getStats(request, response, userId, store) {
- const rows = await listAll(store, VIDEO_WORKFLOW_CLASSES.fileAsset, parseUserWhere(userId, {}), 5000);
- return response.json({ code: 200, success: true, data: buildStats(rows) });
- }
- async function adminInspect(request, response, adminSession, store) {
- const targetUserId = String(pickParam(request, 'targetUserId', 'inspectUserId') || '').trim();
- const limit = safeLimit(pickParam(request, 'limit'), 100, 500);
- const where = targetUserId
- ? { projectKey: 'video-workflow', ownerId: targetUserId }
- : { projectKey: 'video-workflow' };
- const rows = await listAll(store, VIDEO_WORKFLOW_CLASSES.fileAsset, where, limit * 20);
- const users = {};
- for (const row of rows) {
- const userId = row.ownerId || '';
- if (!userId) continue;
- if (!users[userId]) {
- users[userId] = {
- userId,
- totalFiles: 0,
- totalBytes: 0,
- byKind: {},
- latestCreatedAt: '',
- };
- }
- addAssetStats(users[userId], row);
- }
- return response.json({
- code: 200,
- success: true,
- data: {
- adminUserId: adminSession.userId,
- targetUserId,
- users: Object.values(users)
- .sort((a, b) => b.totalBytes - a.totalBytes || b.totalFiles - a.totalFiles)
- .slice(0, limit),
- generatedAt: new Date().toISOString(),
- },
- });
- }
- async function bindAsset(request, response, userId, store) {
- const assetId = requiredText(request, 'assetId', 'id');
- const row = await store.findFirst(VIDEO_WORKFLOW_CLASSES.fileAsset, parseUserWhere(userId, { objectId: assetId }), { limit: 1 });
- if (!row?.objectId) return response.json({ code: 200, success: true, data: null });
- const bizType = String(pickParam(request, 'bizType') || '').trim();
- const bizId = String(pickParam(request, 'bizId') || '').trim();
- const sourceModule = String(pickParam(request, 'sourceModule') || '').trim();
- const patch = {};
- if (bizType) patch.bizType = bizType;
- if (bizId) patch.bizId = bizId;
- if (sourceModule) patch.sourceModule = sourceModule;
- const updated = Object.keys(patch).length
- ? await store.update(VIDEO_WORKFLOW_CLASSES.fileAsset, row.objectId, patch)
- : row;
- return response.json({ code: 200, success: true, data: rowToAsset(updated) });
- }
- async function deleteAsset(request, response, userId, store) {
- const assetId = requiredText(request, 'assetId', 'id');
- const row = await store.findFirst(VIDEO_WORKFLOW_CLASSES.fileAsset, parseUserWhere(userId, { objectId: assetId }), { limit: 1 });
- if (!row?.objectId) return response.json({ code: 200, success: true, data: null });
- const updated = await store.update(VIDEO_WORKFLOW_CLASSES.fileAsset, row.objectId, { status: 'deleted' });
- return response.json({ code: 200, success: true, data: rowToAsset(updated) });
- }
- async function purgeAsset(request, response, userId, store) {
- const assetId = requiredText(request, 'assetId', 'id');
- const reason = String(pickParam(request, 'reason') || '').trim();
- if (!reason) return response.json({ code: 400, success: false, error: '清理文件资产必须提供 reason' });
- const row = await store.findFirst(VIDEO_WORKFLOW_CLASSES.fileAsset, parseUserWhere(userId, { objectId: assetId }), { limit: 1 });
- if (!row?.objectId) return response.json({ code: 200, success: true, data: null });
- const updated = await store.update(VIDEO_WORKFLOW_CLASSES.fileAsset, row.objectId, {
- status: 'purged',
- metadata: { ...normalizeObject(row.metadata), purged: true, purgedAt: new Date().toISOString(), reason },
- });
- return response.json({ code: 200, success: true, data: rowToAsset(updated) });
- }
- function readAssetInput(request) {
- return {
- spaceId: String(pickParam(request, 'spaceId') || ''),
- url: String(pickParam(request, 'url') || ''),
- bucket: String(pickParam(request, 'bucket') || ''),
- mimeType: String(pickParam(request, 'mimeType', 'contentType') || ''),
- kind: normalizeKind(String(pickParam(request, 'kind') || '')),
- sizeBytes: Math.max(0, Number(pickParam(request, 'sizeBytes', 'size') || 0)),
- sha256: String(pickParam(request, 'sha256') || '').slice(0, 128),
- sourceModule: String(pickParam(request, 'sourceModule') || ''),
- bizType: String(pickParam(request, 'bizType') || ''),
- bizId: String(pickParam(request, 'bizId') || ''),
- status: String(pickParam(request, 'status') || 'active'),
- metadata: normalizeObject(pickParam(request, 'metadata') || {}),
- };
- }
- function rowToAsset(row) {
- return {
- assetId: row.objectId,
- id: row.objectId,
- userId: row.ownerId || '',
- spaceId: row.spaceId || '',
- qiniuKey: row.qiniuKey,
- url: row.url || '',
- bucket: row.bucket || '',
- mimeType: row.mimeType || '',
- kind: row.kind || '',
- sizeBytes: Number(row.sizeBytes || 0),
- sha256: row.sha256 || '',
- sourceModule: row.sourceModule || '',
- bizType: row.bizType || '',
- bizId: row.bizId || '',
- status: row.status || 'active',
- metadata: normalizeObject(row.metadata),
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- };
- }
- async function listAll(store, className, where, maxRows) {
- const all = [];
- const pageSize = 1000;
- for (let skip = 0; skip < maxRows; skip += pageSize) {
- const rows = await store.list(className, where, { order: '-createdAt', limit: Math.min(pageSize, maxRows - skip), skip });
- all.push(...rows);
- if (rows.length < pageSize || all.length >= maxRows) break;
- }
- return all.slice(0, maxRows);
- }
- function buildStats(rows) {
- const byKind = {};
- let total = 0;
- let totalBytes = 0;
- for (const row of rows) {
- const aggregate = { totalFiles: 0, totalBytes: 0, byKind, latestCreatedAt: '' };
- addAssetStats(aggregate, row);
- total += 1;
- totalBytes += Number(row.sizeBytes || 0);
- }
- return { total, totalBytes, byKind, generatedAt: new Date().toISOString() };
- }
- function addAssetStats(target, row) {
- const kind = row.kind || 'unknown';
- const status = row.status || 'active';
- const bytes = Number(row.sizeBytes || 0);
- if (!target.byKind[kind]) target.byKind[kind] = { total: 0, bytes: 0, byStatus: {} };
- target.byKind[kind].total += 1;
- target.byKind[kind].bytes += bytes;
- target.byKind[kind].byStatus[status] = (target.byKind[kind].byStatus[status] || 0) + 1;
- target.totalFiles += 1;
- target.totalBytes += bytes;
- const latest = row.createdAt || '';
- if (latest && String(latest) > String(target.latestCreatedAt || '')) target.latestCreatedAt = latest;
- }
- function assertUserScopedKey(qiniuKey, userId) {
- const expected = `users/${userId}/`;
- if (!String(qiniuKey || '').startsWith(expected)) {
- const error = new Error('File key does not belong to current user partition');
- error.status = 403;
- throw error;
- }
- }
- function normalizeKind(kind) {
- if (['image', 'video', 'audio', 'export', 'snapshot'].includes(kind)) return kind;
- return 'image';
- }
- function requiredText(request, ...names) {
- const value = pickParam(request, ...names);
- const text = String(value || '').trim();
- if (!text) {
- const error = new Error(`Missing ${names[0]}`);
- error.status = 400;
- throw error;
- }
- return text;
- }
- function pickParam(request, ...names) {
- const sources = [request.params, request.body, request.query, request];
- for (const src of sources) {
- if (!src || typeof src !== 'object') continue;
- for (const name of names) {
- const value = src[name];
- if (value !== undefined && value !== null && value !== '') return value;
- }
- }
- return null;
- }
- function normalizeObject(value) {
- if (typeof value === 'string') {
- try { return JSON.parse(value); } catch { return {}; }
- }
- if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
- return value;
- }
- function safeLimit(value, fallback, max) {
- const parsed = parseInt(value || fallback, 10);
- return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), max) : fallback;
- }
|