diff --git a/ocs/.dockerignore b/ocs/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..8ac3e180bc14f8a051f3cbfa5a23f61cab709dbf
--- /dev/null
+++ b/ocs/.dockerignore
@@ -0,0 +1,6 @@
+.dockerignore
+.git*
+debian/Dockerfile
+
+# From .gitignore
+target/
diff --git a/ocs/.env b/ocs/.env
new file mode 100644
index 0000000000000000000000000000000000000000..9ab2cde021a7e4f8208bf22dc9e365fe6329f4c2
--- /dev/null
+++ b/ocs/.env
@@ -0,0 +1,2 @@
+WEB.ADDR=0.0.0.0:8080
+
diff --git a/ocs/.gitignore b/ocs/.gitignore
index ae9ea50078ef99e806d95bb3ac4275a15df0c52f..8faf6c27b56686a3ccd4a724cbad1b64d47cd45e 100644
--- a/ocs/.gitignore
+++ b/ocs/.gitignore
@@ -19,3 +19,27 @@ Cargo.lock
# IDE
.idea
+
+site/node_modules/
+site/docs/.vuepress/.cache/
+site/docs/.vuepress/.temp/
+site/docs/.vuepress/dist/
+# npm
+site/package-lock.json
+site/node_modules
+site/yarn-error.log
+
+# vscode
+site/.vscode
+
+#yarn
+site/yarn.lock
+
+
+# 百度链接推送
+site/urls.txt
+
+# mac
+site/.DS_Store
+
+site/vdoing
\ No newline at end of file
diff --git a/ocs/Cargo.toml b/ocs/Cargo.toml
index b8a14a416e02d843f6f6529463daeea68d087e06..5bdbf700699b6201a43e8080850f628e43125100 100644
--- a/ocs/Cargo.toml
+++ b/ocs/Cargo.toml
@@ -6,3 +6,14 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+axum = "0.4"
+config = "0.11"
+dotenv = "0.15"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["full"] }
+tower-http = { version = "0.2.5", features = ["fs", "trace"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+rust-embed = "6.3"
+mime_guess = "2"
\ No newline at end of file
diff --git a/ocs/README.md b/ocs/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..78d81d35c102784b0f52af68d99764d1376882cd
--- /dev/null
+++ b/ocs/README.md
@@ -0,0 +1,31 @@
+# openEuler Cheat Sheet
+
+## 运行
+
+前端需要 `yarn install` 添加依赖包
+然后可以使用 `yarn dev` 进行预览
+
+需要使用 `yarn build` 进行前端构建
+然后在后端运行 `cargo run`
+后端构建需要使用 `cargo build --release`
+
+## 贡献软件包
+
+在 `site/docs/02.软件包/`目录下创建 markdown 文件。
+文件名为 `序号.xxx.md` xxx 为软件包名
+
+可以用 h2 标题创建一个 描述。
+
+这里我们用到一个组件 ,需要提供操作系统的名称,和安装命令。
+
+```md
+
+```
+
+参考 `01.sudo.md` 文件
+
+## 文档开发
+
+使用 vuepress 构建和 vdoing 主题。参考 vuepress markdown 和 vdoing 书写规范。侧边栏需要修改 config.js。
+
+在更新文档后运行 `yarn build` 构建前端界面
diff --git a/ocs/debian/Dockerfile b/ocs/debian/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..09fc02ee6aa1411c371003f38b6151250e4bf580
--- /dev/null
+++ b/ocs/debian/Dockerfile
@@ -0,0 +1,45 @@
+FROM rust:1.60-buster as builder
+
+RUN USER=root cargo new --bin ocs
+WORKDIR ./ocs
+COPY ./Cargo.toml ./Cargo.toml
+COPY ./.env ./.env
+COPY ./debian/config /usr/local/cargo
+
+RUN CARGO_HTTP_MULTIPLEXING=false cargo fetch
+
+RUN cargo build --release \
+ && rm src/*.rs target/release/deps/ocs*
+
+ADD . ./
+
+RUN cargo build --release
+
+
+FROM debian:buster-slim
+
+ARG APP=/usr/src/app
+
+RUN apt-get update \
+ && apt-get install -y ca-certificates tzdata \
+ && rm -rf /var/lib/apt/lists/*
+
+EXPOSE 8080
+
+ENV TZ=Etc/UTC \
+ APP_USER=appuser
+
+RUN groupadd $APP_USER \
+ && useradd -g $APP_USER $APP_USER \
+ && mkdir -p ${APP}
+
+COPY --from=builder /ocs/target/release/ocs ${APP}/ocs
+COPY --from=builder ocs/.env ${APP}/.env
+
+
+RUN chown -R $APP_USER:$APP_USER ${APP}
+
+USER $APP_USER
+WORKDIR ${APP}
+
+CMD ["./ocs"]
\ No newline at end of file
diff --git a/ocs/debian/config b/ocs/debian/config
new file mode 100644
index 0000000000000000000000000000000000000000..401c3cefcdc460fb593238f22077bf598445c251
--- /dev/null
+++ b/ocs/debian/config
@@ -0,0 +1,5 @@
+[source.crates-io]
+replace-with = 'tuna'
+
+[source.tuna]
+registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
diff --git a/ocs/netlify.toml b/ocs/netlify.toml
new file mode 100644
index 0000000000000000000000000000000000000000..1a30e7cee61a5b970f9f3cedd532ca67df147077
--- /dev/null
+++ b/ocs/netlify.toml
@@ -0,0 +1,4 @@
+[build]
+base = "site"
+publish = "site/docs/.vuepress/public"
+command = "yarn docs:build"
diff --git a/ocs/openeuler/Dockerfile b/ocs/openeuler/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..e537e19bff63f73ccdf3128c342c0bb01f52ff27
--- /dev/null
+++ b/ocs/openeuler/Dockerfile
@@ -0,0 +1,45 @@
+FROM rust:1.60-buster as builder
+
+RUN USER=root cargo new --bin ocs
+WORKDIR ./ocs
+COPY ./Cargo.toml ./Cargo.toml
+COPY ./.env ./.env
+COPY ./openeuler/config /usr/local/cargo
+
+RUN CARGO_HTTP_MULTIPLEXING=false cargo fetch
+
+RUN cargo build --release \
+ && rm src/*.rs target/release/deps/ocs*
+
+ADD . ./
+
+RUN cargo build --release
+
+
+FROM openeuler/openeuler:22.03-lts
+
+ARG APP=/usr/src/app
+
+RUN dnf update \
+ && dnf install -y ca-certificates tzdata \
+ && rm -rf /var/lib/apt/lists/*
+
+EXPOSE 8080
+
+ENV TZ=Etc/UTC \
+ APP_USER=appuser
+
+RUN groupadd $APP_USER \
+ && useradd -g $APP_USER $APP_USER \
+ && mkdir -p ${APP}
+
+COPY --from=builder /ocs/target/release/ocs ${APP}/ocs
+COPY --from=builder ocs/.env ${APP}/.env
+
+
+RUN chown -R $APP_USER:$APP_USER ${APP}
+
+USER $APP_USER
+WORKDIR ${APP}
+
+CMD ["./ocs"]
\ No newline at end of file
diff --git a/ocs/openeuler/config b/ocs/openeuler/config
new file mode 100644
index 0000000000000000000000000000000000000000..401c3cefcdc460fb593238f22077bf598445c251
--- /dev/null
+++ b/ocs/openeuler/config
@@ -0,0 +1,5 @@
+[source.crates-io]
+replace-with = 'tuna'
+
+[source.tuna]
+registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
diff --git a/ocs/screenshot/screenshot01.png b/ocs/screenshot/screenshot01.png
new file mode 100644
index 0000000000000000000000000000000000000000..84e6960150c9dba3745cecd778fdc9c77c6a2d5e
Binary files /dev/null and b/ocs/screenshot/screenshot01.png differ
diff --git a/ocs/screenshot/screenshot02.png b/ocs/screenshot/screenshot02.png
new file mode 100644
index 0000000000000000000000000000000000000000..33b727f52da80e153f0b851df6ca7ef6aa11509b
Binary files /dev/null and b/ocs/screenshot/screenshot02.png differ
diff --git a/ocs/screenshot/screenshot03.png b/ocs/screenshot/screenshot03.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0bb50ec33ed3bb121fda903e2122f50ab0b33d3
Binary files /dev/null and b/ocs/screenshot/screenshot03.png differ
diff --git a/ocs/site b/ocs/site
new file mode 160000
index 0000000000000000000000000000000000000000..2cbd5d01e13d557225c1f89a51053e0ccb3a1723
--- /dev/null
+++ b/ocs/site
@@ -0,0 +1 @@
+Subproject commit 2cbd5d01e13d557225c1f89a51053e0ccb3a1723
diff --git a/ocs/src/config.rs b/ocs/src/config.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bac115b74f75e8157c2e906c6ad0f2c25a0460ee
--- /dev/null
+++ b/ocs/src/config.rs
@@ -0,0 +1,27 @@
+//! 配置文件
+
+use serde::Deserialize;
+
+/// Web配置
+#[derive(Deserialize)]
+pub struct WebConfig {
+ /// Web服务监听地址
+ pub addr: String,
+}
+
+#[derive(Deserialize)]
+pub struct Config {
+ /// Web配置
+ pub web: WebConfig,
+}
+
+impl Config {
+ /// 从环境变量中初始化配置
+ pub fn from_env() -> Result {
+ let mut cfg = config::Config::new();
+ // 尝试合并环境变量设置
+ cfg.merge(config::Environment::new())?;
+ // 转换成我们自己的Config对象
+ cfg.try_into()
+ }
+}
diff --git a/ocs/src/error.rs b/ocs/src/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..386c310ee343ff3419f236f8c1130ee18aec7c58
--- /dev/null
+++ b/ocs/src/error.rs
@@ -0,0 +1,77 @@
+//! 自定义错误
+use crate::response::Response;
+use axum::{response::IntoResponse, Json};
+use std::fmt::Display;
+
+/// 错误的类型
+#[derive(Debug)]
+pub enum AppErrorType {
+ /// 数据库错误
+ DbError,
+ /// 未找到
+ NotFound,
+}
+
+/// 应用错误
+#[derive(Debug)]
+pub struct AppError {
+ /// 错误信息
+ pub message: Option,
+ /// 错误原因(上一级的错误)
+ pub cause: Option,
+ /// 错误类型
+ pub error_type: AppErrorType,
+}
+
+impl AppError {
+ /// 错误代码
+ fn code(&self) -> i32 {
+ match self.error_type {
+ AppErrorType::DbError => 1,
+ AppErrorType::NotFound => 2,
+ }
+ }
+ /// 从上级错误中创建应用错误
+ fn from_err(err: impl ToString, error_type: AppErrorType) -> Self {
+ Self {
+ message: None,
+ cause: Some(err.to_string()),
+ error_type,
+ }
+ }
+ /// 从字符串创建应用错误
+ fn from_str(msg: &str, error_type: AppErrorType) -> Self {
+ Self {
+ message: Some(msg.to_string()),
+ cause: None,
+ error_type,
+ }
+ }
+ /// 数据库错误
+ pub fn db_error(err: impl ToString) -> Self {
+ Self::from_err(err, AppErrorType::DbError)
+ }
+ /// 未找到
+ pub fn not_found() -> Self {
+ Self::from_str("不存在的记录", AppErrorType::NotFound)
+ }
+}
+impl std::error::Error for AppError {}
+impl Display for AppError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+/// 实现 IntoResponse
+impl IntoResponse for AppError {
+ fn into_response(self) -> axum::response::Response {
+ let code = (&self).code();
+ let msg = match self.message {
+ Some(msg) => msg,
+ None => "有错误发生".to_string(),
+ };
+ let res: Response<()> = Response::err(code, msg);
+ Json(res).into_response()
+ }
+}
diff --git a/ocs/src/handler/mod.rs b/ocs/src/handler/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..41345dc9ec11afb72acbc4ef0a6aeb116c3cdbe4
--- /dev/null
+++ b/ocs/src/handler/mod.rs
@@ -0,0 +1,2 @@
+mod staticHandler;
+pub use staticHandler::*;
diff --git a/ocs/src/handler/staticHandler.rs b/ocs/src/handler/staticHandler.rs
new file mode 100644
index 0000000000000000000000000000000000000000..05d465e97a9665e5313b483ec0db5fb436530ff0
--- /dev/null
+++ b/ocs/src/handler/staticHandler.rs
@@ -0,0 +1,44 @@
+use axum::{http::{Uri, Response, header, StatusCode}, response::IntoResponse, body::{boxed, Full}};
+use rust_embed::RustEmbed;
+#[derive(RustEmbed)]
+#[folder = "site/docs/.vuepress/dist"]
+struct Asset;
+struct StaticFile(pub T);
+impl IntoResponse for StaticFile
+where
+ T: Into,
+{
+ fn into_response(self) -> axum::response::Response {
+ let path = self.0.into();
+ match Asset::get(path.as_str()) {
+ Some(content) => {
+ let body = boxed(Full::from(content.data));
+ let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream();
+ Response::builder()
+ .header(header::CONTENT_TYPE, mime.as_ref())
+ .body(body)
+ .unwrap()
+ }
+ None => Response::builder()
+ .status(StatusCode::NOT_FOUND)
+ .body(boxed(Full::from(format!("{} not found", path))))
+ .unwrap(),
+ }
+ }
+}
+
+
+
+pub async fn static_handler(uri: Uri) -> impl IntoResponse {
+ let path = uri.path().trim_start_matches('/').to_string();
+ StaticFile(path)
+}
+
+pub async fn index_handler() -> impl IntoResponse {
+ static_handler("/index.html".parse().unwrap()).await
+}
+
+pub async fn handler(pkgName: &str) -> impl IntoResponse {
+ let path = format!("/{}.html", pkgName);
+ StaticFile(path)
+}
diff --git a/ocs/src/main.rs b/ocs/src/main.rs
index e7a11a969c037e00a796aafeff6258501ec15e9a..6d12b72d19cadbe364cca0e4b79a639b8e5c68e7 100644
--- a/ocs/src/main.rs
+++ b/ocs/src/main.rs
@@ -1,3 +1,83 @@
-fn main() {
- println!("Hello, world!");
+mod config;
+mod error;
+mod handler;
+mod response;
+use crate::{
+ config::Config,
+ handler::{index_handler, static_handler},
+};
+use axum::{
+ handler::Handler,
+ http::StatusCode,
+ routing::{get, get_service},
+ Router,
+};
+use dotenv::dotenv;
+use tokio::signal;
+use tower_http::{services::ServeDir, trace::TraceLayer};
+use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt};
+
+/// 定义自己的 Result
+type Result = std::result::Result;
+
+#[tokio::main]
+async fn main() {
+ // 初始化日志
+ tracing_subscriber::registry()
+ .with(tracing_subscriber::EnvFilter::new(
+ std::env::var("RUST_LOG")
+ .unwrap_or_else(|_| "openEuler-cheat-sheet=debug,tower_http=debug".into()),
+ ))
+ .with(tracing_subscriber::fmt::layer())
+ .init();
+
+ dotenv().ok(); // 解析 .env 文件
+ let cfg = Config::from_env().expect("初始化项目配置失败");
+
+ let app = Router::new()
+ .route("/", get(index_handler))
+ .nest(
+ "/static",
+ get_service(ServeDir::new("../front/public")).handle_error(|err| async move {
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("处理静态资源出错: {:?}", err),
+ )
+ }),
+ )
+ .fallback(static_handler.into_service())
+ .layer(TraceLayer::new_for_http());
+
+ let addr = cfg.web.addr;
+ tracing::info!("服务器监听于:{}", addr);
+ axum::Server::bind(&addr.parse().unwrap())
+ .serve(app.into_make_service())
+ .with_graceful_shutdown(shutdown_signal())
+ .await
+ .unwrap();
}
+
+async fn shutdown_signal() {
+ let ctrl_c = async {
+ signal::ctrl_c()
+ .await
+ .expect("failed to install Ctrl+C handler");
+ };
+
+ #[cfg(unix)]
+ let terminate = async {
+ signal::unix::signal(signal::unix::SignalKind::terminate())
+ .expect("failed to install signal handler")
+ .recv()
+ .await;
+ };
+
+ #[cfg(not(unix))]
+ let terminate = std::future::pending::<()>();
+
+ tokio::select! {
+ _ = ctrl_c => {},
+ _ = terminate => {},
+ }
+ println!("signal received, starting graceful shutdown");
+}
\ No newline at end of file
diff --git a/ocs/src/response.rs b/ocs/src/response.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9b4ac018d92fde6be307eab9cfee35c2d44e48ff
--- /dev/null
+++ b/ocs/src/response.rs
@@ -0,0 +1,24 @@
+//! 自定义响应
+use serde::Serialize;
+
+#[derive(Serialize)]
+pub struct Response {
+ pub code: i32,
+ pub msg: String,
+ pub data: Option,
+}
+
+impl Response
+where
+ T: Serialize,
+{
+ pub fn new(code: i32, msg: String, data: Option) -> Self {
+ Self { code, msg, data }
+ }
+ pub fn ok(data: T) -> Self {
+ Self::new(0, "OK".to_string(), Some(data))
+ }
+ pub fn err(code: i32, msg: String) -> Self {
+ Self::new(code, msg, None)
+ }
+}
\ No newline at end of file