Skip to content

Commit

Permalink
feat: http api for upload/download
Browse files Browse the repository at this point in the history
  • Loading branch information
cupen committed Nov 24, 2024
1 parent 47694f9 commit f8b2ac9
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 113 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Release

permissions:
contents: write

on:
push:
tags:
- v[0-9]+.*
workflow_dispatch:
inputs:
tag:
description: 'Tag name'
required: true
default: 'v0.0.0rc'

env:
CARGO_TERM_COLOR: always

jobs:
# create-release:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: taiki-e/create-gh-release-action@v1
# with:
# changelog: CHANGELOG.md
# token: ${{ secrets.GITHUB_TOKEN }}

upload-assets:
# needs: create-release
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: taiki-e/upload-rust-binary-action@v1
with:
bin: fake-cdn
target: ${{ matrix.target }}
tar: unix
zip: windows
token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ target/
.venv/
__pycache__/
*.html
*.css
*.js
*.tar.gz


# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
Expand Down
14 changes: 9 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ name = "fake-cdn"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true
# [lib]
# proc-macro = true

[dependencies]
actix-files = "0.6.6"
actix-multipart = "0.7.2"
actix-web = "4.9.0"
clap = "4.5.18"
clap = "4.5.20"
colog = "1.3.0"
flate2 = "1.0.34"
lazy_static = "1.5.0"
log = "0.4.22"
# rocket = "0.5.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.122"
serde_json = "1.0.132"
structopt = "0.3.26"
tokio = { version = "1.40.0", features= ["fs"] }
tar = "0.4.42"
tokio = { version = "1.41.1", features= ["fs"] }
toml = "0.8.19"
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
Fake CDN

# Features
* Upload files
* Download files
* Upload / Download files
* Serve static site

# Dependencies
Expand Down
37 changes: 31 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
use serde::{Serialize};
use structopt::StructOpt;

use std::sync::OnceLock;


#[derive(Debug, StructOpt)]
#[structopt(name = "args")]
pub struct Args {
#[structopt(subcommand)]
pub command: Command,

#[structopt(short, long)]
pub verbose: bool,
// #[structopt(short, long)]
// pub verbose: bool,
}

#[derive(StructOpt, Debug)]
pub enum Command {
Web {
#[structopt(long)]
#[structopt(long, env="FAKECDN_LISTEN", default_value="127.0.0.1:9527")]
listen: String,

#[structopt(long, env="FAKECDN_DIR", default_value=".uploads")]
dir: String,

#[structopt(long, env="FAKECDN_TOKEN", default_value="")]
token: String,
},
}

pub fn get_args() -> &'static Args {
static ARGS: OnceLock<Args> = OnceLock::new();
return ARGS.get_or_init(|| parse_args());
}


pub fn get_args_token() -> &'static String {
let args = get_args();
match &args.command {
Command::Web { token, .. } => return token,
}
}

pub fn parse_args() -> Args {
return Args::from_args()
}
return Args::from_args();
}

// pub fn parse_args() -> &'static Mutex<Args> {
// // return Args::from_args()
// static ARGS: OnceLock<Mutex<Args>> = OnceLock::new();
// return ARGS.get_or_init(|| Mutex::new(Args::from_args()))
// }
15 changes: 15 additions & 0 deletions src/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
use flate2::read::GzDecoder;
use tar::Archive;

pub(crate) fn uncompress_tgz(path: &PathBuf, dest: &Path) -> Result<(), std::io::Error> {
// let path = "archive.tar.gz";
let tar_gz = File::open(path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(dest)?;

Ok(())
}
Empty file removed src/lib.rs
Empty file.
175 changes: 109 additions & 66 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
use colog;
use log::{debug, error, info};
use std::io::Read;
use std::net::SocketAddr;
use std::path::Path;
use std::path::PathBuf;

mod cli;
use cli::Command;

mod files;

// use lazy_static::lazy_static;
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use actix_web::{
get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder,
};
// use actix_web::http::header::{HeaderMap, HeaderValue};
use actix_files::Files;
use tokio::fs;
Expand All @@ -16,6 +23,7 @@ use actix_multipart::form::{tempfile::TempFile, MultipartForm};
// use actix_multipart::Multipart;
// use serde::Deserialize;
use std::fmt::Debug;
use serde_json::json;

const UPLOAD_DIR: &str = ".uploads";
// const listen: &str = "127.0.0.1:9527";
Expand All @@ -26,96 +34,131 @@ struct UploadForm {
file: TempFile,
}

// #[post("/")]
// async fn upload(req: HttpRequest) -> impl Responder {
// println!("http upload: {:?}", req);
// let path = req.path();
// if path.contains("..") {
// return HttpResponse::BadRequest().body("Bad path")
// }
// let full_path = format!("{}/{}", upload_dir, path);
// // File::create(full_path).await.expect("Unable to create file");
// fs::write(full_path, req.body()).await.expect("Unable to write file");
// return HttpResponse::Ok().body("File created");
// }


#[post("/{path:.*}")]
async fn upload(args: web::Path<(String,)>, MultipartForm(mut form): MultipartForm<UploadForm>) -> impl Responder {
async fn upload(
args: web::Path<(String,)>,
req: HttpRequest,
MultipartForm(mut form): MultipartForm<UploadForm>,
) -> impl Responder {
let fpath = args.into_inner().0;
if fpath.contains("..") {
return HttpResponse::BadRequest().body("Bad path");
}
let fname = form.file.file_name.unwrap();
println!("[upload] path:{} fname:{} size:{}", fpath, fname, form.file.size);
let full_path = PathBuf::from(UPLOAD_DIR).join(fpath);
debug!("[upload] {:?}", form);
let token = req.headers().get("Authorization");
match token {
Some(t) => {
info!("[upload] token: ...");
let token = cli::get_args_token();
if token.eq(t.to_str().unwrap()) {
info!("[upload] token: ok");
} else {
info!("[upload] token: invalid");
return HttpResponse::Unauthorized().body("Unauthorized");
}
}
None => {
info!("[upload] no token");
return HttpResponse::Unauthorized().body("Unauthorized");
}
}
info!("[upload] {} size: {}", fpath, form.file.size);
let full_path = PathBuf::from(UPLOAD_DIR).join(fpath.clone());

let dpath = full_path.parent().unwrap();
if !dpath.exists() {
fs::create_dir_all(dpath).await.expect("Unable to create dir");
fs::create_dir_all(dpath)
.await
.expect("Unable to create dir");
}

if cfg!(windows) {
let buf :&mut Vec<u8> = &mut Vec::new();
// let data = form.file.file.as_file();
let fp = form.file.file.as_file_mut();
let a = fp.read_to_end(buf).unwrap();
// let a = fp.read_to_end(buf).unwrap();
if a == 0 {
// if cfg!(windows) {
let buf: &mut Vec<u8> = &mut Vec::new();
// let data = form.file.file.as_file();
let fp = form.file.file.as_file_mut();
match fp.read_to_end(buf) {
Ok(s) => {
info!("[upload] => saved: {} {} bytes", full_path.display(), s);
fs::write(full_path.clone(), buf)
.await
.expect("Unable to write file");


let fname = form.file.file_name.unwrap();
if fname.contains(".") {
fname.split(".").last().unwrap();
let mut ext = Path::new(&fname)
.extension()
.and_then(|s| s.to_str())
.unwrap();
if fname.ends_with(".tar.gz") || fname.ends_with(".tgz") {
ext = "tar.gz";
}
match ext {
"tar.gz" => {
info!("[upload] {} is tar.gz", full_path.display());
let full_dir = full_path.parent().unwrap();
files::uncompress_tgz(&full_path, full_dir).expect("Unable to uncompress");
}
"zip" => {
info!("[upload] {} is zip", fpath);
}
"html" => {
info!("[upload] {} is html", fpath);
}
_ => {
info!("[upload] {} is {}", fpath, ext);
}
}
}
}

_ => {
error!("[upload] => save failed: {}", full_path.display());
return HttpResponse::BadRequest().body("Bad file");
}
fs::write(full_path, buf).await.expect("Unable to write file");
} else if cfg!(unix) {
form.file.file.persist(full_path).unwrap();
}
return HttpResponse::Ok().body("");
}


#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
println!("[echo] {:?}", req_body);
HttpResponse::Ok().body(req_body)
}

#[get("/status")]
async fn status() -> impl Responder {
println!("[status] ok");
HttpResponse::Ok()
info!("[status] ok");
HttpResponse::Ok().json(json!({
"status": "ok",
"version": "0.1.0"
}))
}


#[actix_web::main]
async fn main() -> std::io::Result<()> {
let args = cli::parse_args();
match args.command {
Command::Web { listen } => {
let lis = listen.parse::<SocketAddr>().expect("Invalid listen address");
println!("Listening on: {}", lis);
HttpServer::new(|| {
App::new()
.service(echo)
.service(status)
.service(upload)
.service(
Files::new("/" , UPLOAD_DIR)
colog::init();

let args = cli::get_args();
match &args.command {
Command::Web { listen, dir, token:_ } => {
let addr = listen
.parse::<SocketAddr>()
.expect("Invalid listen address");
println!("Listening on: {}", addr);
// let upload_dir = dir.clone();
let mut upload_dir = PathBuf::from(dir.clone());
if dir.eq("") {
upload_dir = PathBuf::from(UPLOAD_DIR);
}
if !upload_dir.exists() {
error!("--dir {} doesn't exists", upload_dir.display());
}
HttpServer::new(move || {
App::new().service(status).service(upload).service(
Files::new("/", &upload_dir)
.prefer_utf8(true)
.show_files_listing())
.show_files_listing(),
)
})
.bind(lis)?
.bind(addr)?
.run()
.await
},
_ => {
panic!("Invalid command");
// HttpServer::new(|| {
// App::new()
// .service(echo)
// .service(status)
// .service(upload)
// .service(Files::new("/" , UPLOAD_DIR).prefer_utf8(true))
// })
// .run()
// .await
}
}
}
Loading

0 comments on commit f8b2ac9

Please sign in to comment.