From a811f7750afb80a9f37547d9d80c85b1341b1410 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Sat, 29 Mar 2025 23:54:55 +0100 Subject: [PATCH] WIP: v1.0.0 --- README.md | 12 +++++- migrations/20250321201214_initial.sql | 20 ++++++---- src/config.rs | 3 +- src/http_api.rs | 53 +++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 114ea62..921adfb 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - outgoing webhooks + ## Upload steps - client to app: request to upload something, returns `{upload_id}` @@ -34,4 +35,13 @@ - client to caos: `PATCH /uploads/{upload_id}` with upload data, optionally using tus - app to caos: `GET /staging-area/{upload_id}`, returns metadata (including `{hash}`) as soon as the upload is complete -- app to caos: `POST /staging-area/{upload_id}/accept` with target bucket IDs \ No newline at end of file +- app to caos: `POST /staging-area/{upload_id}/accept` with target bucket IDs + +## Roadmap + +- basic uploading +- upload expiration +- media type detection +- metadata endpoints +- accepting uploads +- more storage backends \ No newline at end of file diff --git a/migrations/20250321201214_initial.sql b/migrations/20250321201214_initial.sql index 755a283..73765fa 100644 --- a/migrations/20250321201214_initial.sql +++ b/migrations/20250321201214_initial.sql @@ -1,8 +1,8 @@ create table objects ( - hash text not null, + hash text not null, -- BLAKE3, 265 bits, base 16 size integer not null, -- in bytes - media_type text not null, + media_type text not null, -- RFC 6838 format creation_date text not null, -- RFC 3339 format primary key (hash) ) without rowid, strict; @@ -16,13 +16,19 @@ create table object_replicas foreign key (hash) references objects (hash) on delete restrict on update restrict ) strict; -create table uploads +create table ongoing_uploads ( id text not null, current_size integer not null, -- in bytes total_size integer, -- in bytes, or null if the upload was not started yet + primary key (id) +) without rowid, strict; - hash text, - primary key (id), - foreign key (hash) references objects (hash) on delete restrict on update restrict -) \ No newline at end of file +create table finished_uploads +( + id text not null, + size integer not null, -- in bytes + hash text not null, -- BLAKE3, 265 bits, base 16 + media_type text not null, -- RFC 6838 format + primary key (id) +) without rowid, strict; \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index b9b440a..4f12e3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -42,7 +42,8 @@ fn validate_buckets(buckets: &Vec) -> Result<(), ValidationError> Ok(()) } -static BUCKET_ID_PATTERN: Lazy = Lazy::new(|| Regex::new(r"\w*$").unwrap()); +// a-zA-z0-9 and _, but not "staging" +static BUCKET_ID_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^(?!staging$)\w*$").unwrap()); #[derive(Debug, Serialize, Deserialize, Validate)] pub struct ConfigBucket { diff --git a/src/http_api.rs b/src/http_api.rs index e653624..73883d5 100644 --- a/src/http_api.rs +++ b/src/http_api.rs @@ -6,9 +6,12 @@ use rocket::form::validate::Len; use rocket::http::Status; use rocket::outcome::Outcome::Success; use rocket::request::{FromRequest, Outcome}; -use rocket::{Request, State, post, routes}; -use serde::Deserialize; +use rocket::response::Responder; +use rocket::serde::json::Json; +use rocket::{Request, State, post, response, routes}; +use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; +use std::borrow::Cow; pub async fn start_http_api_server(config: &Config, database: SqlitePool) -> Result<()> { let rocket_app = rocket::custom(rocket::config::Config { @@ -39,22 +42,56 @@ pub async fn start_http_api_server(config: &Config, database: SqlitePool) -> Res Ok(()) } -#[derive(Debug, Deserialize)] -struct CreateUploadRequest {} +#[derive(Debug)] +enum ApiError { + BodyValidationFailed { + path: Cow<'static, str>, + message: Cow<'static, str>, + }, +} -#[post("/uploads")] -async fn create_upload(_accessor: AuthorizedApiAccessor, database: &State) { - let total_size = 20; +impl<'r> Responder<'r, 'static> for ApiError { + fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { + todo!() + } +} + +#[derive(Debug, Deserialize)] +struct CreateUploadRequest { + size: u64, +} + +#[derive(Debug, Serialize)] +struct CreateUploadResponse { + upload_id: String, +} + +#[post("/uploads", data = "")] +async fn create_upload( + _accessor: AuthorizedApiAccessor, + database: &State, + request: Json, +) -> Result, ApiError> { let id = nanoid!(); + let total_size: i64 = request + .size + .try_into() + .map_err(|_| ApiError::BodyValidationFailed { + path: "size".into(), + message: "".into(), + })?; + sqlx::query!( - "INSERT INTO uploads (id, current_size, total_size, hash) VALUES(?, 0, ?, null)", + "INSERT INTO ongoing_uploads (id, total_size, current_size) VALUES(?, ?, 0)", id, total_size ) .execute(database.inner()) .await .unwrap(); + + Ok(Json(CreateUploadResponse { upload_id: id })) } struct CorrectApiSecret(FStr<64>);