guanghulab/scripts/grid-db/sync-to-drive.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

273 lines
7.9 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.

/**
* scripts/grid-db/sync-to-drive.js
*
* 功能:将 grid-db/ 中的关键文件同步到 Google Drive 镜像目录
*
* 同步范围:
* grid-db/memory/DEV-XXX/*.json → Drive/mirror/memory/DEV-XXX/
* grid-db/outbox/latest/*.json → Drive/mirror/outbox/
* grid-db/rules/*.json → Drive/mirror/rules/
* grid-db/drive-index/*.json → Drive/mirror/(每个 DEV 的 index.json
*
* 同步策略:
* - 比较文件内容 MD5 hash仅同步有变更的文件节省 API 配额)
* - 文件不存在则创建,已存在则覆盖更新
* - Drive 中多出的文件不删除(防误删)
*
* 注意:
* - 不同步 interactions/ 和 training-lake/(数据量大 + 无需 Gemini 读取)
* - 不同步 inbox/ 和 processing/(系统内部流转区)
*
* 环境变量:
* - GDRIVE_CLIENT_ID: OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
* - DRIVE_FOLDER_ID: Drive 镜像根文件夹 ID
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
*/
const { google } = require('googleapis');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const GRID_DB_ROOT = path.join(__dirname, '../../grid-db');
// 同步目录映射:本地相对路径 → Drive 镜像子目录
const SYNC_DIRS = [
{ local: 'memory', drivePrefix: 'mirror/memory', recursive: true },
{ local: 'outbox/latest', drivePrefix: 'mirror/outbox', recursive: false },
{ local: 'rules', drivePrefix: 'mirror/rules', recursive: false },
{ local: 'drive-index', drivePrefix: 'mirror', recursive: false }
];
/**
* 计算文件内容的 MD5 哈希
*/
function md5(content) {
return crypto.createHash('md5').update(content).digest('hex');
}
/**
* 递归收集目录下的所有 JSON 文件
*/
function collectFiles(dirPath, recursive) {
const results = [];
if (!fs.existsSync(dirPath)) return results;
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory() && recursive) {
results.push(...collectFiles(fullPath, true));
} else if (entry.isFile() && entry.name.endsWith('.json')) {
results.push(fullPath);
}
}
return results;
}
/**
* 转义 Drive API 查询中的单引号
*/
function escapeQuery(str) {
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
/**
* 在 Drive 中查找或创建文件夹(按路径逐级创建)
*/
async function getOrCreateDriveFolder(drive, parentId, folderPath) {
const parts = folderPath.split('/').filter(Boolean);
let currentParentId = parentId;
for (const part of parts) {
const safePart = escapeQuery(part);
const query = `name='${safePart}' and '${currentParentId}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false`;
const res = await drive.files.list({
q: query,
fields: 'files(id, name)',
pageSize: 1
});
if (res.data.files && res.data.files.length > 0) {
currentParentId = res.data.files[0].id;
} else {
const createRes = await drive.files.create({
requestBody: {
name: part,
mimeType: 'application/vnd.google-apps.folder',
parents: [currentParentId]
},
fields: 'id'
});
currentParentId = createRes.data.id;
console.log(`[sync-to-drive] Created folder: ${part}`);
}
}
return currentParentId;
}
/**
* 在 Drive 目标文件夹中查找同名文件
*/
async function findFileInDrive(drive, folderId, fileName) {
const safeFileName = escapeQuery(fileName);
const query = `name='${safeFileName}' and '${folderId}' in parents and trashed=false`;
const res = await drive.files.list({
q: query,
fields: 'files(id, name, md5Checksum)',
pageSize: 1
});
if (res.data.files && res.data.files.length > 0) {
return res.data.files[0];
}
return null;
}
/**
* 上传或更新单个文件到 Drive
*/
async function uploadFile(drive, folderId, fileName, content) {
const existingFile = await findFileInDrive(drive, folderId, fileName);
const { Readable } = require('stream');
const media = {
mimeType: 'application/json',
body: Readable.from([content])
};
if (existingFile) {
// 检查内容是否变更Drive 对上传文件自动计算 MD5
const localHash = md5(content);
if (existingFile.md5Checksum === localHash) {
return { action: 'skipped', fileName };
}
await drive.files.update({
fileId: existingFile.id,
media
});
return { action: 'updated', fileName };
} else {
await drive.files.create({
requestBody: {
name: fileName,
parents: [folderId]
},
media,
fields: 'id'
});
return { action: 'created', fileName };
}
}
/**
* 主同步流程
*/
async function main() {
// 验证环境变量
const rootFolderId = process.env.DRIVE_FOLDER_ID;
if (!rootFolderId) {
console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping');
return;
}
// OAuth2 认证(统一入口)
let drive;
try {
const { getDriveClient } = require('./drive-auth');
drive = getDriveClient();
console.log('[sync-to-drive] ✅ OAuth2 credentials configured');
} catch (err) {
console.error(`[sync-to-drive] 🔴 OAuth2 auth failed: ${err.message}`);
process.exit(1);
}
// 预生成 Drive 索引文件
try {
const { main: generateIndex } = require('./generate-drive-index');
generateIndex();
} catch (err) {
console.log('[sync-to-drive] Index generation skipped (non-fatal):', err.message);
}
console.log('[sync-to-drive] Starting Grid-DB → Drive mirror sync');
let totalCreated = 0;
let totalUpdated = 0;
let totalSkipped = 0;
let totalErrors = 0;
// 遍历每个同步目录
for (const syncDir of SYNC_DIRS) {
const localDir = path.join(GRID_DB_ROOT, syncDir.local);
const files = collectFiles(localDir, syncDir.recursive);
if (files.length === 0) {
console.log(`[sync-to-drive] ${syncDir.local}: no files to sync`);
continue;
}
console.log(`[sync-to-drive] ${syncDir.local}: found ${files.length} file(s)`);
for (const filePath of files) {
try {
// 计算 Drive 中的目标路径
const relativePath = path.relative(localDir, filePath);
const dirParts = path.dirname(relativePath);
const fileName = path.basename(filePath);
// 构建 Drive 文件夹路径
let driveFolderPath = syncDir.drivePrefix;
if (dirParts !== '.') {
driveFolderPath = `${syncDir.drivePrefix}/${dirParts}`;
}
// 确保 Drive 文件夹存在
const targetFolderId = await getOrCreateDriveFolder(
drive, rootFolderId, driveFolderPath
);
// 读取文件内容并上传
const content = fs.readFileSync(filePath, 'utf8');
const result = await uploadFile(drive, targetFolderId, fileName, content);
if (result.action === 'created') {
totalCreated++;
console.log(`[sync-to-drive] + ${relativePath} (created)`);
} else if (result.action === 'updated') {
totalUpdated++;
console.log(`[sync-to-drive] ~ ${relativePath} (updated)`);
} else {
totalSkipped++;
}
} catch (err) {
totalErrors++;
console.error(`[sync-to-drive] ! ${filePath}: ${err.message}`);
}
}
}
// 输出统计
console.log('[sync-to-drive] Sync complete:');
console.log(` Created: ${totalCreated}`);
console.log(` Updated: ${totalUpdated}`);
console.log(` Skipped (unchanged): ${totalSkipped}`);
console.log(` Errors: ${totalErrors}`);
if (totalErrors > 0) {
process.exit(1);
}
}
main().catch(err => {
console.error('[sync-to-drive] Fatal error:', err.message);
process.exit(1);
});