feat(media): index e delete em lote de mídias (Wasabi, coordenado DB↔storage)

- StorageProvider.deleteObjects(paths): delete seletivo em lote (chunks de 1000),
  no provider concreto (Wasabi DeleteObjectsCommand + fallback local).
- GET /api/instances/:id/media-index: index/auditoria por chat/tipo/período
  (baseado no DB, com agregado byType, paginado).
- POST /api/instances/:id/media-batch-delete: delete coordenado DB↔Wasabi,
  dryRun por padrão (precisa dryRun:false p/ apagar); limpa mediaPath/mediaUrl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-28 14:52:25 +02:00
parent 809e378117
commit e47b64a5b5
6 changed files with 184 additions and 1 deletions
@@ -271,6 +271,42 @@ class UnifiedStorageProviderPlugin {
}
return { deleted: deletedCount };
}
/** Delete em lote seletivo: apaga exatamente os paths informados (não por prefixo). */
async deleteObjects(paths) {
const keys = paths.filter((p) => p && !p.startsWith('http'));
if (keys.length === 0)
return { deleted: 0 };
let deletedCount = 0;
// 1. Wasabi — em chunks de 1000 (limite do DeleteObjectsCommand)
if (this.s3 && this.bucket) {
for (let i = 0; i < keys.length; i += 1000) {
const chunk = keys.slice(i, i + 1000);
try {
await this.s3.send(new client_s3_1.DeleteObjectsCommand({
Bucket: this.bucket,
Delete: { Objects: chunk.map((Key) => ({ Key })), Quiet: true },
}));
deletedCount += chunk.length;
this.ctx.logger.info(`[UnifiedStorageProvider] ${chunk.length} objetos deletados do Wasabi (lote seletivo)`);
}
catch (err) {
this.ctx.logger.error(`[UnifiedStorageProvider] Erro no delete em lote do Wasabi: ${err.message}`);
}
}
}
// 2. Fallback local
for (const key of keys) {
const localPath = path_1.default.join(this.localFallbackDir, key);
try {
if (fs_1.default.existsSync(localPath))
fs_1.default.unlinkSync(localPath);
}
catch (err) {
this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao remover fallback local ${key}: ${err.message}`);
}
}
return { deleted: deletedCount };
}
async upload(payload) {
const { category, file } = payload;
if (!VALID_CATEGORIES.includes(category))
File diff suppressed because one or more lines are too long
@@ -301,6 +301,43 @@ class UnifiedStorageProviderPlugin implements PluginInstance, StorageProviderInt
return { deleted: deletedCount };
}
/** Delete em lote seletivo: apaga exatamente os paths informados (não por prefixo). */
async deleteObjects(paths: string[]): Promise<{ deleted: number }> {
const keys = paths.filter((p) => p && !p.startsWith('http'));
if (keys.length === 0) return { deleted: 0 };
let deletedCount = 0;
// 1. Wasabi — em chunks de 1000 (limite do DeleteObjectsCommand)
if (this.s3 && this.bucket) {
for (let i = 0; i < keys.length; i += 1000) {
const chunk = keys.slice(i, i + 1000);
try {
await this.s3.send(new DeleteObjectsCommand({
Bucket: this.bucket,
Delete: { Objects: chunk.map((Key) => ({ Key })), Quiet: true },
}));
deletedCount += chunk.length;
this.ctx.logger.info(`[UnifiedStorageProvider] ${chunk.length} objetos deletados do Wasabi (lote seletivo)`);
} catch (err: any) {
this.ctx.logger.error(`[UnifiedStorageProvider] Erro no delete em lote do Wasabi: ${err.message}`);
}
}
}
// 2. Fallback local
for (const key of keys) {
const localPath = path.join(this.localFallbackDir, key);
try {
if (fs.existsSync(localPath)) fs.unlinkSync(localPath);
} catch (err: any) {
this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao remover fallback local ${key}: ${err.message}`);
}
}
return { deleted: deletedCount };
}
async upload(payload: StorageUploadPayload): Promise<{ provider: string; path: string }> {
const { category, file } = payload;
if (!VALID_CATEGORIES.includes(category)) throw new Error(`Categoria inválida: ${category}`);