Construindo uma AWS: S3
Olá a todos, hoje venho mostrar a todos sobre meu novo projeto, estou tentando recriar a AWS do zero (exatamente), a infra por trás da AWS, orquestradores, clusters, HA e por aí vai, e inclusive serviços como EC2, S3, VPC e por aí.
Já publiquei um artigo sobre no TabNews caso alguém tenha interesse sobre.
Bom não vou estender muito, vamos ao que interessa, a criação de um S3, bom como comecei agora o projeto, o S3 que estou fazendo ainda é um simples upload e download de arquivos nada mais, sem restrição(porque ainda não fiz o equivalente a IAM), mas já é algo muito grandioso para mim porque estou estudando Rust, então o servidor web foi inteiro escrito nele, e com esse artigo vou tentar passar minha experiência com ele(que já vou adiantar que foi animal).
Quando você é programador JavaScript (ou qualquer linguagem high level) e vai para um Rust da vida, não vou mentir, assusta um pouco por causa da sintaxe, porém, como um bom brasileiro que sou, não desisto nunca, então acabei desenvolvendo esse webserver simples.
Para desenvolver isso, usei o ActixRS que é um framework web (equivalente ao express só que com mais ecossistema próprio), é uma lib. Rust bem simples, mas super poderosa, não tem muito o que falar dela, caso queira saber mais, só visitar ActixRS que é a documentação oficial deles, lá tem bastante exemplo, vai ajudar muito.
Bom, vamos lá, o S3 comparado aos outros recursos, ele é um dos mais simples. Aqui não vou me estender sobre a parte de ACLs nem nada disso, até porque, ainda, nem banco de dados tem para armazenar isso. Vou mostrar o código que fiz e o principal, a organização das pastas (e isso tem que levar MUITO em conta quando se fala de Rust).
Rust tem uma organização de pastas baseadas em mod, (módulos) onde cada arquivo é um módulo à parte, exemplo.
Dentro da pasta Soure, tem dois arquivos, o main.rs e count.rs. No count tem uma função chamada contar. Para que eu possa usar essa função dentro da main.rs(o entrypoint da aplicação), eu preciso declarar no escopo do arquivo main.rs.
mod count;
use count::contar;
fn main() {
contar();
}
Então você usa o “mod” count; para especificar ao compilador Rust que você quer importar o módulo count. Logo abaixo tem o use count::contar que é equivalente ao import { contar } from './count';
no TS/JS, por exemplo.
Mas por que estou dizendo isso? Bom, como disse para quem vem do TS/JS e está acostumado a usar o import export isso é MUITO desafiador, vou dar o exemplo do meu projeto e como está a estrutura de pasta.
Bom, como podem ver, são muitas pastas e arquivos, agora como pensei para organizar isso. A resposta é simples: NestJS, sim, eu me baseei no framework NestJS que utiliza uma estrutura onde cada pasta é um serviço do aplicativo e dentro tem os arquivos necessario para funcionar aquele serviço como: upload.module.ts
, upload.controller.ts
, upload.service.ts
.
Mas na minha estrutura tem dois arquivos a mais em uploads/, sendo eles o router.rs
e o mod.rs
, o que são cada um?
Bom, é de se imaginar que o router faça toda a parte de roteamento do controller, define rotas, os métodos de cada rota e tudo mais. Porém, tem um arquivo chamado mod.rs
, que é um arquivo equivalente ao index{.ts, js}, por exemplo, que é um arquivo padrão chamado como exportador de módulos, e o que isso quer dizer? Como expliquei, você tem que definir para o compilador Rust quais são os módulos externos, e expliquei que você pode usar o {nome do mod}.rs, porém tem outro jeito: você colocar o mod.rs
em uma pasta que essa pasta será o nome do módulo, no caso upload. Essa foi a melhor forma que encontrei para deixar o código organizado e limpo, onde fica 100% definido de quem é quem.
Agora vamos falar sobre como iniciar um projeto com ActixRS, e é bem simples.
mod upload;
use actix_web::{web, App, HttpServer, Responder};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.configure(upload::router::UploadRouter::new)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Aqui tem o código do meu main.rs, onde inicio a função main com uma macro do Actix onde espero um retorno de Result (que é basicamente um retorno que espera 2 parâmetros, um de sucesso e outro de erro, mas no nosso caso estamos só passando um parâmetro vazio), depois criamos um servidor web e iniciamos um App novo, fazendo novamente um paralelo com o express, seria algo mais ou menos assim.
import express from 'express';
import http from 'http';
const app = express();
const server = http.createServer(app);
server.listen(8080);
Então, o seu servidor web já está pronto, porém tem .configure ali que está depois que crio uma nova instância de app, o que isso quer dizer? Bom, vamos vooooltar para JS de novo.
Esse .configure é o equivalente um .use, só que um pouco menos genérico. Com ele, você vai configurar um serviço para fazer configurações específicas para sua aplicação web (vou mostrar mais para frente).
No .configure tem um parâmetro que é a execução do uma nova instância de rota para upload, vamos ver como está no router.rs
.
use crate::upload::controller::UploadController;
use actix_web::web;
pub struct UploadRouter;
impl UploadRouter {
pub fn new(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/upload")
.route("/{bucket}", web::post().to(UploadController::create))
.route(
"/{bucket}/{file:.*}",
web::get().to(UploadController::index),
),
);
}
}
No router.rs
está assim, onde criamos uma struct chamada UploadRouter(não vou estender sobre struct, impl e trait, quem quiser saber mais é só acessar aqui).
Então definimos as rotas e os métodos que vão ser executados com forme for causando o match, vou mostrar o que quer dizer isso no JS.
import { Router } from 'express';
import { UserController } from './user.controller';
export class UploadRouter {
private router: Router;
private controller: UserController;
constructor() {
this.router = Router();
this.controller = new UserController();
}
bootstrap() {
this.router.get("/upload/:bucket", this.controller.index as RequestHandler);
this.router.post("/upload/:bucket/:file", this.controller.create as RequestHandler);
return this.router;
}
}
Pronto, basicamente é isso aí, criamos um router que faz todo o controle das requisições. Sem muito segredo, agora no UserController ficou assim.
use crate::upload::dto::Upload;
use crate::upload::service::UploadService;
use actix_multipart::form::MultipartForm;
use actix_web::{web, Responder};
use actix_files as fs;
pub struct UploadController;
impl UploadController {
pub async fn index(
params: web::Path<(String, String)>,
) -> Result<fs::NamedFile, actix_web::Error> {
UploadService::download(params.0.to_string(), params.1.to_string()).await
}
pub async fn create(
MultipartForm(form): MultipartForm<Upload>,
bucket: web::Path<String>,
) -> Result<impl Responder, actix_web::Error> {
Ok(UploadService::upload(bucket, actix_multipart::form::MultipartForm(form)).await)
}
}
Aqui já o negócio começa a ficar doido, criamos uma struct e passamos duas funções para ela, uma chamada index e outra create. A index recebe um parâmetro chamado params do tipo web::Path<(String, String)>, que é o path params que vem da URL e assim repassamos para o UploadService. O path vem em formato de tupla, porque são dois parâmetros, então é assim que pegamos ambos os parâmetros.
Já para o create utilizamos dois parâmetros na função, um é um parâmetro para pegar o path e o outro para pegar o MultipartForm para podermos enviar as imagens para o servidor e salvar em pasta.
O Actix para enviar imagem, também depende de uma lib externa também (porém é própria deles), chamado actix_files que é o equivalente ao multer. Agora vamos ver como a imagem é salva no servidor.
use crate::upload::dto::{Upload, UploadResponse};
use actix_files as fs;
use actix_multipart::form::MultipartForm;
use actix_web::{
http::{
header::{ContentDisposition, DispositionType},
Error,
},
web, HttpResponse, Responder,
};
pub struct UploadService;
impl UploadService {
pub async fn download(
bucket: String,
file: String,
) -> Result<fs::NamedFile, actix_web::error::Error> {
let path = format!(
"{}/temp/{}/{}",
std::env::current_dir().unwrap().display(),
bucket,
file
);
if !std::path::Path::new(&path).exists() {
return Err(actix_web::error::ErrorNotFound("File not found"));
}
let file = fs::NamedFile::open(path).unwrap();
Ok(file
.use_last_modified(true)
.set_content_disposition(ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![],
}))
}
pub async fn upload(
bucket: web::Path<String>,
MultipartForm(form): MultipartForm<Upload>,
) -> Result<impl Responder, Error> {
let path = format!(
"{}/temp/{}",
std::env::current_dir().unwrap().display(),
bucket
);
std::fs::create_dir_all(path).unwrap();
for f in form.files {
let path = format!(
"{}/temp/{}/{}",
std::env::current_dir().unwrap().display(),
bucket,
f.file_name.unwrap()
);
f.file.persist(path.clone()).unwrap();
}
Ok(HttpResponse::Ok().json(UploadResponse { success: true }))
}
}
Na struct UploadService, passamos duas funções, a de upload e a de download. A de upload é tranquila, recebemos dois parâmetros, um é o bucket que será salvo e outro o arquivo em si. O código define o path utilizando a macro format para formatar o texto e sobrescrever em tempo de compilação os {} por dados que desejamos que seja passado como argumento da função em ordem.
Logo em seguida, criamos a pasta do bucket e posteriormente pegamos todos os arquivos enviados ao servidor e os salvamos lá na path. E está pronto o sorvetinho, já conseguimos salvar as imagens no servidor :).
Agora, para fazer o download dela é mais fácil ainda, na função recebemos dois parâmetros, onde um é o bucket e o outro o nome do arquivo em si, definimos novamente o path com o format, depois verificamos se o arquivo e o diretório existem, se não retornamos um erro, se existir, baixaremos.
Pronto, esse é meu super mega blaster mini S3, obviamente vou aprimorar ainda mais ele com ACLs, banco de dados (estou em dúvidas entre o Redis ou etcd).
Esse meu projeto está servindo para estudar toda a infra da AWS e construindo a minha própria, com serviço de orquestrador de contêineres próprio, HA próprio e tudo mais, e ainda por cima construindo os principais serviços da AWS que poderá subir via GUI, obrigado a todos que leu isso e até um futuro próximo 👋.