@@ -30,7 +30,11 @@ module.exports = class SystemConfig {
|
||||
static getLocalKitPath(){
|
||||
let path = "./"
|
||||
if ( this.isWindows() ) {
|
||||
path = require('path').dirname(require.main.filename)
|
||||
if( require && require.main && require.main.filename ){
|
||||
path = require('path').dirname(require.main.filename)
|
||||
} else {
|
||||
path = process.cwd()
|
||||
}
|
||||
} else {
|
||||
path = require('path').dirname(process.execPath);
|
||||
}
|
||||
|
||||
@@ -35,16 +35,16 @@ module.exports = {
|
||||
|
||||
let file = cli.getParamValue("file");
|
||||
if (!file) {
|
||||
file = "kit"
|
||||
file = "kitx"
|
||||
}
|
||||
|
||||
const scriptFile = `./${file}.js`;
|
||||
const stat = fs.existsSync(scriptFile)
|
||||
if (!stat) {
|
||||
if (!cli.getParamValue("file")) {
|
||||
fs.writeFileSync("./kit.js", `
|
||||
fs.writeFileSync("./kitx.js", `
|
||||
async function main(){
|
||||
console.log("hello kit!")
|
||||
console.log("hello kit!");
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
@@ -82,7 +82,7 @@ async function main(){
|
||||
}
|
||||
});
|
||||
|
||||
parser.addCmdLine("remote", "验证远程仓库;", async function () {
|
||||
parser.addCmdLine("remote", "验证远程仓库可达;", async function () {
|
||||
let remote = await $context.get("remote")
|
||||
if (!(await remote.check())) {
|
||||
$logger.error("远程仓库不可用")
|
||||
|
||||
@@ -329,7 +329,11 @@ function stringifyLogValue(value) {
|
||||
function printCallLog(name, args, result) {
|
||||
console.log(`调用 ${name} 功能`);
|
||||
console.log(`入参 ${args.map(stringifyLogValue).join(" ")}`);
|
||||
console.log(`结果 ${stringifyLogValue(result)}`);
|
||||
if (result === undefined) {
|
||||
console.log("执行结束");
|
||||
} else {
|
||||
console.log(`执行结果 ${stringifyLogValue(result)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function withCallLog(name, fn) {
|
||||
|
||||
+345
-218
@@ -1,239 +1,366 @@
|
||||
/**
|
||||
* remote 模块用来处理远程仓库相关事务
|
||||
*/
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const JSON5 = require('json5')
|
||||
const { pipeline, Transform} = require("stream/promises");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { pipeline } = require("stream/promises");
|
||||
const JSON5 = require("json5");
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function normalizeRemotePath(value) {
|
||||
if (!value || value === "/") {
|
||||
return "/";
|
||||
}
|
||||
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$|^$/g, "") || "/";
|
||||
}
|
||||
|
||||
function normalizeOs(value) {
|
||||
const name = String(value || os.platform()).toLowerCase();
|
||||
if (["windows_nt", "win32", "windows"].includes(name)) {
|
||||
return "windows";
|
||||
}
|
||||
if (["macos", "mac", "darwin"].includes(name)) {
|
||||
return "darwin";
|
||||
}
|
||||
if (["linux", "alpine"].includes(name)) {
|
||||
return "linux";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function normalizeArch(value) {
|
||||
const name = String(value || os.arch()).toLowerCase();
|
||||
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
|
||||
return "x86_64";
|
||||
}
|
||||
if (["arm64", "aarch64"].includes(name)) {
|
||||
return "aarch64";
|
||||
}
|
||||
if (["ia32", "i386", "x86"].includes(name)) {
|
||||
return "i386";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function localModuleRoot(projectId) {
|
||||
return path.resolve($sConfig.getLocalStorePath(), "modules", projectId);
|
||||
}
|
||||
|
||||
function readJsonFile(filePath, fallback) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return JSON5.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
}
|
||||
} catch (e) {
|
||||
$logger.warn(`读取 JSON 失败 ${filePath}: ${e.message}`);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function writeJsonFile(filePath, data) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 4), "utf-8");
|
||||
}
|
||||
|
||||
function safeRemove(target) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function copyRecursive(from, to) {
|
||||
const stat = fs.statSync(from);
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(to, { recursive: true });
|
||||
for (const item of fs.readdirSync(from)) {
|
||||
copyRecursive(path.join(from, item), path.join(to, item));
|
||||
}
|
||||
} else {
|
||||
fs.mkdirSync(path.dirname(to), { recursive: true });
|
||||
fs.copyFileSync(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = class Remote {
|
||||
|
||||
constructor() {
|
||||
// this.__init()
|
||||
this.func_publish = require("./publish")
|
||||
this.func_install = require("./install")
|
||||
this.func_publish = require("./publish");
|
||||
this.func_install = require("./install");
|
||||
this.root = "/";
|
||||
}
|
||||
|
||||
|
||||
|
||||
async init() {
|
||||
this.webdav = await import("webdav");
|
||||
const webdav = await import("webdav");
|
||||
this.webdav = webdav.createClient ? webdav : webdav.default;
|
||||
const uConfig = await $context.get("uConfig");
|
||||
if ( uConfig.get("remoteServer") ){
|
||||
let server = uConfig.get("remoteServer");
|
||||
$logger.info("server " + server)
|
||||
if ( server.startsWith("webdav") ) {
|
||||
server = "http" + server.substring(6)
|
||||
}
|
||||
|
||||
if ( uConfig.get("remoteAuth") ) {
|
||||
let up = uConfig.get("remoteAuth");
|
||||
$logger.info("remoteAuth " + up)
|
||||
up = up.split(":", 2)
|
||||
this.client = this.webdav.createClient(
|
||||
server,
|
||||
{
|
||||
username: up[0],
|
||||
password: up[1]
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.client = this.webdav.createClient(
|
||||
server
|
||||
);
|
||||
}
|
||||
let server = uConfig.get("remoteServer");
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (server.startsWith("webdav")) {
|
||||
server = "http" + server.substring(6);
|
||||
}
|
||||
|
||||
const options = {};
|
||||
const auth = uConfig.get("remoteAuth");
|
||||
if (auth) {
|
||||
const [username, password] = auth.split(":", 2);
|
||||
options.username = username;
|
||||
options.password = password;
|
||||
}
|
||||
|
||||
this.client = this.webdav.createClient(server, options);
|
||||
}
|
||||
|
||||
async check() {
|
||||
|
||||
if ( ! this.webdav ) {
|
||||
$logger.info("webdav unable")
|
||||
return false
|
||||
}
|
||||
|
||||
if ( ! this.client ) {
|
||||
$logger.info("client unable")
|
||||
return false
|
||||
if (!this.client) {
|
||||
$logger.info("remote client unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// Get directory contents
|
||||
const directoryItems = await this.client.getDirectoryContents("/");
|
||||
$logger.info("directoryItems", directoryItems)
|
||||
// 是否包含 index.json ?
|
||||
// 不包含尝试创建
|
||||
// 包含了尝试下载
|
||||
const indexJsonFile = this.getIndexJson( directoryItems );
|
||||
$logger.info("indexJsonFile", indexJsonFile)
|
||||
if ( indexJsonFile ) {
|
||||
// 包含了尝试下载
|
||||
// 下载有缓存
|
||||
this.download(indexJsonFile)
|
||||
$logger.info("download index")
|
||||
|
||||
} else {
|
||||
// 不包含尝试创建
|
||||
await this.affireRemoteStore()
|
||||
$logger.info("affireRemoteStore")
|
||||
}
|
||||
|
||||
return true
|
||||
}catch ( e ) {
|
||||
$logger.error("remote check 过程中出错", e)
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 先对比时间和长度,有不同才会真正下载,否则使用本地缓存。
|
||||
* 本地缓存路径即本地仓库路径
|
||||
* @param rFile
|
||||
*/
|
||||
download(rFile){
|
||||
const lFilePath = path.resolve($sConfig.getLocalStorePath(), rFile.filename);
|
||||
if ( fs.existsSync( lFilePath ) ) {
|
||||
const lFileInfo = fs.statSync(lFilePath);
|
||||
if ( lFileInfo.size === rFile.length
|
||||
&& lFileInfo.mtime === rFile.lastmod
|
||||
) {
|
||||
// 使用缓存
|
||||
$logger.debug("use cache")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 下载并覆盖
|
||||
// 不知道是同步还是异步
|
||||
this.wDownload( rFile.filename, lFilePath );
|
||||
|
||||
|
||||
}
|
||||
|
||||
async affireRemoteStore(){
|
||||
// Get directory contents
|
||||
const directoryItems = await this.client.getDirectoryContents("/");
|
||||
// 是否包含 index.json ?
|
||||
// 不包含尝试创建
|
||||
// 包含了尝试下载
|
||||
const indexJsonFile = this.getIndexJson( directoryItems );
|
||||
if ( ! indexJsonFile ) {
|
||||
// 创建 index
|
||||
await this.client.putFileContents("/index.json", JSON5.stringify({
|
||||
modules : [],
|
||||
tmpls : [],
|
||||
lists : []
|
||||
}, null, 4) , { overwrite: false });
|
||||
}
|
||||
|
||||
const modulesFolder = this.getFileInfoByName(directoryItems, "modules")
|
||||
if ( ! modulesFolder ) {
|
||||
// 创建文件夹
|
||||
await this.client.createDirectory("/modules");
|
||||
}
|
||||
|
||||
const tmplsFolder = this.getFileInfoByName(directoryItems, "tmpls")
|
||||
if ( ! tmplsFolder ) {
|
||||
// 创建文件夹
|
||||
await this.client.createDirectory("/tmpls");
|
||||
}
|
||||
|
||||
const listsFolder = this.getFileInfoByName(directoryItems, "lists")
|
||||
if ( ! listsFolder ) {
|
||||
// 创建文件夹
|
||||
await this.client.createDirectory("/lists");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getIndexJson(items){
|
||||
return this.getFileInfoByName( items, "index.json" );
|
||||
}
|
||||
getFileInfoByName(items, name){
|
||||
if ( ! items || items.length < 1 ) {
|
||||
await this.ensureDir("/");
|
||||
await this.client.getDirectoryContents("/");
|
||||
return true;
|
||||
} catch (e) {
|
||||
$logger.error("remote check 过程中出错 " + e.message);
|
||||
return false;
|
||||
}
|
||||
for ( let item of items ) {
|
||||
if ( item.basename === name ) {
|
||||
return item;
|
||||
}
|
||||
|
||||
async publish(localFile, remotePath) {
|
||||
return await this.func_publish(this, localFile, remotePath);
|
||||
}
|
||||
|
||||
async install(id) {
|
||||
return await this.func_install(this, id);
|
||||
}
|
||||
|
||||
async list(projectId) {
|
||||
const state = this.getInstallState(projectId);
|
||||
for (const version of state.installed_versions) {
|
||||
const current = version === state.current_version ? "*" : " ";
|
||||
$logger.info(`${current} ${version}${current === "*" ? " (current)" : ""}`);
|
||||
}
|
||||
return state.installed_versions;
|
||||
}
|
||||
|
||||
async use(projectVersion) {
|
||||
const { projectId, version } = this.parseProjectVersion(projectVersion);
|
||||
return this.activate(projectId, version, "switch");
|
||||
}
|
||||
|
||||
async rollback(projectId) {
|
||||
const state = this.getInstallState(projectId);
|
||||
const history = [...state.history].reverse();
|
||||
const current = state.current_version;
|
||||
const previous = history.find((item) => item.version && item.version !== current && state.installed_versions.includes(item.version));
|
||||
if (!previous) {
|
||||
throw new Error(`没有可回滚版本 ${projectId}`);
|
||||
}
|
||||
return this.activate(projectId, previous.version, "rollback");
|
||||
}
|
||||
|
||||
async remove(projectVersion) {
|
||||
const { projectId, version } = this.parseProjectVersion(projectVersion);
|
||||
const root = localModuleRoot(projectId);
|
||||
const state = this.getInstallState(projectId);
|
||||
const targetVersion = version || state.current_version;
|
||||
if (!targetVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.current_version === targetVersion) {
|
||||
throw new Error(`不能删除当前激活版本 ${projectId}@${targetVersion}`);
|
||||
}
|
||||
|
||||
safeRemove(path.resolve(root, "versions", targetVersion));
|
||||
state.installed_versions = state.installed_versions.filter((item) => item !== targetVersion);
|
||||
this.pushHistory(state, "uninstall", targetVersion);
|
||||
this.saveInstallState(projectId, state);
|
||||
return true;
|
||||
}
|
||||
|
||||
parseProjectVersion(value) {
|
||||
const text = String(value || "kit");
|
||||
const index = text.lastIndexOf("@");
|
||||
if (index <= 0) {
|
||||
return { projectId: text, version: undefined };
|
||||
}
|
||||
return { projectId: text.substring(0, index), version: text.substring(index + 1) };
|
||||
}
|
||||
|
||||
getLocalModuleRoot(projectId) {
|
||||
const root = localModuleRoot(projectId);
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
return root;
|
||||
}
|
||||
|
||||
getInstallState(projectId) {
|
||||
const statePath = path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json");
|
||||
const state = readJsonFile(statePath, {
|
||||
project_id: projectId,
|
||||
installed_versions: [],
|
||||
current_version: undefined,
|
||||
last_updated: undefined,
|
||||
history: [],
|
||||
options: {}
|
||||
});
|
||||
state.project_id = state.project_id || projectId;
|
||||
state.installed_versions = state.installed_versions || [];
|
||||
state.history = state.history || [];
|
||||
state.options = state.options || {};
|
||||
return state;
|
||||
}
|
||||
|
||||
saveInstallState(projectId, state) {
|
||||
state.last_updated = now();
|
||||
writeJsonFile(path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json"), state);
|
||||
}
|
||||
|
||||
pushHistory(state, action, version, from) {
|
||||
state.history.push({ timestamp: now(), action, version, from });
|
||||
}
|
||||
|
||||
async activate(projectId, version, action = "switch") {
|
||||
const root = this.getLocalModuleRoot(projectId);
|
||||
const versionDir = path.resolve(root, "versions", version);
|
||||
if (!fs.existsSync(versionDir)) {
|
||||
throw new Error(`本地版本不存在 ${projectId}@${version}`);
|
||||
}
|
||||
|
||||
const current = path.resolve(root, "current");
|
||||
safeRemove(current);
|
||||
try {
|
||||
fs.symlinkSync(path.relative(root, versionDir), current, "junction");
|
||||
} catch (e) {
|
||||
copyRecursive(versionDir, current);
|
||||
}
|
||||
|
||||
const state = this.getInstallState(projectId);
|
||||
const from = state.current_version;
|
||||
state.current_version = version;
|
||||
if (!state.installed_versions.includes(version)) {
|
||||
state.installed_versions.push(version);
|
||||
}
|
||||
this.pushHistory(state, action, version, from);
|
||||
this.saveInstallState(projectId, state);
|
||||
fs.writeFileSync(path.resolve(root, "version"), version, "utf-8");
|
||||
return version;
|
||||
}
|
||||
|
||||
getPlatformCandidates() {
|
||||
return [
|
||||
{ os: normalizeOs(os.platform()), arch: normalizeArch(os.arch()) },
|
||||
{ os: "any", arch: "any" }
|
||||
];
|
||||
}
|
||||
|
||||
async ensureDir(remotePath) {
|
||||
const target = normalizeRemotePath(remotePath);
|
||||
if (target === "/") {
|
||||
return;
|
||||
}
|
||||
if (await this.client.exists(target)) {
|
||||
return;
|
||||
}
|
||||
await this.ensureDir(path.posix.dirname(target));
|
||||
await this.client.createDirectory(target);
|
||||
}
|
||||
|
||||
async putJson(remotePath, data) {
|
||||
await this.ensureDir(path.posix.dirname(remotePath));
|
||||
await this.client.putFileContents(normalizeRemotePath(remotePath), JSON.stringify(data, null, 4), { overwrite: true });
|
||||
}
|
||||
|
||||
async getJson(remotePath, fallback) {
|
||||
try {
|
||||
const text = await this.client.getFileContents(normalizeRemotePath(remotePath), { format: "text" });
|
||||
return JSON5.parse(text);
|
||||
} catch (e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async wUpload(from, to) {
|
||||
await this.ensureDir(path.posix.dirname(to));
|
||||
const rs = fs.createReadStream(from);
|
||||
const ws = this.client.createWriteStream(normalizeRemotePath(to));
|
||||
await pipeline(rs, ws);
|
||||
}
|
||||
|
||||
async wDownload(from, to) {
|
||||
fs.mkdirSync(path.dirname(to), { recursive: true });
|
||||
const rs = this.client.createReadStream(normalizeRemotePath(from));
|
||||
const ws = fs.createWriteStream(to);
|
||||
await pipeline(rs, ws);
|
||||
}
|
||||
|
||||
async wMkdir(folder) {
|
||||
return this.ensureDir(folder);
|
||||
}
|
||||
|
||||
async wGetModuleInfo(projectId) {
|
||||
return this.getProjectMetadata(projectId);
|
||||
}
|
||||
|
||||
async getProjectMetadata(projectId) {
|
||||
const metadata = await this.getJson(`/${projectId}/metadata.json`, undefined);
|
||||
if (metadata) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const legacy = await this.getJson(`/modules/${projectId}/meta.json`, undefined);
|
||||
if (legacy) {
|
||||
return {
|
||||
project_id: legacy.id || projectId,
|
||||
description: legacy.desc || legacy.name || "",
|
||||
latest_version: legacy.latest && legacy.latest.version,
|
||||
versions: (legacy.versions || []).map((item) => item.version || item),
|
||||
extra: legacy
|
||||
};
|
||||
}
|
||||
throw new Error(`远程模块不存在 ${projectId}`);
|
||||
}
|
||||
|
||||
async getVersionMetadata(projectId, version) {
|
||||
const metadata = await this.getJson(`/${projectId}/${version}/version.json`, undefined);
|
||||
if (metadata) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const project = await this.getProjectMetadata(projectId);
|
||||
const legacyVersion = project.extra && project.extra.versions && project.extra.versions.find((item) => (item.version || item) === version);
|
||||
if (legacyVersion && legacyVersion.dist) {
|
||||
return {
|
||||
version,
|
||||
artifacts: legacyVersion.dist.map((dist) => ({
|
||||
path: `${normalizeOs(dist.os)}/${normalizeArch(dist.platform)}/${dist.filename}`,
|
||||
type: "executable"
|
||||
}))
|
||||
};
|
||||
}
|
||||
throw new Error(`远程版本不存在 ${projectId}@${version}`);
|
||||
}
|
||||
|
||||
async downloadArtifact(projectId, version, artifact, versionDir) {
|
||||
const remotePath = `/${projectId}/${version}/${artifact.path}`;
|
||||
const localPath = path.resolve(versionDir, artifact.path);
|
||||
await this.wDownload(remotePath, localPath);
|
||||
return localPath;
|
||||
}
|
||||
|
||||
getMatchingArtifacts(versionMetadata) {
|
||||
const artifacts = versionMetadata.artifacts || [];
|
||||
const candidates = this.getPlatformCandidates();
|
||||
for (const candidate of candidates) {
|
||||
const prefix = `${candidate.os}/${candidate.arch}/`;
|
||||
const matched = artifacts.filter((item) => String(item.path).replace(/\\/g, "/").startsWith(prefix));
|
||||
if (matched.length > 0) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
|
||||
async publish(localFile, remotePath){
|
||||
return await this.func_publish( this , localFile, remotePath )
|
||||
}
|
||||
|
||||
async install(id){
|
||||
return this.func_install( this, id )
|
||||
}
|
||||
|
||||
async wUpload(from, to, debug){
|
||||
$logger.info(`from=${from} to=${to}`)
|
||||
const totalSize = fs.statSync(from).size;
|
||||
debug ? console.log("totalSize", totalSize) : null
|
||||
const rs = fs.createReadStream( from );
|
||||
let wSize = 0;
|
||||
let rSize = 0;
|
||||
rs.on("data", (r)=>{
|
||||
rSize += r.length;
|
||||
debug ? console.log("r", rSize * 100 / totalSize + "%") : null
|
||||
})
|
||||
const ws = this.client.createWriteStream(to)
|
||||
ws.on("data", (r)=>{
|
||||
wSize += r.length;
|
||||
debug ? console.log("w ", wSize * 100 / totalSize + "%") : null
|
||||
})
|
||||
await pipeline( rs, ws);
|
||||
debug ? console.log("finish") : null
|
||||
}
|
||||
|
||||
async wDownload(from, to){
|
||||
$logger.info("download", from, to )
|
||||
const stat = await this.client.stat(from);
|
||||
const totalSize = stat.size;
|
||||
let wSize = 0;
|
||||
let rSize = 0;
|
||||
|
||||
const rs = this.client.createReadStream(from);
|
||||
rs.on("data", (r)=>{
|
||||
rSize += r.length;
|
||||
// console.log("r", rSize * 100 / totalSize)
|
||||
})
|
||||
|
||||
const ws = fs.createWriteStream(to);
|
||||
ws.on("data", (r)=>{
|
||||
wSize += r.length;
|
||||
// console.log("w", wSize * 100 / totalSize)
|
||||
})
|
||||
return await pipeline(
|
||||
rs, ws
|
||||
)
|
||||
}
|
||||
|
||||
async wMkdir(folder){
|
||||
|
||||
const folderfather = path.dirname( folder );
|
||||
if ( ! await this.client.exists( folderfather ) ) {
|
||||
// await remote.client.createDirectory( folderfather )
|
||||
await this.wMkdir( folderfather )
|
||||
}
|
||||
|
||||
if ( await this.client.exists( folder ) ) {
|
||||
return
|
||||
} else {
|
||||
await this.client.createDirectory( folder )
|
||||
}
|
||||
}
|
||||
|
||||
async wGetModuleInfo(id){
|
||||
let meta = await this.client.getFileContents("/modules/" + id + "/meta.json", { format: "text" });
|
||||
let metaJson = JSON5.parse( meta );
|
||||
return metaJson
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+70
-40
@@ -1,53 +1,83 @@
|
||||
const os = require('os');
|
||||
const fs = require("fs");
|
||||
const osutil = require("../util/osutil")
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
function matchDistName(cur, dists){
|
||||
const list = []
|
||||
for (let dist of dists) {
|
||||
if ( cur.curOS === dist.os && cur.curPlatform === dist.platform) {
|
||||
list.push( dist )
|
||||
}
|
||||
else if ( cur.curOS === dist.os && "cross" === dist.platform) {
|
||||
list.unshift( dist )
|
||||
}
|
||||
else if ( "cross" === dist.os && cur.curPlatform === dist.platform) {
|
||||
list.unshift( dist )
|
||||
}
|
||||
}
|
||||
return list
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function verifyChecksum(filePath, checksum) {
|
||||
if (!checksum) {
|
||||
return;
|
||||
}
|
||||
const [alg, expected] = checksum.split(":", 2);
|
||||
if (!alg || !expected) {
|
||||
return;
|
||||
}
|
||||
const crypto = require("crypto");
|
||||
const actual = crypto.createHash(alg).update(fs.readFileSync(filePath)).digest("hex");
|
||||
if (actual !== expected) {
|
||||
throw new Error(`文件校验失败 ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async function install(remote, id){
|
||||
const cur = osutil.cur();
|
||||
|
||||
if ( ! id ) {
|
||||
id = "kit"
|
||||
function unpackArtifact(artifact, localPath, versionDir) {
|
||||
if (artifact.type !== "archive" || !artifact.unpack) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取本地版本
|
||||
const localInfo = osutil.choise([
|
||||
require("../windows/local").getModuleInfo,
|
||||
require("../linux/local").getModuleInfo,
|
||||
])( id )
|
||||
const extractDir = path.resolve(path.dirname(localPath), "_extracted");
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
const format = artifact.archive_format || "zip";
|
||||
if (format === "zip") {
|
||||
if (process.platform === "win32") {
|
||||
const source = localPath.replace(/'/g, "''");
|
||||
const target = extractDir.replace(/'/g, "''");
|
||||
execFileSync("powershell.exe", ["-NoProfile", "-Command", `Expand-Archive -Path '${source}' -DestinationPath '${target}' -Force`], { stdio: "inherit" });
|
||||
} else {
|
||||
execFileSync("unzip", ["-o", localPath, "-d", extractDir], { stdio: "inherit" });
|
||||
}
|
||||
} else if (["tar", "tar.gz", "tgz", "tar.xz"].includes(format)) {
|
||||
execFileSync("tar", ["-xf", localPath, "-C", extractDir], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
$logger.info("local version", localInfo.version)
|
||||
artifact.extracted_path = path.relative(versionDir, extractDir).replace(/\\/g, "/");
|
||||
if (artifact.entry_point) {
|
||||
artifact.extracted_entry_point = path.posix.join(artifact.extracted_path, artifact.entry_point.replace(/\\/g, "/"));
|
||||
}
|
||||
}
|
||||
|
||||
// 确认远程版本文件是否存在
|
||||
const remoteInfo = await remote.wGetModuleInfo(id)
|
||||
$logger.info("remote latest version", remoteInfo.latest.version)
|
||||
module.exports = async function install(remote, id) {
|
||||
const parsed = remote.parseProjectVersion(id || "kit");
|
||||
const projectId = parsed.projectId;
|
||||
const project = await remote.getProjectMetadata(projectId);
|
||||
const version = parsed.version || project.latest_version;
|
||||
if (!version) {
|
||||
throw new Error(`远程模块没有可安装版本 ${projectId}`);
|
||||
}
|
||||
|
||||
const dists = matchDistName(cur, remoteInfo.latest.dist)
|
||||
const dist = dists.pop()
|
||||
const versionMeta = await remote.getVersionMetadata(projectId, version);
|
||||
const artifacts = remote.getMatchingArtifacts(versionMeta);
|
||||
if (artifacts.length < 1) {
|
||||
throw new Error(`没有匹配当前平台的文件 ${projectId}@${version}`);
|
||||
}
|
||||
|
||||
await remote.wDownload(
|
||||
"/modules/" + id + "/" + remoteInfo.latest.version + "/" + cur.curOS + "/" + cur.curPlatform + "/" + dist.filename,
|
||||
localInfo.homeDir + "/" + dist.filename,
|
||||
)
|
||||
const root = remote.getLocalModuleRoot(projectId);
|
||||
const versionDir = path.resolve(root, "versions", version);
|
||||
fs.mkdirSync(versionDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync( localInfo.homeDir + "/version", remoteInfo.latest.version );
|
||||
for (const artifact of artifacts) {
|
||||
const localPath = await remote.downloadArtifact(projectId, version, artifact, versionDir);
|
||||
verifyChecksum(localPath, artifact.checksum || (versionMeta.checksums && versionMeta.checksums[artifact.path]));
|
||||
unpackArtifact(artifact, localPath, versionDir);
|
||||
}
|
||||
|
||||
$logger.info("当前版本", remoteInfo.latest.version )
|
||||
const localVersionMeta = Object.assign({}, versionMeta, { installed_at: now(), artifacts });
|
||||
fs.writeFileSync(path.resolve(versionDir, ".version.json"), JSON.stringify(localVersionMeta, null, 4), "utf-8");
|
||||
|
||||
}
|
||||
const state = remote.getInstallState(projectId);
|
||||
const action = state.current_version ? "switch" : "install";
|
||||
await remote.activate(projectId, version, action);
|
||||
$logger.info(`当前版本 ${projectId}@${version}`);
|
||||
return version;
|
||||
};
|
||||
|
||||
+166
-97
@@ -1,107 +1,176 @@
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const JSON5 = require('json5')
|
||||
const { pipeline } = require('stream/promises');
|
||||
const objutil = require("../util/objutil")
|
||||
const datautil = require("../util/datautil")
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const JSON5 = require("json5");
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function normalizeOs(value) {
|
||||
const name = String(value || "any").toLowerCase();
|
||||
if (["windows_nt", "win32", "windows"].includes(name)) {
|
||||
return "windows";
|
||||
}
|
||||
if (["macos", "mac", "darwin"].includes(name)) {
|
||||
return "darwin";
|
||||
}
|
||||
if (["linux", "alpine"].includes(name)) {
|
||||
return "linux";
|
||||
}
|
||||
if (["cross", "all", "any"].includes(name)) {
|
||||
return "any";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function normalizeArch(value) {
|
||||
const name = String(value || "any").toLowerCase();
|
||||
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
|
||||
return "x86_64";
|
||||
}
|
||||
if (["arm64", "aarch64"].includes(name)) {
|
||||
return "aarch64";
|
||||
}
|
||||
if (["ia32", "i386", "x86"].includes(name)) {
|
||||
return "i386";
|
||||
}
|
||||
if (["cross", "all", "any"].includes(name)) {
|
||||
return "any";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function sha256(filePath) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
||||
}
|
||||
|
||||
function guessArtifactType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if ([".zip", ".tgz", ".gz", ".xz", ".tar"].includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
return "executable";
|
||||
}
|
||||
|
||||
function archiveFormat(filePath) {
|
||||
const name = path.basename(filePath).toLowerCase();
|
||||
if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
|
||||
return "tar.gz";
|
||||
}
|
||||
if (name.endsWith(".tar.xz")) {
|
||||
return "tar.xz";
|
||||
}
|
||||
if (name.endsWith(".zip")) {
|
||||
return "zip";
|
||||
}
|
||||
if (name.endsWith(".tar")) {
|
||||
return "tar";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildVersion(localMeta) {
|
||||
if (localMeta.release_version) {
|
||||
return localMeta.release_version;
|
||||
}
|
||||
if (localMeta.version) {
|
||||
return localMeta.version;
|
||||
}
|
||||
return new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
||||
}
|
||||
|
||||
function readLocalMeta(localFile) {
|
||||
const base = localFile ? path.resolve(localFile) : process.cwd();
|
||||
const metaFile = fs.statSync(base).isDirectory() ? path.resolve(base, "meta.json") : path.resolve(process.cwd(), "meta.json");
|
||||
if (fs.existsSync(metaFile)) {
|
||||
return JSON5.parse(fs.readFileSync(metaFile, "utf-8"));
|
||||
}
|
||||
|
||||
if (!localFile) {
|
||||
throw new Error("当前路径不包含 meta.json");
|
||||
}
|
||||
|
||||
return {
|
||||
id: path.basename(localFile, path.extname(localFile)).toLowerCase(),
|
||||
version: buildVersion({}),
|
||||
desc: "",
|
||||
dist: [{ path: localFile, os: "any", platform: "any", filename: path.basename(localFile) }]
|
||||
};
|
||||
}
|
||||
|
||||
function buildArtifacts(localMeta) {
|
||||
const dists = localMeta.dist || localMeta.artifacts || [];
|
||||
return dists.map((dist) => {
|
||||
const localPath = path.resolve(dist.path || dist.localPath || dist.file || dist.filename);
|
||||
const osName = normalizeOs(dist.os);
|
||||
const archName = normalizeArch(dist.arch || dist.platform);
|
||||
const filename = dist.filename || path.basename(localPath);
|
||||
const artifactPath = dist.remotePath || `${osName}/${archName}/${filename}`;
|
||||
const type = dist.type || guessArtifactType(localPath);
|
||||
const artifact = {
|
||||
path: artifactPath.replace(/\\/g, "/"),
|
||||
type,
|
||||
checksum: `sha256:${sha256(localPath)}`,
|
||||
localPath
|
||||
};
|
||||
if (type === "archive") {
|
||||
artifact.archive_format = dist.archive_format || archiveFormat(localPath);
|
||||
artifact.entry_point = dist.entry_point;
|
||||
artifact.unpack = Boolean(dist.unpack);
|
||||
}
|
||||
return artifact;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = async function publish(remote, localFile, remotePath) {
|
||||
if (remotePath) {
|
||||
const target = path.posix.resolve("/", remotePath.replace(/\\/g, "/"));
|
||||
await remote.wUpload(localFile, path.posix.join(target, path.basename(localFile)));
|
||||
$logger.info("文件上传成功");
|
||||
return;
|
||||
}
|
||||
|
||||
if( remotePath ) {
|
||||
const localMeta = readLocalMeta(localFile);
|
||||
const projectId = localMeta.project_id || localMeta.id || localMeta.name;
|
||||
if (!projectId) {
|
||||
throw new Error("meta.json 缺少 project_id/id/name");
|
||||
}
|
||||
|
||||
// 如果有 meta
|
||||
const version = buildVersion(localMeta);
|
||||
const artifacts = buildArtifacts(localMeta);
|
||||
await remote.ensureDir(`/${projectId}/${version}`);
|
||||
|
||||
// 如果没有 meta
|
||||
$logger.info("远程路径没有 meta, 按照默认规则上传");
|
||||
const versionMeta = {
|
||||
version,
|
||||
release_date: localMeta.release_date || now(),
|
||||
official_download_url: localMeta.official_download_url || "",
|
||||
artifacts: artifacts.map(({ localPath, ...artifact }) => artifact),
|
||||
checksums: Object.fromEntries(artifacts.map((artifact) => [artifact.path, artifact.checksum]))
|
||||
};
|
||||
|
||||
// 确认目录
|
||||
let folder = path.posix.resolve("/" + remotePath + "/data/" + datautil.now() );
|
||||
if ( ! await remote.client.exists( folder ) ) {
|
||||
// 创建文件夹
|
||||
await remote.client.createDirectory(folder, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
for (const artifact of artifacts) {
|
||||
await remote.wUpload(artifact.localPath, `/${projectId}/${version}/${artifact.path}`);
|
||||
}
|
||||
await remote.putJson(`/${projectId}/${version}/version.json`, versionMeta);
|
||||
|
||||
let filename = path.basename(localFile);
|
||||
const metadata = await remote.getJson(`/${projectId}/metadata.json`, {
|
||||
project_id: projectId,
|
||||
official_url: localMeta.official_url || localMeta.vcs || "",
|
||||
description: localMeta.description || localMeta.desc || "",
|
||||
latest_version: version,
|
||||
versions: [],
|
||||
fetch_rules: localMeta.fetch_rules || [],
|
||||
extra: {}
|
||||
});
|
||||
|
||||
// 上传文件
|
||||
await remote.wUpload( localFile , path.posix.resolve(folder , filename ));
|
||||
metadata.project_id = projectId;
|
||||
metadata.official_url = metadata.official_url || localMeta.official_url || localMeta.vcs || "";
|
||||
metadata.description = localMeta.description || localMeta.desc || metadata.description || "";
|
||||
metadata.latest_version = version;
|
||||
metadata.versions = Array.from(new Set([...(metadata.versions || []), version]));
|
||||
metadata.extra = Object.assign({}, metadata.extra || {}, localMeta.extra || {});
|
||||
|
||||
} else {
|
||||
|
||||
let curPath = process.cwd();
|
||||
const metaFilePath = path.resolve( curPath, "meta.json" );
|
||||
if ( ! fs.existsSync( metaFilePath ) ) {
|
||||
$logger.error("当前路径不包含 meta.json")
|
||||
return
|
||||
}
|
||||
|
||||
const metaFile = fs.readFileSync(metaFilePath);
|
||||
|
||||
const localMeta = JSON5.parse( metaFile );
|
||||
|
||||
// 更新版本
|
||||
const date = new Date();
|
||||
localMeta.version = `${localMeta.version}.${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, '0')}${date.getDate().toString().padStart(2, '0')}${date.getHours().toString().padStart(2, '0')}${date.getMinutes().toString().padStart(2, '0')}${date.getSeconds().toString().padStart(2, '0')}`;
|
||||
|
||||
|
||||
// 确认目录
|
||||
// const directoryItems = await remote.getDirectoryContents("/modules");
|
||||
// const modulesFolder = remote.getFileInfoByName(directoryItems, localMeta.id)
|
||||
if ( ! await remote.client.exists("/modules/" + localMeta.id) ) {
|
||||
// 创建文件夹
|
||||
await remote.client.createDirectory("/modules/" + localMeta.id);
|
||||
}
|
||||
|
||||
// 先更新文件
|
||||
for (let dist of localMeta.dist ) {
|
||||
|
||||
const folder = "/modules/" + localMeta.id + "/" + localMeta.version + "/" + dist.os + "/" + dist.platform
|
||||
await remote.wMkdir( folder );
|
||||
|
||||
$logger.info("upload dist", dist.path, fs.existsSync(dist.path))
|
||||
|
||||
await remote.wUpload( dist.path , folder + "/" + dist.filename );
|
||||
|
||||
}
|
||||
|
||||
// remote meta
|
||||
let remoteMeta = {
|
||||
"versions" : []
|
||||
}
|
||||
try {
|
||||
const remoteMetaStr = await remote.client.getFileContents("/modules/" + localMeta.id + "/meta.json", { format: "text" });
|
||||
remoteMeta = JSON5.parse( remoteMetaStr )
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
remoteMeta.id = localMeta.id;
|
||||
remoteMeta.name = localMeta.name;
|
||||
remoteMeta.desc = localMeta.desc;
|
||||
remoteMeta.type = localMeta.type;
|
||||
remoteMeta.vcs = localMeta.vcs;
|
||||
// 废弃
|
||||
// remoteMeta.platform = "cross" === localMeta.platform ? ["X86_64", "ARM64"] : localMeta.platform;
|
||||
// remoteMeta.os = "cross" === localMeta.os ? ["Windows", "Linux", "Alpine"] : localMeta.os;
|
||||
const dist = objutil.copy(localMeta.dist)
|
||||
for (let d of dist) {
|
||||
delete dist.path
|
||||
}
|
||||
remoteMeta.latest = {
|
||||
version : localMeta.version,
|
||||
dist
|
||||
}
|
||||
remoteMeta.versions.push(remoteMeta.latest)
|
||||
if ( remoteMeta.versions.length >= 20 ) {
|
||||
remoteMeta.versions = remoteMeta.versions.slice(0, 20);
|
||||
}
|
||||
|
||||
await remote.client.putFileContents("/modules/" + localMeta.id + "/meta.json", JSON5.stringify(remoteMeta, null, 4) , { overwrite: true });
|
||||
|
||||
}
|
||||
|
||||
$logger.info("文件上传成功")
|
||||
}
|
||||
await remote.putJson(`/${projectId}/metadata.json`, metadata);
|
||||
$logger.info(`文件上传成功 ${projectId}@${version}`);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user