This commit is contained in:
熊潇 2025-05-07 22:43:18 +08:00
commit 652e71c4a8
20 changed files with 1895 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
node_modules
dist
pack-dist
logs
.env*
!.en*example
kevisual-sync.db

13
kevisual.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.0.1",
"description": "Sync Projects",
"ignore": [
"node_modules",
"dist"
],
"sync": {
"./tsconfig.json": {
"url": "https://kevisual.cn/root/ai/tsconfig.json"
}
}
}

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "@kevisual/sync",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "bun src/test/index.ts "
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.2",
"type": "module",
"devDependencies": {
"@kevisual/db": "^0.0.1",
"@kevisual/router": "^0.0.13",
"@kevisual/types": "^0.0.9",
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.2.12",
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.15.13",
"commander": "^13.1.0",
"crypto-js": "^4.2.0",
"eventemitter3": "^5.0.1",
"fast-glob": "^3.3.3",
"sequelize": "^6.37.7",
"sqlite3": "^5.1.7"
},
"pnpm": {
"onlyBuiltDependencies": [
"sqlite3"
]
}
}

1473
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
src/app.ts Normal file
View File

@ -0,0 +1,6 @@
import { QueryRouterServer } from '@kevisual/router';
import { SyncConfig } from './modules/sync-config/config.ts';
export const app = new QueryRouterServer();
export const syncConfig = new SyncConfig();

47
src/db/file-model.ts Normal file
View File

@ -0,0 +1,47 @@
import { DBModel, DataTypes, SyncOptions } from '@kevisual/db';
import { Stat } from './stat.ts';
export class FileModel extends DBModel {
declare id: string;
declare filepath: string; // 是唯一的
declare filename: string;
declare stat: Stat;
declare hash: string;
declare cwd: string;
static async initModel(sequelize: any, syncOpts?: SyncOptions) {
FileModel.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
filepath: {
type: DataTypes.STRING,
allowNull: true,
},
filename: {
type: DataTypes.STRING,
allowNull: true,
},
stat: {
type: DataTypes.JSON,
allowNull: true,
},
hash: {
type: DataTypes.STRING,
allowNull: true,
},
},
{
sequelize,
modelName: 'file',
},
);
if (syncOpts) {
await FileModel.sync(syncOpts);
}
}
}

4
src/db/manage.ts Normal file
View File

@ -0,0 +1,4 @@
import { DataSource } from '@kevisual/db';
import { FileModel } from './file-model.ts';
export const dataSource = new DataSource();

72
src/db/model.ts Normal file
View File

@ -0,0 +1,72 @@
import { DBModel, DataTypes, SyncOptions } from '@kevisual/db';
import { Stat } from './stat.ts';
export class SyncModel extends DBModel {
declare id: string;
/**
* URL
*/
declare url: string;
declare syncData: Object;
declare remoteData: Object;
declare markId: string;
declare filepath: string; // 是唯一的
declare filename: string;
declare stat: Stat;
declare hash: string;
static async initModel(sequelize: any, syncOpts?: SyncOptions) {
SyncModel.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
url: {
type: DataTypes.STRING,
allowNull: true,
},
syncData: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: {},
},
remoteData: {
type: DataTypes.JSON,
allowNull: true,
defaultValue: {},
},
markId: {
type: DataTypes.UUID,
allowNull: true,
},
filepath: {
type: DataTypes.STRING,
allowNull: true,
},
filename: {
type: DataTypes.STRING,
allowNull: true,
},
stat: {
type: DataTypes.JSON,
allowNull: true,
},
hash: {
type: DataTypes.STRING,
allowNull: true,
},
},
{
sequelize,
modelName: 'sync',
},
);
if (syncOpts) {
await SyncModel.sync(syncOpts);
}
}
}

23
src/db/stat.ts Normal file
View File

@ -0,0 +1,23 @@
import fs from 'node:fs';
export type Stat = {
dev: number;
ino: number;
mode: number;
nlink: number;
uid: number;
gid: number;
rdev: number;
size: number;
blksize: number;
blocks: number;
atimeMs: number;
mtimeMs: number;
ctimeMs: number;
birthtimeMs: number;
};
export const getStat = async (path: string): Promise<Stat> => {
const _stat = await fs.promises.stat(path);
return JSON.parse(JSON.stringify(_stat));
};

4
src/index.ts Normal file
View File

@ -0,0 +1,4 @@
import { app, syncConfig } from './app.ts';
import './routes/index.ts';
export { app, syncConfig };

View File

@ -0,0 +1,56 @@
import { EventEmitter } from 'eventemitter3';
import { KevisualConfig, readKevisualConfig, writeKevisualConfig } from './kevisual.ts';
import { createSqlite } from '@kevisual/db';
const KEVISUAL_CONFIG_FILENAME = 'kevisual.json';
const DB_FILENAME = 'kevisual-sync.db';
import path from 'path';
export const initConfigPath = (rootPath: string) => {
return {
workPath: path.resolve(rootPath),
dbPath: path.join(rootPath, DB_FILENAME),
configPath: path.join(rootPath, KEVISUAL_CONFIG_FILENAME),
};
};
export type SyncConfigOpts = {
workPath?: string;
init?: boolean;
emitter?: EventEmitter;
};
export class SyncConfig {
workPath: string;
emitter: EventEmitter;
isInit: boolean = false;
configPath: ReturnType<typeof initConfigPath>;
config: KevisualConfig;
sequelize: ReturnType<typeof createSqlite>;
constructor(opts?: SyncConfigOpts) {
this.workPath = opts?.workPath || process.cwd();
if (opts?.init) this.init();
this.emitter = opts?.emitter ?? new EventEmitter();
this.configPath = initConfigPath(this.workPath);
}
async init() {
this.isInit = true;
this.config = await readKevisualConfig(this.configPath.configPath);
this.emitter.emit('init', true);
}
async writeConfig(config: KevisualConfig) {
this.config = { ...this.config, ...config };
await writeKevisualConfig(this.configPath.configPath, this.config);
return this.config;
}
async initDB() {
if (this.sequelize) return this.sequelize;
console.log('init db', this.configPath.dbPath);
this.sequelize = createSqlite({ storage: this.configPath.dbPath, logging: false });
return this.sequelize;
}
async onInit() {
if (this.isInit) return true;
return await new Promise((resolve) => {
this.emitter.once('init', resolve);
});
}
}

View File

@ -0,0 +1,26 @@
import fs from 'node:fs';
export type KevisualConfig = {
ignore?: string[];
sync?: {
[key: string]: {
url: string;
};
};
};
export const readKevisualConfig = async (path: string): Promise<KevisualConfig> => {
try {
const data = await fs.promises.readFile(path, 'utf-8');
return JSON.parse(data);
} catch (e) {
return {};
}
};
export const writeKevisualConfig = async (path: string, config: KevisualConfig): Promise<KevisualConfig> => {
let _config = await readKevisualConfig(path);
_config = { ..._config, ...config };
await fs.promises.writeFile(path, JSON.stringify(_config, null, 2));
return _config;
};

View File

@ -0,0 +1 @@
import './list.ts'

65
src/routes/files/list.ts Normal file
View File

@ -0,0 +1,65 @@
import { app, syncConfig } from '@/app.ts';
import FastGlob from 'fast-glob';
import { MD5 } from 'crypto-js';
import fs from 'node:fs';
import { getStat } from '@/db/stat.ts';
import { FileModel } from '@/db/file-model.ts';
app
.route({
path: 'file',
key: 'list',
})
.define(async (ctx) => {
const files = await FastGlob(['**/*'], {
cwd: syncConfig.workPath,
onlyFiles: true,
absolute: false,
ignore: ['node_modules'],
});
ctx.body = files;
})
.addTo(app);
app
.route({
path: 'file',
key: 'init',
})
.define(async (ctx) => {
const files = await FastGlob(['**/*'], {
cwd: syncConfig.workPath,
onlyFiles: true,
absolute: false,
ignore: ['node_modules'],
});
const result: any[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const content = fs.readFileSync(file, 'utf-8');
const hash = MD5(content).toString();
const stat = await getStat(file);
const filename = file.split('/').pop();
result.push({ filepath: file, filename: filename, hash, stat });
}
const sequelize = await syncConfig.initDB();
await FileModel.initModel(sequelize, { alter: true });
await FileModel.destroy({ where: {} })
const create = await FileModel.bulkCreate(result);
console.log(create);
ctx.body = result;
})
.addTo(app);
app
.route({
path: 'file',
key: 'get-sql',
})
.define(async (ctx) => {
const sequelize = await syncConfig.initDB();
await FileModel.initModel(sequelize);
const sql = await FileModel.findAll();
ctx.body = sql.map((item) => item.toJSON());
})
.addTo(app);

1
src/routes/index.ts Normal file
View File

@ -0,0 +1 @@
import './files/index.ts'

3
src/test/cmd.ts Normal file
View File

@ -0,0 +1,3 @@
import { app, syncConfig } from '@/index.ts';
import { program, Command } from 'commander';
export { program, app, syncConfig, Command };

View File

@ -0,0 +1,30 @@
import { app, syncConfig, program, Command } from '../cmd.ts';
import fs from 'node:fs';
import util from 'node:util';
const main = async () => {
const res = await app.queryRoute({ path: 'file', key: 'list' });
console.log(res);
const kv = syncConfig.configPath.configPath;
const stat = fs.statSync(kv);
console.log(JSON.parse(JSON.stringify(stat)));
};
const cmd = new Command('file-list').description('list files').action(main);
program.addCommand(cmd);
const initMain = async () => {
const res = await app.queryRoute({ path: 'file', key: 'init' });
console.log(util.inspect(res, { depth: 10, colors: true }));
};
const cmd2 = new Command('file-init').description('list files').action(initMain);
program.addCommand(cmd2);
const getSql = async () => {
const res = await app.queryRoute({ path: 'file', key: 'get-sql' });
console.log(util.inspect(res, { depth: 10, colors: true }));
};
const cmd3 = new Command('file-get-sql').description('list files').action(getSql);
program.addCommand(cmd3);

View File

@ -0,0 +1,3 @@
import path from 'node:path'
console.log('resolve', path.resolve('./file-list.ts').replace(process.cwd()+'/', ''))

4
src/test/index.ts Normal file
View File

@ -0,0 +1,4 @@
import { program } from './cmd.ts';
import './command/file-list.ts';
program.parse(process.argv);

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "@kevisual/types/json/backend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"node_modules/@types"
],
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
]
}