corp-transfer/scripts/migrate-corpid-receiver.js
2026-04-03 10:26:45 +08:00

199 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
const express = require("express");
const crypto = require("crypto");
const { MongoClient } = require("mongodb");
const { EJSON } = require("bson");
const CONFIG = {
// 目标环境 MongoDB内网
targetMongoUri: "mongodb://root:ykt%26GK87892@10.16.146.81:27017/admin",
// receiver 监听端口(与内网 nginx proxy_pass 对应)
port: 8081,
// 迁移鉴权 token请改成高强度随机串
token: "ca764511bdace06c64f22bb97dd19911bcc4f0c9a07b2211cbbc1d43e9271657",
};
const args = parseArgs(process.argv.slice(2));
const PORT = Number(args.port || CONFIG.port);
const TARGET_MONGO_URI = args.targetMongoUri || CONFIG.targetMongoUri;
const TOKEN = args.token || CONFIG.token;
if (!TARGET_MONGO_URI) {
console.error("[receiver] Missing TARGET_MONGO_URI in environment");
process.exit(1);
}
if (!TOKEN) {
console.error("[receiver] Missing migration token");
process.exit(1);
}
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i += 1) {
const a = argv[i];
if (a === "--port") out.port = argv[++i];
else if (a === "--targetMongoUri") out.targetMongoUri = argv[++i];
else if (a === "--token") out.token = argv[++i];
}
return out;
}
const app = express();
app.use(express.json({ limit: "30mb" }));
let client;
const appliedIndexes = new Set();
function safeDecode(value) {
return EJSON.deserialize(value, { relaxed: false });
}
function auth(req, res, next) {
const incoming = req.headers["x-migration-token"];
if (!incoming || incoming !== TOKEN) {
return res.status(401).json({ success: false, message: "unauthorized" });
}
next();
}
function getJobId(corpId) {
return crypto.createHash("sha1").update(String(corpId)).digest("hex");
}
async function ensureClient() {
if (client) return client;
client = new MongoClient(TARGET_MONGO_URI, {
maxPoolSize: 30,
minPoolSize: 2,
connectTimeoutMS: 30000,
socketTimeoutMS: 60000,
serverSelectionTimeoutMS: 30000,
});
await client.connect();
return client;
}
app.get("/health", (_req, res) => {
res.json({ success: true, service: "migration-receiver", port: PORT });
});
app.post("/migrate/init", auth, async (req, res) => {
try {
const { corpId } = req.body || {};
if (!corpId) {
return res.status(400).json({ success: false, message: "corpId required" });
}
await ensureClient();
return res.json({ success: true, jobId: getJobId(corpId) });
} catch (err) {
return res.status(500).json({ success: false, message: err.message });
}
});
app.post("/migrate/indexes", auth, async (req, res) => {
try {
const { dbName, collectionName, indexes } = req.body || {};
if (!dbName || !collectionName || !Array.isArray(indexes)) {
return res.status(400).json({ success: false, message: "invalid payload" });
}
const cli = await ensureClient();
const col = cli.db(dbName).collection(collectionName);
let created = 0;
for (const rawIdx of indexes) {
const idx = safeDecode(rawIdx);
if (!idx || idx.name === "_id_" || !idx.key) continue;
const cacheKey = `${dbName}.${collectionName}.${idx.name}`;
if (appliedIndexes.has(cacheKey)) continue;
const options = { ...idx };
delete options.key;
delete options.v;
delete options.ns;
await col.createIndex(idx.key, options);
appliedIndexes.add(cacheKey);
created += 1;
}
return res.json({ success: true, created });
} catch (err) {
return res.status(500).json({ success: false, message: err.message });
}
});
app.post("/migrate/chunk", auth, async (req, res) => {
try {
const { dbName, collectionName, docs } = req.body || {};
if (!dbName || !collectionName || !Array.isArray(docs)) {
return res.status(400).json({ success: false, message: "invalid payload" });
}
const cli = await ensureClient();
const col = cli.db(dbName).collection(collectionName);
if (!docs.length) {
return res.json({ success: true, upserted: 0, modified: 0 });
}
const operations = docs.map((raw) => {
const doc = safeDecode(raw);
if (!doc || typeof doc !== "object") {
throw new Error("invalid document in chunk");
}
if (!doc._id) {
throw new Error("_id is required for each document");
}
return {
replaceOne: {
filter: { _id: doc._id },
replacement: doc,
upsert: true,
},
};
});
const result = await col.bulkWrite(operations, { ordered: false });
return res.json({
success: true,
upserted: result.upsertedCount || 0,
modified: result.modifiedCount || 0,
matched: result.matchedCount || 0,
});
} catch (err) {
return res.status(500).json({ success: false, message: err.message });
}
});
app.post("/migrate/finish", auth, async (req, res) => {
const { corpId } = req.body || {};
return res.json({ success: true, corpId, jobId: getJobId(corpId || "") });
});
app.listen(PORT, () => {
console.log(`[receiver] Listening on 0.0.0.0:${PORT}`);
});
process.on("SIGINT", async () => {
try {
if (client) await client.close();
} finally {
process.exit(0);
}
});
process.on("SIGTERM", async () => {
try {
if (client) await client.close();
} finally {
process.exit(0);
}
});