Skip to content

Commit d3fe49c

Browse files
committed
feat: 支持 Go 语言环境安装
1 parent 42c46d6 commit d3fe49c

File tree

15 files changed

+1275
-120
lines changed

15 files changed

+1275
-120
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ go.mod
3131
.Rhistory
3232
program
3333
.env
34+
*.gz

scripts/sync-clojure-versions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function getConfig() {
7272
ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
7373
ossBucket: process.env.OSS_BUCKET,
7474
cdnDomain: process.env.CDN_DOMAIN, // 自定义 CDN 域名(可选)
75+
githubToken: process.env.GITHUB_TOKEN, // GitHub Personal Access Token(可选,用于提高 API 速率限制)
7576
githubRepo: 'clojure/brew-install',
7677
ossPrefix: 'global/plugins/clojure/',
7778
tempDir: path.join(__dirname, '.temp-clojure'),
@@ -130,6 +131,11 @@ function httpGet(url, isJson = true) {
130131
}
131132
};
132133

134+
// 如果是 GitHub API 请求且配置了 Token,添加认证头
135+
if (url.includes('api.github.com') && CONFIG.githubToken) {
136+
options.headers['Authorization'] = `token ${CONFIG.githubToken}`;
137+
}
138+
133139
client.get(url, options, (res) => {
134140
if (res.statusCode === 302 || res.statusCode === 301) {
135141
// 处理重定向

scripts/sync-go-versions.js

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* 同步 Go 版本到阿里云 OSS
5+
*
6+
* 功能:
7+
* 1. 从 GitHub API 获取 Go 所有版本
8+
* 2. 下载版本文件
9+
* 3. 使用阿里云官方 ali-oss SDK 上传到 OSS /global/plugins/go/ 目录
10+
* 4. 生成 metadata.json 文件
11+
*
12+
* 使用方法:
13+
* node scripts/sync-go-versions.js
14+
*
15+
* 环境变量(可以在 .env 文件中配置):
16+
* - OSS_REGION: 阿里云 OSS 区域
17+
* - OSS_ACCESS_KEY_ID: 阿里云访问密钥 ID
18+
* - OSS_ACCESS_KEY_SECRET: 阿里云访问密钥 Secret
19+
* - OSS_BUCKET: OSS Bucket 名称
20+
* - CDN_DOMAIN: 自定义 CDN 域名(可选)
21+
*/
22+
23+
import https from 'https';
24+
import http from 'http';
25+
import fs from 'fs';
26+
import path from 'path';
27+
import crypto from 'crypto';
28+
import { fileURLToPath } from 'url';
29+
import OSS from 'ali-oss';
30+
31+
const __filename = fileURLToPath(import.meta.url);
32+
const __dirname = path.dirname(__filename);
33+
34+
function loadEnv() {
35+
const envPath = path.join(__dirname, '..', '.env');
36+
if (fs.existsSync(envPath)) {
37+
const envContent = fs.readFileSync(envPath, 'utf8');
38+
let loadedCount = 0;
39+
40+
envContent.split('\n').forEach(line => {
41+
line = line.trim();
42+
if (!line || line.startsWith('#')) return;
43+
44+
const match = line.match(/^([^=]+)=(.*)$/);
45+
if (match) {
46+
const key = match[1].trim();
47+
let value = match[2].trim();
48+
value = value.replace(/^["']|["']$/g, '');
49+
if (!process.env[key]) {
50+
process.env[key] = value;
51+
loadedCount++;
52+
}
53+
}
54+
});
55+
56+
console.log(`✓ 已从 .env 文件加载 ${loadedCount} 个环境变量\n`);
57+
} else {
58+
console.log('⚠ 未找到 .env 文件,将使用环境变量或默认值\n');
59+
}
60+
}
61+
62+
function getConfig() {
63+
return {
64+
ossRegion: process.env.OSS_REGION || 'oss-cn-hangzhou',
65+
ossAccessKeyId: process.env.OSS_ACCESS_KEY_ID,
66+
ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
67+
ossBucket: process.env.OSS_BUCKET,
68+
cdnDomain: process.env.CDN_DOMAIN,
69+
githubToken: process.env.GITHUB_TOKEN,
70+
githubRepo: 'golang/go',
71+
ossPrefix: 'global/plugins/go/',
72+
tempDir: path.join(__dirname, '.temp-go'),
73+
platformMap: {
74+
'darwin-arm64': 'macos-aarch64',
75+
'darwin-amd64': 'macos-x86_64',
76+
'linux-arm64': 'linux-aarch64',
77+
'linux-amd64': 'linux-x86_64',
78+
'windows-amd64': 'windows-x86_64'
79+
}
80+
};
81+
}
82+
83+
let CONFIG;
84+
85+
function validateConfig() {
86+
const missing = [];
87+
if (!CONFIG.ossAccessKeyId) missing.push('OSS_ACCESS_KEY_ID');
88+
if (!CONFIG.ossAccessKeySecret) missing.push('OSS_ACCESS_KEY_SECRET');
89+
if (!CONFIG.ossBucket) missing.push('OSS_BUCKET');
90+
91+
if (missing.length > 0) {
92+
console.error('错误: 请设置以下环境变量:');
93+
missing.forEach(key => console.error(` - ${key}`));
94+
console.error('\n提示: 可以在 .env 文件中配置这些变量');
95+
console.error('示例: cp .env.example .env');
96+
process.exit(1);
97+
}
98+
99+
console.log('配置信息:');
100+
console.log(` OSS Region: ${CONFIG.ossRegion}`);
101+
console.log(` OSS Bucket: ${CONFIG.ossBucket}`);
102+
console.log(` CDN Domain: ${CONFIG.cdnDomain || '未配置 (使用默认 OSS 域名)'}`);
103+
console.log('');
104+
}
105+
106+
function ensureTempDir() {
107+
if (!fs.existsSync(CONFIG.tempDir)) {
108+
fs.mkdirSync(CONFIG.tempDir, { recursive: true });
109+
}
110+
}
111+
112+
function cleanupTempDir() {
113+
if (fs.existsSync(CONFIG.tempDir)) {
114+
fs.rmSync(CONFIG.tempDir, { recursive: true, force: true });
115+
}
116+
}
117+
118+
function httpGet(url, isJson = true) {
119+
return new Promise((resolve, reject) => {
120+
const client = url.startsWith('https') ? https : http;
121+
const options = {
122+
headers: {
123+
'User-Agent': 'CodeForge-Sync-Script'
124+
}
125+
};
126+
127+
if (url.includes('api.github.com') && CONFIG.githubToken) {
128+
options.headers['Authorization'] = `token ${CONFIG.githubToken}`;
129+
}
130+
131+
client.get(url, options, (res) => {
132+
if (res.statusCode === 302 || res.statusCode === 301) {
133+
return httpGet(res.headers.location, isJson).then(resolve).catch(reject);
134+
}
135+
136+
if (res.statusCode !== 200) {
137+
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
138+
return;
139+
}
140+
141+
const chunks = [];
142+
res.on('data', chunk => chunks.push(chunk));
143+
res.on('end', () => {
144+
const data = Buffer.concat(chunks);
145+
if (isJson) {
146+
try {
147+
resolve(JSON.parse(data.toString()));
148+
} catch (e) {
149+
reject(new Error(`JSON 解析失败: ${e.message}`));
150+
}
151+
} else {
152+
resolve(data);
153+
}
154+
});
155+
}).on('error', reject);
156+
});
157+
}
158+
159+
async function downloadFile(url, destPath) {
160+
console.log(` 下载: ${url}`);
161+
const data = await httpGet(url, false);
162+
fs.writeFileSync(destPath, data);
163+
return destPath;
164+
}
165+
166+
async function getGoReleases() {
167+
console.log('正在获取 Go 版本列表...');
168+
const url = 'https://go.dev/dl/?mode=json&include=all';
169+
return await httpGet(url);
170+
}
171+
172+
function calculateMD5(filePath) {
173+
const buffer = fs.readFileSync(filePath);
174+
return crypto.createHash('md5').update(buffer).digest('hex');
175+
}
176+
177+
function getFileSize(filePath) {
178+
const stats = fs.statSync(filePath);
179+
return stats.size;
180+
}
181+
182+
function createOSSClient() {
183+
return new OSS({
184+
region: CONFIG.ossRegion,
185+
accessKeyId: CONFIG.ossAccessKeyId,
186+
accessKeySecret: CONFIG.ossAccessKeySecret,
187+
bucket: CONFIG.ossBucket
188+
});
189+
}
190+
191+
async function uploadToOSS(client, localPath, ossPath) {
192+
try {
193+
await client.put(ossPath, localPath);
194+
console.log(` ✓ 上传成功: ${ossPath}`);
195+
} catch (error) {
196+
throw new Error(`上传失败: ${error.message}`);
197+
}
198+
}
199+
200+
async function uploadMetadata(client, metadata) {
201+
try {
202+
const metadataJson = JSON.stringify(metadata, null, 2);
203+
const buffer = Buffer.from(metadataJson, 'utf8');
204+
const ossPath = `${CONFIG.ossPrefix}metadata.json`;
205+
206+
await client.put(ossPath, buffer);
207+
console.log(`✓ metadata.json 上传成功`);
208+
} catch (error) {
209+
throw new Error(`上传 metadata 失败: ${error.message}`);
210+
}
211+
}
212+
213+
async function main() {
214+
try {
215+
console.log('=== Go 版本同步工具 ===\n');
216+
217+
loadEnv();
218+
CONFIG = getConfig();
219+
validateConfig();
220+
221+
const ossClient = createOSSClient();
222+
ensureTempDir();
223+
224+
const releases = await getGoReleases();
225+
console.log(`找到 ${releases.length} 个版本\n`);
226+
227+
const metadata = {
228+
language: 'go',
229+
last_updated: new Date().toISOString(),
230+
releases: []
231+
};
232+
233+
const goOsMap = {
234+
'darwin': ['macos-aarch64', 'macos-x86_64'],
235+
'linux': ['linux-aarch64', 'linux-x86_64'],
236+
'windows': ['windows-x86_64']
237+
};
238+
239+
const goArchMap = {
240+
'arm64': 'aarch64',
241+
'amd64': 'x86_64'
242+
};
243+
244+
for (const release of releases) {
245+
const version = release.version.replace(/^go/, '');
246+
console.log(`处理版本: ${version}`);
247+
248+
const archiveFiles = release.files.filter(f => f.kind === 'archive');
249+
250+
if (archiveFiles.length === 0) {
251+
console.log(` ⚠ 跳过: 未找到归档文件`);
252+
continue;
253+
}
254+
255+
let processedCount = 0;
256+
257+
for (const file of archiveFiles) {
258+
const mappedArch = goArchMap[file.arch] || file.arch;
259+
const osPlatforms = goOsMap[file.os];
260+
261+
if (!osPlatforms) continue;
262+
263+
const platform = osPlatforms.find(p => p.includes(mappedArch));
264+
if (!platform) continue;
265+
266+
try {
267+
const fileName = file.filename;
268+
const localPath = path.join(CONFIG.tempDir, fileName);
269+
const goDevUrl = `https://go.dev/dl/${fileName}`;
270+
271+
console.log(` 下载 ${fileName}...`);
272+
await downloadFile(goDevUrl, localPath);
273+
274+
const fileSize = getFileSize(localPath);
275+
const md5 = calculateMD5(localPath);
276+
277+
const ossPath = `${CONFIG.ossPrefix}${version}/${fileName}`;
278+
await uploadToOSS(ossClient, localPath, ossPath);
279+
280+
const cdnUrl = CONFIG.cdnDomain
281+
? `${CONFIG.cdnDomain}/${ossPath}`
282+
: `https://${CONFIG.ossBucket}.${CONFIG.ossRegion}.aliyuncs.com/${ossPath}`;
283+
284+
metadata.releases.push({
285+
version: version,
286+
display_name: `Go ${release.version}`,
287+
published_at: new Date().toISOString(),
288+
download_url: cdnUrl,
289+
github_url: goDevUrl,
290+
file_name: fileName,
291+
size: fileSize,
292+
md5: md5,
293+
supported_platforms: [platform]
294+
});
295+
296+
processedCount++;
297+
fs.unlinkSync(localPath);
298+
} catch (error) {
299+
console.error(` ✗ 处理文件 ${file.filename} 失败: ${error.message}`);
300+
}
301+
}
302+
303+
if (processedCount > 0) {
304+
console.log(` ✓ 版本 ${release.version} 处理完成 (${processedCount} 个平台)\n`);
305+
} else {
306+
console.log(` ⚠ 版本 ${release.version} 没有可用的平台文件\n`);
307+
}
308+
}
309+
310+
metadata.releases.sort((a, b) => {
311+
const versionA = a.version.split('.').map(Number);
312+
const versionB = b.version.split('.').map(Number);
313+
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
314+
const numA = versionA[i] || 0;
315+
const numB = versionB[i] || 0;
316+
if (numA !== numB) return numB - numA;
317+
}
318+
return 0;
319+
});
320+
321+
console.log('\n上传 metadata.json...');
322+
await uploadMetadata(ossClient, metadata);
323+
324+
console.log(`\n✓ 同步完成!共处理 ${metadata.releases.length} 个版本`);
325+
326+
const metadataUrl = CONFIG.cdnDomain
327+
? `${CONFIG.cdnDomain}/${CONFIG.ossPrefix}metadata.json`
328+
: `https://${CONFIG.ossBucket}.${CONFIG.ossRegion}.aliyuncs.com/${CONFIG.ossPrefix}metadata.json`;
329+
console.log(`\nmetadata URL: ${metadataUrl}`);
330+
331+
} catch (error) {
332+
console.error('\n✗ 错误:', error.message);
333+
process.exit(1);
334+
} finally {
335+
cleanupTempDir();
336+
}
337+
}
338+
339+
main();

scripts/sync-scala-versions.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function getConfig() {
6969
ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
7070
ossBucket: process.env.OSS_BUCKET,
7171
cdnDomain: process.env.CDN_DOMAIN,
72+
githubToken: process.env.GITHUB_TOKEN,
7273
githubRepo: 'lampepfl/dotty',
7374
ossPrefix: 'global/plugins/scala/',
7475
tempDir: path.join(__dirname, '.temp-scala'),
@@ -130,6 +131,10 @@ function httpGet(url, isJson = true) {
130131
}
131132
};
132133

134+
if (url.includes('api.github.com') && CONFIG.githubToken) {
135+
options.headers['Authorization'] = `token ${CONFIG.githubToken}`;
136+
}
137+
133138
client.get(url, options, (res) => {
134139
if (res.statusCode === 302 || res.statusCode === 301) {
135140
return httpGet(res.headers.location, isJson).then(resolve).catch(reject);

0 commit comments

Comments
 (0)