Skip to content

Commit 0c3ca0a

Browse files
committed
fix: add migration to clean up duplicate backup folders with batching
1 parent 34cefe4 commit 0c3ca0a

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
3+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4+
const MAX_ATTEMPTS = 10;
5+
const BATCH_SIZE = 100;
6+
const SLEEP_TIME_MS = 5000;
7+
8+
/** @type {import('sequelize-cli').Migration} */
9+
module.exports = {
10+
async up(queryInterface) {
11+
let totalDeleted = 0;
12+
let batchCount = 0;
13+
let attempts = 0;
14+
15+
console.info(`Batch size: ${BATCH_SIZE} duplicate groups per batch`);
16+
17+
console.info('Starting cleanup of duplicate backup folders...');
18+
19+
const deleteQuery = `
20+
WITH duplicate_groups AS (
21+
SELECT
22+
plain_name,
23+
bucket,
24+
user_id,
25+
MIN(id) as id_to_keep
26+
FROM folders
27+
WHERE
28+
created_at >= '2025-12-17 14:16:00'
29+
AND created_at <= '2026-01-05 21:50:00'
30+
AND parent_id IS NULL
31+
AND parent_uuid IS NULL
32+
AND deleted = false
33+
AND removed = false
34+
AND plain_name IS NOT NULL
35+
GROUP BY plain_name, bucket, user_id
36+
HAVING COUNT(*) > 1
37+
LIMIT ${BATCH_SIZE}
38+
),
39+
folders_to_delete AS (
40+
SELECT f.id
41+
FROM folders f
42+
INNER JOIN duplicate_groups dg
43+
ON f.plain_name = dg.plain_name
44+
AND f.bucket = dg.bucket
45+
AND f.user_id = dg.user_id
46+
WHERE
47+
f.id != dg.id_to_keep
48+
AND NOT EXISTS (
49+
SELECT 1
50+
FROM files
51+
WHERE folder_id = f.id
52+
AND deleted = false
53+
)
54+
)
55+
UPDATE folders
56+
SET
57+
deleted = true,
58+
deleted_at = NOW(),
59+
removed = true,
60+
removed_at = NOW()
61+
FROM folders_to_delete
62+
WHERE folders.id = folders_to_delete.id
63+
AND folders.deleted = false
64+
RETURNING folders.id;
65+
`;
66+
67+
let hasMore = true;
68+
69+
while (hasMore) {
70+
try {
71+
const [results] = await queryInterface.sequelize.query(deleteQuery);
72+
const deletedInBatch = results.length;
73+
batchCount++;
74+
totalDeleted += deletedInBatch;
75+
attempts = 0;
76+
77+
console.info(
78+
`Batch ${batchCount}: Deleted ${deletedInBatch} folders (Total: ${totalDeleted})`,
79+
);
80+
81+
hasMore = deletedInBatch > 0;
82+
83+
if (hasMore) {
84+
await sleep(SLEEP_TIME_MS);
85+
}
86+
} catch (err) {
87+
attempts++;
88+
console.error(
89+
`[ERROR]: Error in batch ${batchCount} (attempt ${attempts}/${MAX_ATTEMPTS}): ${err.message}`,
90+
);
91+
92+
if (attempts >= MAX_ATTEMPTS) {
93+
console.error(
94+
'[ERROR]: Maximum retry attempts reached, exiting migration.',
95+
);
96+
break;
97+
}
98+
99+
await sleep(SLEEP_TIME_MS);
100+
}
101+
}
102+
103+
console.info('\n=== Cleanup Complete ===');
104+
console.info(`Total batches processed: ${batchCount}`);
105+
console.info(`Total folders deleted: ${totalDeleted}`);
106+
},
107+
async down() {},
108+
};

0 commit comments

Comments
 (0)