Skip to content

Commit e0379a6

Browse files
committed
feat: 支持 Scala 版本同步
1 parent 51fa03b commit e0379a6

4 files changed

Lines changed: 393 additions & 6 deletions

File tree

scripts/sync-scala-versions.js

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

src-tauri/src/config.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl Default for AppConfig {
6060
}),
6161
environment_mirror: Some(EnvironmentMirrorConfig {
6262
enabled: Some(false),
63-
base_url: Some("http://cdn.global.devlive.top".to_string()),
63+
base_url: Some("https://cdn.global.devlive.top".to_string()),
6464
fallback_enabled: Some(false),
6565
}),
6666
}
@@ -130,7 +130,7 @@ impl ConfigManager {
130130
if config.environment_mirror.is_none() {
131131
config.environment_mirror = Some(EnvironmentMirrorConfig {
132132
enabled: Some(false),
133-
base_url: Some("http://cdn.global.devlive.top".to_string()),
133+
base_url: Some("https://cdn.global.devlive.top".to_string()),
134134
fallback_enabled: Some(false),
135135
});
136136
println!("读取配置 -> 添加默认 environment_mirror 配置");
@@ -243,7 +243,7 @@ impl ConfigManager {
243243
}),
244244
environment_mirror: Some(EnvironmentMirrorConfig {
245245
enabled: Some(false),
246-
base_url: Some("http://cdn.global.devlive.top".to_string()),
246+
base_url: Some("https://cdn.global.devlive.top".to_string()),
247247
fallback_enabled: Some(false),
248248
}),
249249
}

0 commit comments

Comments
 (0)