WIP: v1.0.0
This commit is contained in:
parent
cc84c6ac37
commit
a811f7750a
4 changed files with 71 additions and 17 deletions
12
README.md
12
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
|
||||
- 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
|
|
@ -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
|
||||
)
|
||||
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;
|
|
@ -42,7 +42,8 @@ fn validate_buckets(buckets: &Vec<ConfigBucket>) -> Result<(), ValidationError>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
static BUCKET_ID_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"\w*$").unwrap());
|
||||
// a-zA-z0-9 and _, but not "staging"
|
||||
static BUCKET_ID_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(?!staging$)\w*$").unwrap());
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Validate)]
|
||||
pub struct ConfigBucket {
|
||||
|
|
|
@ -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<SqlitePool>) {
|
||||
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 = "<request>")]
|
||||
async fn create_upload(
|
||||
_accessor: AuthorizedApiAccessor,
|
||||
database: &State<SqlitePool>,
|
||||
request: Json<CreateUploadRequest>,
|
||||
) -> Result<Json<CreateUploadResponse>, 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>);
|
||||
|
|
Loading…
Add table
Reference in a new issue