347 lines
9.4 KiB
JavaScript
347 lines
9.4 KiB
JavaScript
const db = require('../../database/database');
|
|
const AdminRepository = require('../../repositories/admin/AdminRepository');
|
|
const { s3 } = require('../../utils/exoscaleUploader');
|
|
const { ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
|
const { logger } = require('../../middleware/logger');
|
|
const path = require('path');
|
|
|
|
async function executeDump(sql) {
|
|
const conn = await db.getMultiStatementConnection();
|
|
try {
|
|
const [rows, fields] = await conn.query(sql);
|
|
return { rows, fields };
|
|
} finally {
|
|
try { await conn.end(); } catch (_) {}
|
|
}
|
|
}
|
|
|
|
async function listAllKeys(prefix) {
|
|
const keys = [];
|
|
let token = undefined;
|
|
do {
|
|
const res = await s3.send(new ListObjectsV2Command({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Prefix: prefix,
|
|
ContinuationToken: token
|
|
}));
|
|
const batch = (res && res.Contents ? res.Contents : [])
|
|
.map(item => item && item.Key)
|
|
.filter(Boolean);
|
|
keys.push(...batch);
|
|
token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
|
|
} while (token);
|
|
return keys;
|
|
}
|
|
|
|
async function listTopLevelFolders(prefix) {
|
|
const folders = [];
|
|
let token = undefined;
|
|
do {
|
|
const res = await s3.send(new ListObjectsV2Command({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Prefix: prefix,
|
|
Delimiter: '/',
|
|
ContinuationToken: token
|
|
}));
|
|
const batch = (res && res.CommonPrefixes ? res.CommonPrefixes : [])
|
|
.map(item => item && item.Prefix)
|
|
.filter(Boolean);
|
|
folders.push(...batch);
|
|
token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
|
|
} while (token);
|
|
return folders;
|
|
}
|
|
|
|
function getTimestamp() {
|
|
const d = new Date();
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
}
|
|
|
|
async function objectExists(key) {
|
|
try {
|
|
await s3.send(new HeadObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Key: key
|
|
}));
|
|
return true;
|
|
} catch (e) {
|
|
const status = e && (e.$metadata && e.$metadata.httpStatusCode);
|
|
if (status === 404) return false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function appendTimestampToKey(key) {
|
|
const ext = path.posix.extname(key);
|
|
const base = ext ? key.slice(0, -ext.length) : key;
|
|
return `${base}-${getTimestamp()}${ext}`;
|
|
}
|
|
|
|
async function getUsersForScan() {
|
|
const conn = await db.getConnection();
|
|
try {
|
|
return await AdminRepository.getUserList(conn);
|
|
} finally {
|
|
try { conn.release(); } catch (_) {}
|
|
}
|
|
}
|
|
|
|
function getDisplayName(user) {
|
|
if (!user) return null;
|
|
if ((user.user_type || '').toLowerCase() === 'company') return user.company_name || null;
|
|
const first = user.first_name || '';
|
|
const last = user.last_name || '';
|
|
const full = `${first} ${last}`.trim();
|
|
return full || null;
|
|
}
|
|
|
|
async function listFolderStructureIssues() {
|
|
const users = await getUsersForScan();
|
|
const results = [];
|
|
let scannedUsers = 0;
|
|
|
|
for (const user of users) {
|
|
const userType = (user.user_type || '').toLowerCase();
|
|
if (!['personal', 'company'].includes(userType)) continue;
|
|
scannedUsers += 1;
|
|
|
|
const contractCategory = userType === 'company' ? 'company' : 'personal';
|
|
const basePrefix = `contracts/${contractCategory}/${user.id}/`;
|
|
const keys = await listAllKeys(basePrefix);
|
|
if (!keys.length) continue;
|
|
|
|
const hasContractFolder = keys.some(k => k.startsWith(`${basePrefix}contract/`));
|
|
const hasGdprFolder = keys.some(k => k.startsWith(`${basePrefix}gdpr/`));
|
|
if (hasContractFolder && hasGdprFolder) continue;
|
|
|
|
results.push({
|
|
userId: user.id,
|
|
email: user.email,
|
|
userType,
|
|
name: getDisplayName(user),
|
|
contractCategory,
|
|
basePrefix,
|
|
totalObjects: keys.length,
|
|
hasContractFolder,
|
|
hasGdprFolder
|
|
});
|
|
}
|
|
|
|
return {
|
|
list: results,
|
|
meta: {
|
|
scannedUsers,
|
|
invalidCount: results.length
|
|
}
|
|
};
|
|
}
|
|
|
|
async function createFolderStructure({ userId } = {}) {
|
|
const { list } = await listFolderStructureIssues();
|
|
const targetId = userId ? Number(userId) : null;
|
|
const targets = targetId ? list.filter(u => u.userId === targetId) : list;
|
|
const results = [];
|
|
let createdTotal = 0;
|
|
let errorCount = 0;
|
|
|
|
for (const target of targets) {
|
|
const prefix = target.basePrefix;
|
|
const keys = await listAllKeys(prefix);
|
|
const hasContractFolder = keys.some(k => k.startsWith(`${prefix}contract/`));
|
|
const hasGdprFolder = keys.some(k => k.startsWith(`${prefix}gdpr/`));
|
|
|
|
let created = 0;
|
|
const errors = [];
|
|
|
|
const ensureFolder = async (folderKey) => {
|
|
try {
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Key: folderKey,
|
|
Body: ''
|
|
}));
|
|
created += 1;
|
|
} catch (e) {
|
|
errorCount += 1;
|
|
const message = e && (e.message || e.toString());
|
|
logger.error('DevManagementService.createFolderStructure:create_failed', { folderKey, error: message });
|
|
errors.push({ folderKey, message });
|
|
}
|
|
};
|
|
|
|
if (!hasContractFolder) await ensureFolder(`${prefix}contract/`);
|
|
if (!hasGdprFolder) await ensureFolder(`${prefix}gdpr/`);
|
|
|
|
createdTotal += created;
|
|
results.push({
|
|
userId: target.userId,
|
|
created,
|
|
errors
|
|
});
|
|
}
|
|
|
|
return {
|
|
results,
|
|
meta: {
|
|
processedUsers: targets.length,
|
|
createdTotal,
|
|
errorCount
|
|
}
|
|
};
|
|
}
|
|
|
|
async function listLooseFiles() {
|
|
const users = await getUsersForScan();
|
|
const results = [];
|
|
let scannedUsers = 0;
|
|
|
|
for (const user of users) {
|
|
const userType = (user.user_type || '').toLowerCase();
|
|
if (!['personal', 'company'].includes(userType)) continue;
|
|
scannedUsers += 1;
|
|
|
|
const contractCategory = userType === 'company' ? 'company' : 'personal';
|
|
const basePrefix = `contracts/${contractCategory}/${user.id}/`;
|
|
const keys = await listAllKeys(basePrefix);
|
|
if (!keys.length) continue;
|
|
|
|
const looseKeys = keys
|
|
.filter(k => !!k)
|
|
.filter(k => !k.endsWith('/'))
|
|
.filter(k => !k.startsWith(`${basePrefix}contract/`))
|
|
.filter(k => !k.startsWith(`${basePrefix}gdpr/`));
|
|
|
|
if (!looseKeys.length) continue;
|
|
|
|
results.push({
|
|
userId: user.id,
|
|
email: user.email,
|
|
userType,
|
|
name: getDisplayName(user),
|
|
contractCategory,
|
|
basePrefix,
|
|
looseObjects: looseKeys.length,
|
|
sampleKeys: looseKeys.slice(0, 5)
|
|
});
|
|
}
|
|
|
|
return {
|
|
list: results,
|
|
meta: {
|
|
scannedUsers,
|
|
looseCount: results.length
|
|
}
|
|
};
|
|
}
|
|
|
|
async function moveLooseFilesToContract({ userId } = {}) {
|
|
const { list } = await listLooseFiles();
|
|
const targetId = userId ? Number(userId) : null;
|
|
const targets = targetId ? list.filter(u => u.userId === targetId) : list;
|
|
const results = [];
|
|
let movedTotal = 0;
|
|
let errorCount = 0;
|
|
|
|
for (const target of targets) {
|
|
const prefix = target.basePrefix;
|
|
const keys = await listAllKeys(prefix);
|
|
const toMove = keys
|
|
.filter(k => !!k)
|
|
.filter(k => !k.endsWith('/'))
|
|
.filter(k => !k.startsWith(`${prefix}contract/`))
|
|
.filter(k => !k.startsWith(`${prefix}gdpr/`));
|
|
|
|
let moved = 0;
|
|
let skipped = 0;
|
|
const errors = [];
|
|
|
|
for (const key of toMove) {
|
|
const baseName = path.posix.basename(key);
|
|
let destKey = `${prefix}contract/${baseName}`;
|
|
if (await objectExists(destKey)) {
|
|
destKey = appendTimestampToKey(destKey);
|
|
}
|
|
if (destKey === key) {
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
try {
|
|
const copySource = `${process.env.EXOSCALE_BUCKET}/${encodeURIComponent(key)}`;
|
|
await s3.send(new CopyObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
CopySource: copySource,
|
|
Key: destKey
|
|
}));
|
|
await s3.send(new DeleteObjectCommand({
|
|
Bucket: process.env.EXOSCALE_BUCKET,
|
|
Key: key
|
|
}));
|
|
moved += 1;
|
|
} catch (e) {
|
|
errorCount += 1;
|
|
const message = e && (e.message || e.toString());
|
|
logger.error('DevManagementService.moveLooseFilesToContract:move_failed', { key, destKey, error: message });
|
|
errors.push({ key, destKey, message });
|
|
}
|
|
}
|
|
|
|
movedTotal += moved;
|
|
results.push({
|
|
userId: target.userId,
|
|
moved,
|
|
skipped,
|
|
errors
|
|
});
|
|
}
|
|
|
|
return {
|
|
results,
|
|
meta: {
|
|
processedUsers: targets.length,
|
|
movedTotal,
|
|
errorCount
|
|
}
|
|
};
|
|
}
|
|
|
|
async function listGhostDirectories() {
|
|
const users = await getUsersForScan();
|
|
const userIds = new Set(users.map(u => Number(u.id)).filter(n => Number.isFinite(n)));
|
|
const results = [];
|
|
|
|
const categories = ['personal', 'company'];
|
|
for (const category of categories) {
|
|
const base = `contracts/${category}/`;
|
|
const prefixes = await listTopLevelFolders(base);
|
|
for (const prefix of prefixes) {
|
|
const idPart = prefix.replace(base, '').replace(/\/$/, '');
|
|
if (!/^[0-9]+$/.test(idPart)) continue;
|
|
const uid = Number(idPart);
|
|
if (!userIds.has(uid)) {
|
|
results.push({
|
|
userId: uid,
|
|
contractCategory: category,
|
|
basePrefix: prefix
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
list: results,
|
|
meta: {
|
|
ghostCount: results.length
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
executeDump,
|
|
listFolderStructureIssues,
|
|
createFolderStructure,
|
|
listLooseFiles,
|
|
moveLooseFilesToContract,
|
|
listGhostDirectories
|
|
};
|