Compare commits

...

3 Commits

Author SHA1 Message Date
076449b9f9
进行一波页面改进 2024-08-24 22:46:15 +08:00
00f9926016
aaaaaaa 2024-08-24 22:19:05 +08:00
cdadd234d5
我真的写了个网页( 2024-08-24 22:19:00 +08:00
8 changed files with 553 additions and 67 deletions

2
Cargo.lock generated
View File

@ -2591,7 +2591,7 @@ dependencies = [
[[package]] [[package]]
name = "sr_download" name = "sr_download"
version = "1.1.1" version = "1.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",

102
pages.md Normal file
View File

@ -0,0 +1,102 @@
# sr-download 提供的网络 api
## 设置
要想设置这部分内容
请编辑 `config.toml` 文件
```toml
# 这个部分
[serve]
# 服务的地址和端口
host_with_port = "0.0.0.0:10002"
# 数据库最大连接数
db_max_connect = 10
# 是否启用 serve 模式
enable = true
```
## 页面
`/dashboard`
展示当前信息
## API
### GET `/last/data`
```json
{
"code": 200,
"msg": "ok",
"data": {
"save_id": 1322273,
"save_type": "save",
"len": 2955,
"blake_hash": "1e327361ae30604f7828f3e1a0987098a61a16df0ce830352237e60c9db434fe"
}
}
```
### GET `/last/save`
```json
{
"code": 200,
"msg": "ok",
"data": {
"save_id": 1322273,
"len": 2955,
"blake_hash": "1e327361ae30604f7828f3e1a0987098a61a16df0ce830352237e60c9db434fe"
}
}
```
### GET `/last/ship`
```json
{
"code": 200,
"msg": "ok",
"data": {
"save_id": 1322271,
"len": 13721,
"blake_hash": "79c97ca4fe9fa982209e58d1e11df6ebf22cf2e96a2fc8cc48f9316982e6d7d5"
}
}
```
### GET `/info/:id`
```json
{
"code": 200,
"msg": "ok",
"data": {
"save_id": 1322271,
"save_type": "ship",
"len": 13721,
"blake_hash": "79c97ca4fe9fa982209e58d1e11df6ebf22cf2e96a2fc8cc48f9316982e6d7d5"
}
}
```
### GET `/download/:id`
```json
{
"code": 200,
"msg": "ok",
"data": {
"info": {
"save_id": 1322271,
"save_type": "ship",
"len": 13721,
"blake_hash": "79c97ca4fe9fa982209e58d1e11df6ebf22cf2e96a2fc8cc48f9316982e6d7d5"
},
"raw_data": "<Ship version=\"1\" liftedOff ..."
}
}
```

View File

@ -10,39 +10,7 @@ Rewritten in Rust !
现在支持提供 api 了 现在支持提供 api 了
- `GET /last_data` 获取最新的数据信息 具体 API 请参考 [这个页面](./pages.md)
- 返回范例:
```json
{
"save_id": 1322269,
"save_type": "save",
"len": 3404,
"blake_hash": "0b4758dbda98fea0ab6ad58fd589ccc7bb14c29ab8b22e6e49b670db8fec8da9"
}
```
- `GET /last_save` 获取最新的存档信息
- 返回范例:
```json
{
"save_id": 1322269,
"len": 3404,
"blake_hash": "0b4758dbda98fea0ab6ad58fd589ccc7bb14c29ab8b22e6e49b670db8fec8da9"
}
```
- `GET /last_ship` 获取最新的船只信息
- 返回范例:
```json
{
"save_id": 1322267,
"len": 38967,
"blake_hash": "9474267203155e5cf31e0e7e34ec014773f8f89c78d262f5bd57b6e27fdc25b2"
}
```
## V1 ## V1

View File

@ -1,6 +1,6 @@
[package] [package]
name = "sr_download" name = "sr_download"
version = "1.1.1" version = "1.2.0"
edition = "2021" edition = "2021"
default-run = "sr_download" default-run = "sr_download"

208
sr_download/src/info.html Normal file
View File

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<title>sr-download 信息页面</title>
<style>
body {
background-color: #F5F5F5FF;
font-family: sans-serif;
}
.container {
display: flex;
flex-direction: row;
justify-content: center;
gap: 20px;
width: 80%;
margin: 0 auto;
}
.box {
width: 80%;
padding: 40px;
border: 2px solid #000;
text-align: center;
font-size: 24px;
}
.box:nth-child(1) {
background-color: #FFE0B2;
/* 淡橙色 */
}
.box:nth-child(2) {
background-color: #C8E6C9;
/* 淡绿色 */
}
.box2 {
width: 90%;
padding: 20px;
border: 2px solid #000;
text-align: center;
font-size: 24px;
}
.box2:nth-child(1) {
background-color: #BBDEFB;
/* 淡蓝色 */
}
.title {
font-size: 40px;
margin-bottom: 10px;
}
.monospace {
font-family: monospace;
/* 启用等宽字体 */
border: 1px solid #000;
/* 添加边框 */
padding: 2px;
/* 添加内边距 */
background-color: #343942ba;
/* 浅灰色背景 */
color: #fff;
}
.spacer {
height: 40px;
/* 设置行间距 */
}
.input-section {
flex-basis: 50%;
/* 占满宽度 */
padding: 20px;
border: 2px solid #000;
text-align: center;
font-size: 24px;
background-color: #FFCDD2;
/* 淡蓝色 */
}
.input-section input {
padding: 10px;
font-size: 18px;
margin-right: 10px;
}
.input-section button {
padding: 10px 20px;
font-size: 18px;
background-color: #b3ffe8;
/* 淡蓝色 */
border: none;
cursor: pointer;
}
.input-section button:hover {
background-color: #90CAF9;
/* 深蓝色 */
}
.result-display {
flex-basis: 100%;
/* 占满宽度 */
padding: 20px;
border: 2px solid #000;
text-align: center;
font-size: 24px;
background-color: #E1BEE7;
/* 淡紫色 */
}
</style>
</head>
<body>
<div class="spacer"></div>
<div class="container">
<div class="box2">
<div class="title">最新数据</div>
<div>最大 id: |MAX_ID|</div>
<div>类型: |MAX_SAVE_TYPE|</div>
<div>长度: |MAX_LEN|</div>
<div>blake hash: <span class="monospace">|MAX_HASH|</span></div>
</div>
</div>
<div class="spacer"></div>
<div class="container">
<div class="box">
<div class="title">最新飞船</div>
<div>最大飞船 id: |MAX_SHIP_ID|</div>
<div>长度: |MAX_SHIP_LEN|</div>
<div>blake hash: <span class="monospace">|MAX_SHIP_HASH|</span></div>
</div>
<div class="box">
<div class="title">最新存档</div>
<div>最大存档 id: |MAX_SAVE_ID|</div>
<div>长度: |MAX_SAVE_LEN|</div>
<div>blake hash: <span class="monospace">|MAX_SAVE_HASH|</span></div>
</div>
</div>
<div class="spacer"></div>
<div class="container">
<div class="input-section">
<input type="number" id="dataId" placeholder="输入ID">
<button onclick="fetchData()">获取数据</button>
</div>
</div>
<div class="spacer"></div>
<div class="container">
<div class="result-display">
<div class="title">请求结果</div>
</div>
</div>
<script>
function fetchData() {
// 获取输入框中的 ID
const dataId = document.getElementById('dataId').value;
if (!dataId) {
alert('请输入 ID');
return;
}
if (dataId < 76858) {
alert('ID 不能小于 76858 (这个是目前最小的 ID)');
return;
}
// 发送请求
fetch(`/info/${dataId}`)
.then(response => response.json())
.then(data => {
// 获取结果显示区域
const resultDisplay = document.querySelector('.result-display');
// 清空结果显示区域
resultDisplay.innerHTML = '';
// 创建结果显示区域的元素
const resultTitle = document.createElement('div');
resultTitle.classList.add('title');
resultTitle.innerText = '请求结果';
resultDisplay.appendChild(resultTitle);
// 先判断数据拿没拿到
if (data["code"] != 200) {
// 没拿到
const resultContent = document.createElement('div');
resultContent.innerText = data["msg"];
resultDisplay.appendChild(resultContent);
} else {
// 拿到了
// 创建结果显示区域的元素
const resultContent = document.createElement('div');
const inner_data = data["data"];
// 添加数据
resultContent.innerHTML = `<div>id: ${inner_data["save_id"]}</div>
<div>类型: ${inner_data["save_type"]}</div>
<div>长度: ${inner_data["len"]}</div>
<div>blake hash: <span class="monospace">${inner_data["blake_hash"]}</span></div>`;
resultDisplay.appendChild(resultContent);
}
});
}
</script>
</body>
</html>

View File

@ -1,10 +1,60 @@
use axum::{extract::State, routing::get, Json, Router}; use axum::{
extract::{Path, State},
http::StatusCode,
response::{Html, IntoResponse},
routing::get,
Json, Router,
};
use sea_orm::{ActiveEnum, DatabaseConnection}; use sea_orm::{ActiveEnum, DatabaseConnection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db_part; use crate::db_part;
use migration::SaveId; use migration::SaveId;
pub mod traits;
use traits::FromDb;
#[derive(Serialize, Deserialize)]
pub struct WebResponse<T> {
pub code: u32,
pub msg: String,
pub data: Option<T>,
}
impl<T> WebResponse<T> {
pub fn new(data: Option<T>) -> Self {
match data {
Some(data) => Self::new_normal(data),
None => Self::new_missing("internal error?".to_string()),
}
}
pub fn new_normal(data: T) -> Self {
Self {
code: StatusCode::OK.as_u16() as u32,
msg: "ok".to_string(),
data: Some(data),
}
}
pub fn new_missing(msg: String) -> Self {
Self {
code: StatusCode::NOT_FOUND.as_u16() as u32,
msg,
data: None,
}
}
pub fn new_error(status: StatusCode, msg: String) -> Self {
Self {
code: status.as_u16() as u32,
msg,
data: None,
}
}
}
/// 最后一个数据的信息 /// 最后一个数据的信息
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LastData { pub struct LastData {
@ -15,8 +65,7 @@ pub struct LastData {
} }
impl LastData { impl LastData {
pub async fn from_db(db: &DatabaseConnection) -> Option<Self> { pub async fn from_db_by_id(db: &DatabaseConnection, id: SaveId) -> Option<Self> {
let id = db_part::find_max_id(db).await;
let data = db_part::get_raw_data(id, db).await?; let data = db_part::get_raw_data(id, db).await?;
Some(Self { Some(Self {
save_id: data.save_id, save_id: data.save_id,
@ -35,17 +84,6 @@ pub struct LastSave {
pub blake_hash: String, pub blake_hash: String,
} }
impl LastSave {
pub async fn from_db(db: &DatabaseConnection) -> Option<Self> {
let data = db_part::find_max_save(db).await?;
Some(Self {
save_id: data.save_id,
len: data.len,
blake_hash: data.blake_hash,
})
}
}
/// 最后一个飞船的信息 /// 最后一个飞船的信息
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LastShip { pub struct LastShip {
@ -54,27 +92,135 @@ pub struct LastShip {
pub blake_hash: String, pub blake_hash: String,
} }
impl LastShip { /// 实际信息
pub async fn from_db(db: &DatabaseConnection) -> Option<Self> { #[derive(Serialize, Deserialize)]
let data = db_part::find_max_ship(db).await?; pub struct RawData {
pub info: LastData,
pub raw_data: String,
}
impl RawData {
pub async fn from_db_by_id(db: &DatabaseConnection, id: SaveId) -> Option<Self> {
let data = db_part::get_raw_data(id, db).await?;
Some(Self { Some(Self {
info: LastData {
save_id: data.save_id, save_id: data.save_id,
save_type: data.save_type.to_value().to_string(),
len: data.len, len: data.len,
blake_hash: data.blake_hash, blake_hash: data.blake_hash,
},
raw_data: data.text?,
}) })
} }
} }
async fn get_last_data(State(db): State<DatabaseConnection>) -> Json<Option<LastData>> { async fn get_last_data(State(db): State<DatabaseConnection>) -> Json<WebResponse<LastData>> {
Json(LastData::from_db(&db).await) Json(WebResponse::new(LastData::from_db(&db).await))
} }
async fn get_last_save(State(db): State<DatabaseConnection>) -> Json<Option<LastSave>> { async fn get_last_save(State(db): State<DatabaseConnection>) -> Json<WebResponse<LastSave>> {
Json(LastSave::from_db(&db).await) Json(WebResponse::new(LastSave::from_db(&db).await))
} }
async fn get_last_ship(State(db): State<DatabaseConnection>) -> Json<Option<LastShip>> { async fn get_last_ship(State(db): State<DatabaseConnection>) -> Json<WebResponse<LastShip>> {
Json(LastShip::from_db(&db).await) Json(WebResponse::new(LastShip::from_db(&db).await))
}
async fn get_data_info_by_id(
State(db): State<DatabaseConnection>,
Path(raw_id): Path<String>,
) -> Json<WebResponse<LastData>> {
match raw_id.parse::<SaveId>() {
Ok(id) => match LastData::from_db_by_id(&db, id).await {
Some(data) => Json(WebResponse::new_normal(data)),
None => Json(WebResponse::new_missing("data not found".to_string())),
},
Err(e) => Json(WebResponse::new_error(
StatusCode::BAD_REQUEST,
format!("id parse error: {:?}", e),
)),
}
}
async fn get_data_by_id(
State(db): State<DatabaseConnection>,
Path(raw_id): Path<String>,
) -> Json<WebResponse<RawData>> {
match raw_id.parse::<SaveId>() {
Ok(id) => match RawData::from_db_by_id(&db, id).await {
Some(data) => Json(WebResponse::new_normal(data)),
None => Json(WebResponse::new_missing("data not found".to_string())),
},
Err(e) => Json(WebResponse::new_error(
StatusCode::BAD_REQUEST,
format!("id parse error: {:?}", e),
)),
}
}
async fn jump_to_info(Path(path): Path<String>) -> impl IntoResponse {
// html jump
(
StatusCode::MOVED_PERMANENTLY,
Html(format!(
"<h1>Jumping from {} to /dashboard</h1><script>location.href='/dashboard'</script>",
path
)),
)
}
/// 下面这段话是用于喂给 GitHub Copilot 让他帮我生成一个好用的 info 页面的 prompt
/// 页面背景 F5F5F5FF
/// 页面标题为 "sr-download 信息页面"
/// 页面内容为三个白色框, 横向排列
/// 里面分别是最大 id, 最大飞船 id, 最大存档 id 的信息展示
/// 框内文字居中,字体大小 24px, 字体为浏览器默认给的
/// 最大 id 部分的文字为 "最大 id: |MAX_ID|\n存档类型: |MAX_SAVE_TYPE|\n长度: |MAX_LEN|\nblake hash: |MAX_HASH|"
/// 最大飞船 id 部分展示相关信息, 存档 id 部分同理, 用 相关 |xxx| 作为占位符
/// 同时展示 长度, blake hash
/// 两个部分分别会展示三行字
/// 三个框之间的间距为 20px, 宽度为 80%, 高度为 100%
/// 框上面分别是 "最新数据" "最新飞船" "最新存档" 的标题
const INFO_PAGE: &str = include_str!("info.html");
async fn dashboard_page(State(db): State<DatabaseConnection>) -> Html<String> {
let max_id = db_part::find_max_id(&db).await;
let max_id_data = db_part::get_raw_data(max_id, &db).await.unwrap();
let max_ship = db_part::find_max_ship(&db).await;
let max_save = db_part::find_max_save(&db).await;
let mut page_content = INFO_PAGE
.replace("|MAX_ID|", &max_id_data.save_id.to_string())
.replace(
"|MAX_SAVE_TYPE|",
&max_id_data.save_type.to_value().to_string(),
)
.replace("|MAX_LEN|", &max_id_data.len.to_string())
.replace("|MAX_HASH|", &max_id_data.blake_hash);
if let Some(max_ship) = max_ship {
page_content = page_content
.replace("|MAX_SHIP_ID|", &max_ship.save_id.to_string())
.replace("|MAX_SHIP_LEN|", &max_ship.len.to_string())
.replace("|MAX_SHIP_HASH|", &max_ship.blake_hash);
} else {
page_content = page_content
.replace("|MAX_SHIP_ID|", "not found")
.replace("|MAX_SHIP_LEN|", "not found")
.replace("|MAX_SHIP_HASH|", "not found");
}
if let Some(max_save) = max_save {
page_content = page_content
.replace("|MAX_SAVE_ID|", &max_save.save_id.to_string())
.replace("|MAX_SAVE_LEN|", &max_save.len.to_string())
.replace("|MAX_SAVE_HASH|", &max_save.blake_hash);
} else {
page_content = page_content
.replace("|MAX_SAVE_ID|", "not found")
.replace("|MAX_SAVE_LEN|", "not found")
.replace("|MAX_SAVE_HASH|", "not found");
}
Html(page_content)
} }
pub async fn web_main() -> anyhow::Result<()> { pub async fn web_main() -> anyhow::Result<()> {
@ -83,12 +229,23 @@ pub async fn web_main() -> anyhow::Result<()> {
let listener = tokio::net::TcpListener::bind(conf.serve.host_with_port.clone()).await?; let listener = tokio::net::TcpListener::bind(conf.serve.host_with_port.clone()).await?;
let db = db_part::connect_server(&conf).await?; let db = db_part::connect_server(&conf).await?;
let app = Router::new() let app = Router::new()
// get /last_data // 获取最后一个数据
.route("/last_data", get(get_last_data)) .route("/last/data", get(get_last_data).post(get_last_data))
// get /last_save // 获取最后一个存档
.route("/last_save", get(get_last_save)) .route("/last/save", get(get_last_save).post(get_last_save))
// get /last_ship // 获取最后一个飞船
.route("/last_ship", get(get_last_ship)) .route("/last/ship", get(get_last_ship).post(get_last_ship))
// 获取指定 id 的数据(也有可能返回 not found)
.route(
"/info/:id",
get(get_data_info_by_id).post(get_data_info_by_id),
)
// 获取下载指定 id 的数据
.route("/download/:id", get(get_data_by_id).post(get_data_by_id))
// info 页面
.route("/dashboard", get(dashboard_page).post(dashboard_page))
// 其他所有路径, 直接跳转到 info 页面
.route("/*path", get(jump_to_info).post(jump_to_info))
// db // db
.with_state(db); .with_state(db);

View File

@ -0,0 +1,45 @@
use sea_orm::{ActiveEnum, DatabaseConnection};
use super::{LastData, LastSave, LastShip};
use crate::db_part;
pub trait FromDb {
async fn from_db(db: &DatabaseConnection) -> Option<Self>
where
Self: Sized;
}
impl FromDb for LastData {
async fn from_db(db: &DatabaseConnection) -> Option<Self> {
let id = db_part::find_max_id(db).await;
let data = db_part::get_raw_data(id, db).await?;
Some(Self {
save_id: data.save_id,
save_type: data.save_type.to_value().to_string(),
len: data.len,
blake_hash: data.blake_hash,
})
}
}
impl FromDb for LastSave {
async fn from_db(db: &DatabaseConnection) -> Option<Self> {
let data = db_part::find_max_save(db).await?;
Some(Self {
save_id: data.save_id,
len: data.len,
blake_hash: data.blake_hash,
})
}
}
impl FromDb for LastShip {
async fn from_db(db: &DatabaseConnection) -> Option<Self> {
let data = db_part::find_max_ship(db).await?;
Some(Self {
save_id: data.save_id,
len: data.len,
blake_hash: data.blake_hash,
})
}
}

View File

@ -1,5 +1,11 @@
# v1.0 # v1.0
## 1.2.0
加入了不少 api
甚至加入了 `/dashboard` 页面
修改了之前的那堆 api 的路径和返回值
## 1.1.1 ## 1.1.1
修复了 serve 模式如果在配置文件里禁用, 实际上还是会启动的问题 修复了 serve 模式如果在配置文件里禁用, 实际上还是会启动的问题