Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
program-updater
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
周田
program-updater
Commits
033861e9
Commit
033861e9
authored
Oct 29, 2026
by
周田
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
http://39.170.91.81:10020/zhoutian/program-updater
parents
1fe37dcd
e77e72fc
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
290 additions
and
44 deletions
+290
-44
.dockerignore
.dockerignore
+14
-0
Dockerfile
Dockerfile
+23
-0
README.md
README.md
+8
-0
compose.yaml
compose.yaml
+13
-0
README.md
distribution-server/README.md
+37
-2
routes.py
distribution-server/api/routes.py
+7
-2
app.py
distribution-server/app.py
+7
-1
service.py
distribution-server/core/service.py
+26
-2
store.py
distribution-server/core/store.py
+20
-8
app.css
distribution-server/frontend/assets/app.css
+4
-1
app.js
distribution-server/frontend/assets/app.js
+5
-1
downloads.css
distribution-server/frontend/assets/downloads.css
+9
-5
downloads.js
distribution-server/frontend/assets/downloads.js
+22
-8
downloads.html
distribution-server/frontend/downloads.html
+3
-2
index.html
distribution-server/frontend/index.html
+16
-7
test_distribution.py
distribution-server/tests/test_distribution.py
+76
-5
No files found.
.dockerignore
0 → 100644
View file @
033861e9
*
!pyproject.toml
!uv.lock
!distribution-server/
distribution-server/*
!distribution-server/__init__.py
!distribution-server/app.py
!distribution-server/server.py
!distribution-server/settings.py
!distribution-server/api/
!distribution-server/core/
!distribution-server/frontend/
**/__pycache__/
**/*.py[cod]
Dockerfile
0 → 100644
View file @
033861e9
FROM
ghcr.io/astral-sh/uv:0.12.13 AS uv
FROM
python:3.12-slim AS dependencies
COPY
--from=uv /uv /usr/local/bin/uv
WORKDIR
/app
COPY
pyproject.toml uv.lock ./
RUN
uv sync
--locked
--extra
server
--no-dev
--no-install-project
--no-cache
FROM
python:3.12-slim
ENV
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR
/app
COPY
--from=dependencies /app/.venv /app/.venv
COPY
distribution-server/ ./distribution-server/
RUN
groupadd
--system
--gid
10001 distribution
\
&&
useradd
--system
--uid
10001
--gid
distribution
--no-create-home
distribution
\
&&
mkdir
-p
/app/distribution-server/data
\
&&
chown distribution:distribution /app/distribution-server/data
USER
distribution
EXPOSE
8000
HEALTHCHECK
--interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/app/.venv/bin/python", "-c", "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/healthz', timeout=3).close()"]
CMD
["/app/.venv/bin/python", "-m", "distribution-server.server"]
README.md
View file @
033861e9
...
@@ -8,6 +8,14 @@
...
@@ -8,6 +8,14 @@
默认安装只包含 updater 运行依赖;管理 Web 的 FastAPI/Uvicorn 通过
`server`
extra 启用,不会进入目标应用的基础安装。
默认安装只包含 updater 运行依赖;管理 Web 的 FastAPI/Uvicorn 通过
`server`
extra 启用,不会进入目标应用的基础安装。
Docker Compose 部署分发服务(默认仅本机访问,数据保存在 Docker 命名卷):
```
bash
docker compose up
-d
--build
```
局域网访问、安全限制及数据迁移说明见
[
`distribution-server/README.md`
](
./distribution-server/README.md
)
。
## 启动 Web 与测试
## 启动 Web 与测试
需要 Python 3.12 和 uv。从仓库根目录使用
`python -m`
启动,不需要先设置环境变量;uv 会根据根
`pyproject.toml`
自动准备服务器依赖:
需要 Python 3.12 和 uv。从仓库根目录使用
`python -m`
启动,不需要先设置环境变量;uv 会根据根
`pyproject.toml`
自动准备服务器依赖:
...
...
compose.yaml
0 → 100644
View file @
033861e9
services
:
distribution-server
:
build
:
context
:
.
dockerfile
:
Dockerfile
restart
:
unless-stopped
ports
:
-
"
18000:8000"
volumes
:
-
distribution-data:/app/distribution-server/data
volumes
:
distribution-data
:
distribution-server/README.md
View file @
033861e9
...
@@ -15,7 +15,7 @@ distribution-server/
...
@@ -15,7 +15,7 @@ distribution-server/
└─ settings.py
└─ settings.py
```
```
管理 API 以
`/api`
开头;
上传时服务器流式计算文件大小和 SHA-256,并记录文件名、格式、平台、架构、Markdown 版本说明
、上传时间和发布状态。支持 EXE、MSI、ZIP、7Z、DMG、PKG、DEB、RPM、AppImage 和 TAR.GZ。Markdown 说明支持标题、列表、粗体、链接和代码。
管理 API 以
`/api`
开头;
创建应用时可填写应用说明。上传版本必须填写 Markdown 更新内容;服务器流式计算文件大小和 SHA-256,并记录文件名、格式、平台、架构、更新内容
、上传时间和发布状态。支持 EXE、MSI、ZIP、7Z、DMG、PKG、DEB、RPM、AppImage 和 TAR.GZ。Markdown 说明支持标题、列表、粗体、链接和代码。
版本说明也可以在管理页的程序包记录中单独编辑,无需重新上传程序包;保存后会同时显示在管理记录和公开下载页。
版本说明也可以在管理页的程序包记录中单独编辑,无需重新上传程序包;保存后会同时显示在管理记录和公开下载页。
...
@@ -23,6 +23,41 @@ distribution-server/
...
@@ -23,6 +23,41 @@ distribution-server/
管理后台在
`/`
;公开程序列表在
`/downloads`
,单个程序及其已发布版本在
`/download/{app_id}`
。只有已发布的程序包会出现在公开下载页并允许下载。每个程序的更新清单地址为
`/updates/{app_id}/latest.json`
,格式继续兼容 updater 当前的
`app_id`
、
`version`
、
`url`
、
`size`
和
`sha256`
协议。标记为 Helper 更新包的 Windows EXE 会更新此清单;其他格式用于公开下载,不会交给当前仅支持 Windows EXE 替换流程的 updater helper。
管理后台在
`/`
;公开程序列表在
`/downloads`
,单个程序及其已发布版本在
`/download/{app_id}`
。只有已发布的程序包会出现在公开下载页并允许下载。每个程序的更新清单地址为
`/updates/{app_id}/latest.json`
,格式继续兼容 updater 当前的
`app_id`
、
`version`
、
`url`
、
`size`
和
`sha256`
协议。标记为 Helper 更新包的 Windows EXE 会更新此清单;其他格式用于公开下载,不会交给当前仅支持 Windows EXE 替换流程的 updater helper。
管理页、下载页和静态资源要求浏览器在使用缓存前重新验证;更新前端后如仍看到旧页面,可重新加载页面。
## Docker Compose 部署
安装 Docker Engine 和 Docker Compose 后,在仓库根目录运行:
```
bash
docker compose up
-d
--build
docker compose ps
docker compose logs
-f
distribution-server
```
默认仅在部署主机的
`127.0.0.1:8000`
监听,访问
`http://127.0.0.1:8000/`
。
容器以非 root 用户运行,并从根目录的
`uv.lock`
安装服务器依赖。SQLite 数据库和
程序包保存在 Docker 命名卷
`distribution-data`
中,不会随容器重新创建而丢失;
该卷与本地直接运行时的
`distribution-server/data/`
相互独立。升级时重新运行
`docker compose up -d --build`
,并定期备份整个卷。
`docker compose down`
保留数据,
`docker compose down -v`
会删除数据卷,请谨慎操作。
可通过环境变量调整宿主机绑定地址和端口。例如,限制好防火墙后允许局域网访问:
```
bash
DISTRIBUTION_BIND_ADDRESS
=
192.168.1.20
DISTRIBUTION_PORT
=
8000 docker compose up
-d
--build
```
将地址替换为部署主机的局域网 IP。管理接口没有登录验证,Docker 端口映射或反向代理
可能使服务看到的是网关/代理地址,而不是访问者地址,因此不能依赖服务内的
`ALLOWED_CIDRS`
在容器环境中可靠地限制外部访问。开放非回环地址前,必须在宿主机、
网络或可信反向代理上限制管理接口的访问;不要将整个端口直接暴露给不可信网络。
反向代理只开放公开下载路径时,也应正确转发 Host 并按实际情况限制管理路径。
如需迁移现有数据,先停止所有使用该数据库的服务,再将原数据目录完整备份并复制到
Docker 数据卷,确保容器内 UID/GID
`10001`
可读写。不要让本地服务和容器同时使用
同一份数据。容器内运行参数沿用
`settings.py`
,修改后需重新构建镜像。
## 直接启动
## 直接启动
分发服务器可运行在 Linux、Windows 或 macOS。它只接收、保存并分发 Windows exe,不会在服务器上运行这些 exe。开发或试用时不必手动创建虚拟环境、设置环境变量或执行
`pip install`
;在仓库根目录运行:
分发服务器可运行在 Linux、Windows 或 macOS。它只接收、保存并分发 Windows exe,不会在服务器上运行这些 exe。开发或试用时不必手动创建虚拟环境、设置环境变量或执行
`pip install`
;在仓库根目录运行:
...
@@ -69,7 +104,7 @@ http://192.168.1.20:8000/updates/{app_id}/latest.json
...
@@ -69,7 +104,7 @@ http://192.168.1.20:8000/updates/{app_id}/latest.json
清单包含
`app_id`
、
`version`
、
`url`
、
`size`
和
`sha256`
,与 updater 当前协议一致。只有上传并发布为 Helper 更新包的 Windows EXE 会成为该清单的目标;发布其他格式不会覆盖最近一次可用的 Helper 版本。
清单包含
`app_id`
、
`version`
、
`url`
、
`size`
和
`sha256`
,与 updater 当前协议一致。只有上传并发布为 Helper 更新包的 Windows EXE 会成为该清单的目标;发布其他格式不会覆盖最近一次可用的 Helper 版本。
SQLite 数据库和程序文件默认存储在
`distribution-server/data/`
,备份时应备份整个目录。上传大小默认上限为 2 GiB。已有数据库会在服务启动时自动补充程序包元数据字段。端口、存储目录、上传大小和允许网段集中配置在
`distribution-server/settings.py`
,修改后重启服务即可生效。
SQLite 数据库和程序文件默认存储在
`distribution-server/data/`
,备份时应备份整个目录。上传大小默认上限为 2 GiB。已有数据库会在服务启动时自动补充
应用说明和
程序包元数据字段。端口、存储目录、上传大小和允许网段集中配置在
`distribution-server/settings.py`
,修改后重启服务即可生效。
## 网络访问
## 网络访问
...
...
distribution-server/api/routes.py
View file @
033861e9
...
@@ -12,6 +12,7 @@ release_router = APIRouter()
...
@@ -12,6 +12,7 @@ release_router = APIRouter()
class
ApplicationRequest
(
BaseModel
):
class
ApplicationRequest
(
BaseModel
):
app_id
:
str
=
Field
(
min_length
=
1
,
max_length
=
128
)
app_id
:
str
=
Field
(
min_length
=
1
,
max_length
=
128
)
display_name
:
str
=
Field
(
min_length
=
1
,
max_length
=
120
)
display_name
:
str
=
Field
(
min_length
=
1
,
max_length
=
120
)
description
:
str
=
Field
(
default
=
""
,
max_length
=
2
_000
)
class
ReleaseDescriptionRequest
(
BaseModel
):
class
ReleaseDescriptionRequest
(
BaseModel
):
...
@@ -42,7 +43,11 @@ def create_application(
...
@@ -42,7 +43,11 @@ def create_application(
payload
:
ApplicationRequest
,
payload
:
ApplicationRequest
,
request
:
Request
,
request
:
Request
,
)
->
dict
[
str
,
object
]:
)
->
dict
[
str
,
object
]:
return
_service
(
request
)
.
create_application
(
payload
.
app_id
,
payload
.
display_name
)
return
_service
(
request
)
.
create_application
(
payload
.
app_id
,
payload
.
display_name
,
payload
.
description
,
)
@router.delete
(
"/apps/{app_id}"
,
status_code
=
204
)
@router.delete
(
"/apps/{app_id}"
,
status_code
=
204
)
...
@@ -79,7 +84,7 @@ async def upload_release(
...
@@ -79,7 +84,7 @@ async def upload_release(
platform
:
str
=
Form
(
default
=
"windows"
),
platform
:
str
=
Form
(
default
=
"windows"
),
architecture
:
str
=
Form
(
default
=
"x64"
),
architecture
:
str
=
Form
(
default
=
"x64"
),
helper_compatible
:
bool
|
None
=
Form
(
default
=
None
),
helper_compatible
:
bool
|
None
=
Form
(
default
=
None
),
description
:
str
=
Form
(
default
=
""
),
description
:
str
=
Form
(
...
),
)
->
dict
[
str
,
object
]:
)
->
dict
[
str
,
object
]:
try
:
try
:
return
await
run_in_threadpool
(
return
await
run_in_threadpool
(
...
...
distribution-server/app.py
View file @
033861e9
...
@@ -95,7 +95,13 @@ def create_app(settings: DistributionSettings | None = None) -> FastAPI:
...
@@ -95,7 +95,13 @@ def create_app(settings: DistributionSettings | None = None) -> FastAPI:
content
=
{
"detail"
:
"Package upload exceeds the configured size limit."
},
content
=
{
"detail"
:
"Package upload exceeds the configured size limit."
},
)
)
return
await
call_next
(
request
)
response
=
await
call_next
(
request
)
if
request
.
method
in
{
"GET"
,
"HEAD"
}
and
(
request
.
url
.
path
in
{
"/"
,
"/downloads"
}
or
request
.
url
.
path
.
startswith
((
"/download/"
,
"/assets/"
))
):
response
.
headers
[
"Cache-Control"
]
=
"no-cache"
return
response
@application.get
(
"/"
,
include_in_schema
=
False
)
@application.get
(
"/"
,
include_in_schema
=
False
)
def
frontend
()
->
FileResponse
:
def
frontend
()
->
FileResponse
:
...
...
distribution-server/core/service.py
View file @
033861e9
...
@@ -14,6 +14,7 @@ from ..settings import DistributionSettings
...
@@ -14,6 +14,7 @@ from ..settings import DistributionSettings
_CHUNK_SIZE
=
1024
*
1024
_CHUNK_SIZE
=
1024
*
1024
_MAX_DESCRIPTION_LENGTH
=
20
_000
_MAX_DESCRIPTION_LENGTH
=
20
_000
_MAX_APPLICATION_DESCRIPTION_LENGTH
=
2
_000
_SUPPORTED_SUFFIXES
=
(
_SUPPORTED_SUFFIXES
=
(
".appimage"
,
".appimage"
,
".deb"
,
".deb"
,
...
@@ -50,9 +51,25 @@ class DistributionService:
...
@@ -50,9 +51,25 @@ class DistributionService:
def
list_applications
(
self
)
->
list
[
dict
[
str
,
object
]]:
def
list_applications
(
self
)
->
list
[
dict
[
str
,
object
]]:
return
self
.
store
.
list_applications
()
return
self
.
store
.
list_applications
()
def
create_application
(
self
,
app_id
:
str
,
display_name
:
str
)
->
dict
[
str
,
object
]:
def
create_application
(
self
,
app_id
:
str
,
display_name
:
str
,
description
:
str
=
""
,
)
->
dict
[
str
,
object
]:
normalized_description
=
description
.
strip
()
if
len
(
normalized_description
)
>
_MAX_APPLICATION_DESCRIPTION_LENGTH
:
raise
DistributionError
(
422
,
"Application description must be at most "
f
"{_MAX_APPLICATION_DESCRIPTION_LENGTH} characters."
,
)
try
:
try
:
return
self
.
store
.
create_application
(
app_id
,
display_name
)
return
self
.
store
.
create_application
(
app_id
,
display_name
,
normalized_description
,
)
except
ProtocolError
as
exc
:
except
ProtocolError
as
exc
:
raise
DistributionError
(
422
,
str
(
exc
))
from
exc
raise
DistributionError
(
422
,
str
(
exc
))
from
exc
except
ValueError
as
exc
:
except
ValueError
as
exc
:
...
@@ -136,6 +153,8 @@ class DistributionService:
...
@@ -136,6 +153,8 @@ class DistributionService:
self
.
_validate_app_id
(
app_id
)
self
.
_validate_app_id
(
app_id
)
self
.
_validate_version
(
version
)
self
.
_validate_version
(
version
)
normalized_description
=
description
.
strip
()
normalized_description
=
description
.
strip
()
if
not
normalized_description
:
raise
DistributionError
(
422
,
"Release description is required."
)
if
len
(
normalized_description
)
>
_MAX_DESCRIPTION_LENGTH
:
if
len
(
normalized_description
)
>
_MAX_DESCRIPTION_LENGTH
:
raise
DistributionError
(
raise
DistributionError
(
422
,
422
,
...
@@ -187,6 +206,8 @@ class DistributionService:
...
@@ -187,6 +206,8 @@ class DistributionService:
f
"description must be at most {_MAX_DESCRIPTION_LENGTH} characters."
,
f
"description must be at most {_MAX_DESCRIPTION_LENGTH} characters."
,
)
)
description
=
description
.
strip
()
description
=
description
.
strip
()
if
not
description
:
raise
DistributionError
(
422
,
"Release description is required."
)
if
helper_compatible
is
None
:
if
helper_compatible
is
None
:
helper_compatible
=
suffix
==
".exe"
and
platform
==
"windows"
helper_compatible
=
suffix
==
".exe"
and
platform
==
"windows"
if
helper_compatible
and
(
suffix
!=
".exe"
or
platform
!=
"windows"
):
if
helper_compatible
and
(
suffix
!=
".exe"
or
platform
!=
"windows"
):
...
@@ -269,6 +290,8 @@ class DistributionService:
...
@@ -269,6 +290,8 @@ class DistributionService:
release
=
self
.
store
.
get_release
(
app_id
,
version
)
release
=
self
.
store
.
get_release
(
app_id
,
version
)
if
release
is
None
:
if
release
is
None
:
raise
DistributionError
(
404
,
"Release not found."
)
raise
DistributionError
(
404
,
"Release not found."
)
if
not
str
(
release
[
"description"
])
.
strip
():
raise
DistributionError
(
422
,
"Release description is required before publishing."
)
package_path
=
self
.
store
.
package_path
(
app_id
,
version
)
package_path
=
self
.
store
.
package_path
(
app_id
,
version
)
if
not
package_path
.
is_file
():
if
not
package_path
.
is_file
():
raise
DistributionError
(
409
,
"Release package is missing."
)
raise
DistributionError
(
409
,
"Release package is missing."
)
...
@@ -342,6 +365,7 @@ class DistributionService:
...
@@ -342,6 +365,7 @@ class DistributionService:
return
{
return
{
"app_id"
:
app_id
,
"app_id"
:
app_id
,
"display_name"
:
application
[
"display_name"
],
"display_name"
:
application
[
"display_name"
],
"description"
:
application
[
"description"
],
"current_version"
:
application
[
"current_version"
],
"current_version"
:
application
[
"current_version"
],
"packages"
:
packages
,
"packages"
:
packages
,
}
}
...
...
distribution-server/core/store.py
View file @
033861e9
...
@@ -39,6 +39,7 @@ class DistributionStore:
...
@@ -39,6 +39,7 @@ class DistributionStore:
CREATE TABLE IF NOT EXISTS applications (
CREATE TABLE IF NOT EXISTS applications (
app_id TEXT PRIMARY KEY,
app_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
current_version TEXT,
current_version TEXT,
helper_version TEXT,
helper_version TEXT,
created_at TEXT NOT NULL
created_at TEXT NOT NULL
...
@@ -66,6 +67,10 @@ class DistributionStore:
...
@@ -66,6 +67,10 @@ class DistributionStore:
application_columns
=
{
application_columns
=
{
row
[
"name"
]
for
row
in
connection
.
execute
(
"PRAGMA table_info(applications)"
)
row
[
"name"
]
for
row
in
connection
.
execute
(
"PRAGMA table_info(applications)"
)
}
}
if
"description"
not
in
application_columns
:
connection
.
execute
(
"ALTER TABLE applications ADD COLUMN description TEXT NOT NULL DEFAULT ''"
)
if
"helper_version"
not
in
application_columns
:
if
"helper_version"
not
in
application_columns
:
connection
.
execute
(
"ALTER TABLE applications ADD COLUMN helper_version TEXT"
)
connection
.
execute
(
"ALTER TABLE applications ADD COLUMN helper_version TEXT"
)
...
@@ -116,7 +121,12 @@ class DistributionStore:
...
@@ -116,7 +121,12 @@ class DistributionStore:
"""
"""
)
)
def
create_application
(
self
,
app_id
:
str
,
display_name
:
str
)
->
dict
[
str
,
object
]:
def
create_application
(
self
,
app_id
:
str
,
display_name
:
str
,
description
:
str
=
""
,
)
->
dict
[
str
,
object
]:
validate_app_id
(
app_id
)
validate_app_id
(
app_id
)
normalized_name
=
display_name
.
strip
()
normalized_name
=
display_name
.
strip
()
if
not
normalized_name
or
len
(
normalized_name
)
>
120
:
if
not
normalized_name
or
len
(
normalized_name
)
>
120
:
...
@@ -125,10 +135,10 @@ class DistributionStore:
...
@@ -125,10 +135,10 @@ class DistributionStore:
with
self
.
_connection
()
as
connection
:
with
self
.
_connection
()
as
connection
:
connection
.
execute
(
connection
.
execute
(
"""
"""
INSERT INTO applications (app_id, display_name, created_at)
INSERT INTO applications (app_id, display_name,
description,
created_at)
VALUES (?, ?, ?)
VALUES (?, ?, ?
, ?
)
"""
,
"""
,
(
app_id
,
normalized_name
,
_now
()),
(
app_id
,
normalized_name
,
description
,
_now
()),
)
)
application
=
self
.
get_application
(
app_id
)
application
=
self
.
get_application
(
app_id
)
if
application
is
None
:
if
application
is
None
:
...
@@ -147,8 +157,9 @@ class DistributionStore:
...
@@ -147,8 +157,9 @@ class DistributionStore:
with
self
.
_connection
()
as
connection
:
with
self
.
_connection
()
as
connection
:
rows
=
connection
.
execute
(
rows
=
connection
.
execute
(
"""
"""
SELECT a.app_id, a.display_name, a.current_version, a.helper_version,
SELECT a.app_id, a.display_name, a.description, a.current_version,
a.created_at, r.size AS current_size, r.sha256 AS current_sha256,
a.helper_version, a.created_at,
r.size AS current_size, r.sha256 AS current_sha256,
r.filename AS current_filename, r.platform AS current_platform,
r.filename AS current_filename, r.platform AS current_platform,
r.architecture AS current_architecture
r.architecture AS current_architecture
FROM applications AS a
FROM applications AS a
...
@@ -163,7 +174,8 @@ class DistributionStore:
...
@@ -163,7 +174,8 @@ class DistributionStore:
with
self
.
_connection
()
as
connection
:
with
self
.
_connection
()
as
connection
:
row
=
connection
.
execute
(
row
=
connection
.
execute
(
"""
"""
SELECT app_id, display_name, current_version, helper_version, created_at
SELECT app_id, display_name, description, current_version,
helper_version, created_at
FROM applications
FROM applications
WHERE app_id = ?
WHERE app_id = ?
"""
,
"""
,
...
@@ -291,7 +303,7 @@ class DistributionStore:
...
@@ -291,7 +303,7 @@ class DistributionStore:
with
self
.
_connection
()
as
connection
:
with
self
.
_connection
()
as
connection
:
rows
=
connection
.
execute
(
rows
=
connection
.
execute
(
"""
"""
SELECT a.app_id, a.display_name, a.current_version,
SELECT a.app_id, a.display_name, a.
description, a.
current_version,
r.filename, r.description, r.platform, r.architecture,
r.filename, r.description, r.platform, r.architecture,
r.size, r.sha256,
r.size, r.sha256,
r.published_at
r.published_at
...
...
distribution-server/frontend/assets/app.css
View file @
033861e9
...
@@ -82,6 +82,8 @@ h2 { margin-top: 4px; font: 700 17px Manrope, "PingFang SC", sans-serif; }
...
@@ -82,6 +82,8 @@ h2 { margin-top: 4px; font: 700 17px Manrope, "PingFang SC", sans-serif; }
.detail-row
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
12px
;
padding
:
11px
0
;
border-bottom
:
1px
solid
#eff1f5
;
color
:
var
(
--muted
);
font-size
:
11px
;
}
.detail-row
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
12px
;
padding
:
11px
0
;
border-bottom
:
1px
solid
#eff1f5
;
color
:
var
(
--muted
);
font-size
:
11px
;
}
.detail-row
:last-child
{
border-bottom
:
0
;
}
.detail-row
:last-child
{
border-bottom
:
0
;
}
.detail-row
strong
,
.detail-row
code
{
color
:
var
(
--ink
);
font-size
:
11px
;
font-weight
:
600
;
}
.detail-row
strong
,
.detail-row
code
{
color
:
var
(
--ink
);
font-size
:
11px
;
font-weight
:
600
;
}
.application-description-row
{
display
:
grid
;
gap
:
5px
;
padding
:
11px
0
;
border-bottom
:
1px
solid
#eff1f5
;
color
:
var
(
--muted
);
font-size
:
13px
;
}
.application-description-row
p
{
color
:
var
(
--ink
);
line-height
:
1.6
;
overflow-wrap
:
anywhere
;
}
.upload-panel
,
.releases-panel
{
padding
:
20px
22px
;
margin-bottom
:
17px
;
}
.upload-panel
,
.releases-panel
{
padding
:
20px
22px
;
margin-bottom
:
17px
;
}
.section-heading
{
margin-bottom
:
17px
;
}
.section-heading
{
margin-bottom
:
17px
;
}
...
@@ -213,8 +215,9 @@ h2 { font-size: 19px; }
...
@@ -213,8 +215,9 @@ h2 { font-size: 19px; }
.publish-button
{
font-size
:
12px
;
}
.publish-button
{
font-size
:
12px
;
}
.no-releases
{
font-size
:
14px
;
}
.no-releases
{
font-size
:
14px
;
}
.upload-form
.markdown-field
{
grid-column
:
1
/
-1
;
}
.upload-form
.markdown-field
{
grid-column
:
1
/
-1
;
}
.release-description
{
grid-column
:
1
/
-1
;
min-width
:
0
;
padding
:
12px
14px
;
border-radius
:
8px
;
color
:
var
(
--muted
);
background
:
#f7f8fc
;
font-size
:
13px
;
}
.release-description
{
grid-column
:
1
/
-1
;
min-width
:
0
;
color
:
var
(
--muted
)
;
font-size
:
13px
;
}
.release-description-title
{
display
:
block
;
margin-bottom
:
6px
;
color
:
#536077
;
font-size
:
13px
;
}
.release-description-title
{
display
:
block
;
margin-bottom
:
6px
;
color
:
#536077
;
font-size
:
13px
;
}
.release-description-content
{
padding
:
12px
14px
;
border-radius
:
8px
;
background
:
#f7f8fc
;
}
.description-dialog
{
width
:
min
(
640px
,
calc
(
100vw
-
32px
));
max-height
:
calc
(
100vh
-
32px
);
overflow
:
auto
;
}
.description-dialog
{
width
:
min
(
640px
,
calc
(
100vw
-
32px
));
max-height
:
calc
(
100vh
-
32px
);
overflow
:
auto
;
}
.description-preview
{
max-height
:
220px
;
overflow
:
auto
;
padding
:
12px
14px
;
border
:
1px
solid
var
(
--line
);
border-radius
:
8px
;
background
:
#f7f8fc
;
}
.description-preview
{
max-height
:
220px
;
overflow
:
auto
;
padding
:
12px
14px
;
border
:
1px
solid
var
(
--line
);
border-radius
:
8px
;
background
:
#f7f8fc
;
}
.markdown-body
{
color
:
#465268
;
font-size
:
14px
;
line-height
:
1.7
;
overflow-wrap
:
anywhere
;
}
.markdown-body
{
color
:
#465268
;
font-size
:
14px
;
line-height
:
1.7
;
overflow-wrap
:
anywhere
;
}
...
...
distribution-server/frontend/assets/app.js
View file @
033861e9
...
@@ -96,6 +96,7 @@ async function renderApplication() {
...
@@ -96,6 +96,7 @@ async function renderApplication() {
document
.
querySelector
(
"#page-title"
).
textContent
=
application
.
display_name
;
document
.
querySelector
(
"#page-title"
).
textContent
=
application
.
display_name
;
document
.
querySelector
(
"#app-name"
).
textContent
=
application
.
display_name
;
document
.
querySelector
(
"#app-name"
).
textContent
=
application
.
display_name
;
document
.
querySelector
(
"#app-id"
).
textContent
=
application
.
app_id
;
document
.
querySelector
(
"#app-id"
).
textContent
=
application
.
app_id
;
document
.
querySelector
(
"#app-description"
).
textContent
=
application
.
description
||
"—"
;
document
.
querySelector
(
"#current-version"
).
textContent
=
application
.
current_version
||
"未发布"
;
document
.
querySelector
(
"#current-version"
).
textContent
=
application
.
current_version
||
"未发布"
;
document
.
querySelector
(
"#current-status"
).
textContent
=
application
.
current_version
?
"正在分发"
:
"未发布"
;
document
.
querySelector
(
"#current-status"
).
textContent
=
application
.
current_version
?
"正在分发"
:
"未发布"
;
document
.
querySelector
(
"#current-status"
).
className
=
`badge
${
application
.
current_version
?
"badge-live"
:
"badge-neutral"
}
`
;
document
.
querySelector
(
"#current-status"
).
className
=
`badge
${
application
.
current_version
?
"badge-live"
:
"badge-neutral"
}
`
;
...
@@ -146,9 +147,11 @@ function renderReleases(releases, currentVersion, helperVersion) {
...
@@ -146,9 +147,11 @@ function renderReleases(releases, currentVersion, helperVersion) {
let
notes
;
let
notes
;
if
(
release
.
description
)
{
if
(
release
.
description
)
{
notes
=
element
(
"section"
,
"release-description"
);
notes
=
element
(
"section"
,
"release-description"
);
const
content
=
element
(
"div"
,
"release-description-content"
);
content
.
append
(
renderMarkdown
(
release
.
description
));
notes
.
append
(
notes
.
append
(
element
(
"strong"
,
"release-description-title"
,
"版本更新内容"
),
element
(
"strong"
,
"release-description-title"
,
"版本更新内容"
),
renderMarkdown
(
release
.
description
)
,
content
,
);
);
}
}
const
size
=
element
(
"div"
,
"release-size"
,
`
${(
release
.
size
/
1024
/
1024
).
toFixed
(
1
)}
MiB`
);
const
size
=
element
(
"div"
,
"release-size"
,
`
${(
release
.
size
/
1024
/
1024
).
toFixed
(
1
)}
MiB`
);
...
@@ -337,6 +340,7 @@ document.querySelector("#create-form").addEventListener("submit", async event =>
...
@@ -337,6 +340,7 @@ document.querySelector("#create-form").addEventListener("submit", async event =>
body
:
JSON
.
stringify
({
body
:
JSON
.
stringify
({
app_id
:
data
.
get
(
"app_id"
),
app_id
:
data
.
get
(
"app_id"
),
display_name
:
data
.
get
(
"display_name"
),
display_name
:
data
.
get
(
"display_name"
),
description
:
data
.
get
(
"description"
),
}),
}),
});
});
form
.
reset
();
form
.
reset
();
...
...
distribution-server/frontend/assets/downloads.css
View file @
033861e9
...
@@ -29,8 +29,10 @@ h1 { margin: 8px 0 7px; font-size: clamp(24px, 4vw, 32px); letter-spacing: -.03e
...
@@ -29,8 +29,10 @@ h1 { margin: 8px 0 7px; font-size: clamp(24px, 4vw, 32px); letter-spacing: -.03e
.package-list
{
display
:
grid
;
gap
:
12px
;
}
.package-list
{
display
:
grid
;
gap
:
12px
;
}
.package-card
{
min-width
:
0
;
padding
:
20px
22px
;
border
:
1px
solid
var
(
--line
);
border-radius
:
13px
;
background
:
white
;
box-shadow
:
0
2px
8px
#25314b05
;
}
.package-card
{
min-width
:
0
;
padding
:
20px
22px
;
border
:
1px
solid
var
(
--line
);
border-radius
:
13px
;
background
:
white
;
box-shadow
:
0
2px
8px
#25314b05
;
}
.package-heading
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
16px
;
align-items
:
start
;
}
.package-heading
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
16px
;
align-items
:
start
;
}
.package-version
{
font-size
:
18px
;
font-weight
:
750
;
letter-spacing
:
-.02em
;
}
.package-identity
{
display
:
flex
;
min-width
:
0
;
flex-wrap
:
wrap
;
gap
:
9px
;
align-items
:
center
;
}
.package-name
{
margin-top
:
3px
;
color
:
var
(
--muted
);
font-size
:
11px
;
}
.package-version
{
flex
:
0
0
auto
;
padding
:
4px
8px
;
border-radius
:
999px
;
color
:
#536077
;
background
:
#f0f2f6
;
font-size
:
11px
;
font-weight
:
700
;
}
.package-name
{
min-width
:
0
;
color
:
var
(
--ink
);
font-size
:
18px
;
font-weight
:
700
;
letter-spacing
:
-.02em
;
overflow-wrap
:
anywhere
;
}
.package-app-description
{
margin
:
10px
0
0
;
color
:
var
(
--muted
);
font-size
:
13px
;
line-height
:
1.6
;
overflow-wrap
:
anywhere
;
}
.package-badge
{
flex
:
0
0
auto
;
padding
:
5px
9px
;
border-radius
:
999px
;
color
:
#56647a
;
background
:
#f0f2f6
;
font-size
:
10px
;
font-weight
:
700
;
}
.package-badge
{
flex
:
0
0
auto
;
padding
:
5px
9px
;
border-radius
:
999px
;
color
:
#56647a
;
background
:
#f0f2f6
;
font-size
:
10px
;
font-weight
:
700
;
}
.package-badge-helper
{
color
:
#6652b8
;
background
:
#f0edff
;
}
.package-badge-helper
{
color
:
#6652b8
;
background
:
#f0edff
;
}
.package-metadata
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
gap
:
12px
;
margin
:
18px
0
;
padding
:
13px
0
;
border-top
:
1px
solid
#eff1f5
;
border-bottom
:
1px
solid
#eff1f5
;
}
.package-metadata
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
minmax
(
0
,
1
fr
));
gap
:
12px
;
margin
:
18px
0
;
padding
:
13px
0
;
border-top
:
1px
solid
#eff1f5
;
border-bottom
:
1px
solid
#eff1f5
;
}
...
@@ -70,8 +72,8 @@ body { font-size: 15px; }
...
@@ -70,8 +72,8 @@ body { font-size: 15px; }
.brand
small
,
.live-status
{
font-size
:
12px
;
}
.brand
small
,
.live-status
{
font-size
:
12px
;
}
.eyebrow
{
font-size
:
11px
;
}
.eyebrow
{
font-size
:
11px
;
}
.hero
p
{
font-size
:
15px
;
}
.hero
p
{
font-size
:
15px
;
}
.package-version
{
font-size
:
20
px
;
}
.package-version
{
font-size
:
12
px
;
}
.package-name
{
font-size
:
12
px
;
}
.package-name
{
font-size
:
20
px
;
}
.package-badge
{
font-size
:
12px
;
}
.package-badge
{
font-size
:
12px
;
}
.metadata-item
span
{
font-size
:
12px
;
}
.metadata-item
span
{
font-size
:
12px
;
}
.metadata-item
strong
{
font-size
:
13px
;
}
.metadata-item
strong
{
font-size
:
13px
;
}
...
@@ -81,8 +83,10 @@ body { font-size: 15px; }
...
@@ -81,8 +83,10 @@ body { font-size: 15px; }
.published-date
,
.history-link
{
font-size
:
12px
;
}
.published-date
,
.history-link
{
font-size
:
12px
;
}
.empty-card
,
.error-card
{
font-size
:
14px
;
}
.empty-card
,
.error-card
{
font-size
:
14px
;
}
footer
{
font-size
:
12px
;
}
footer
{
font-size
:
12px
;
}
.package-description
{
margin
:
14px
0
0
;
padding
:
13px
15px
;
border-radius
:
8px
;
color
:
#68758a
;
background
:
#f7f8fc
;
font-size
:
13px
;
}
.package-description
{
margin
:
14px
0
0
;
color
:
#68758a
;
font-size
:
13px
;
}
.package-description-title
{
display
:
block
;
margin-bottom
:
6px
;
color
:
#536077
;
font-size
:
13px
;
}
.package-description-title
{
display
:
block
;
margin-bottom
:
6px
;
color
:
#536077
;
font-size
:
13px
;
}
.package-description-content
{
padding
:
13px
15px
;
border-radius
:
8px
;
background
:
#f7f8fc
;
}
.application-description
{
margin-top
:
8px
!important
;
color
:
var
(
--muted
);
font-size
:
15px
;
line-height
:
1.7
;
overflow-wrap
:
anywhere
;
}
.markdown-body
{
color
:
#465268
;
font-size
:
14px
;
line-height
:
1.7
;
overflow-wrap
:
anywhere
;
}
.markdown-body
{
color
:
#465268
;
font-size
:
14px
;
line-height
:
1.7
;
overflow-wrap
:
anywhere
;
}
.markdown-body
>
:first-child
{
margin-top
:
0
;
}
.markdown-body
>
:first-child
{
margin-top
:
0
;
}
.markdown-body
>
:last-child
{
margin-bottom
:
0
;
}
.markdown-body
>
:last-child
{
margin-bottom
:
0
;
}
...
...
distribution-server/frontend/assets/downloads.js
View file @
033861e9
...
@@ -36,13 +36,17 @@ function formatSize(bytes) {
...
@@ -36,13 +36,17 @@ function formatSize(bytes) {
return
`
${
value
.
toFixed
(
1
)}
${
units
[
unit
]}
`
;
return
`
${
value
.
toFixed
(
1
)}
${
units
[
unit
]}
`
;
}
}
function
packageCard
(
packageInfo
,
appName
,
{
compact
=
false
}
=
{})
{
function
packageCard
(
packageInfo
,
appName
,
{
compact
=
false
,
applicationDescription
=
""
}
=
{},
)
{
const
card
=
node
(
"article"
,
`package-card
${
compact
?
" package-card-compact"
:
""
}
`
);
const
card
=
node
(
"article"
,
`package-card
${
compact
?
" package-card-compact"
:
""
}
`
);
const
heading
=
node
(
"div"
,
"package-heading"
);
const
heading
=
node
(
"div"
,
"package-heading"
);
const
identity
=
node
(
"div"
,
""
);
const
identity
=
node
(
"div"
,
"
package-identity
"
);
identity
.
append
(
identity
.
append
(
node
(
"div"
,
"package-version"
,
packageInfo
.
version
),
node
(
"div"
,
"package-name"
,
appName
),
node
(
"div"
,
"package-name"
,
appName
),
node
(
"span"
,
"package-version"
,
packageInfo
.
version
),
);
);
const
badge
=
node
(
const
badge
=
node
(
"span"
,
"span"
,
...
@@ -50,15 +54,19 @@ function packageCard(packageInfo, appName, { compact = false } = {}) {
...
@@ -50,15 +54,19 @@ function packageCard(packageInfo, appName, { compact = false } = {}) {
packageInfo
.
helper_compatible
?
"Helper 更新包"
:
packageInfo
.
package_type
.
toUpperCase
(),
packageInfo
.
helper_compatible
?
"Helper 更新包"
:
packageInfo
.
package_type
.
toUpperCase
(),
);
);
heading
.
append
(
identity
,
badge
);
heading
.
append
(
identity
,
badge
);
card
.
append
(
heading
);
if
(
applicationDescription
)
{
card
.
append
(
node
(
"p"
,
"package-app-description"
,
applicationDescription
));
}
if
(
packageInfo
.
description
)
{
if
(
packageInfo
.
description
)
{
const
notes
=
node
(
"section"
,
"package-description"
);
const
notes
=
node
(
"section"
,
"package-description"
);
const
content
=
node
(
"div"
,
"package-description-content"
);
content
.
append
(
renderMarkdown
(
packageInfo
.
description
));
notes
.
append
(
notes
.
append
(
node
(
"strong"
,
"package-description-title"
,
"版本更新内容"
),
node
(
"strong"
,
"package-description-title"
,
"版本更新内容"
),
renderMarkdown
(
packageInfo
.
description
)
,
content
,
);
);
card
.
append
(
heading
,
notes
);
card
.
append
(
notes
);
}
else
{
card
.
append
(
heading
);
}
}
const
metadata
=
node
(
"div"
,
"package-metadata"
);
const
metadata
=
node
(
"div"
,
"package-metadata"
);
...
@@ -105,7 +113,10 @@ function renderLibrary(applications) {
...
@@ -105,7 +113,10 @@ function renderLibrary(applications) {
const
card
=
packageCard
({
const
card
=
packageCard
({
...
application
,
...
application
,
version
:
application
.
current_version
,
version
:
application
.
current_version
,
},
application
.
display_name
,
{
compact
:
true
});
},
application
.
display_name
,
{
compact
:
true
,
applicationDescription
:
application
.
description
,
});
const
detail
=
node
(
"a"
,
"history-link"
,
"查看已发布版本"
);
const
detail
=
node
(
"a"
,
"history-link"
,
"查看已发布版本"
);
detail
.
href
=
`/download/
${
encodeURIComponent
(
application
.
app_id
)}
`
;
detail
.
href
=
`/download/
${
encodeURIComponent
(
application
.
app_id
)}
`
;
card
.
querySelector
(
".package-actions"
).
append
(
detail
);
card
.
querySelector
(
".package-actions"
).
append
(
detail
);
...
@@ -117,6 +128,9 @@ function renderApplication(application) {
...
@@ -117,6 +128,9 @@ function renderApplication(application) {
document
.
querySelector
(
"#page-title"
).
textContent
=
application
.
display_name
;
document
.
querySelector
(
"#page-title"
).
textContent
=
application
.
display_name
;
document
.
querySelector
(
"#page-description"
).
textContent
=
document
.
querySelector
(
"#page-description"
).
textContent
=
`
${
application
.
packages
.
length
}
个已发布程序包 · App ID:
${
application
.
app_id
}
`
;
`
${
application
.
packages
.
length
}
个已发布程序包 · App ID:
${
application
.
app_id
}
`
;
const
applicationDescription
=
document
.
querySelector
(
"#application-description"
);
applicationDescription
.
textContent
=
application
.
description
||
""
;
applicationDescription
.
hidden
=
!
application
.
description
;
packageList
.
replaceChildren
();
packageList
.
replaceChildren
();
for
(
const
packageInfo
of
application
.
packages
)
{
for
(
const
packageInfo
of
application
.
packages
)
{
packageList
.
append
(
packageCard
(
packageInfo
,
application
.
display_name
));
packageList
.
append
(
packageCard
(
packageInfo
,
application
.
display_name
));
...
...
distribution-server/frontend/downloads.html
View file @
033861e9
...
@@ -5,8 +5,8 @@
...
@@ -5,8 +5,8 @@
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
>
<meta
name=
"color-scheme"
content=
"light"
>
<meta
name=
"color-scheme"
content=
"light"
>
<title>
程序下载中心
</title>
<title>
程序下载中心
</title>
<link
rel=
"stylesheet"
href=
"/assets/downloads.css"
>
<link
rel=
"stylesheet"
href=
"/assets/downloads.css
?v=20261028-1
"
>
<script
type=
"module"
src=
"/assets/downloads.js"
></script>
<script
type=
"module"
src=
"/assets/downloads.js
?v=20261028-1
"
></script>
</head>
</head>
<body>
<body>
<main
class=
"page"
>
<main
class=
"page"
>
...
@@ -21,6 +21,7 @@
...
@@ -21,6 +21,7 @@
<div
class=
"eyebrow"
>
DOWNLOAD LIBRARY
</div>
<div
class=
"eyebrow"
>
DOWNLOAD LIBRARY
</div>
<h1
id=
"page-title"
>
获取程序与更新包
</h1>
<h1
id=
"page-title"
>
获取程序与更新包
</h1>
<p
id=
"page-description"
>
浏览已发布的程序版本,下载前请核对平台与架构。
</p>
<p
id=
"page-description"
>
浏览已发布的程序版本,下载前请核对平台与架构。
</p>
<p
id=
"application-description"
class=
"application-description"
hidden
></p>
</section>
</section>
<div
id=
"error"
class=
"error-card"
role=
"status"
hidden
></div>
<div
id=
"error"
class=
"error-card"
role=
"status"
hidden
></div>
<section
id=
"package-list"
class=
"package-list"
aria-live=
"polite"
></section>
<section
id=
"package-list"
class=
"package-list"
aria-live=
"polite"
></section>
...
...
distribution-server/frontend/index.html
View file @
033861e9
...
@@ -4,8 +4,8 @@
...
@@ -4,8 +4,8 @@
<meta
charset=
"utf-8"
>
<meta
charset=
"utf-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
>
<title>
程序分发管理
</title>
<title>
程序分发管理
</title>
<link
rel=
"stylesheet"
href=
"/assets/app.css"
>
<link
rel=
"stylesheet"
href=
"/assets/app.css
?v=20261028-1
"
>
<script
type=
"module"
src=
"/assets/app.js"
></script>
<script
type=
"module"
src=
"/assets/app.js
?v=20261028-1
"
></script>
</head>
</head>
<body>
<body>
<div
class=
"shell"
>
<div
class=
"shell"
>
...
@@ -70,6 +70,10 @@
...
@@ -70,6 +70,10 @@
<div
class=
"detail-row"
><span>
应用名称
</span><strong
id=
"app-name"
>
—
</strong></div>
<div
class=
"detail-row"
><span>
应用名称
</span><strong
id=
"app-name"
>
—
</strong></div>
<div
class=
"detail-row"
><span>
App ID
</span><code
id=
"app-id"
>
—
</code></div>
<div
class=
"detail-row"
><span>
App ID
</span><code
id=
"app-id"
>
—
</code></div>
<div
class=
"detail-row"
><span>
版本总数
</span><strong
id=
"release-count"
>
0
</strong></div>
<div
class=
"detail-row"
><span>
版本总数
</span><strong
id=
"release-count"
>
0
</strong></div>
<div
class=
"application-description-row"
>
<span>
应用说明
</span>
<p
id=
"app-description"
>
—
</p>
</div>
</article>
</article>
</section>
</section>
...
@@ -120,9 +124,9 @@
...
@@ -120,9 +124,9 @@
</label>
</label>
<button
id=
"upload-button"
class=
"button button-primary"
type=
"submit"
>
上传版本
</button>
<button
id=
"upload-button"
class=
"button button-primary"
type=
"submit"
>
上传版本
</button>
<label
class=
"field markdown-field"
>
<label
class=
"field markdown-field"
>
<span>
版本
说明(Markdown,可选
)
</span>
<span>
版本
更新内容(必填
)
</span>
<textarea
name=
"description"
rows=
"4"
maxlength=
"20000"
placeholder=
"
# 更新内容 - 新增功能 - 修复问题"
></textarea>
<textarea
name=
"description"
rows=
"4"
maxlength=
"20000"
placeholder=
"
- 新增功能 - 修复问题"
required
></textarea>
<small>
支持标题、列表、粗体、链接和代码;会显示在公开下载页。
</small>
<small>
支持
Markdown
标题、列表、粗体、链接和代码;会显示在公开下载页。
</small>
</label>
</label>
</form>
</form>
<div
id=
"upload-progress"
class=
"progress-wrap"
hidden
>
<div
id=
"upload-progress"
class=
"progress-wrap"
hidden
>
...
@@ -160,6 +164,11 @@
...
@@ -160,6 +164,11 @@
<span>
显示名称
</span>
<span>
显示名称
</span>
<input
name=
"display_name"
placeholder=
"我的程序"
maxlength=
"120"
required
>
<input
name=
"display_name"
placeholder=
"我的程序"
maxlength=
"120"
required
>
</label>
</label>
<label
class=
"field"
>
<span>
应用说明
</span>
<textarea
name=
"description"
rows=
"3"
maxlength=
"2000"
placeholder=
"介绍程序用途、主要功能或适用人群"
></textarea>
<small>
可选,创建后会显示在公开下载页。
</small>
</label>
<div
class=
"dialog-actions"
>
<div
class=
"dialog-actions"
>
<button
class=
"button button-muted"
type=
"button"
data-close-dialog
>
取消
</button>
<button
class=
"button button-muted"
type=
"button"
data-close-dialog
>
取消
</button>
<button
id=
"create-button"
class=
"button button-primary"
type=
"submit"
value=
"default"
>
创建应用
</button>
<button
id=
"create-button"
class=
"button button-primary"
type=
"submit"
value=
"default"
>
创建应用
</button>
...
@@ -175,8 +184,8 @@
...
@@ -175,8 +184,8 @@
</div>
</div>
<p
id=
"description-version"
class=
"muted"
></p>
<p
id=
"description-version"
class=
"muted"
></p>
<label
class=
"field"
>
<label
class=
"field"
>
<span>
Markdown 说明
</span>
<span>
版本更新内容(必填)
</span>
<textarea
name=
"description"
rows=
"8"
maxlength=
"20000"
placeholder=
"
# 更新内容 - 新增功能 - 修复问题"
></textarea>
<textarea
name=
"description"
rows=
"8"
maxlength=
"20000"
placeholder=
"
- 新增功能 - 修复问题"
required
></textarea>
<small>
保存后会立即显示在程序包记录和公开下载页。
</small>
<small>
保存后会立即显示在程序包记录和公开下载页。
</small>
</label>
</label>
<div
id=
"description-preview"
class=
"markdown-body description-preview"
hidden
></div>
<div
id=
"description-preview"
class=
"markdown-body description-preview"
hidden
></div>
...
...
distribution-server/tests/test_distribution.py
View file @
033861e9
...
@@ -33,17 +33,25 @@ class DistributionTests(unittest.TestCase):
...
@@ -33,17 +33,25 @@ class DistributionTests(unittest.TestCase):
self
.
client
.
__exit__
(
None
,
None
,
None
)
self
.
client
.
__exit__
(
None
,
None
,
None
)
self
.
temp_dir
.
cleanup
()
self
.
temp_dir
.
cleanup
()
def
create_application
(
self
,
app_id
:
str
=
"my-program"
)
->
None
:
def
create_application
(
self
,
app_id
:
str
=
"my-program"
,
description
:
str
=
""
,
)
->
None
:
response
=
self
.
client
.
post
(
response
=
self
.
client
.
post
(
"/api/apps"
,
"/api/apps"
,
json
=
{
"app_id"
:
app_id
,
"display_name"
:
app_id
},
json
=
{
"app_id"
:
app_id
,
"display_name"
:
app_id
,
"description"
:
description
,
},
)
)
self
.
assertEqual
(
response
.
status_code
,
201
,
response
.
text
)
self
.
assertEqual
(
response
.
status_code
,
201
,
response
.
text
)
def
upload
(
self
,
app_id
:
str
,
version
:
str
,
content
:
bytes
)
->
dict
[
str
,
object
]:
def
upload
(
self
,
app_id
:
str
,
version
:
str
,
content
:
bytes
)
->
dict
[
str
,
object
]:
response
=
self
.
client
.
post
(
response
=
self
.
client
.
post
(
f
"/api/apps/{app_id}/releases"
,
f
"/api/apps/{app_id}/releases"
,
data
=
{
"version"
:
version
},
data
=
{
"version"
:
version
,
"description"
:
"Release notes."
},
files
=
{
"file"
:
(
f
"{app_id}.exe"
,
content
,
"application/octet-stream"
)},
files
=
{
"file"
:
(
f
"{app_id}.exe"
,
content
,
"application/octet-stream"
)},
)
)
self
.
assertEqual
(
response
.
status_code
,
201
,
response
.
text
)
self
.
assertEqual
(
response
.
status_code
,
201
,
response
.
text
)
...
@@ -104,11 +112,37 @@ class DistributionTests(unittest.TestCase):
...
@@ -104,11 +112,37 @@ class DistributionTests(unittest.TestCase):
self
.
create_application
()
self
.
create_application
()
response
=
self
.
client
.
post
(
response
=
self
.
client
.
post
(
"/api/apps/my-program/releases"
,
"/api/apps/my-program/releases"
,
data
=
{
"version"
:
"1.0.0"
},
data
=
{
"version"
:
"1.0.0"
,
"description"
:
"Package description."
},
files
=
{
"file"
:
(
"program.exe"
,
b
"not an executable"
,
"application/octet-stream"
)},
files
=
{
"file"
:
(
"program.exe"
,
b
"not an executable"
,
"application/octet-stream"
)},
)
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_release_description_is_required
(
self
)
->
None
:
self
.
create_application
()
for
description
in
(
None
,
" "
):
data
=
{
"version"
:
"1.0.0"
}
if
description
is
not
None
:
data
[
"description"
]
=
description
response
=
self
.
client
.
post
(
"/api/apps/my-program/releases"
,
data
=
data
,
files
=
{
"file"
:
(
"program.exe"
,
b
"MZ-package"
,
"application/octet-stream"
)},
)
self
.
assertEqual
(
response
.
status_code
,
422
,
response
.
text
)
self
.
assertEqual
(
self
.
client
.
get
(
"/api/apps/my-program/releases"
)
.
json
(),
[])
def
test_application_description_is_returned_to_management_and_public_pages
(
self
)
->
None
:
description
=
"一个用于展示更新说明的示例程序。"
self
.
create_application
(
description
=
description
)
self
.
assertEqual
(
self
.
client
.
get
(
"/api/apps"
)
.
json
()[
0
][
"description"
],
description
)
self
.
upload
(
"my-program"
,
"1.0.0"
,
b
"MZ-package"
)
self
.
client
.
post
(
"/api/apps/my-program/releases/1.0.0/publish"
)
public_application
=
self
.
client
.
get
(
"/api/public/apps/my-program"
)
public_library
=
self
.
client
.
get
(
"/api/public/apps"
)
self
.
assertEqual
(
public_application
.
json
()[
"description"
],
description
)
self
.
assertEqual
(
public_library
.
json
()[
0
][
"description"
],
description
)
def
test_generic_package_is_recorded_and_published_for_public_download
(
self
)
->
None
:
def
test_generic_package_is_recorded_and_published_for_public_download
(
self
)
->
None
:
self
.
create_application
()
self
.
create_application
()
package
=
b
"example mac package"
package
=
b
"example mac package"
...
@@ -216,6 +250,12 @@ class DistributionTests(unittest.TestCase):
...
@@ -216,6 +250,12 @@ class DistributionTests(unittest.TestCase):
)
)
self
.
assertEqual
(
response
.
status_code
,
422
)
self
.
assertEqual
(
response
.
status_code
,
422
)
empty_response
=
self
.
client
.
patch
(
"/api/apps/my-program/releases/1.0.0/description"
,
json
=
{
"description"
:
" "
},
)
self
.
assertEqual
(
empty_response
.
status_code
,
422
)
def
test_non_helper_release_does_not_replace_updater_manifest
(
self
)
->
None
:
def
test_non_helper_release_does_not_replace_updater_manifest
(
self
)
->
None
:
self
.
create_application
()
self
.
create_application
()
executable
=
b
"MZ-original"
executable
=
b
"MZ-original"
...
@@ -229,6 +269,7 @@ class DistributionTests(unittest.TestCase):
...
@@ -229,6 +269,7 @@ class DistributionTests(unittest.TestCase):
"platform"
:
"linux"
,
"platform"
:
"linux"
,
"architecture"
:
"x64"
,
"architecture"
:
"x64"
,
"helper_compatible"
:
"false"
,
"helper_compatible"
:
"false"
,
"description"
:
"Linux build."
,
},
},
files
=
{
"file"
:
(
"sample.tar.gz"
,
b
"linux package"
,
"application/gzip"
)},
files
=
{
"file"
:
(
"sample.tar.gz"
,
b
"linux package"
,
"application/gzip"
)},
)
)
...
@@ -298,6 +339,7 @@ class DistributionTests(unittest.TestCase):
...
@@ -298,6 +339,7 @@ class DistributionTests(unittest.TestCase):
manifest_response
=
legacy_client
.
get
(
"/updates/legacy-app/latest.json"
)
manifest_response
=
legacy_client
.
get
(
"/updates/legacy-app/latest.json"
)
download_response
=
legacy_client
.
get
(
"/packages/legacy-app/1.0.0/download"
)
download_response
=
legacy_client
.
get
(
"/packages/legacy-app/1.0.0/download"
)
releases_response
=
legacy_client
.
get
(
"/api/apps/legacy-app/releases"
)
releases_response
=
legacy_client
.
get
(
"/api/apps/legacy-app/releases"
)
applications_response
=
legacy_client
.
get
(
"/api/apps"
)
self
.
assertEqual
(
manifest_response
.
status_code
,
200
,
manifest_response
.
text
)
self
.
assertEqual
(
manifest_response
.
status_code
,
200
,
manifest_response
.
text
)
self
.
assertEqual
(
manifest_response
.
json
()[
"version"
],
"1.0.0"
)
self
.
assertEqual
(
manifest_response
.
json
()[
"version"
],
"1.0.0"
)
...
@@ -305,6 +347,7 @@ class DistributionTests(unittest.TestCase):
...
@@ -305,6 +347,7 @@ class DistributionTests(unittest.TestCase):
self
.
assertEqual
(
download_response
.
content
,
package
)
self
.
assertEqual
(
download_response
.
content
,
package
)
self
.
assertEqual
(
releases_response
.
status_code
,
200
,
releases_response
.
text
)
self
.
assertEqual
(
releases_response
.
status_code
,
200
,
releases_response
.
text
)
self
.
assertEqual
(
releases_response
.
json
()[
0
][
"description"
],
""
)
self
.
assertEqual
(
releases_response
.
json
()[
0
][
"description"
],
""
)
self
.
assertEqual
(
applications_response
.
json
()[
0
][
"description"
],
""
)
def
test_upload_rejects_duplicate_version
(
self
)
->
None
:
def
test_upload_rejects_duplicate_version
(
self
)
->
None
:
self
.
create_application
()
self
.
create_application
()
...
@@ -312,7 +355,7 @@ class DistributionTests(unittest.TestCase):
...
@@ -312,7 +355,7 @@ class DistributionTests(unittest.TestCase):
self
.
upload
(
"my-program"
,
"1.0.0"
,
executable
)
self
.
upload
(
"my-program"
,
"1.0.0"
,
executable
)
response
=
self
.
client
.
post
(
response
=
self
.
client
.
post
(
"/api/apps/my-program/releases"
,
"/api/apps/my-program/releases"
,
data
=
{
"version"
:
"1.0.0"
},
data
=
{
"version"
:
"1.0.0"
,
"description"
:
"Release notes."
},
files
=
{
"file"
:
(
"program.exe"
,
executable
,
"application/octet-stream"
)},
files
=
{
"file"
:
(
"program.exe"
,
executable
,
"application/octet-stream"
)},
)
)
self
.
assertEqual
(
response
.
status_code
,
409
)
self
.
assertEqual
(
response
.
status_code
,
409
)
...
@@ -352,6 +395,34 @@ class DistributionTests(unittest.TestCase):
...
@@ -352,6 +395,34 @@ class DistributionTests(unittest.TestCase):
self
.
assertIn
(
"body { font-size: 15px; }"
,
stylesheet
.
text
)
self
.
assertIn
(
"body { font-size: 15px; }"
,
stylesheet
.
text
)
self
.
assertIn
(
"body { font-size: 15px; }"
,
download_stylesheet
.
text
)
self
.
assertIn
(
"body { font-size: 15px; }"
,
download_stylesheet
.
text
)
def
test_frontend_pages_and_assets_revalidate_cached_content
(
self
)
->
None
:
for
path
in
(
"/"
,
"/downloads"
,
"/download/my-program"
,
"/assets/app.js?v=20261028-1"
,
"/assets/app.css?v=20261028-1"
,
"/assets/downloads.js?v=20261028-1"
,
"/assets/downloads.css?v=20261028-1"
,
):
with
self
.
subTest
(
path
=
path
):
response
=
self
.
client
.
get
(
path
)
self
.
assertEqual
(
response
.
status_code
,
200
,
response
.
text
)
self
.
assertEqual
(
response
.
headers
[
"cache-control"
],
"no-cache"
)
conditional
=
self
.
client
.
get
(
path
,
headers
=
{
"If-None-Match"
:
response
.
headers
[
"etag"
]},
)
self
.
assertEqual
(
conditional
.
status_code
,
304
if
path
.
startswith
(
"/assets/"
)
else
200
)
self
.
assertEqual
(
conditional
.
headers
[
"cache-control"
],
"no-cache"
)
dashboard
=
self
.
client
.
get
(
"/"
)
downloads
=
self
.
client
.
get
(
"/downloads"
)
for
asset
in
(
"app.js"
,
"app.css"
):
self
.
assertIn
(
f
"/assets/{asset}?v=20261028-1"
,
dashboard
.
text
)
for
asset
in
(
"downloads.js"
,
"downloads.css"
):
self
.
assertIn
(
f
"/assets/{asset}?v=20261028-1"
,
downloads
.
text
)
def
test_public_download_surfaces_are_remote_but_management_is_not
(
self
)
->
None
:
def
test_public_download_surfaces_are_remote_but_management_is_not
(
self
)
->
None
:
self
.
create_application
()
self
.
create_application
()
executable
=
b
"MZ-public"
executable
=
b
"MZ-public"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment