import { program as app, Command } from '@/program.ts'; import { SyncBase } from './modules/base.ts'; import { baseURL, query, storage } from '@/module/query.ts'; import { fetchLink, fetchAiList } from '@/module/download/install.ts'; import fs from 'node:fs'; import { upload } from '@/module/download/upload.ts'; import { logger, printClickableLink } from '@/module/logger.ts'; import { chalk } from '@/module/chalk.ts'; import path from 'node:path'; import { fileIsExist } from '@/uitls/file.ts'; const command = new Command('sync') .option('-d --dir ') .description('同步项目') .action(() => { console.log('同步项目'); }); const syncUpload = new Command('upload') .option('-d --dir ', '配置目录') .option('-c --config ', '配置文件的名字', 'kevisual.json') .option('-f --file ', '操作的对应的文件名') .description('上传项目, 上传需要和registry的地址同步。') .action(async (opts) => { console.log('上传项目'); const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config }); const syncList = await sync.getSyncList({ getFile: true }); logger.debug(syncList); const nodonwArr: (typeof syncList)[number][] = []; const token = storage.getItem('token'); const meta: Record = { ...sync.config.metadata, }; const filepath = sync.getRelativePath(opts.file); const newInfos = []; for (const item of syncList) { if (!item.auth || !item.exist) { nodonwArr.push(item); continue; } if (!sync.canDone(item.type, 'upload')) { nodonwArr.push(item); continue; } if (filepath && item.filepath !== filepath.absolute) { continue; } const res = await upload({ token, file: fs.readFileSync(item.filepath), url: item.url, needHash: true, hash: item.hash, meta: item.metadata ?? meta, }); if (res.code === 200) { if (res.data?.isNew) { newInfos.push(['上传成功', item.key, chalk.green(item.url), chalk.green('文件上传')]); } else if (res.data?.isNewMeta) { newInfos.push(['上传成功', item.key, chalk.green(item.url), chalk.green('元数据更新')]); } else { // 文件未更新 logger.debug('上传成功', item.key, chalk.green(item.url), chalk.blue('文件未更新')); } } logger.debug(res); } if (newInfos.length) { logger.info('上传成功的文件\n'); newInfos.forEach((item) => { logger.info(...item); }); } if (nodonwArr.length && !filepath) { logger.warn('以下文件未上传\n', nodonwArr.map((item) => item.key).join(',')); } }); const syncDownload = new Command('download') .option('-d --dir ', '配置目录') .option('-c --config ', '配置文件的名字', 'kevisual.json') .option('-f --file ', '操作的对应的文件名') .description('下载项目') .action(async (opts) => { const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config }); const syncList = await sync.getSyncList(); logger.debug(syncList); const nodonwArr: (typeof syncList)[number][] = []; const filepath = sync.getRelativePath(opts.file); for (const item of syncList) { if (!sync.canDone(item.type, 'download')) { nodonwArr.push(item); continue; } if (filepath && item.filepath !== filepath.absolute) { continue; } const hash = sync.getHash(item.filepath); const { content, status } = await fetchLink(item.url, { setToken: item.auth, returnContent: true, hash }); if (status === 200) { await sync.getDir(item.filepath, true); fs.writeFileSync(item.filepath, content); logger.info('下载成功', item.key, chalk.green(item.url)); } else if (status === 304) { logger.info('文件未修改', item.key, chalk.green(item.url)); } else { logger.error('下载失败', item.key, chalk.red(item.url)); } } if (nodonwArr.length && !filepath) { logger.warn('以下文件未下载', nodonwArr.map((item) => item.key).join(',')); } }); const syncList = new Command('list') .option('-d --dir ', '配置目录') .option('-c --config ', '配置文件的名字', 'kevisual.json') .option('-a --all', '显示所有的文件') .description('列出同步列表') .action(async (opts) => { const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config }); const syncList = await sync.getSyncList(); logger.debug(syncList); logger.info('同步列表\n'); syncList.forEach((item) => { if (opts.all) { logger.info(item); } else { logger.info(chalk.green(printClickableLink({ url: item.url, text: item.key, print: false })), chalk.gray(item.type)); } }); }); const syncCreateList = new Command('create') .option('-d --dir ', '配置目录') .option('-c --config ', '配置文件的名字', 'kevisual.json') .option('-o --output ', '输出文件') .description('创建文件, 获取远程的目录列表,然后创建新的配置文件') .action(async (opts) => { const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config }); const syncList = await sync.getSyncList(); logger.debug(syncList); logger.info('同步列表\n'); let newSync = {}; syncList.forEach((item) => { logger.info(chalk.blue(item.key), chalk.gray(item.type), chalk.green(item.url)); newSync[item.key] = item.url; }); const newJson = { ...sync.config }; newJson.sync = newSync; const filepath = sync.getRelativePath(opts.output); if (filepath) { logger.debug('输出文件', filepath); fs.writeFileSync(filepath.absolute, JSON.stringify(newJson, null, 2)); } else { logger.info('输出内容\n'); logger.info(newJson); } }); const clone = new Command('clone') .option('-d --dir ', '配置目录') .option('-c --config ', '配置文件的名字', 'kevisual.json') .option('-i --link ', '克隆链接, 比 kevisual.json 优先级更高') .description('检查目录') .action(async (opts) => { const link = opts.link || ''; const sync = new SyncBase({ dir: opts.dir, baseURL: baseURL, configFilename: opts.config }); if (link) { const res = await query.fetchText(link); if (res.code === 200) { fs.writeFileSync(sync.configPath, JSON.stringify(res.data, null, 2)); } sync.init() } const syncList = await sync.getSyncList(); logger.debug(syncList); logger.info('检查目录\n'); const checkList = await sync.getCheckList(); logger.info('检查列表', checkList); for (const item of checkList) { if (!item.auth) { continue; } if (!item.enabled) { logger.info('提示:', item.key, chalk.yellow('未启用')); continue; } const res = await fetchAiList(item.url, { recursive: true }); if (res.code === 200) { const data = res?.data || []; let matchObjectList = data.filter((dataItem) => { // 把 pathname 和 path 合并成一个路径 dataItem.pathname = path.join(item.key || '', dataItem.path); return dataItem; }); matchObjectList = sync.getMatchList({ ignore: item.ignore, matchObjectList }).matchObjectList; const matchList = matchObjectList .map((item2) => { const rp = sync.getRelativePath(item2.pathname); if (!rp) return false; if (rp.absolute.endsWith('gitignore.txt')) { // 修改为 .gitignore const newPath = rp.absolute.replace('gitignore.txt', '.gitignore'); rp.absolute = newPath; rp.relative = path.relative(sync.dir, newPath); } else if (rp.absolute.endsWith('.dot')) { const filename = path.basename(rp.absolute, '.dot'); const newPath = path.join(path.dirname(rp.absolute), `.${filename}`); rp.absolute = newPath; rp.relative = path.relative(sync.dir, newPath); } return { ...item2, relative: rp.relative, absolute: rp.absolute }; }) .filter((i) => i); for (const matchItem of matchList) { if (!matchItem) continue; let needDownload = true; let hash = ''; await sync.getDir(matchItem.absolute, true); logger.debug('文件路径', matchItem.absolute); if (fileIsExist(matchItem.absolute)) { hash = sync.getHash(matchItem.absolute); if (hash !== matchItem.etag) { logger.error('文件不一致', matchItem.pathname, chalk.red(matchItem.url), chalk.red('文件不一致')); } else { needDownload = false; logger.info('文件一致', matchItem.pathname, chalk.green(matchItem.url), chalk.green('文件一致')); } } if (needDownload) { const { content, status } = await fetchLink(matchItem.url, { setToken: item.auth, returnContent: true, hash }); if (status === 200) { fs.writeFileSync(matchItem.absolute, content); logger.info('下载成功', matchItem.pathname, chalk.green(matchItem.url)); } else if (status === 304) { logger.info('文件未修改', matchItem.pathname, chalk.green(matchItem.url)); } else { logger.error('下载失败', matchItem.pathname, chalk.red(matchItem.url)); } } } } else { logger.error('检查失败', item.url, res.code); break; } } }); command.addCommand(syncUpload); command.addCommand(syncDownload); command.addCommand(syncList); command.addCommand(syncCreateList); command.addCommand(clone); app.addCommand(command);