feat: add Flathub mirror support

This commit is contained in:
xixu-me committed 2026-03-14 16:23:38 +08:00
1 parent 6c7b15454d
commit 88c351a9ac
10 files changed
+474 -19

No files matched your search

+66
View File
@@ -256,6 +256,7 @@ Using the pre-deployed instance **`xget.xi-xu.me`** or your own deployed instanc
| openSUSE | `opensuse` | `https://download.opensuse.org/...` | `https://xget.xi-xu.me/opensuse/...` |
| Arch Linux | `arch` | `https://geo.mirror.pkgbuild.com/...` | `https://xget.xi-xu.me/arch/...` |
| arXiv | `arxiv` | `https://arxiv.org/...` | `https://xget.xi-xu.me/arxiv/...` |
| Flathub | `flathub` | `https://dl.flathub.org/...` | `https://xget.xi-xu.me/flathub/...` |
| F-Droid | `fdroid` | `https://f-droid.org/...` | `https://xget.xi-xu.me/fdroid/...` |
| Jenkins Plugins | `jenkins` | `https://updates.jenkins.io/...` | `https://xget.xi-xu.me/jenkins/...` |
| Container Registries | `cr` | See [Container Registries](#container-registries) | See [Container Registries](#container-registries) |
@@ -707,6 +708,22 @@ https://f-droid.org/api/v1/packages/org.fdroid.fdroid
https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Flathub
```url
# Flathub repository original URL
https://dl.flathub.org/repo/summary
# Converted (add flathub prefix)
https://xget.xi-xu.me/flathub/repo/summary
# Flathub app reference original URL
https://dl.flathub.org/repo/appstream/org.gnome.gedit.flatpakref
# Converted (add flathub prefix)
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
```
#### Jenkins Plugins
```url
@@ -1866,6 +1883,55 @@ task checkFDroidAvailability {
}
```
### Flathub Repository Mirror
#### Configure Flatpak / Flathub to Use Xget Mirror
```bash
# Add the Flathub remote via the rewritten .flatpakrepo descriptor
flatpak remote-add --if-not-exists flathub \
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# Or add the OSTree repository directly
flatpak remote-add --if-not-exists flathub \
https://xget.xi-xu.me/flathub/repo/
```
#### Supported Flathub Services
```url
# OSTree repository metadata
https://xget.xi-xu.me/flathub/repo/summary
https://xget.xi-xu.me/flathub/repo/summary.sig
# Flatpak remote descriptor
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# App reference descriptor
https://xget.xi-xu.me/flathub/repo/appstream/[app-id].flatpakref
# Repository objects and static deltas
https://xget.xi-xu.me/flathub/repo/objects/...
https://xget.xi-xu.me/flathub/repo/deltas/...
```
#### Usage Examples
```bash
# Inspect remote contents
flatpak remote-ls flathub
# Install an app from the Xget-backed Flathub mirror
flatpak install flathub org.gnome.gedit
# Install directly from a rewritten .flatpakref
flatpak install --from \
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
# Update installed apps and runtimes
flatpak update
```
### Jenkins Plugin Download
#### Use Xget to Accelerate Jenkins Plugin Download and Update
+66
View File
@@ -256,6 +256,7 @@ classDiagram
| openSUSE | `opensuse` | `https://download.opensuse.org/...` | `https://xget.xi-xu.me/opensuse/...` |
| Arch Linux | `arch` | `https://geo.mirror.pkgbuild.com/...` | `https://xget.xi-xu.me/arch/...` |
| arXiv | `arxiv` | `https://arxiv.org/...` | `https://xget.xi-xu.me/arxiv/...` |
| Flathub | `flathub` | `https://dl.flathub.org/...` | `https://xget.xi-xu.me/flathub/...` |
| F-Droid | `fdroid` | `https://f-droid.org/...` | `https://xget.xi-xu.me/fdroid/...` |
| Jenkins 插件 | `jenkins` | `https://updates.jenkins.io/...` | `https://xget.xi-xu.me/jenkins/...` |
| 容器注册表 | `cr` | 见[容器注册表](#容器注册表) | 见[容器注册表](#容器注册表) |
@@ -707,6 +708,22 @@ https://f-droid.org/api/v1/packages/org.fdroid.fdroid
https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Flathub
```url
# Flathub 存储库原始 URL
https://dl.flathub.org/repo/summary
# 转换后(添加 flathub 前缀)
https://xget.xi-xu.me/flathub/repo/summary
# Flathub 应用引用原始 URL
https://dl.flathub.org/repo/appstream/org.gnome.gedit.flatpakref
# 转换后(添加 flathub 前缀)
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
```
#### Jenkins 插件
```url
@@ -1866,6 +1883,55 @@ task checkFDroidAvailability {
}
```
### Flathub 存储库镜像
#### 配置 Flatpak / Flathub 使用 Xget 镜像
```bash
# 通过重写后的 .flatpakrepo 描述文件添加 Flathub 远程仓库
flatpak remote-add --if-not-exists flathub \
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# 或者直接添加 OSTree 存储库地址
flatpak remote-add --if-not-exists flathub \
https://xget.xi-xu.me/flathub/repo/
```
#### 支持的 Flathub 服务
```url
# OSTree 存储库元数据
https://xget.xi-xu.me/flathub/repo/summary
https://xget.xi-xu.me/flathub/repo/summary.sig
# Flatpak 远程仓库描述文件
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# 应用引用描述文件
https://xget.xi-xu.me/flathub/repo/appstream/[应用 ID].flatpakref
# 存储库对象与静态增量
https://xget.xi-xu.me/flathub/repo/objects/...
https://xget.xi-xu.me/flathub/repo/deltas/...
```
#### 使用示例
```bash
# 查看远程仓库内容
flatpak remote-ls flathub
# 从 Xget 加速的 Flathub 镜像安装应用
flatpak install flathub org.gnome.gedit
# 直接通过重写后的 .flatpakref 安装
flatpak install --from \
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
# 更新已安装的应用和运行时
flatpak update
```
### Jenkins 插件下载
#### 使用 Xget 加速 Jenkins 插件下载和更新
+66
View File
@@ -256,6 +256,7 @@ classDiagram
| openSUSE | `opensuse` | `https://download.opensuse.org/...` | `https://xget.xi-xu.me/opensuse/...` |
| Arch Linux | `arch` | `https://geo.mirror.pkgbuild.com/...` | `https://xget.xi-xu.me/arch/...` |
| arXiv | `arxiv` | `https://arxiv.org/...` | `https://xget.xi-xu.me/arxiv/...` |
| Flathub | `flathub` | `https://dl.flathub.org/...` | `https://xget.xi-xu.me/flathub/...` |
| F-Droid | `fdroid` | `https://f-droid.org/...` | `https://xget.xi-xu.me/fdroid/...` |
| Jenkins 外掛程式 | `jenkins` | `https://updates.jenkins.io/...` | `https://xget.xi-xu.me/jenkins/...` |
| 容器註冊表 | `cr` | 見[容器註冊表](#容器註冊表) | 見[容器註冊表](#容器註冊表) |
@@ -707,6 +708,22 @@ https://f-droid.org/api/v1/packages/org.fdroid.fdroid
https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Flathub
```url
# Flathub 儲存庫原始 URL
https://dl.flathub.org/repo/summary
# 轉換後(新增 flathub 前綴)
https://xget.xi-xu.me/flathub/repo/summary
# Flathub 應用程式引用原始 URL
https://dl.flathub.org/repo/appstream/org.gnome.gedit.flatpakref
# 轉換後(新增 flathub 前綴)
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
```
#### Jenkins 外掛程式
```url
@@ -1866,6 +1883,55 @@ task checkFDroidAvailability {
}
```
### Flathub 儲存庫鏡像
#### 配置 Flatpak / Flathub 使用 Xget 鏡像
```bash
# 透過重寫後的 .flatpakrepo 描述檔新增 Flathub 遠端儲存庫
flatpak remote-add --if-not-exists flathub \
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# 或者直接新增 OSTree 儲存庫位址
flatpak remote-add --if-not-exists flathub \
https://xget.xi-xu.me/flathub/repo/
```
#### 支援的 Flathub 服務
```url
# OSTree 儲存庫中繼資料
https://xget.xi-xu.me/flathub/repo/summary
https://xget.xi-xu.me/flathub/repo/summary.sig
# Flatpak 遠端儲存庫描述檔
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# 應用程式引用描述檔
https://xget.xi-xu.me/flathub/repo/appstream/[應用程式 ID].flatpakref
# 儲存庫物件與靜態增量
https://xget.xi-xu.me/flathub/repo/objects/...
https://xget.xi-xu.me/flathub/repo/deltas/...
```
#### 使用範例
```bash
# 檢視遠端儲存庫內容
flatpak remote-ls flathub
# 從 Xget 加速的 Flathub 鏡像安裝應用程式
flatpak install flathub org.gnome.gedit
# 直接透過重寫後的 .flatpakref 安裝
flatpak install --from \
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
# 更新已安裝的應用程式與執行時
flatpak update
```
### Jenkins 外掛程式下載
#### 使用 Xget 加速 Jenkins 外掛程式下載和更新
+2
View File
@@ -63,6 +63,7 @@
*
* **Other Resources:**
* - `arxiv` - arXiv papers (arxiv.org)
* - `flathub` - Flathub Flatpak repository (dl.flathub.org)
* - `fdroid` - F-Droid Android apps (f-droid.org)
* - `jenkins` - Jenkins plugins (updates.jenkins.io)
*
@@ -171,6 +172,7 @@ export const PLATFORMS = {
// Other Resources
arxiv: 'https://arxiv.org',
flathub: 'https://dl.flathub.org',
fdroid: 'https://f-droid.org',
jenkins: 'https://updates.jenkins.io',
+19 -19
View File
@@ -22,6 +22,11 @@ import {
} from './protocols/docker.js';
import { configureGitHeaders, isGitLFSRequest, isGitRequest } from './protocols/git.js';
import { PerformanceMonitor, addPerformanceHeaders } from './utils/performance.js';
import {
isFlatpakReferenceFilePath,
rewriteTextResponse,
shouldRewriteTextResponse
} from './utils/rewrite.js';
import { addCorsHeaders, addSecurityHeaders, createErrorResponse } from './utils/security.js';
import { getAllowedMethods, isDockerRequest, validateRequest } from './utils/validation.js';
@@ -546,26 +551,21 @@ async function handleRequest(request, env, ctx) {
let rewrittenContentLength = null;
if (
platform === 'pypi' &&
response.headers.get('content-type')?.includes('text/html')
shouldRewriteTextResponse(
platform,
effectivePath,
response.headers.get('content-type') || ''
)
) {
const originalText = await response.text();
const rewrittenText = originalText.replace(
/https:\/\/files\.pythonhosted\.org/g,
`${url.origin}/pypi/files`
);
responseBody = rewrittenText;
rewrittenContentLength = new TextEncoder().encode(rewrittenText).byteLength;
}
if (
platform === 'npm' &&
response.headers.get('content-type')?.includes('application/json')
) {
const originalText = await response.text();
const rewrittenText = originalText.replace(
/https:\/\/registry.npmjs.org\/([^/]+)/g,
`${url.origin}/npm/$1`
const originalText =
platform === 'flathub' && isFlatpakReferenceFilePath(effectivePath)
? new TextDecoder().decode(await response.arrayBuffer())
: await response.text();
const rewrittenText = rewriteTextResponse(
platform,
effectivePath,
originalText,
url.origin
);
responseBody = rewrittenText;
rewrittenContentLength = new TextEncoder().encode(rewrittenText).byteLength;
+65
View File
@@ -0,0 +1,65 @@
/**
* Xget - Platform-specific upstream response rewriting helpers.
*/
const FLATHUB_REPO_BASE_URL_PATTERN = /https:\/\/(?:dl\.)?flathub\.org\/repo\//g;
const FLATPAK_REFERENCE_FILE_PATTERN = /\.(flatpakrepo|flatpakref)$/i;
/**
* Checks whether a successful upstream response should be rewritten before returning it.
* @param {string} platform
* @param {string} requestPath
* @param {string} contentType
* @returns {boolean} True when the upstream response body should be rewritten.
*/
export function shouldRewriteTextResponse(platform, requestPath, contentType = '') {
if (platform === 'pypi') {
return contentType.includes('text/html');
}
if (platform === 'npm') {
return contentType.includes('application/json');
}
if (platform === 'flathub') {
return FLATPAK_REFERENCE_FILE_PATTERN.test(requestPath);
}
return false;
}
/**
* Checks whether a request path points to a Flatpak descriptor file.
* @param {string} requestPath
* @returns {boolean} True when the request targets a `.flatpakrepo` or `.flatpakref` file.
*/
export function isFlatpakReferenceFilePath(requestPath) {
return FLATPAK_REFERENCE_FILE_PATTERN.test(requestPath);
}
/**
* Rewrites upstream text responses so follow-up requests continue flowing through Xget.
* @param {string} platform
* @param {string} requestPath
* @param {string} originalText
* @param {string} origin
* @returns {string} Rewritten response text.
*/
export function rewriteTextResponse(platform, requestPath, originalText, origin) {
if (platform === 'pypi') {
return originalText.replace(/https:\/\/files\.pythonhosted\.org/g, `${origin}/pypi/files`);
}
if (platform === 'npm') {
return originalText.replace(
/https:\/\/registry\.npmjs\.org\/([^/]+)/g,
`${origin}/npm/$1`
);
}
if (platform === 'flathub' && isFlatpakReferenceFilePath(requestPath)) {
return originalText.replace(FLATHUB_REPO_BASE_URL_PATTERN, `${origin}/flathub/repo/`);
}
return originalText;
}
+8
View File
@@ -93,6 +93,14 @@ describe('Xget Core Functionality', () => {
expect(response.status).not.toBe(400);
});
it('should handle Flathub URLs correctly', async () => {
const testUrl = 'https://example.com/flathub/repo/summary';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to Flathub
expect(response.status).not.toBe(400);
});
it('should not treat nested /v2/ path segments as container registry requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/releases/download/v2/file.tar.gz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import { PLATFORMS, transformPath } from '../../src/config/platforms.js';
describe('Flathub Platform Configuration', () => {
it('should have Flathub platform configured', () => {
expect(PLATFORMS.flathub).toBe('https://dl.flathub.org');
});
it('should transform Flathub repository paths correctly', () => {
const testCases = [
{
input: '/flathub/repo/summary',
expected: '/repo/summary',
description: 'repository summary'
},
{
input: '/flathub/repo/summary.sig',
expected: '/repo/summary.sig',
description: 'repository summary signature'
},
{
input: '/flathub/repo/flathub.flatpakrepo',
expected: '/repo/flathub.flatpakrepo',
description: 'remote descriptor'
},
{
input: '/flathub/repo/appstream/org.gnome.gedit.flatpakref',
expected: '/repo/appstream/org.gnome.gedit.flatpakref',
description: 'application reference'
},
{
input: '/flathub/repo/objects/12/34567890abcdef.filez',
expected: '/repo/objects/12/34567890abcdef.filez',
description: 'content-addressed object'
},
{
input: '/flathub/repo/deltas/ABCD.superblock',
expected: '/repo/deltas/ABCD.superblock',
description: 'static delta'
}
];
testCases.forEach(({ input, expected, description }) => {
const result = transformPath(input, 'flathub');
expect(result, `Failed for ${description}: ${input}`).toBe(expected);
});
});
it('should handle root path correctly', () => {
expect(transformPath('/flathub/', 'flathub')).toBe('/');
});
it('should preserve already transformed Flathub repository paths', () => {
expect(transformPath('/repo/summary', 'flathub')).toBe('/repo/summary');
});
});
+114
View File
@@ -0,0 +1,114 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
/** @type {ExecutionContext} */
const executionContext = {
waitUntil() {},
passThroughOnException() {}
};
/**
* Reads a response body as UTF-8 text without relying on the response MIME type.
* @param {Response} response
* @returns {Promise<string>} Decoded UTF-8 response text.
*/
async function readUtf8Text(response) {
return new TextDecoder().decode(await response.arrayBuffer());
}
describe('Flathub Response Rewriting', () => {
beforeEach(() => {
vi.stubGlobal('caches', {
default: {
match: vi.fn(async () => null),
put: vi.fn(async () => undefined)
}
});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('rewrites .flatpakrepo URLs to stay on the Xget mirror', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(
[
'[Flatpak Repo]',
'Url=https://dl.flathub.org/repo/',
'Icon=https://dl.flathub.org/repo/logo.svg',
'Homepage=https://flathub.org/'
].join('\n'),
{
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
}
)
);
const response = await worker.fetch(
new Request('https://example.com/flathub/repo/flathub.flatpakrepo'),
{},
executionContext
);
expect(response.status).toBe(200);
const body = await readUtf8Text(response);
expect(body).toContain('Url=https://example.com/flathub/repo/');
expect(body).toContain('Icon=https://example.com/flathub/repo/logo.svg');
expect(body).toContain('Homepage=https://flathub.org/');
expect(response.headers.get('Content-Length')).toBe(String(new TextEncoder().encode(body).length));
});
it('rewrites .flatpakref URLs to stay on the Xget mirror', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(
[
'[Flatpak Ref]',
'Name=org.gnome.gedit',
'Url=https://dl.flathub.org/repo/',
'RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo'
].join('\n'),
{
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
}
)
);
const response = await worker.fetch(
new Request('https://example.com/flathub/repo/appstream/org.gnome.gedit.flatpakref'),
{},
executionContext
);
expect(response.status).toBe(200);
const body = await readUtf8Text(response);
expect(body).toContain('Url=https://example.com/flathub/repo/');
expect(body).toContain(
'RuntimeRepo=https://example.com/flathub/repo/flathub.flatpakrepo'
);
});
it('does not rewrite binary repository metadata like summary files', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('summary-binary-payload', {
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
})
);
const response = await worker.fetch(
new Request('https://example.com/flathub/repo/summary'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(await readUtf8Text(response)).toBe('summary-binary-payload');
});
});
+12
View File
@@ -16,6 +16,7 @@ describe('Platform Configuration', () => {
'npm',
'pypi',
'conda',
'flathub',
'homebrew'
];
@@ -134,6 +135,13 @@ describe('Platform Configuration', () => {
).toBe('/conda-forge/linux-64/repodata.json');
});
it('should transform Flathub paths correctly', () => {
expect(transformPath('/flathub/repo/summary', 'flathub')).toBe('/repo/summary');
expect(transformPath('/flathub/repo/flathub.flatpakrepo', 'flathub')).toBe(
'/repo/flathub.flatpakrepo'
);
});
it('should transform container registry paths correctly', () => {
expect(
transformPath('/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest', 'cr-ghcr')
@@ -191,6 +199,10 @@ describe('Platform Configuration', () => {
expect(PLATFORMS['conda-community']).toBe('https://conda.anaconda.org');
});
it('should have correct Flathub base URL', () => {
expect(PLATFORMS.flathub).toBe('https://dl.flathub.org');
});
it('should have correct container registry base URLs', () => {
expect(PLATFORMS['cr-ghcr']).toBe('https://ghcr.io');
expect(PLATFORMS['cr-gcr']).toBe('https://gcr.io');