frpc-desktop/electron/api/github.ts
2025-01-09 11:40:12 +08:00

599 lines
18 KiB
TypeScript
Raw 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.

import electron, {
app,
dialog,
BrowserWindow,
ipcMain,
net,
shell
} from "electron";
import {
deleteVersionById,
getVersionById,
insertVersion,
listVersion
} from "../storage/version";
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"],
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"]
};
const platform = process.platform;
const arch = process.arch;
let currArch = `${platform}_${arch}`;
const frpArch = versionRelation[currArch];
const unTarGZ = (tarGzPath: string, targetPath: string) => {
const tar = require("tar");
const unzip = zlib.createGunzip();
logInfo(
LogModule.APP,
`Starting to extract tar.gz: ${tarGzPath} to ${targetPath}`
);
const readStream = fs.createReadStream(tarGzPath);
if (!fs.existsSync(unzip)) {
fs.mkdirSync(targetPath, { recursive: true, mode: 0o777 });
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
}
readStream.on("error", err => {
logError(LogModule.APP, `Error reading tar.gz file: ${err.message}`);
});
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}`
);
});
return path.join("frp", path.basename(tarGzPath, ".tar.gz"));
};
const unZip = (zipPath: string, targetPath: string) => {
if (!fs.existsSync(path.join(targetPath, path.basename(zipPath, ".zip")))) {
fs.mkdirSync(path.join(targetPath, path.basename(zipPath, ".zip")), {
recursive: true
});
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
logInfo(
LogModule.APP,
`Created directory for zip extraction: ${path.basename(zipPath, ".zip")}`
);
}
logDebug(
LogModule.APP,
`Starting to unzip: ${zipPath} to target directory: ${targetPath}`
);
logInfo(LogModule.APP, `Starting to extract zip file: ${zipPath}`);
const zip = new AdmZip(zipPath);
try {
zip.extractAllTo(targetPath, true); // 第二个参数为 true表示覆盖已存在的文件
const frpcPath = path.join("frp", path.basename(zipPath, ".zip"));
logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
logDebug(
LogModule.APP,
`Unzip completed. Extracted directory: ${frpcPath}`
);
return frpcPath;
} catch (error) {
logError(LogModule.APP, `Error extracting zip file: ${error.message}`);
}
return null;
};
export const initGitHubApi = win => {
// 版本
let versions: FrpVersion[] = [];
const getVersionByGithubVersionId = versionId => {
logDebug(LogModule.APP, `Attempting to get version with ID: ${versionId}`);
const version = versions.find(f => f.id === versionId);
if (version) {
logInfo(
LogModule.APP,
`Version details ID:${version.id}, Name:${version.name}, Published At:${version.published_at}`
);
} else {
logWarn(LogModule.APP, `No version found for ID: ${versionId}`);
}
return version;
};
const getVersionByAssetName = (assetName: string) => {
logDebug(
LogModule.APP,
`Attempting to get version with asset name: ${assetName}`
);
const version = versions.find(f =>
f.assets.some(asset => asset.name === assetName)
);
if (version) {
logInfo(
LogModule.APP,
`Version details ID:${version.id}, Name:${version.name}, Published At:${version.published_at}`
);
} else {
logWarn(LogModule.APP, `No version found for asset name: ${assetName}`);
}
return version;
};
const getAdaptiveAsset = versionId => {
const { assets } = getVersionByGithubVersionId(versionId);
if (!assets || assets.length === 0) {
logWarn(LogModule.GITHUB, `No assets found for version ID: ${versionId}`);
return null;
}
const asset = assets.find(f => {
const a = frpArch;
if (a) {
const flag = a.every(item => f.name.includes(item));
if (flag) {
logInfo(
LogModule.GITHUB,
`Found matching asset: ${f.name} for version ID: ${versionId}`
);
}
return flag;
}
logWarn(
LogModule.GITHUB,
`No architecture match found for asset: ${f.name}`
);
return false;
});
if (!asset) {
logError(
LogModule.GITHUB,
`No adaptive asset found for version ID: ${versionId}`
);
}
return asset;
};
/**
* handle github api release json
* @param githubReleaseJsonStr jsonStr
* @returns versions
*/
const handleApiResponse = (githubReleaseJsonStr: string) => {
const downloadPath = path.join(app.getPath("userData"), "download");
const frpPath = path.join(app.getPath("userData"), "frp");
logInfo(LogModule.GITHUB, "Parsing GitHub release JSON response.");
versions = JSON.parse(githubReleaseJsonStr);
if (versions) {
logInfo(
LogModule.GITHUB,
"Successfully parsed versions from GitHub response."
);
const returnVersionsData = versions
.filter(f => getAdaptiveAsset(f.id))
.map(m => {
const asset = getAdaptiveAsset(m.id);
const download_count = m.assets.reduce(
(sum, item) => sum + item.download_count,
0
);
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(asset.size);
logInfo(
LogModule.GITHUB,
`Asset found: ${asset.name}, download count: ${download_count}`
);
} else {
logWarn(LogModule.GITHUB, `No asset found for version ID: ${m.id}`);
}
return m;
});
logDebug(
LogModule.GITHUB,
`Retrieved FRP versions: ${JSON.stringify(returnVersionsData)}`
);
return returnVersionsData;
} else {
logError(
LogModule.GITHUB,
"Failed to parse versions: No versions found in response."
);
return [];
}
};
/**
* conventMirrorUrl
* @param mirror mirror
* @returns mirrorUrl
*/
const conventMirrorUrl = (mirror: string) => {
switch (mirror) {
case "github":
return {
api: "https://api.github.com",
asset: "https://github.com"
};
default:
return {
api: "https://api.github.com",
asset: "https://github.com"
};
}
};
/**
* 获取github上的frp所有版本
*/
ipcMain.on("github.getFrpVersions", async (event, mirror: string) => {
const { api } = conventMirrorUrl(mirror);
const mirrorUrl = api;
logInfo(LogModule.GITHUB, `Requesting mirror URL: ${mirrorUrl}`);
const request = net.request({
method: "get",
url: `${mirrorUrl}/repos/fatedier/frp/releases?page=1&per_page=1000`
});
let githubReleaseJsonStr = null;
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) {
githubReleaseJsonStr = responseData.toString();
logInfo(
LogModule.GITHUB,
"Successfully retrieved GitHub release data."
);
} else {
logWarn(
LogModule.GITHUB,
"Failed to retrieve data, using local JSON instead. Status code: " +
response.statusCode
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
}
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
});
request.on("error", error => {
logError(
LogModule.GITHUB,
"Error occurred while requesting GitHub releases: " + error
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
request.end();
});
const decompressFrp = (frpFilename: string, compressedFilePath: string) => {
const targetPath = path.resolve(path.join(app.getPath("userData"), "frp"));
const ext = path.extname(frpFilename);
let frpcVersionPath = "";
try {
if (ext === ".zip") {
unZip(
// path.join(
// path.join(app.getPath("userData"), "download"),
// `${frpFilename}`
// ),
compressedFilePath,
targetPath
);
logInfo(LogModule.APP, `Unzipped file to path: ${frpcVersionPath}`);
frpcVersionPath = path.join("frp", path.basename(frpFilename, ".zip"));
} else if (ext === ".gz" && frpFilename.includes(".tar.gz")) {
unTarGZ(
// path.join(
// path.join(app.getPath("userData"), "download"),
// `${frpFilename}`
// ),
compressedFilePath,
targetPath
);
frpcVersionPath = path.join(
"frp",
path.basename(frpFilename, ".tar.gz")
);
logInfo(LogModule.APP, `Untarred file to path: ${frpcVersionPath}`);
}
} catch (error) {
logError(LogModule.APP, `Error during extraction: ${error.message}`);
}
return frpcVersionPath;
};
/**
* 下载请求
*/
ipcMain.on("github.download", async (event, args) => {
const { versionId, mirror } = args;
const version = getVersionByGithubVersionId(versionId);
const asset = getAdaptiveAsset(versionId);
const { browser_download_url } = asset;
let url = browser_download_url.replace(
"https://github.com",
conventMirrorUrl(mirror).asset
);
logDebug(
LogModule.GITHUB,
`Starting download for versionId: ${versionId}, mirror: ${mirror}, download URL: ${url}`
);
await download(BrowserWindow.getFocusedWindow(), url, {
filename: `${asset.name}`,
directory: path.join(app.getPath("userData"), "download"),
onProgress: progress => {
event.reply("Download.frpVersionDownloadOnProgress", {
id: versionId,
progress: progress
});
logDebug(
LogModule.GITHUB,
`Download progress for versionId: ${versionId} is ${
progress.percent * 100
}%`
);
},
onCompleted: () => {
logInfo(
LogModule.GITHUB,
`Download completed for versionId: ${versionId}, asset: ${asset.name}`
);
const frpcVersionPath = decompressFrp(
asset.name,
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
)
);
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
listVersion((err, doc) => {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.frpVersionDownloadOnCompleted", versionId);
version.download_completed = true;
logInfo(
LogModule.GITHUB,
`Version ${versionId} has been inserted successfully.`
);
});
} else {
logError(LogModule.GITHUB, `Error inserting version: ${err}`);
}
});
}
});
});
/**
* 删除下载
*/
ipcMain.on("github.deleteVersion", async (event, args) => {
const { absPath, id } = args;
logDebug(
LogModule.GITHUB,
`Attempting to delete version with ID: ${id} and path: ${absPath}`
);
if (fs.existsSync(absPath)) {
// if (process.platform === 'darwin') {
// fs.unlinkSync(absPath.replace(/ /g, '\\ '));
// }else{
// fs.unlinkSync(absPath);
// }
fs.rmSync(absPath, { recursive: true, force: true });
deleteVersionById(id, () => {
logInfo(
LogModule.GITHUB,
`Successfully deleted version with ID: ${id}`
);
});
} else {
logWarn(
LogModule.GITHUB,
`Version with ID: ${id} not found at path: ${absPath}`
);
}
listVersion((err, doc) => {
if (err) {
logError(LogModule.GITHUB, `Error listing versions: ${err}`);
} else {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.deleteVersion.hook", {
err: null,
data: "删除成功"
});
}
});
});
/**
* 获取最后版本
*/
ipcMain.on("github.getFrpcDesktopLastVersions", async event => {
logInfo(LogModule.GITHUB, "Requesting the latest version from GitHub.");
const request = net.request({
method: "get",
url: "https://api.github.com/repos/luckjiawei/frpc-desktop/releases/latest"
});
request.on("response", response => {
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
try {
versions = JSON.parse(responseData.toString());
logInfo(
LogModule.GITHUB,
"Successfully retrieved the latest version."
);
event.reply("github.getFrpcDesktopLastVersionsHook", versions);
} catch (error) {
logError(
LogModule.GITHUB,
`Error parsing response data: ${error.message}`
);
}
});
});
request.on("error", error => {
logError(LogModule.GITHUB, `Request error: ${error.message}`);
});
request.end();
});
ipcMain.on(
"download.importFrpFile",
async (event, filePath: string, targetPath: string) => {
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "Frp 文件", extensions: ["tar.gz", "zip"] } // 允许选择的文件类型,分开后缀以确保可以选择
]
});
if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
logWarn(LogModule.GITHUB, "User canceled the file import operation.");
return;
} else {
const filePath = result.filePaths[0];
// const fileExtension = path.extname(filePath);
logInfo(LogModule.APP, `User selected file: ${filePath}`);
const checksum = calculateFileChecksum(filePath);
logInfo(LogModule.APP, `Calculated checksum for the file: ${checksum}`);
const frpName = frpChecksums[checksum];
if (frpName) {
logInfo(LogModule.APP, `FRP file name found: ${frpName}`);
if (frpArch.every(item => frpName.includes(item))) {
logInfo(
LogModule.APP,
`Architecture matches for FRP file: ${frpName}`
);
const version = getVersionByAssetName(frpName);
getVersionById(version.id, (err, existingVersion) => {
if (!err && existingVersion) {
logInfo(
LogModule.APP,
`Version already exists: ${JSON.stringify(existingVersion)}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,版本已存在`
});
return; // 终止后续执行
}
const frpcVersionPath = decompressFrp(frpName, filePath);
logInfo(
LogModule.APP,
`Successfully decompressed FRP file: ${frpName} to path: ${frpcVersionPath}`
);
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
listVersion((err, doc) => {
event.reply("Config.versions.hook", { err, data: doc });
version.download_completed = true;
event.reply("Download.importFrpFile.hook", {
success: true,
data: `导入成功`
});
});
} else {
logError(LogModule.APP, `Error inserting version: ${err}`);
event.reply("Download.importFrpFile.hook", {
success: true,
data: `导入失败,未知错误`
});
}
});
});
} else {
logWarn(
LogModule.APP,
`Architecture does not match for FRP file: ${frpName}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,所选 frp 架构与操作系统不符`
});
}
} else {
logWarn(
LogModule.APP,
`No matching FRP file name found for checksum: ${checksum}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,无法识别文件`
});
}
}
}
);
};