This commit is contained in:
刘嘉伟 2025-02-21 18:30:13 +08:00
parent 3ef92a8af9
commit 8d56faeb80
30 changed files with 1328 additions and 501 deletions

View File

@ -1,28 +1,20 @@
import electron, {
app,
dialog,
BrowserWindow,
ipcMain,
net,
shell
} from "electron";
import { app, BrowserWindow, dialog, ipcMain, net } from "electron";
import {
deleteVersionById,
getVersionById,
insertVersion,
listVersion
} from "../storage/version";
import frpReleasesJson from "../json/frp-releases.json";
import frpChecksums from "../json/frp_all_sha256_checksums.json";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
// import { calculateFileChecksum, formatBytes } from "../utils/FileUtils";
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const { download } = require("electron-dl");
const AdmZip = require("adm-zip");
import frpReleasesJson from "../json/frp-releases.json";
import frpChecksums from "../json/frp_all_sha256_checksums.json";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
import { calculateFileChecksum, formatBytes } from "../utils/file";
import { el } from "element-plus/es/locale";
const versionRelation = {
win32_x64: ["window", "amd64"],
@ -311,7 +303,7 @@ export const initGitHubApi = win => {
});
});
request.on("error", error => {
request.on("error", jerror => {
logError(
LogModule.GITHUB,
"Error occurred while requesting GitHub releases: " + error

View File

@ -8,13 +8,11 @@ class LogController extends BaseController {
constructor(logService: LogService) {
super();
this._logService = logService;
console.log("logService2", this._logService);
}
getFrpLogContent(req: ControllerRequest) {
console.log("logService3", this._logService);
getFrpLogContent(req: ControllerParam) {
this._logService.getFrpLogContent().then(data => {
req.event.reply(req.reply, success(data));
req.event.reply(req.channel, success(data));
});
}
@ -25,7 +23,7 @@ class LogController extends BaseController {
// });
// }
openFrpcLogFile(req: ControllerRequest) {
openFrpcLogFile(req: ControllerParam) {
this._logService.openFrpcLogFile().then(data => {
if (data) {
success(null);

View File

@ -1,17 +1,25 @@
import BaseController from "./BaseController";
import ServerService from "../service/ServerService";
import IpcMainEvent = Electron.IpcMainEvent;
import { success } from "../utils/response";
class ServerController extends BaseController {
serverService: ServerService;
private readonly _serverService: ServerService;
constructor(serverService: ServerService) {
super();
this.serverService = serverService;
this._serverService = serverService;
}
saveConfig(event: IpcMainEvent, ...args: any[]) {
console.log("test", args);
saveConfig(req: ControllerParam) {
console.log("save", req.args);
}
getServerConfig(req: ControllerParam) {
console.log("get", req.args);
this._serverService.getServerConfig().then(data => {
req.event.reply(req.channel, success(data));
});
}
}

View File

@ -0,0 +1,27 @@
import BaseController from "./BaseController";
import VersionService from "../service/VersionService";
import { success } from "../utils/response";
class VersionController extends BaseController {
private readonly _versionService: VersionService;
constructor(versionService: VersionService) {
super();
this._versionService = versionService;
}
getVersions(req: ControllerParam) {
this._versionService
.getFrpVersionsByGitHub()
.then(data => {
req.event.reply(req.channel, success(data));
})
.catch(() => {
this._versionService.getFrpVersionByLocalJson().then(localData => {
req.event.reply(req.channel, success(localData));
});
});
}
}
export default VersionController;

View File

@ -0,0 +1,24 @@
import path from "path";
import { app } from "electron";
import SecureUtils from "../utils/SecureUtils";
class GlobalConstant {
public static FRPC_STORAGE_FOLDER = "";
public static ZIP_EXT = ".zip";
public static GZ_EXT = ".gz";
public static TAR_GZ_EXT = ".tar.gz";
// public static APP_DATA_PATH = app.getPath("userData");
// public static DOWNLOAD_STORAGE_PATH = path.join(
// GlobalConstant.APP_DATA_PATH,
// SecureUtils.calculateMD5("download")
// );
//
// public static VERSION_STORAGE_PATH = path.join(
// GlobalConstant.APP_DATA_PATH,
// SecureUtils.calculateMD5("frpc")
// );
}
export default GlobalConstant;

View File

@ -2,73 +2,142 @@ import ServerController from "../controller/ServerController";
import ServerDao from "../dao/ServerDao";
import ServerService from "../service/ServerService";
import LogService from "../service/LogService";
import VersionService from "../service/VersionService";
import { BrowserWindow, ipcMain } from "electron";
import LogController from "../controller/LogController";
import VersionController from "../controller/VersionController";
import FileService from "../service/FileService";
import VersionDao from "../dao/VersionDao";
import GitHubService from "../service/GitHubService";
type IpcRouter = {
path: string;
reply: string;
controller: any;
instance: any;
export const ipcRouters: IpcRouters = {
SERVER: {
saveConfig: {
path: "server/saveConfig",
controller: "serverController.saveConfig"
},
getServerConfig: {
path: "server/getServerConfig",
controller: "serverController.getServerConfig"
}
},
LOG: {
getFrpLogContent: {
path: "log/getFrpLogContent",
controller: "logController.getFrpLogContent"
},
openFrpcLogFile: {
path: "log/openFrpcLogFile",
controller: "logController.openFrpcLogFile"
}
},
VERSION: {
getVersions: {
path: "version/getVersions",
controller: "versionController.getVersions"
}
}
};
export const listeners: Listeners = {
watchFrpcLog: {
listenerMethod: "logService.watchFrpcLog",
channel: "log:watchFrpcLog"
}
};
class IpcRouterConfigurate {
ipcRouters: Array<IpcRouter>;
private readonly _beans: Map<string, any> = new Map<string, any>();
private readonly _win: BrowserWindow;
constructor(win: BrowserWindow) {
this._win = win;
/**
* initBeans
* @private
*/
private initializeBeans() {
const serverDao = new ServerDao();
const versionDao = new VersionDao();
const fileService = new FileService();
const serverService = new ServerService(serverDao);
const gitHubService = new GitHubService();
const versionService = new VersionService(
versionDao,
fileService,
gitHubService
);
const logService = new LogService(fileService);
const serverController = new ServerController(serverService);
const versionController = new VersionController(versionService);
const logController = new LogController(logService);
logService.watchFrpcLog(win);
this.ipcRouters = [
{
path: "server/test",
reply: "server/test.hook",
controller: serverController.saveConfig,
instance: serverController
},
{
path: "log/getFrpLogContent",
reply: "log/getFrpLogContent.hook",
controller: logController.getFrpLogContent,
instance: logController
},
// {
// path: "log/watchFrpcLogContent",
// reply: "log/watchFrpcLogContent.hook",
// controller: logController.watchFrpcLogContent,
// instance: logController
// },
{
path: "log/openFrpcLogFile",
reply: "log/openFrpcLogFile.hook",
controller: logController.openFrpcLogFile,
instance: logController
}
];
this._beans.set("serverDao", serverDao);
this._beans.set("versionDao", versionDao);
this._beans.set("fileService", fileService);
this._beans.set("serverService", serverService);
this._beans.set("versionService", versionService);
this._beans.set("logService", logService);
this._beans.set("serverController", serverController);
this._beans.set("versionController", versionController);
this._beans.set("logController", logController);
}
init() {
this.ipcRouters.forEach(router => {
ipcMain.on(router.path, (event, args) => {
const req: ControllerRequest = {
win: this._win,
reply: router.reply,
event: event,
args: args
};
router.controller.call(router.instance, req);
/**
* initJob
* @private
*/
private initializeListeners() {
Object.keys(listeners).forEach(listenerKey => {
console.log(listenerKey, "listenerKey", listeners[listenerKey]);
const { listenerMethod, channel } = listeners[listenerKey];
const [beanName, method] = listenerMethod.split(".");
const bean = this._beans.get(beanName);
const listenerParam: ListenerParam = {
win: this._win,
channel: channel,
args: []
};
bean[method].call(bean, listenerParam);
});
console.log("initialize listeners success");
// this._beans.get("logService").watchFrpcLog(this._win);
}
/**
* initRouters
* @private
*/
private initializeRouters() {
Object.keys(ipcRouters).forEach(routerKey => {
const routerGroup = ipcRouters[routerKey];
Object.keys(routerGroup).forEach(method => {
const router = routerGroup[method];
ipcMain.on(router.path, (event, args) => {
const req: ControllerParam = {
win: this._win,
channel: `${router.path}:hook`,
event: event,
args: args
};
const [beanName, method] = router.controller.split(".");
const bean = this._beans.get(beanName);
bean[method].call(bean, req);
// bean[method].call(bean, req);
});
});
});
console.log("ipcRouter init success.");
}
/**
* constructor
* @param win mainWindows
*/
constructor(win: BrowserWindow) {
this._win = win;
this.initializeBeans();
this.initializeListeners();
this.initializeRouters();
}
}

View File

@ -1,24 +1,34 @@
import Datastore from "nedb";
import path from "path";
import { app } from "electron";
import Snowflakify from "snowflakify";
import GlobalConstant from "../core/GlobalConstant";
import PathUtils from "../utils/PathUtils";
interface BaseDaoInterface<T> {
db: Datastore;
// interface BaseDaoInterface<T> {
// db: Datastore;
//
// insert(t: T): Promise<T>;
//
// //
// updateById(id: string, t: T): Promise<T>;
//
// //
// // deleteById(id: string): void;
// //
// // findAll(): T[];
//
// findById(id: string): Promise<T>;
// }
insert(t: T): Promise<T>;
updateById(t: T): T;
deleteById(id: string): void;
findAll(): T[];
}
class BaseDao<T> implements BaseDaoInterface<T> {
db: Datastore;
class BaseDao<T> {
protected readonly db: Datastore;
constructor(dbName: string) {
const dbFilename = path.join(app.getPath("userData"), `${dbName}-v2.db`);
const dbFilename = path.join(
PathUtils.getAppData(),
`${dbName}-v2.db`
);
this.db = new Datastore({
autoload: true,
filename: dbFilename
@ -26,22 +36,72 @@ class BaseDao<T> implements BaseDaoInterface<T> {
// todo log
}
async insert(t: T): Promise<T> {
protected genId(): string {
return IdUtils.genUUID();
}
// async insert(t: T): Promise<T> {
// return new Promise<T>((resolve, reject) => {
// resolve(t);
// });
// }
//
insert(t: T): Promise<T> {
return new Promise<T>((resolve, reject) => {
resolve(t);
t["_id"] = this.genId();
this.db.insert(t, (err, document) => {
if (err) {
reject(err);
}
resolve(document);
});
});
}
updateById(t: T): T {
return null;
updateById(id: string, t: T): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.db.update(
{ _id: id },
t,
{ upsert: true },
(err, numberOfUpdated, upsert) => {
if (err) {
reject(err);
} else {
t["_id"] = id;
resolve(t);
// this.findById(id)
// .then(data => {
// resolve(t);
// })
// .catch(err2 => {
// reject(err2);
// });
}
}
);
});
}
deleteById(id: string): void {
return null;
}
//
// deleteById(id: string): void {
// return null;
// }
//
// findAll(): T[] {
// return null;
// }
findAll(): T[] {
return null;
findById(id: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.db.findOne({ _id: id }, (err, document) => {
if (err) {
reject(err);
} else {
resolve(document);
}
});
});
}
}

View File

@ -2,7 +2,19 @@ import BaseDao from "./BaseDao";
class ServerDao extends BaseDao<FrpcDesktopServer> {
constructor() {
super("config");
super("server");
}
exists(id: string): Promise<boolean> {
return new Promise((resolve, reject) => {
this.db.count({ _id: id }, (err, count) => {
if (err) {
reject(err);
} else {
resolve(count > 0);
}
});
});
}
}

View File

@ -0,0 +1,33 @@
import BaseDao from "./BaseDao";
class VersionDao extends BaseDao<FrpcVersion> {
constructor() {
super("version");
}
findByGithubReleaseId(githubReleaseId: number): Promise<FrpcVersion> {
return new Promise<FrpcVersion>((resolve, reject) => {
this.db.findOne({ githubReleaseId: githubReleaseId }, (err, document) => {
if (err) {
reject(err);
} else {
resolve(document);
}
});
});
}
exists(githubReleaseId: number): Promise<boolean> {
return new Promise((resolve, reject) => {
this.db.count({ githubReleaseId: githubReleaseId }, (err, count) => {
if (err) {
reject(err);
} else {
resolve(count > 0);
}
});
});
}
}
export default VersionDao;

View File

@ -193,7 +193,6 @@ app.whenReady().then(() => {
}
}
const ipcRouterConfig = new IpcRouterConfigurate(win);
ipcRouterConfig.init();
// Initialize APIs
try {
initGitHubApi(win);

View File

@ -1,15 +1,16 @@
import BaseDao from "../dao/BaseDao";
interface BaseServiceInterface<T> {
dao: BaseDao<T>;
// dao: BaseDao<T>;
}
class BaseService<T> implements BaseServiceInterface<T> {
dao: BaseDao<T>;
constructor(dao: BaseDao<T>) {
this.dao = dao;
}
// dao: BaseDao<T>;
//
// constructor(dao: BaseDao<T>) {
// this.dao = dao;
// }
}
export default BaseService;

View File

@ -1,9 +1,16 @@
import { shell } from "electron";
import path from "path";
import fs from "fs";
import zlib from "zlib";
import admZip from "adm-zip";
import GlobalConstant from "../core/GlobalConstant";
import { logError, logInfo, LogModule } from "../utils/log";
// import tar from "tar";
class FileService {
constructor() {}
openFile(filePath: string) {
openLocalFile(filePath: string) {
return new Promise<boolean>((resolve, reject) => {
shell
.openPath(filePath)
@ -19,6 +26,62 @@ class FileService {
});
});
}
decompressZipFile(zipFilePath: string, targetPath: string) {
if (!zipFilePath.endsWith(GlobalConstant.ZIP_EXT)) {
throw new Error("The file is not a .zip file");
}
if (!fs.existsSync(zipFilePath)) {
throw new Error("The file does not exist");
}
// const zipBasename = path.basename(zipFilePath, GlobalConstant.ZIP_EXT);
const targetFolder = path.join(targetPath, targetPath);
if (!fs.existsSync) {
// not exists. do mkdir
fs.mkdirSync(targetFolder, {
recursive: true
});
}
// starting unzip.
const zip = new admZip(zipFilePath);
zip.extractAllTo(targetPath, true); // true: cover exists file.
// todo 2025-02-21 return targetPath.
// const frpcPath = path.join("frp", path.basename(zipFilePath, zipExt));
}
decompressTarGzFile(tarGzPath: string, targetPath: string) {
// const targetFolder = path.join(targetPath, targetPath);
const unzip = zlib.createGunzip();
const readStream = fs.createReadStream(tarGzPath);
// if (!fs.existsSync(unzip)) {
// fs.mkdirSync(targetPath, { recursive: true, mode: 0o777 });
// }
readStream
.pipe(unzip)
.on("error", err => {
logError(LogModule.APP, `Error during gunzip: ${err.message}`);
})
// .pipe(
// tar
// .extract({
// cwd: targetPath,
// filter: filePath => path.basename(filePath) === "frpc"
// })
// .on("error", err => {
// logError(
// LogModule.APP,
// `Error extracting tar file: ${err.message}`
// );
// })
// )
.on("finish", () => {
const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz"));
logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
});
}
}
export default FileService;

View File

@ -0,0 +1,64 @@
class GitHubService {
constructor() {}
getGithubRepoAllReleases(githubRepo: string): Promise<Array<GithubRelease>> {
return new Promise((resolve, reject) => {
const { net } = require("electron");
const request = net.request({
method: "get",
url: `https://api.github.com/repos/${githubRepo}/releases?page=1&per_page=1000`
});
request.on("response", response => {
// logInfo(
// LogModule.GITHUB,
// `Received response with status code: ${response.statusCode}`
// );
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
if (response.statusCode === 200) {
this.parseGitHubVersion(responseData.toString())
.then(data => {
resolve(data);
})
.catch(err => reject(err));
// logInfo(
// LogModule.GITHUB,
// "Successfully retrieved GitHub release data."
// );
} else {
// logWarn(
// LogModule.GITHUB,
// "Failed to retrieve data, using local JSON instead. Status code: " +
// response.statusCode
// );
}
});
});
request.on("error", error => {
reject(error);
});
request.end();
});
}
parseGitHubVersion(
githubReleaseJsonStr: string
): Promise<Array<GithubRelease>> {
return new Promise<Array<GithubRelease>>(resolve => {
const githubReleases: Array<GithubRelease> =
JSON.parse(githubReleaseJsonStr);
resolve(githubReleases);
});
}
}
export default GitHubService;

View File

@ -1,6 +1,6 @@
import fs from "fs";
import path from "path";
import { app, BrowserWindow } from "electron";
import { app } from "electron";
import FileService from "./FileService";
import { success } from "../utils/response";
@ -27,13 +27,14 @@ class LogService {
});
}
watchFrpcLog(win: BrowserWindow) {
watchFrpcLog(listenerParam: ListenerParam) {
fs.watch(this._logPath, (eventType, filename) => {
if (eventType === "change") {
win.webContents.send(
"log/watchFrpcLogContent.hook",
console.log("change", eventType, listenerParam.channel);
listenerParam.win.webContents.send(
listenerParam.channel,
success(true)
)
);
} else {
}
});
@ -45,7 +46,7 @@ class LogService {
openFrpcLogFile(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
this._fileService
.openFile(this._logPath)
.openLocalFile(this._logPath)
.then(result => {
resolve(result);
})

View File

@ -2,11 +2,44 @@ import BaseService from "./BaseService";
import ServerDao from "../dao/ServerDao";
class ServerService extends BaseService<FrpcDesktopServer> {
private readonly _serverDao: ServerDao;
constructor(serverDao: ServerDao) {
super(serverDao);
super();
this._serverDao = serverDao;
}
saveServerConfig(frpcServer: FrpcDesktopServer) {}
saveServerConfig(frpcServer: FrpcDesktopServer): Promise<void> {
return new Promise((resolve, reject) => {
this._serverDao
.updateById("1", frpcServer)
.then(() => {
resolve();
})
.catch(err => reject(err));
});
}
getServerConfig(): Promise<FrpcDesktopServer> {
return new Promise((resolve, reject) => {
this._serverDao
.findById("1")
.then((frpcServer: FrpcDesktopServer) => {
resolve(frpcServer);
})
.catch(err => reject(err));
});
}
hasServerConfig(): Promise<boolean> {
return new Promise((resolve, reject) => {
this._serverDao
.exists("1")
.then(r => {
resolve(r);
})
.catch(err => reject(err));
});
}
}
export default ServerService;

View File

@ -0,0 +1,191 @@
import VersionDao from "../dao/VersionDao";
import BaseService from "./BaseService";
import GitHubService from "./GitHubService";
import FileService from "./FileService";
import frpReleasesJson from "../json/frp-releases.json";
import { download } from "electron-dl";
import { BrowserWindow } from "electron";
import GlobalConstant from "../core/GlobalConstant";
import path from "path";
import fs from "fs";
import FileUtils from "../utils/FileUtils";
import SecureUtils from "../utils/SecureUtils";
import PathUtils from "../utils/PathUtils";
/**
* arch mapping
*/
const versionMapping = {
win32_x64: ["window", "amd64"],
win32_arm64: ["window", "arm64"],
win32_ia32: ["window", "386"],
darwin_arm64: ["darwin", "arm64"],
darwin_x64: ["darwin", "amd64"],
darwin_amd64: ["darwin", "amd64"],
linux_x64: ["linux", "amd64"],
linux_arm64: ["linux", "arm64"]
};
class VersionService extends BaseService<FrpcVersion> {
private readonly _versionDao: VersionDao;
private readonly _fileService: FileService;
private readonly _gitHubService: GitHubService;
private readonly _currFrpArch: Array<string>;
private versions: Array<FrpcVersion> = [];
constructor(
versionDao: VersionDao,
fileService: FileService,
gitHubService: GitHubService
) {
super();
this._versionDao = versionDao;
this._gitHubService = gitHubService;
this._fileService = fileService;
const nodeVersion = `${process.platform}_${process.arch}`;
this._currFrpArch = versionMapping[nodeVersion];
}
downloadFrpVersion(githubReleaseId: number, onProgress: Function) {
return new Promise((resolve, reject) => {
const version = this.versions.find(
f => f.githubReleaseId === githubReleaseId
);
if (!version) {
reject(new Error("version not found"));
}
const url = version.browserDownloadUrl;
const downloadedFilePath = path.join(
PathUtils.getDownloadStoragePath(),
`${version.assetName}`
);
const versionFilePath = path.join(
PathUtils.getVersionStoragePath(),
SecureUtils.calculateMD5(version.name)
);
// const targetPath = path.resolve();
download(BrowserWindow.getFocusedWindow(), url, {
filename: `${version.assetName}`,
directory: PathUtils.getDownloadStoragePath(),
onProgress: progress => {
onProgress(progress);
},
onCompleted: () => {
if (fs.existsSync(versionFilePath)) {
fs.rmSync(versionFilePath, { recursive: true, force: true });
}
const ext = path.extname(version.assetName);
if (ext === GlobalConstant.ZIP_EXT) {
this._fileService.decompressZipFile(
downloadedFilePath,
versionFilePath
);
} else if (
ext === GlobalConstant.GZ_EXT &&
version.assetName.includes(GlobalConstant.TAR_GZ_EXT)
) {
this._fileService.decompressTarGzFile(
downloadedFilePath,
versionFilePath
);
}
version.localPath = versionFilePath;
this._versionDao.insert(version).then(data => {
resolve(data);
});
}
});
});
}
deleteFrpVersion() {}
getFrpVersionsByGitHub(): Promise<Array<FrpcVersion>> {
return new Promise<Array<FrpcVersion>>((resolve, reject) => {
this._gitHubService
.getGithubRepoAllReleases("fatedier/frp")
.then((releases: Array<GithubRelease>) => {
const versions: Array<FrpcVersion> =
this.githubRelease2FrpcVersion(releases);
this.versions = versions;
resolve(versions);
})
.catch(err => reject(err));
});
}
getFrpVersionByLocalJson(): Promise<Array<FrpcVersion>> {
return new Promise<Array<FrpcVersion>>(resolve => {
const versions = this.githubRelease2FrpcVersion(frpReleasesJson);
resolve(versions);
});
}
getFrpVersion() {}
private findCurrentArchitectureAsset(assets: Array<GithubAsset>) {
return assets.find((af: GithubAsset) => {
return this._currFrpArch.every(item => af.name.includes(item));
});
}
private githubRelease2FrpcVersion(
releases: Array<GithubRelease>
): Array<FrpcVersion> {
return releases
.filter(release => {
return this.findCurrentArchitectureAsset(release.assets);
})
.map(m => {
const asset = this.findCurrentArchitectureAsset(m.assets);
// m.assets.forEach((ma: GithubAsset) => {
// // if (asset) {
// // const absPath = path.join(
// // frpPath,
// // asset.name.replace(/(\.tar\.gz|\.zip)$/, "")
// // );
// // m.absPath = absPath;
// // m.download_completed = fs.existsSync(absPath);
// m.download_count = download_count;
// m.size = formatBytes(ma.size);
// // }
// });
// const asset = getAdaptiveAsset(m.id);
const download_count = m.assets.reduce(
(sum, item) => sum + item.download_count,
0
);
const v: FrpcVersion = {
githubReleaseId: m.id,
githubAssetId: asset.id,
githubCreatedAt: asset.created_at,
name: m.name,
assetName: asset.name,
versionDownloadCount: download_count,
assetDownloadCount: asset.download_count,
browserDownloadUrl: asset.browser_download_url,
downloaded: false,
localPath: "",
size: FileUtils.formatBytes(asset.size)
};
return v;
});
}
// private async frpcVersionExists(githubReleaseId: number): boolean {
// const version = await this._versionDao.findByGithubReleaseId(
// githubReleaseId
// );
//
// if (version) {
// return fs.existsSync(version.localPath);
// }
// return false;
// }
}
export default VersionService;

View File

@ -0,0 +1,23 @@
import { createHash } from "crypto";
import fs from "fs";
class FileUtils {
public static formatBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return "0 Bytes";
const k = 1024; // 1 KB = 1024 Bytes
const dm = decimals < 0 ? 0 : decimals; // Ensure decimal places are not less than 0
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); // Calculate unit index
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; // Return formatted string
}
public static calculateFileChecksum(filePath: string) {
const fileBuffer = fs.readFileSync(filePath);
const hash = createHash("sha256");
hash.update(fileBuffer);
return hash.digest("hex");
}
}
export default FileUtils;

39
electron/utils/IdUtils.ts Normal file
View File

@ -0,0 +1,39 @@
class IdUtils {
public static genUUID() {
if (typeof crypto === "object") {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
if (
typeof crypto.getRandomValues === "function" &&
typeof Uint8Array === "function"
) {
const callback = c => {
const num = Number(c);
return (
num ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
).toString(16);
};
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, callback);
}
}
let timestamp = new Date().getTime();
let perforNow =
(typeof performance !== "undefined" &&
performance.now &&
performance.now() * 1000) ||
0;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
let random = Math.random() * 16;
if (timestamp > 0) {
random = (timestamp + random) % 16 | 0;
timestamp = Math.floor(timestamp / 16);
} else {
random = (perforNow + random) % 16 | 0;
perforNow = Math.floor(perforNow / 16);
}
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16);
});
}
}

View File

@ -0,0 +1,26 @@
import SecureUtils from "./SecureUtils";
import { app } from "electron";
import path from "path";
class PathUtils {
public static getDownloadStoragePath() {
return path.join(
PathUtils.getAppData(),
SecureUtils.calculateMD5("download")
);
}
public static getVersionStoragePath() {
return path.join(
PathUtils.getAppData(),
SecureUtils.calculateMD5("frpc")
);
}
public static getAppData() {
return app.getPath("userData");
}
}
export default PathUtils;

View File

@ -0,0 +1,11 @@
import { createHash } from "crypto";
class SecureUtils {
public static calculateMD5(str: string): string {
const hash = createHash("md5");
hash.update(str);
return hash.digest("hex");
}
}
export default SecureUtils;

View File

@ -1,19 +0,0 @@
import { createHash } from "crypto";
export const formatBytes = (bytes: number, decimals: number = 2): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024; // 1 KB = 1024 Bytes
const dm = decimals < 0 ? 0 : decimals; // Ensure decimal places are not less than 0
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); // Calculate unit index
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; // Return formatted string
};
export const calculateFileChecksum = (filePath: string): string => {
const fs = require("fs");
const fileBuffer = fs.readFileSync(filePath);
const hash = createHash("sha256");
hash.update(fileBuffer);
return hash.digest("hex");
};

View File

@ -82,6 +82,7 @@
"intro.js": "^8.0.0-beta.1",
"isbinaryfile": "4.0.10",
"js-base64": "^3.7.7",
"snowflakify": "^1.0.5",
"tar": "^6.2.0",
"unused-filename": "^4.0.1",
"uuid": "^10.0.0"

42
src/utils/ipcUtils.ts Normal file
View File

@ -0,0 +1,42 @@
import { ipcRenderer } from "electron";
export const send = (router: IpcRouter) => {
console.log(router, "send.router");
ipcRenderer.send(router.path);
};
export const on = (router: IpcRouter, listerHandler: (data: any) => void) => {
ipcRenderer.on(`${router.path}:hook`, (event, args: ApiResponse<any>) => {
const { success, data, message } = args;
if (success) {
listerHandler(data);
} else {
// reject(new Error(message));
}
});
};
export const onListener = (
listener: Listener,
listerHandler: (data: any) => void
) => {
// return new Promise((resolve, reject) => {
ipcRenderer.on(`${listener.channel}`, (event, args: ApiResponse<any>) => {
const { success, data, message } = args;
if (success) {
listerHandler(data);
}
});
// });
};
export const removeRouterListeners = (router: IpcRouter) => {
ipcRenderer.removeAllListeners(`${router.path}:hook`);
};
export const removeRouterListeners2 = (listen: Listener) => {
ipcRenderer.removeAllListeners(`${listen.channel}`);
}
// export const removeAllListeners = (listen: Listener) => {
// ipcRenderer.removeAllListeners(`${listen.channel}:hook`);
// };

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,8 @@ import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useDebounceFn } from "@vueuse/core";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { on, send } from "@/utils/ipcUtils";
import ipcRouter, { ipcRouters } from "../../../electron/core/IpcRouter";
defineComponent({
name: "Download"
@ -122,8 +124,18 @@ const handleMirrorChange = () => {
};
onMounted(() => {
handleLoadVersions();
handleInitDownloadHook();
send(ipcRouters.VERSION.getVersions);
on(ipcRouters.VERSION.getVersions, (data) => {
console.log('versionData', data);
// versions.value = args.map(m => {
// m.published_at = moment(m.published_at).format("YYYY-MM-DD");
// return m as FrpVersion;
// }) as Array<FrpVersion>;
});
// handleLoadVersions();
// handleInitDownloadHook();
// ipcRenderer.invoke("process").then((r: any) => {
// console.log(r, "rrr");
// });

View File

@ -1,10 +1,18 @@
<script lang="ts" setup>
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ipcRenderer } from "electron";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { useDebounceFn } from "@vueuse/core";
import { ElMessage } from "element-plus";
import { ipcRouters, listeners } from "../../../electron/core/IpcRouter";
import {
on,
onListener,
removeAllListeners,
removeRouterListeners,
removeRouterListeners2,
send
} from "@/utils/ipcUtils";
defineComponent({
name: "Logger"
@ -36,12 +44,9 @@ const logLoading = ref(true);
// const isWatch = ref(false);
onMounted(() => {
ipcRenderer.send("log/getFrpLogContent");
ipcRenderer.on("log/getFrpLogContent.hook", (event, args) => {
const { success, data } = args;
if (success) {
loggerContent.value = formatLogContent(data);
}
send(ipcRouters.LOG.getFrpLogContent);
on(ipcRouters.LOG.getFrpLogContent, data => {
loggerContent.value = formatLogContent(data as string);
logLoading.value = false;
if (refreshStatus.value) {
//
@ -49,47 +54,23 @@ onMounted(() => {
type: "success",
message: "刷新成功"
});
} else {
// if (!isWatch.value) {
// // ipcRenderer.send("log/watchFrpcLogContent");
// isWatch.value = true;
// }
refreshStatus.value = false;
}
});
ipcRenderer.on("log/watchFrpcLogContent.hook", (event, args) => {
console.log(event,'eevent');
console.log("watchFrpcLogContent", args);
const { success, data } = args;
if (success && data) {
ipcRenderer.send("log/getFrpLogContent");
}
// if (args) {
// loggerContent.value = formatLogContent(args);
// }
on(ipcRouters.LOG.openFrpcLogFile, () => {
ElMessage({
type: "success",
message: "打开日志成功"
});
});
// ipcRenderer.on("log/watchFrpcLogContent.hook", (event, args) => {
// console.log("watchFrpcLogContent", args);
// const { success, data } = args;
// if (success && data) {
// ipcRenderer.send("log/getFrpLogContent");
// }
// // if (args) {
// // loggerContent.value = formatLogContent(args);
// // }
// });
ipcRenderer.on("log/openFrpcLogFile.hook", (event, args) => {
const { success } = args;
if (success) {
ElMessage({
type: "success",
message: "打开日志成功"
});
}
onListener(listeners.watchFrpcLog, data => {
console.log("onListener Data", data);
send(ipcRouters.LOG.getFrpLogContent);
});
});
const openLocalLog = useDebounceFn(() => {
ipcRenderer.send("log/openFrpcLogFile");
send(ipcRouters.LOG.openFrpcLogFile);
}, 1000);
const refreshLog = useDebounceFn(() => {
@ -100,12 +81,12 @@ const refreshLog = useDebounceFn(() => {
// });
refreshStatus.value = true;
logLoading.value = true;
ipcRenderer.send("log/getFrpLogContent");
send(ipcRouters.LOG.getFrpLogContent);
}, 300);
onUnmounted(() => {
ipcRenderer.removeAllListeners("log/getFrpLogContent.hook");
ipcRenderer.removeAllListeners("log/openFrpcLogFile.hook");
removeRouterListeners(ipcRouters.LOG.getFrpLogContent);
removeRouterListeners2(listeners.watchFrpcLog);
});
</script>
<template>

35
types/core.d.ts vendored
View File

@ -4,9 +4,40 @@ interface ApiResponse<T> {
message: string;
}
interface ControllerRequest {
interface ControllerParam {
win: BrowserWindow;
reply: string;
channel: string;
event: Electron.IpcMainEvent;
args: any[];
}
interface ListenerParam {
win: BrowserWindow;
channel: string;
args: any[];
}
type IpcRouter = {
path: string;
controller: string;
}
type Listener = {
channel: string;
listenerMethod: any;
};
enum IpcRouterKeys {
SERVER = "SERVER",
LOG = "LOG",
VERSION = "VERSION",
}
type IpcRouters = Record<
IpcRouterKeys,
{
[method: string]: IpcRouter;
}
>;
type Listeners = Record<string, Listener>;

19
types/frp.d.ts vendored
View File

@ -18,10 +18,27 @@ type WebServerConfig = {
pprofEnable: boolean;
};
type TransportTlsConfig = {
enable: boolean;
certFile: string;
keyFile: string;
trustedCaFile: string;
serverName: string;
disableCustomTLSFirstByte: boolean;
};
type TransportConfig = {
dialServerTimeout: number;
dialServerKeepalive: number;
poolCount: number;
tcpMux: boolean;
tcpMuxKeepaliveInterval: number;
protocol: string;
connectServerLocalIP: string;
proxyURL: string;
tls: TransportTlsConfig;
heartbeatInterval: number;
heartbeatTimeout: number;
};
interface FrpcCommonConfig {
@ -34,7 +51,7 @@ interface FrpcCommonConfig {
webServer: WebServerConfig;
transport: TransportConfig;
udpPacketSize: number;
// metadatas: MetadataConfig;
metadatas: Record<string, any>;
}
interface FrpcProxyConfig {

View File

@ -1,3 +1,29 @@
type FrpcDesktopProxy = FrpcProxyConfig & {};
type FrpcDesktopServer = FrpcCommonConfig & {};
interface FrpcSystemConfiguration {
launchAtStartup: boolean;
silentStartup: boolean;
autoConnectOnStartup: boolean;
}
type FrpcDesktopServer = FrpcCommonConfig & {
frpcVersion: number;
};
type FrpcVersion = {
githubReleaseId: number;
githubAssetId: number;
githubCreatedAt: string;
name: string;
assetName: string;
versionDownloadCount: number;
assetDownloadCount: number;
browserDownloadUrl: string;
downloaded: boolean;
localPath: string;
size: string;
};
type OpenSourceFrpcDesktopServer = FrpcDesktopServer & {
system: FrpcSystemConfiguration;
};

18
types/github.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
interface GithubAsset {
id: number;
name: string;
content_type: string;
download_count: number;
size: number;
browser_download_url: string;
created_at: string;
updated_at: string;
}
interface GithubRelease {
id: number;
name: string;
created_at: string;
published_at: string;
assets: GithubAsset[]
}