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
8de93954
Commit
8de93954
authored
Oct 30, 2026
by
周田
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
http://39.170.91.81:10020/zhoutian/program-updater
parents
9301122e
15fd3ece
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
38 additions
and
16 deletions
+38
-16
README.md
distribution-server/README.md
+1
-1
store.py
distribution-server/core/store.py
+2
-2
downloads.css
distribution-server/frontend/assets/downloads.css
+4
-1
downloads.js
distribution-server/frontend/assets/downloads.js
+17
-6
downloads.html
distribution-server/frontend/downloads.html
+6
-3
test_distribution.py
distribution-server/tests/test_distribution.py
+8
-3
No files found.
distribution-server/README.md
View file @
8de93954
...
@@ -15,7 +15,7 @@ distribution-server/
...
@@ -15,7 +15,7 @@ distribution-server/
└─ settings.py
└─ settings.py
```
```
管理 API 以
`/api`
开头;创建应用时可填写应用说明。上传版本必须填写 Markdown 更新内容;服务器流式计算文件大小和 SHA-256,并记录文件名、格式、平台、架构、更新内容、上传时间和发布状态。支持 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 说明支持标题、列表、粗体、链接和代码。
版本说明也可以在管理页的程序包记录中单独编辑,无需重新上传程序包;保存后会同时显示在管理记录和公开下载页。
版本说明也可以在管理页的程序包记录中单独编辑,无需重新上传程序包;保存后会同时显示在管理记录和公开下载页。
...
...
distribution-server/core/store.py
View file @
8de93954
...
@@ -304,8 +304,8 @@ class DistributionStore:
...
@@ -304,8 +304,8 @@ class DistributionStore:
rows
=
connection
.
execute
(
rows
=
connection
.
execute
(
"""
"""
SELECT a.app_id, a.display_name, a.description, 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
AS release_description
,
r.size, r.sha256,
r.
platform, r.architecture, r.
size, r.sha256,
r.published_at
r.published_at
FROM applications AS a
FROM applications AS a
JOIN releases AS r
JOIN releases AS r
...
...
distribution-server/frontend/assets/downloads.css
View file @
8de93954
...
@@ -33,6 +33,8 @@ h1 { margin: 8px 0 7px; font-size: clamp(24px, 4vw, 32px); letter-spacing: -.03e
...
@@ -33,6 +33,8 @@ h1 { margin: 8px 0 7px; font-size: clamp(24px, 4vw, 32px); letter-spacing: -.03e
.package-version
{
flex
:
0
0
auto
;
padding
:
4px
8px
;
border-radius
:
999px
;
color
:
#536077
;
background
:
#f0f2f6
;
font-size
:
11px
;
font-weight
:
700
;
}
.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-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-app-description
{
margin
:
10px
0
0
;
color
:
var
(
--muted
);
font-size
:
13px
;
line-height
:
1.6
;
overflow-wrap
:
anywhere
;
}
.package-app-description-title
,
.application-description-title
{
display
:
block
;
color
:
#536077
;
font-size
:
13px
;
}
.package-app-description-content
{
margin
:
4px
0
0
;
}
.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
;
}
...
@@ -86,7 +88,8 @@ footer { font-size: 12px; }
...
@@ -86,7 +88,8 @@ footer { font-size: 12px; }
.package-description
{
margin
:
14px
0
0
;
color
:
#68758a
;
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
;
}
.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
;
}
.application-description
{
margin-top
:
8px
;
color
:
var
(
--muted
);
font-size
:
15px
;
line-height
:
1.7
;
overflow-wrap
:
anywhere
;
}
.application-description
p
{
margin
:
4px
0
0
;
}
.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 @
8de93954
...
@@ -39,7 +39,7 @@ function formatSize(bytes) {
...
@@ -39,7 +39,7 @@ function formatSize(bytes) {
function
packageCard
(
function
packageCard
(
packageInfo
,
packageInfo
,
appName
,
appName
,
{
compact
=
false
,
applicationDescription
=
""
}
=
{},
{
compact
=
false
,
applicationDescription
=
""
,
releaseNotes
=
""
}
=
{},
)
{
)
{
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"
);
...
@@ -56,12 +56,17 @@ function packageCard(
...
@@ -56,12 +56,17 @@ function packageCard(
heading
.
append
(
identity
,
badge
);
heading
.
append
(
identity
,
badge
);
card
.
append
(
heading
);
card
.
append
(
heading
);
if
(
applicationDescription
)
{
if
(
applicationDescription
)
{
card
.
append
(
node
(
"p"
,
"package-app-description"
,
applicationDescription
));
const
description
=
node
(
"section"
,
"package-app-description"
);
description
.
append
(
node
(
"strong"
,
"package-app-description-title"
,
"程序介绍"
),
node
(
"p"
,
"package-app-description-content"
,
applicationDescription
),
);
card
.
append
(
description
);
}
}
if
(
packageInfo
.
description
)
{
if
(
releaseNotes
)
{
const
notes
=
node
(
"section"
,
"package-description"
);
const
notes
=
node
(
"section"
,
"package-description"
);
const
content
=
node
(
"div"
,
"package-description-content"
);
const
content
=
node
(
"div"
,
"package-description-content"
);
content
.
append
(
renderMarkdown
(
packageInfo
.
description
));
content
.
append
(
renderMarkdown
(
releaseNotes
));
notes
.
append
(
notes
.
append
(
node
(
"strong"
,
"package-description-title"
,
"版本更新内容"
),
node
(
"strong"
,
"package-description-title"
,
"版本更新内容"
),
content
,
content
,
...
@@ -116,6 +121,7 @@ function renderLibrary(applications) {
...
@@ -116,6 +121,7 @@ function renderLibrary(applications) {
},
application
.
display_name
,
{
},
application
.
display_name
,
{
compact
:
true
,
compact
:
true
,
applicationDescription
:
application
.
description
,
applicationDescription
:
application
.
description
,
releaseNotes
:
application
.
release_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
)}
`
;
...
@@ -129,11 +135,16 @@ function renderApplication(application) {
...
@@ -129,11 +135,16 @@ function renderApplication(application) {
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"
);
const
applicationDescription
=
document
.
querySelector
(
"#application-description"
);
applicationDescription
.
textContent
=
application
.
description
||
""
;
document
.
querySelector
(
"#application-description-content"
).
textContent
=
application
.
description
||
""
;
applicationDescription
.
hidden
=
!
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
,
{
releaseNotes
:
packageInfo
.
description
,
}),
);
}
}
}
}
...
...
distribution-server/frontend/downloads.html
View file @
8de93954
...
@@ -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?v=2026102
8-1
"
>
<link
rel=
"stylesheet"
href=
"/assets/downloads.css?v=2026102
9-2
"
>
<script
type=
"module"
src=
"/assets/downloads.js?v=2026102
8-1
"
></script>
<script
type=
"module"
src=
"/assets/downloads.js?v=2026102
9-2
"
></script>
</head>
</head>
<body>
<body>
<main
class=
"page"
>
<main
class=
"page"
>
...
@@ -21,7 +21,10 @@
...
@@ -21,7 +21,10 @@
<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
id=
"application-description"
class=
"application-description"
hidden
>
<strong
class=
"application-description-title"
>
程序介绍
</strong>
<p
id=
"application-description-content"
></p>
</section>
</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/tests/test_distribution.py
View file @
8de93954
...
@@ -142,6 +142,8 @@ class DistributionTests(unittest.TestCase):
...
@@ -142,6 +142,8 @@ class DistributionTests(unittest.TestCase):
public_library
=
self
.
client
.
get
(
"/api/public/apps"
)
public_library
=
self
.
client
.
get
(
"/api/public/apps"
)
self
.
assertEqual
(
public_application
.
json
()[
"description"
],
description
)
self
.
assertEqual
(
public_application
.
json
()[
"description"
],
description
)
self
.
assertEqual
(
public_library
.
json
()[
0
][
"description"
],
description
)
self
.
assertEqual
(
public_library
.
json
()[
0
][
"description"
],
description
)
self
.
assertEqual
(
public_library
.
json
()[
0
][
"release_description"
],
"Release notes."
)
self
.
assertEqual
(
public_application
.
json
()[
"packages"
][
0
][
"description"
],
"Release notes."
)
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
()
...
@@ -388,6 +390,7 @@ class DistributionTests(unittest.TestCase):
...
@@ -388,6 +390,7 @@ class DistributionTests(unittest.TestCase):
self
.
assertIn
(
".shell"
,
stylesheet
.
text
)
self
.
assertIn
(
".shell"
,
stylesheet
.
text
)
self
.
assertEqual
(
download_script
.
status_code
,
200
)
self
.
assertEqual
(
download_script
.
status_code
,
200
)
self
.
assertIn
(
"renderLibrary"
,
download_script
.
text
)
self
.
assertIn
(
"renderLibrary"
,
download_script
.
text
)
self
.
assertIn
(
'"程序介绍"'
,
download_script
.
text
)
self
.
assertEqual
(
download_stylesheet
.
status_code
,
200
)
self
.
assertEqual
(
download_stylesheet
.
status_code
,
200
)
self
.
assertIn
(
".package-card"
,
download_stylesheet
.
text
)
self
.
assertIn
(
".package-card"
,
download_stylesheet
.
text
)
self
.
assertEqual
(
markdown_script
.
status_code
,
200
)
self
.
assertEqual
(
markdown_script
.
status_code
,
200
)
...
@@ -402,8 +405,8 @@ class DistributionTests(unittest.TestCase):
...
@@ -402,8 +405,8 @@ class DistributionTests(unittest.TestCase):
"/download/my-program"
,
"/download/my-program"
,
"/assets/app.js?v=20261028-1"
,
"/assets/app.js?v=20261028-1"
,
"/assets/app.css?v=20261028-1"
,
"/assets/app.css?v=20261028-1"
,
"/assets/downloads.js?v=2026102
8-1
"
,
"/assets/downloads.js?v=2026102
9-2
"
,
"/assets/downloads.css?v=2026102
8-1
"
,
"/assets/downloads.css?v=2026102
9-2
"
,
):
):
with
self
.
subTest
(
path
=
path
):
with
self
.
subTest
(
path
=
path
):
response
=
self
.
client
.
get
(
path
)
response
=
self
.
client
.
get
(
path
)
...
@@ -418,10 +421,12 @@ class DistributionTests(unittest.TestCase):
...
@@ -418,10 +421,12 @@ class DistributionTests(unittest.TestCase):
dashboard
=
self
.
client
.
get
(
"/"
)
dashboard
=
self
.
client
.
get
(
"/"
)
downloads
=
self
.
client
.
get
(
"/downloads"
)
downloads
=
self
.
client
.
get
(
"/downloads"
)
package_page
=
self
.
client
.
get
(
"/download/my-program"
)
for
asset
in
(
"app.js"
,
"app.css"
):
for
asset
in
(
"app.js"
,
"app.css"
):
self
.
assertIn
(
f
"/assets/{asset}?v=20261028-1"
,
dashboard
.
text
)
self
.
assertIn
(
f
"/assets/{asset}?v=20261028-1"
,
dashboard
.
text
)
for
asset
in
(
"downloads.js"
,
"downloads.css"
):
for
asset
in
(
"downloads.js"
,
"downloads.css"
):
self
.
assertIn
(
f
"/assets/{asset}?v=20261028-1"
,
downloads
.
text
)
self
.
assertIn
(
f
"/assets/{asset}?v=20261029-2"
,
downloads
.
text
)
self
.
assertIn
(
"程序介绍"
,
package_page
.
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
()
...
...
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