Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 33 additions & 14 deletions en/guide/download.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ home: hello
<script setup lang="ts">
import { computed, ref } from 'vue'
import { data } from '../../metadata.data.js'
import { resolveDownloadUrl } from '../../releaseUtils.ts'
import { useLatestVersion } from '../../useLatestVersion.ts'

const { latestVersion, prereleaseVersion, loading, error } = useLatestVersion()
const { latestVersion, prereleaseVersion, latestRelease, prereleaseRelease, nightlyRelease, loading } = useLatestVersion()

interface Package {
os: string
arch: string
gui_pkg_tmpl: record<string, string>
cli_pkg_tmpl: record<string, string> // key: format, value: url
gui_pkg_tmpl: Record<string, string>
cli_pkg_tmpl: Record<string, string> // key: format, value: url
comment?: string
}

Expand Down Expand Up @@ -157,20 +158,30 @@ const all_proxy = new Set(data.github_accels)

const channel = ref('easytier_latest_version')

const url = 'https://github.com/EasyTier/EasyTier/releases/tag/v'
const url = 'https://github.com/EasyTier/EasyTier/releases/tag/'
const filter_os = ref('')
const filter_arch = ref('')
const accel_proxy = ref('')

const vers = computed(() => ({
easytier_latest_version: (!loading.value && !error.value && latestVersion.value) ? latestVersion.value : data.easytier_latest_version,
easytier_pre_release_version: (!loading.value && !error.value && prereleaseVersion.value) ? prereleaseVersion.value : data.easytier_pre_release_version,
easytier_latest_version: latestVersion.value ?? data.easytier_latest_version,
easytier_pre_release_version: prereleaseVersion.value ?? data.easytier_pre_release_version,
}))

const currentVersion = computed(() => vers.value[channel.value as keyof typeof vers.value])
const currentRelease = computed(() => {
if (channel.value === 'nightly')
return nightlyRelease.value
return channel.value === 'easytier_latest_version' ? latestRelease.value : prereleaseRelease.value
})
const currentTag = computed(() => channel.value === 'nightly' ? 'nightly' : currentRelease.value?.tag_name ?? `v${currentVersion.value}`)
const currentTitle = computed(() => channel.value === 'nightly' ? 'Nightly' : currentTag.value)

function renderUrlTmpl(url_tmpl: string): string {
return accel_proxy.value + url_tmpl.replace(/\{\}/g, currentVersion.value)
function renderLinks(templates: Record<string, string>) {
return Object.entries(templates).flatMap(([format, template]) => {
const downloadUrl = resolveDownloadUrl(template, currentRelease.value, channel.value === 'nightly' ? undefined : currentVersion.value)
return downloadUrl ? [{ format, url: accel_proxy.value + downloadUrl }] : []
})
}

</script>
Expand All @@ -192,10 +203,18 @@ The command line program package includes four executables:
<select name="channel-select" id="channel-select" v-model="channel" class="filter-select">
<option value="easytier_latest_version"> Stable Version(v{{ vers.easytier_latest_version }}) </option>
<option value="easytier_pre_release_version"> Pre-release Version(v{{ vers.easytier_pre_release_version }}) </option>
<option value="nightly"> Development (Nightly) </option>
</select>
</div>

## <a :href="url + currentVersion">EasyTier v{{ currentVersion }}</a> { #latest }
<p v-if="channel === 'nightly'">Nightly follows the main branch and is intended for testing the latest features. It may be unstable.</p>

## <a :href="url + currentTag">EasyTier {{ currentTitle }}</a> { #latest }

<p v-if="channel === 'nightly' && !nightlyRelease?.assets.length">
<span v-if="loading">Loading Nightly downloads…</span>
<span v-else>Nightly downloads could not be loaded. Please visit the <a :href="url + 'nightly'">Nightly release page</a>.</span>
</p>

- GitHub Acceleration
<div>
Expand All @@ -221,7 +240,7 @@ The command line program package includes four executables:
</select>
</div>

<table>
<table v-if="channel !== 'nightly' || nightlyRelease?.assets.length">

<thead>
<tr>
Expand All @@ -239,14 +258,14 @@ The command line program package includes four executables:
<td> {{ pkg.arch }} </td>

<td>
<a v-for="(url_tmpl, format) in pkg.gui_pkg_tmpl" class="download-link-span" :href="renderUrlTmpl(url_tmpl)">
{{format}}
<a v-for="link in renderLinks(pkg.gui_pkg_tmpl)" :key="link.format" class="download-link-span" :href="link.url">
{{link.format}}
</a>
</td>

<td>
<a v-for="(url_tmpl, format) in pkg.cli_pkg_tmpl" class="download-link-span" :href="renderUrlTmpl(url_tmpl)">
{{format}}
<a v-for="link in renderLinks(pkg.cli_pkg_tmpl)" :key="link.format" class="download-link-span" :href="link.url">
{{link.format}}
</a>
</td>

Expand Down
47 changes: 33 additions & 14 deletions guide/download.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ home: hello
<script setup lang="ts">
import { computed, ref } from 'vue'
import { data } from '../metadata.data.js'
import { resolveDownloadUrl } from '../releaseUtils.ts'
import { useLatestVersion } from '../useLatestVersion.ts'

const { latestVersion, prereleaseVersion, loading, error } = useLatestVersion()
const { latestVersion, prereleaseVersion, latestRelease, prereleaseRelease, nightlyRelease, loading } = useLatestVersion()

interface Package {
os: string
arch: string
gui_pkg_tmpl: record<string, string>
cli_pkg_tmpl: record<string, string> // key: format, value: url
gui_pkg_tmpl: Record<string, string>
cli_pkg_tmpl: Record<string, string> // key: format, value: url
comment?: string
}

Expand Down Expand Up @@ -157,20 +158,30 @@ const all_proxy = new Set(data.github_accels)

const channel = ref('easytier_latest_version')

const url = 'https://github.com/EasyTier/EasyTier/releases/tag/v'
const url = 'https://github.com/EasyTier/EasyTier/releases/tag/'
const filter_os = ref('')
const filter_arch = ref('')
const accel_proxy = ref('')

const vers = computed(() => ({
easytier_latest_version: (!loading.value && !error.value && latestVersion.value) ? latestVersion.value : data.easytier_latest_version,
easytier_pre_release_version: (!loading.value && !error.value && prereleaseVersion.value) ? prereleaseVersion.value : data.easytier_pre_release_version,
easytier_latest_version: latestVersion.value ?? data.easytier_latest_version,
easytier_pre_release_version: prereleaseVersion.value ?? data.easytier_pre_release_version,
}))

const currentVersion = computed(() => vers.value[channel.value as keyof typeof vers.value])
const currentRelease = computed(() => {
if (channel.value === 'nightly')
return nightlyRelease.value
return channel.value === 'easytier_latest_version' ? latestRelease.value : prereleaseRelease.value
})
const currentTag = computed(() => channel.value === 'nightly' ? 'nightly' : currentRelease.value?.tag_name ?? `v${currentVersion.value}`)
const currentTitle = computed(() => channel.value === 'nightly' ? 'Nightly' : currentTag.value)

function renderUrlTmpl(url_tmpl: string): string {
return accel_proxy.value + url_tmpl.replace(/\{\}/g, currentVersion.value)
function renderLinks(templates: Record<string, string>) {
return Object.entries(templates).flatMap(([format, template]) => {
const downloadUrl = resolveDownloadUrl(template, currentRelease.value, channel.value === 'nightly' ? undefined : currentVersion.value)
return downloadUrl ? [{ format, url: accel_proxy.value + downloadUrl }] : []
})
}

</script>
Expand All @@ -192,10 +203,18 @@ function renderUrlTmpl(url_tmpl: string): string {
<select name="channel-select" id="channel-select" v-model="channel" class="filter-select">
<option value="easytier_latest_version"> 稳定版(v{{ vers.easytier_latest_version }}) </option>
<option value="easytier_pre_release_version"> 预发布版(v{{ vers.easytier_pre_release_version }}) </option>
<option value="nightly"> 开发版(Nightly) </option>
</select>
</div>

## <a :href="url + currentVersion">EasyTier v{{ currentVersion }}</a> { #latest }
<p v-if="channel === 'nightly'">Nightly 跟随 main 分支更新,适合测试最新功能,可能不稳定。</p>

## <a :href="url + currentTag">EasyTier {{ currentTitle }}</a> { #latest }

<p v-if="channel === 'nightly' && !nightlyRelease?.assets.length">
<span v-if="loading">正在加载 Nightly 下载文件…</span>
<span v-else>暂时无法获取 Nightly 下载文件,请前往 <a :href="url + 'nightly'">Nightly 发布页</a>。</span>
</p>

- Github 加速
<div>
Expand All @@ -221,7 +240,7 @@ function renderUrlTmpl(url_tmpl: string): string {
</select>
</div>

<table>
<table v-if="channel !== 'nightly' || nightlyRelease?.assets.length">

<thead>
<tr>
Expand All @@ -239,14 +258,14 @@ function renderUrlTmpl(url_tmpl: string): string {
<td> {{ pkg.arch }} </td>

<td>
<a v-for="(url_tmpl, format) in pkg.gui_pkg_tmpl" class="download-link-span" :href="renderUrlTmpl(url_tmpl)">
{{format}}
<a v-for="link in renderLinks(pkg.gui_pkg_tmpl)" :key="link.format" class="download-link-span" :href="link.url">
{{link.format}}
</a>
</td>

<td>
<a v-for="(url_tmpl, format) in pkg.cli_pkg_tmpl" class="download-link-span" :href="renderUrlTmpl(url_tmpl)">
{{format}}
<a v-for="link in renderLinks(pkg.cli_pkg_tmpl)" :key="link.format" class="download-link-span" :href="link.url">
{{link.format}}
</a>
</td>

Expand Down
2 changes: 1 addition & 1 deletion metadata.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export default {
load() {
return {
easytier_latest_version: '2.6.4',
easytier_pre_release_version: '2.6.3',
easytier_pre_release_version: '2.7.0',
github_accels: [
'https://ghfast.top/',
'https://v6.gh-proxy.org/',
Expand Down
22 changes: 22 additions & 0 deletions releaseUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface Release {
tag_name: string
prerelease?: boolean
draft?: boolean
assets: Array<{ name: string, browser_download_url: string }>
}

export function findPrerelease(releases: Release[]): Release | null {
return releases.find(release => release.prerelease && !release.draft && release.tag_name !== 'nightly') ?? null
}

export function resolveDownloadUrl(template: string, release: Release | null, fallbackVersion?: string): string | undefined {
if (release) {
const filename = template.slice(template.lastIndexOf('/') + 1)
const pattern = filename.split('{}')
.map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('[^/]+')
return release.assets.find(asset => new RegExp(`^${pattern}$`).test(asset.name))?.browser_download_url
}

return fallbackVersion ? template.replace(/\{\}/g, fallbackVersion) : undefined
}
54 changes: 54 additions & 0 deletions tests/releaseUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Release } from '../releaseUtils.ts'
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the built-in runner without adding a test framework.
import { test } from 'node:test'
import { findPrerelease, resolveDownloadUrl } from '../releaseUtils.ts'

const cliTemplate = 'https://github.com/EasyTier/EasyTier/releases/download/v{}/easytier-linux-x86_64-v{}.zip'
const guiTemplate = 'https://github.com/EasyTier/EasyTier/releases/download/v{}/easytier-gui_{}_x64-setup.exe'

function release(tag_name: string, assets: string[] = [], prerelease = true): Release {
return {
tag_name,
prerelease,
assets: assets.map(name => ({ name, browser_download_url: `https://github.com/EasyTier/EasyTier/releases/download/${tag_name}/${name}` })),
}
}

test('ordinary prereleases exclude rolling nightly and draft releases', () => {
const prerelease = release('v2.7.0')
assert.equal(findPrerelease([
release('nightly'),
{ ...release('v2.8.0'), draft: true },
prerelease,
release('v2.6.4', [], false),
]), prerelease)
assert.equal(findPrerelease([release('nightly'), release('v2.6.4', [], false)]), null)
})

test('nightly uses the actual release tag and versioned asset name', () => {
const nightly = release('nightly', ['easytier-linux-x86_64-v2.7.0.zip'])
assert.equal(resolveDownloadUrl(cliTemplate, nightly), 'https://github.com/EasyTier/EasyTier/releases/download/nightly/easytier-linux-x86_64-v2.7.0.zip')
})

test('GUI asset versions can differ from the release version', () => {
const prerelease = release('v2.6.1', ['easytier-gui_2.6.0_x64-setup.exe'])
assert.equal(resolveDownloadUrl(guiTemplate, prerelease), 'https://github.com/EasyTier/EasyTier/releases/download/v2.6.1/easytier-gui_2.6.0_x64-setup.exe')
})

test('Android assets without a version placeholder also resolve', () => {
const nightly = release('nightly', ['app-arm64-release.apk'])
assert.equal(resolveDownloadUrl('https://github.com/EasyTier/EasyTier/releases/download/v{}/app-arm64-release.apk', nightly), 'https://github.com/EasyTier/EasyTier/releases/download/nightly/app-arm64-release.apk')
})

test('missing assets do not produce invented download links', () => {
assert.equal(resolveDownloadUrl(cliTemplate, release('nightly', ['easytier-linux-aarch64-v2.7.0.zip'])), undefined)
assert.equal(resolveDownloadUrl(guiTemplate, release('v2.6.4'), '2.6.4'), undefined)
assert.equal(resolveDownloadUrl(cliTemplate, release('nightly', ['easytier-linux-x86_64-v2.7.0Xzip'])), undefined)
})

test('API failure preserves stable and prerelease fallbacks without guessing nightly assets', () => {
assert.equal(resolveDownloadUrl(cliTemplate, null, '2.6.4'), 'https://github.com/EasyTier/EasyTier/releases/download/v2.6.4/easytier-linux-x86_64-v2.6.4.zip')
assert.equal(resolveDownloadUrl(guiTemplate, null, '2.7.0'), 'https://github.com/EasyTier/EasyTier/releases/download/v2.7.0/easytier-gui_2.7.0_x64-setup.exe')
assert.equal(resolveDownloadUrl(cliTemplate, null), undefined)
})
56 changes: 29 additions & 27 deletions useLatestVersion.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
import { onMounted, ref } from 'vue'
import type { Release } from './releaseUtils'
import { computed, onMounted, ref } from 'vue'
import { findPrerelease } from './releaseUtils'

export function useLatestVersion() {
const latestVersion = ref<string | null>(null)
const prereleaseVersion = ref<string | null>(null)
const latestRelease = ref<Release | null>(null)
const prereleaseRelease = ref<Release | null>(null)
const nightlyRelease = ref<Release | null>(null)
const latestVersion = computed(() => latestRelease.value?.tag_name.replace(/^v/, '') ?? null)
const prereleaseVersion = computed(() => prereleaseRelease.value?.tag_name.replace(/^v/, '') ?? null)
const loading = ref(true)
const error = ref<string | null>(null)

onMounted(async () => {
try {
const [latestRes, releasesRes] = await Promise.all([
fetch('https://api.github.com/repos/EasyTier/EasyTier/releases/latest'),
fetch('https://api.github.com/repos/EasyTier/EasyTier/releases?per_page=5'),
])

if (!latestRes.ok || !releasesRes.ok) {
async function fetchRelease<T>(path: string): Promise<T> {
const response = await fetch(`https://api.github.com/repos/EasyTier/EasyTier/releases${path}`, { signal: AbortSignal.timeout(10000) })
if (!response.ok)
throw new Error('Failed to fetch releases from GitHub API')
}
return response.json()
}

const latestData = await latestRes.json() as { tag_name?: string }
const releases = await releasesRes.json() as Array<{ tag_name?: string, prerelease?: boolean }>
const [latest, releases, nightly] = await Promise.allSettled([
fetchRelease<Release>('/latest'),
fetchRelease<Release[]>('?per_page=100'),
fetchRelease<Release>('/tags/nightly'),
])

latestVersion.value = latestData.tag_name ?? null
prereleaseVersion.value = releases.find(item => item.prerelease)?.tag_name ?? null
}
catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
}
finally {
loading.value = false
}
if (latest.status === 'fulfilled')
latestRelease.value = latest.value
if (releases.status === 'fulfilled')
prereleaseRelease.value = findPrerelease(releases.value)
if (nightly.status === 'fulfilled')
nightlyRelease.value = nightly.value

if (latestVersion.value?.startsWith('v'))
latestVersion.value = latestVersion.value.slice(1)
if (prereleaseVersion.value?.startsWith('v'))
prereleaseVersion.value = prereleaseVersion.value.slice(1)
const failure = [latest, releases, nightly].find(result => result.status === 'rejected')
if (failure?.status === 'rejected')
error.value = failure.reason instanceof Error ? failure.reason.message : 'Unknown error'
loading.value = false
})

return { latestVersion, prereleaseVersion, loading, error }
return { latestVersion, prereleaseVersion, latestRelease, prereleaseRelease, nightlyRelease, loading, error }
}