init
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/.idea
|
39
.gitlab-ci.yml
Normal file
39
.gitlab-ci.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
stages:
|
||||||
|
- generate_image
|
||||||
|
- deploy
|
||||||
|
- notify
|
||||||
|
|
||||||
|
variables:
|
||||||
|
BUILD_NAME: "rust/storage"
|
||||||
|
SERVICE_NAME: "storage"
|
||||||
|
IMAGE_TAG: "dev"
|
||||||
|
|
||||||
|
generate_image_dev:
|
||||||
|
stage: generate_image
|
||||||
|
image: docker:20.10.2
|
||||||
|
script:
|
||||||
|
- docker build -t $BUILD_NAME:$IMAGE_TAG .
|
||||||
|
tags:
|
||||||
|
- '001'
|
||||||
|
only:
|
||||||
|
- dev
|
||||||
|
except:
|
||||||
|
- triggers
|
||||||
|
|
||||||
|
deploy_dev:
|
||||||
|
|
||||||
|
stage: deploy
|
||||||
|
image: docker:20.10.2
|
||||||
|
script:
|
||||||
|
- CONTAINER_NAME=$(docker ps -aq --filter name=$SERVICE_NAME)
|
||||||
|
- echo $CONTAINER_NAME
|
||||||
|
- if [[ -n "$CONTAINER_NAME" ]]; then
|
||||||
|
docker rm -f $CONTAINER_NAME;
|
||||||
|
fi
|
||||||
|
- docker run -d --name $SERVICE_NAME $BUILD_NAME:$IMAGE_TAG
|
||||||
|
tags:
|
||||||
|
- '001'
|
||||||
|
only:
|
||||||
|
- dev
|
||||||
|
except:
|
||||||
|
- triggers
|
2198
Cargo.lock
generated
Normal file
2198
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "web"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytes = "1.5.0"
|
||||||
|
actix-web = "4.4.1"
|
||||||
|
actix-cors = "0.7.0"
|
||||||
|
rust-s3 ={version = "0.33.0" }
|
||||||
|
tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
serde = "1.0.195"
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM rust:1.75 as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN USER=root cargo new storage
|
||||||
|
WORKDIR /app/storage
|
||||||
|
RUN mkdir -p .cargo
|
||||||
|
COPY config.toml .cargo/
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
COPY ./src src
|
||||||
|
RUN cargo install --path . --color always
|
||||||
|
|
||||||
|
# Copy the app to an base Docker image, here we use distroless
|
||||||
|
FROM ubuntu:latest
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y ca-certificates
|
||||||
|
RUN update-ca-certificates
|
||||||
|
RUN mkdir -p /cache
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/storage /storage
|
||||||
|
USER 1000
|
||||||
|
ENTRYPOINT ["/storage"]
|
14
config.toml
Normal file
14
config.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[http]
|
||||||
|
check-revoke = false
|
||||||
|
|
||||||
|
[source.crates-io]
|
||||||
|
replace-with='rsproxy'
|
||||||
|
|
||||||
|
[source.rsproxy]
|
||||||
|
registry="https://rsproxy.cn/crates.io-index"
|
||||||
|
|
||||||
|
[registries.rsproxy]
|
||||||
|
index = "https://rsproxy.cn/crates.io-index"
|
||||||
|
|
||||||
|
[net]
|
||||||
|
git-fetch-with-cli = true
|
135
src/main.rs
Normal file
135
src/main.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use std::env;
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{App, HttpServer, Responder, get, put, post, delete, web, CustomizeResponder};
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::web::Json;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
mod mods;
|
||||||
|
|
||||||
|
use mods::MinioPrivate;
|
||||||
|
use crate::mods::FileManager;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
error_code: u16,
|
||||||
|
error_message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn json_response(status_code: u16) -> CustomizeResponder<Json<Response>> {
|
||||||
|
let message = match status_code {
|
||||||
|
200 => { "Success!" }
|
||||||
|
201 => { "Create success!" }
|
||||||
|
204 => { "Delete success!" }
|
||||||
|
205 => { "Update success!" }
|
||||||
|
404 => { "Not exists!" }
|
||||||
|
409 => { "Resource conflict!" }
|
||||||
|
500 => { "Server error!" }
|
||||||
|
_ => { "Server error!" }
|
||||||
|
};
|
||||||
|
let resp = Response { error_code: status_code, error_message: message.to_string() };
|
||||||
|
Json(resp)
|
||||||
|
.customize()
|
||||||
|
.with_status(StatusCode::from_u16(status_code).unwrap())
|
||||||
|
.insert_header(("content-type", "application/json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_response(file_content: Bytes, mut file_type: String) -> CustomizeResponder<Bytes> {
|
||||||
|
if file_type == "" {
|
||||||
|
file_type = "application/octet-stream".to_string();
|
||||||
|
}
|
||||||
|
return if file_content.is_empty() {
|
||||||
|
Bytes::from_static(b"{\"error_code\": 404,\"error_message\": \"Not exists!\"}")
|
||||||
|
.customize()
|
||||||
|
.with_status(StatusCode::from_u16(404).unwrap())
|
||||||
|
.insert_header(("content-type", "application/json"))
|
||||||
|
} else {
|
||||||
|
file_content
|
||||||
|
.customize()
|
||||||
|
.with_status(StatusCode::from_u16(200).unwrap())
|
||||||
|
.insert_header(("content-type", file_type))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_minio() -> MinioPrivate {
|
||||||
|
let region = env::var("MINIO_REGION").unwrap_or("".to_string());
|
||||||
|
let endpoint = env::var("MINIO_ENDPOINT").unwrap_or("http://192.168.1.100:9000".to_string());
|
||||||
|
let access_key = env::var("MINIO_ACCESS_KEY").unwrap_or("sTkzoRR9lQUmI5Hp".to_string());
|
||||||
|
let secret_key = env::var("MINIO_SECRET_KEY").unwrap_or("oDUG9GRlkNv3V76bVGjovp9228bP25WC".to_string());
|
||||||
|
return MinioPrivate {
|
||||||
|
region,
|
||||||
|
endpoint,
|
||||||
|
access_key,
|
||||||
|
secret_key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[get("/healthy")]
|
||||||
|
async fn healthy() -> impl Responder {
|
||||||
|
"Ok!".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/{bucket_name}/{file_path:.*}")]
|
||||||
|
async fn create_file(path: web::Path<(String, String)>, body: web::Payload) -> impl Responder {
|
||||||
|
let minio = get_minio().await;
|
||||||
|
let path_parameters = path.into_inner();
|
||||||
|
let work_dir = path_parameters.0;
|
||||||
|
let path_name = path_parameters.1;
|
||||||
|
let file_content = body.to_bytes().await.unwrap();
|
||||||
|
let status_code = minio.create(work_dir.as_str(), path_name.as_str(), file_content).await;
|
||||||
|
json_response(status_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/{bucket_name}/{file_path:.*}")]
|
||||||
|
async fn update_file(path: web::Path<(String, String)>, body: web::Payload) -> impl Responder {
|
||||||
|
let minio = get_minio().await;
|
||||||
|
let path_parameters = path.into_inner();
|
||||||
|
let work_dir = path_parameters.0;
|
||||||
|
let path_name = path_parameters.1;
|
||||||
|
let file_content = body.to_bytes().await.unwrap();
|
||||||
|
let status_code = minio.update(work_dir.as_str(), path_name.as_str(), file_content).await;
|
||||||
|
json_response(status_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/{bucket_name}/{file_path:.*}")]
|
||||||
|
async fn get_file(path: web::Path<(String, String)>) -> impl Responder {
|
||||||
|
let minio = get_minio().await;
|
||||||
|
let path_parameters = path.into_inner();
|
||||||
|
let work_dir = path_parameters.0;
|
||||||
|
let path_name = path_parameters.1;
|
||||||
|
|
||||||
|
let result = minio.get(work_dir.as_str(), path_name.as_str()).await;
|
||||||
|
bytes_response(result, "".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/{bucket_name}/{file_path:.*}")]
|
||||||
|
async fn delete_file(path: web::Path<(String, String)>) -> impl Responder {
|
||||||
|
let minio = get_minio().await;
|
||||||
|
let path_parameters = path.into_inner();
|
||||||
|
let work_dir = path_parameters.0;
|
||||||
|
let path_name = path_parameters.1;
|
||||||
|
let status_code = minio.delete(work_dir.as_str(), path_name.as_str()).await;
|
||||||
|
json_response(status_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
HttpServer::new(|| {
|
||||||
|
let cors = Cors::permissive();
|
||||||
|
App::new()
|
||||||
|
.wrap(cors)
|
||||||
|
.service(healthy)
|
||||||
|
.service(get_file)
|
||||||
|
.service(create_file)
|
||||||
|
.service(update_file)
|
||||||
|
.service(delete_file)
|
||||||
|
})
|
||||||
|
.bind(("0.0.0.0", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
86
src/mods.rs
Normal file
86
src/mods.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use bytes::Bytes;
|
||||||
|
use s3::bucket::Bucket;
|
||||||
|
use s3::region::Region;
|
||||||
|
use s3::creds::Credentials;
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
|
||||||
|
pub trait FileManager {
|
||||||
|
async fn exists(&self, work_dir: &str, file_path: &str) -> u16;
|
||||||
|
async fn get(&self, work_dir: &str, file_path: &str) -> Bytes;
|
||||||
|
async fn create(&self, work_dir: &str, file_path: &str, file_content: Bytes) -> u16;
|
||||||
|
async fn update(&self, work_dir: &str, file_path: &str, file_content: Bytes) -> u16;
|
||||||
|
|
||||||
|
async fn delete(&self, work_dir: &str, file_path: &str) -> u16;
|
||||||
|
async fn _bucket(&self, bucket_name: &str) -> Bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MinioPrivate {
|
||||||
|
pub region: String,
|
||||||
|
pub endpoint: String,
|
||||||
|
pub access_key:String,
|
||||||
|
pub secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileManager for MinioPrivate {
|
||||||
|
async fn exists(&self, work_dir: &str, file_path: &str) -> u16 {
|
||||||
|
let bucket = self._bucket(work_dir).await;
|
||||||
|
return match bucket.head_object(file_path).await {
|
||||||
|
Ok(_) => { 200 }
|
||||||
|
Err(_) => { 404 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async fn get(&self, work_dir: &str, file_path: &str) -> Bytes {
|
||||||
|
let bucket = self._bucket(work_dir).await;
|
||||||
|
return match bucket.get_object(file_path).await {
|
||||||
|
Ok(value) => {
|
||||||
|
Bytes::from(value.to_vec())
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
Bytes::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, work_dir: &str, file_path: &str, file_content: Bytes) -> u16 {
|
||||||
|
if self.exists(work_dir, file_path).await == 200 {
|
||||||
|
return 409;
|
||||||
|
}
|
||||||
|
let bucket = self._bucket(work_dir).await;
|
||||||
|
return match bucket.put_object(file_path, file_content.as_ref()).await {
|
||||||
|
Ok(_) => { 201 }
|
||||||
|
Err(_) => { 500 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async fn update(&self, work_dir: &str, file_path: &str, file_content: Bytes) -> u16 {
|
||||||
|
if self.exists(work_dir, file_path).await == 404 {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
let bucket = self._bucket(work_dir).await;
|
||||||
|
|
||||||
|
return match bucket.put_object(file_path, file_content.as_ref()).await {
|
||||||
|
Ok(_) => { 205 }
|
||||||
|
Err(_) => { 500 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, work_dir: &str, file_path: &str) -> u16 {
|
||||||
|
if self.exists(work_dir, file_path).await == 404 {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
let bucket = self._bucket(work_dir).await;
|
||||||
|
return match bucket.delete_object(file_path).await {
|
||||||
|
Ok(_) => { 204 }
|
||||||
|
Err(_) => { 500 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _bucket(&self, bucket_name: &str) -> Bucket {
|
||||||
|
let region = Region::Custom { region: self.region.clone().into(), endpoint: self.endpoint.clone().into() };
|
||||||
|
let credentials = Credentials::new(Some(self.access_key.as_str()), Some(self.secret_key.as_str()), None, None, None).unwrap();
|
||||||
|
let mut bucket = Bucket::new(bucket_name, region, credentials).unwrap().with_path_style();
|
||||||
|
bucket.set_request_timeout(Option::from(Duration::new(60, 0)));
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user