"feat: 同步功能增强与配置优化,支持多类型同步及日志分级"
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { Config, SyncList } from './type.ts';
|
||||
import { Config, SyncList, SyncConfigType, SyncConfig } from './type.ts';
|
||||
import { fileIsExist } from '@/uitls/file.ts';
|
||||
import { getHash } from '@/uitls/hash.ts';
|
||||
import glob from 'fast-glob';
|
||||
|
||||
export type SyncOptions = {
|
||||
dir?: string;
|
||||
@@ -28,6 +30,15 @@ export class SyncBase {
|
||||
const filepath = path.join(dir, filename);
|
||||
if (!fileIsExist(filepath)) throw new Error('config file not found');
|
||||
const config = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
|
||||
const sync = config.sync || {};
|
||||
const keys = Object.keys(sync);
|
||||
const newConfigSync: any = {};
|
||||
for (let key of keys) {
|
||||
const keyPath = path.join(dir, key);
|
||||
const newKey = path.relative(dir, keyPath);
|
||||
newConfigSync[newKey] = sync[key];
|
||||
}
|
||||
config.sync = newConfigSync;
|
||||
this.config = config;
|
||||
return config;
|
||||
} catch (err) {
|
||||
@@ -35,10 +46,15 @@ export class SyncBase {
|
||||
return {} as Config;
|
||||
}
|
||||
}
|
||||
|
||||
async getSyncList(): Promise<SyncList[]> {
|
||||
async canDone(syncType: SyncConfigType, type?: SyncConfigType) {
|
||||
if (syncType === 'sync') return true;
|
||||
return syncType === type;
|
||||
}
|
||||
async getSyncList(opts?: { getFile?: boolean }): Promise<SyncList[]> {
|
||||
const config = this.config!;
|
||||
const sync = config?.sync || {};
|
||||
let sync = config?.sync || {};
|
||||
const syncDirectory = await this.getSyncDirectoryList();
|
||||
sync = this.getMergeSync(sync, syncDirectory.sync);
|
||||
const syncKeys = Object.keys(sync);
|
||||
const baseURL = this.baseURL;
|
||||
const syncList = syncKeys.map((key) => {
|
||||
@@ -52,21 +68,92 @@ export class SyncBase {
|
||||
return false;
|
||||
};
|
||||
if (typeof value === 'string') {
|
||||
const auth = checkAuth(value, baseURL);
|
||||
const type = auth ? 'sync' : 'none';
|
||||
return {
|
||||
key,
|
||||
type: type as any,
|
||||
filepath,
|
||||
url: value,
|
||||
auth: checkAuth(value, baseURL),
|
||||
auth,
|
||||
};
|
||||
}
|
||||
const auth = checkAuth(value.url, baseURL);
|
||||
const type = auth ? 'sync' : 'none';
|
||||
return {
|
||||
key,
|
||||
filepath,
|
||||
...value,
|
||||
type: value?.type ?? type,
|
||||
auth: checkAuth(value.url, baseURL),
|
||||
};
|
||||
});
|
||||
|
||||
if (opts?.getFile) {
|
||||
return this.getSyncListFile(syncList);
|
||||
}
|
||||
return syncList;
|
||||
}
|
||||
getMergeSync(sync: Config['sync'] = {}, fileSync: Config['sync'] = {}) {
|
||||
const syncFileSyncKeys = Object.keys(fileSync);
|
||||
const syncKeys = Object.keys(sync);
|
||||
const keys = [...syncKeys, ...syncFileSyncKeys];
|
||||
const obj: Config['sync'] = {};
|
||||
for (let key of keys) {
|
||||
const value = sync[key] ?? fileSync[key];
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
async getSyncDirectoryList() {
|
||||
const config = this.config;
|
||||
const syncDirectory = config?.syncDirectory || [];
|
||||
let obj: Record<string, string> = {};
|
||||
const keys: string[] = [];
|
||||
for (let item of syncDirectory) {
|
||||
const { registry, ignore = [], files, replace = {} } = item;
|
||||
const cwd = this.#dir;
|
||||
const glob_files = await glob(files, {
|
||||
ignore,
|
||||
onlyFiles: true,
|
||||
cwd,
|
||||
dot: true,
|
||||
absolute: true,
|
||||
});
|
||||
for (let file of glob_files) {
|
||||
const key = path.relative(cwd, file);
|
||||
const _registryURL = new URL(registry);
|
||||
const replaceKeys = Object.keys(replace);
|
||||
let newKey = key;
|
||||
for (let replaceKey of replaceKeys) {
|
||||
if (newKey.startsWith(replaceKey)) {
|
||||
newKey = key.replace(replaceKey, replace[replaceKey]);
|
||||
}
|
||||
}
|
||||
const pathname = path.join(_registryURL.pathname, newKey);
|
||||
_registryURL.pathname = pathname;
|
||||
keys.push(key);
|
||||
obj[key] = _registryURL.toString();
|
||||
}
|
||||
}
|
||||
return { sync: obj, keys };
|
||||
}
|
||||
async getSyncListFile(syncList: SyncList[]) {
|
||||
let syncListFile: SyncList[] = [];
|
||||
for (let item of syncList) {
|
||||
const { filepath, auth } = item;
|
||||
if (filepath && fileIsExist(filepath) && auth) {
|
||||
syncListFile.push({
|
||||
...item,
|
||||
exist: true,
|
||||
hash: getHash(filepath),
|
||||
});
|
||||
} else {
|
||||
syncListFile.push({ ...item, exist: false });
|
||||
}
|
||||
}
|
||||
return syncListFile;
|
||||
}
|
||||
getHash = getHash;
|
||||
async getDir(filepath: string, check = false) {
|
||||
const dir = path.dirname(filepath);
|
||||
if (check) {
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
export type SyncConfigType = 'sync' | 'download' | 'upload' | 'none';
|
||||
export type SyncConfig = {
|
||||
type?: 'sync'; // 是否可以同步
|
||||
type?: SyncConfigType; // 是否可以同步
|
||||
url: string; // 文件具体的 url 的地址
|
||||
};
|
||||
export type SyncDirectory = {
|
||||
/**
|
||||
* 忽略的目录或则文件,默认忽略 node_modules 使用 fast-glob 去匹配,
|
||||
* 当同步文件夹的时候生效
|
||||
**/
|
||||
ignore?: string[];
|
||||
/**
|
||||
* 合并路径的源地址,https://kevisual.xiongxiao.me/root/ai/kevisual
|
||||
*/
|
||||
registry?: string;
|
||||
files?: string[];
|
||||
replace?: Record<string, string>;
|
||||
};
|
||||
export interface Config {
|
||||
name?: string; // 项目名称
|
||||
version?: string; // 项目版本号
|
||||
ignore?: string[]; // 忽略的目录或则文件,默认忽略 node_modules 使用 fast-glob 去匹配
|
||||
|
||||
registry?: string; // 项目仓库地址
|
||||
user?: string; // 同步用户,否则会自动 query 一次
|
||||
metadata?: Record<string, any>; // 元数据, 统一的配置
|
||||
syncDirectory: SyncDirectory[];
|
||||
sync: {
|
||||
[key: string]: SyncConfig | string;
|
||||
};
|
||||
}
|
||||
|
||||
export type SyncList = {
|
||||
key?: string;
|
||||
filepath: string;
|
||||
exist?: boolean;
|
||||
hash?: string;
|
||||
/**
|
||||
* 是否需要鉴权, baseURL 为 kevisual 服务时,需要鉴权
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { program as app, Command } from '@/program.ts';
|
||||
import { SyncBase } from './modules/base.ts';
|
||||
import { baseURL } from '@/module/query.ts';
|
||||
import { baseURL, storage } from '@/module/query.ts';
|
||||
import { fetchLink } from '@/module/download/install.ts';
|
||||
import fs from 'node:fs';
|
||||
import { upload } from '@/module/download/upload.ts';
|
||||
import { logger } from '@/module/logger.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
|
||||
const command = new Command('sync')
|
||||
.option('-d --dir <dir>')
|
||||
@@ -10,21 +13,76 @@ const command = new Command('sync')
|
||||
.action(() => {
|
||||
console.log('同步项目');
|
||||
});
|
||||
const syncUpload = new Command('upload').description('上传项目').action(() => {
|
||||
console.log('上传项目');
|
||||
});
|
||||
const syncUpload = new Command('upload')
|
||||
.option('-d --dir <dir>', '配置目录')
|
||||
.option('-s --share <share>', '共享设置')
|
||||
.description('上传项目')
|
||||
.action(async (opts) => {
|
||||
console.log('上传项目');
|
||||
const sync = new SyncBase({ baseURL: baseURL });
|
||||
const syncList = await sync.getSyncList({ getFile: true });
|
||||
logger.debug(syncList);
|
||||
const nodonwArr: (typeof syncList)[number][] = [];
|
||||
const token = storage.getItem('token');
|
||||
const meta: Record<string, string> = {
|
||||
...sync.config.metadata,
|
||||
};
|
||||
if (opts.share) {
|
||||
meta.share = opts.share;
|
||||
}
|
||||
for (const item of syncList) {
|
||||
if (!item.auth || !item.exist) {
|
||||
nodonwArr.push(item);
|
||||
continue;
|
||||
}
|
||||
if (!sync.canDone(item.type, 'upload')) {
|
||||
nodonwArr.push(item);
|
||||
continue;
|
||||
}
|
||||
const res = await upload({
|
||||
token,
|
||||
file: fs.readFileSync(item.filepath),
|
||||
url: item.url,
|
||||
needHash: true,
|
||||
hash: item.hash,
|
||||
meta,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
logger.info('上传成功', item.key, chalk.green(item.url));
|
||||
}
|
||||
logger.debug(res);
|
||||
}
|
||||
if (nodonwArr.length) {
|
||||
logger.warn('以下文件未上传\n', nodonwArr.map((item) => item.key).join(','));
|
||||
}
|
||||
});
|
||||
const syncDownload = new Command('download')
|
||||
.option('-d --dir <dir>', '配置目录')
|
||||
.description('下载项目')
|
||||
.action(async () => {
|
||||
console.log('下载项目');
|
||||
const sync = new SyncBase({ baseURL: baseURL });
|
||||
const syncList = await sync.getSyncList();
|
||||
console.log(syncList);
|
||||
logger.debug(syncList);
|
||||
const nodonwArr: (typeof syncList)[number][] = [];
|
||||
for (const item of syncList) {
|
||||
const { content } = await fetchLink(item.url, { setToken: item.auth, returnContent: true });
|
||||
await sync.getDir(item.filepath, true);
|
||||
fs.writeFileSync(item.filepath, content);
|
||||
if (!sync.canDone(item.type, 'download')) {
|
||||
nodonwArr.push(item);
|
||||
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) {
|
||||
logger.warn('以下文件未下载', nodonwArr.map((item) => item.key).join(','));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user