diff --git a/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json b/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json index 6607c08..93c4721 100644 --- a/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json +++ b/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json @@ -22,6 +22,11 @@ "ordinal": 3, "name": "name", "type_info": "Text" + }, + { + "ordinal": 4, + "name": "is_admin", + "type_info": "Bool" } ], "parameters": { @@ -33,6 +38,7 @@ false, false, true, + false, false ] }, diff --git a/.sqlx/query-3f3a8f21e93fbce600343a1f65c85db66f9b5ca8368464612f8ffd7948dc6de0.json b/.sqlx/query-c89bcd3e9f0af08c76a37fbb2232ad993ce33040a0990333bf99f05ca526e105.json similarity index 76% rename from .sqlx/query-3f3a8f21e93fbce600343a1f65c85db66f9b5ca8368464612f8ffd7948dc6de0.json rename to .sqlx/query-c89bcd3e9f0af08c76a37fbb2232ad993ce33040a0990333bf99f05ca526e105.json index 162a6e5..7357ab6 100644 --- a/.sqlx/query-3f3a8f21e93fbce600343a1f65c85db66f9b5ca8368464612f8ffd7948dc6de0.json +++ b/.sqlx/query-c89bcd3e9f0af08c76a37fbb2232ad993ce33040a0990333bf99f05ca526e105.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM users WHERE TRUE AND ($1::int8[] IS NULL OR array_position($1, id) IS NOT NULL) AND ($2::timestamptz[] IS NULL OR array_position($2, created_at) IS NOT NULL) AND ($3::timestamptz[] IS NULL OR array_position($3, disabled_at) IS NOT NULL) AND ($4::text[] IS NULL OR array_position($4, name) IS NOT NULL)", + "query": "SELECT * FROM users WHERE TRUE AND ($1::int8[] IS NULL OR array_position($1, id) IS NOT NULL) AND ($2::timestamptz[] IS NULL OR array_position($2, created_at) IS NOT NULL) AND ($3::timestamptz[] IS NULL OR array_position($3, disabled_at) IS NOT NULL) AND ($4::text[] IS NULL OR array_position($4, name) IS NOT NULL) AND ($5::bool[] IS NULL OR array_position($5, is_admin) IS NOT NULL)", "describe": { "columns": [ { @@ -22,6 +22,11 @@ "ordinal": 3, "name": "name", "type_info": "Text" + }, + { + "ordinal": 4, + "name": "is_admin", + "type_info": "Bool" } ], "parameters": { @@ -29,15 +34,17 @@ "Int8Array", "TimestamptzArray", "TimestamptzArray", - "TextArray" + "TextArray", + "BoolArray" ] }, "nullable": [ false, false, true, + false, false ] }, - "hash": "3f3a8f21e93fbce600343a1f65c85db66f9b5ca8368464612f8ffd7948dc6de0" + "hash": "c89bcd3e9f0af08c76a37fbb2232ad993ce33040a0990333bf99f05ca526e105" } diff --git a/.sqlx/query-e4568529cfbdc9207c1ba481ae77489e756927d45b7963842215098d51bc3d0b.json b/.sqlx/query-e4568529cfbdc9207c1ba481ae77489e756927d45b7963842215098d51bc3d0b.json index 8249e46..b840cfb 100644 --- a/.sqlx/query-e4568529cfbdc9207c1ba481ae77489e756927d45b7963842215098d51bc3d0b.json +++ b/.sqlx/query-e4568529cfbdc9207c1ba481ae77489e756927d45b7963842215098d51bc3d0b.json @@ -22,6 +22,11 @@ "ordinal": 3, "name": "name", "type_info": "Text" + }, + { + "ordinal": 4, + "name": "is_admin", + "type_info": "Bool" } ], "parameters": { @@ -33,6 +38,7 @@ false, false, true, + false, false ] }, diff --git a/Cargo.lock b/Cargo.lock index 29989c7..a6e108b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,6 +387,7 @@ dependencies = [ "clusterizer-util", "dirs", "reqwest", + "sha2 0.11.0", "tempfile", "tokio", "tracing", @@ -435,6 +436,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "url", ] [[package]] diff --git a/api/src/client.rs b/api/src/client.rs index b43d38d..2ad2fa0 100644 --- a/api/src/client.rs +++ b/api/src/client.rs @@ -1,7 +1,7 @@ use clusterizer_common::{ - errors::{FetchTasksError, RegisterError, SubmitResultError}, - records::{Get, Task}, - requests::{FetchTasksRequest, RegisterRequest, SubmitResultRequest}, + errors::{CreateFileError, FetchTasksError, RegisterError, SubmitResultError}, + records::{File, Get, Task}, + requests::{CreateFileRequest, FetchTasksRequest, RegisterRequest, SubmitResultRequest}, responses::RegisterResponse, types::Id, }; @@ -56,6 +56,14 @@ impl ApiClient { Ok(()) } + pub async fn create_file( + &self, + request: &CreateFileRequest, + ) -> ApiResult, CreateFileError> { + let url = format!("{}/files", self.url); + Ok(self.send_post(url, request).await?.json().await?) + } + async fn send_post( &self, url: impl IntoUrl, diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5b0543b..1ab17e8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,6 +11,7 @@ clusterizer-common = { version = "0.1.0", path = "../common" } clusterizer-util = { version = "0.1.0", path = "../util" } dirs = "6.0.0" reqwest = { version = "0.13.2" } +sha2 = "0.11.0" tempfile = "3.27.0" tokio = { version = "1.50.0", features = ["full"] } tracing = "0.1.44" diff --git a/cli/src/args.rs b/cli/src/args.rs index 50d54d9..85362f1 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -24,6 +24,8 @@ pub enum Commands { Register(RegisterArgs), /// Start crunching on clusterizer Run(RunArgs), + /// Create a new file on the server + CreateFile(CreateFileArgs), } #[derive(Debug, Args)] @@ -52,6 +54,12 @@ impl RunArgs { } } +#[derive(Debug, Args)] +pub struct CreateFileArgs { + #[arg(long, short)] + pub url: String, +} + fn cache_dir() -> Resettable { dirs::cache_dir() .map(|path| path.join("clusterizer").into_os_string().into()) diff --git a/cli/src/main.rs b/cli/src/main.rs index e0b5599..3061fdd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,8 +2,9 @@ use args::{ClusterizerArgs, Commands}; use clap::Parser; use clusterizer_api::client::ApiClient; use clusterizer_client::result::ClientResult; -use clusterizer_common::requests::RegisterRequest; -use tracing::{debug, error}; +use clusterizer_common::requests::{CreateFileRequest, RegisterRequest}; +use sha2::{Digest, Sha256}; +use tracing::{debug, error, info}; mod args; mod client; @@ -32,6 +33,25 @@ async fn run() -> ClientResult<()> { println!("{}", response.api_key); } Commands::Run(args) => client::run(client, args).await?, + Commands::CreateFile(args) => { + debug!("Creating new file..."); + let bytes = reqwest::get(&args.url) + .await? + .error_for_status()? + .bytes() + .await?; + let hash = Sha256::digest(bytes).0; + + let response = client + .create_file(&CreateFileRequest { + url: args.url, + hash, + }) + .await?; + + println!("{}", response); + info!("Successfully created new file with ID: {}", response); + } } Ok(()) diff --git a/common/src/errors/create_file_error.rs b/common/src/errors/create_file_error.rs new file mode 100644 index 0000000..01b7d8c --- /dev/null +++ b/common/src/errors/create_file_error.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Clone, Hash, Debug, Serialize, Deserialize, Error)] +pub enum CreateFileError { + #[error("forbidden")] + Forbidden, + #[error("url is invalid")] + InvalidUrl, +} diff --git a/common/src/errors/mod.rs b/common/src/errors/mod.rs index 825a820..6050738 100644 --- a/common/src/errors/mod.rs +++ b/common/src/errors/mod.rs @@ -1,3 +1,4 @@ +pub mod create_file_error; pub mod fetch_tasks_error; pub mod infallible; pub mod not_found; @@ -6,6 +7,7 @@ pub mod submit_result_error; pub mod validate_fetch_error; pub mod validate_submit_error; +pub use create_file_error::CreateFileError; pub use fetch_tasks_error::FetchTasksError; pub use infallible::Infallible; pub use not_found::NotFound; diff --git a/common/src/records/file.rs b/common/src/records/file.rs index 93e5522..fcd312f 100644 --- a/common/src/records/file.rs +++ b/common/src/records/file.rs @@ -10,7 +10,7 @@ record_impl! { id: Id, created_at: DateTime, url: String, - hash: Vec, + hash: [u8; 32], } FileFilter { @@ -21,14 +21,14 @@ record_impl! { "$3::text[] IS NULL OR array_position($3, url) IS NOT NULL" url: Vec, "$4::bytea[] IS NULL OR array_position($4, hash) IS NOT NULL" - hash: Vec>, + hash: Vec<[u8; 32]>, } FileBuilder { "url" "$1" url: String, "hash" "$2" - hash: Vec, + hash: [u8; 32], } UpdateFile {} diff --git a/common/src/records/user.rs b/common/src/records/user.rs index dcf6620..2ef43b7 100644 --- a/common/src/records/user.rs +++ b/common/src/records/user.rs @@ -11,6 +11,7 @@ record_impl! { created_at: DateTime, disabled_at: Option>, name: String, + is_admin: bool, } UserFilter { @@ -22,6 +23,8 @@ record_impl! { disabled_at: Vec>>, "$4::text[] IS NULL OR array_position($4, name) IS NOT NULL" name: Vec, + "$5::bool[] IS NULL OR array_position($5, is_admin) IS NOT NULL" + is_admin: bool, } UserBuilder { diff --git a/common/src/requests/create_file_request.rs b/common/src/requests/create_file_request.rs new file mode 100644 index 0000000..7465dca --- /dev/null +++ b/common/src/requests/create_file_request.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Hash, Debug, Serialize, Deserialize)] +pub struct CreateFileRequest { + pub url: String, + pub hash: [u8; 32], +} diff --git a/common/src/requests/mod.rs b/common/src/requests/mod.rs index 6bbfe7e..573ce32 100644 --- a/common/src/requests/mod.rs +++ b/common/src/requests/mod.rs @@ -1,8 +1,10 @@ +pub mod create_file_request; pub mod fetch_tasks_request; pub mod register_request; pub mod submit_result_request; pub mod validate_submit_request; +pub use create_file_request::CreateFileRequest; pub use fetch_tasks_request::FetchTasksRequest; pub use register_request::RegisterRequest; pub use submit_result_request::SubmitResultRequest; diff --git a/server/Cargo.toml b/server/Cargo.toml index c180f8a..22bcad5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,3 +18,4 @@ tokio = { version = "1.50.0", features = ["full"] } tower-http = { version = "0.6.8", features = ["trace"] } tracing = "0.1.44" tracing-subscriber = "0.3.23" +url = "2.5.8" diff --git a/server/migrations/20250426220809_init.sql b/server/migrations/20250426220809_init.sql index 8da7fb6..dba7f17 100644 --- a/server/migrations/20250426220809_init.sql +++ b/server/migrations/20250426220809_init.sql @@ -2,7 +2,8 @@ CREATE TABLE users ( id int8 GENERATED ALWAYS AS IDENTITY NOT NULL PRIMARY KEY, created_at timestamptz NOT NULL DEFAULT now(), disabled_at timestamptz, - name text NOT NULL + name text NOT NULL, + is_admin boolean NOT NULL DEFAULT false ); CREATE UNIQUE INDEX users_name_key diff --git a/server/src/main.rs b/server/src/main.rs index 0e51f0d..451d977 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -55,6 +55,7 @@ async fn serve_task(state: AppState, address: String) { .route("/submit_result/{id}", post(routes::submit_result)) .route("/validate_fetch/{id}", get(routes::validate_fetch)) .route("/validate_submit", post(routes::validate_submit)) + .route("/files", post(routes::create_file)) .layer(TraceLayer::new_for_http()) .with_state(state); diff --git a/server/src/result/status.rs b/server/src/result/status.rs index a605b67..4a9ba41 100644 --- a/server/src/result/status.rs +++ b/server/src/result/status.rs @@ -1,7 +1,7 @@ use axum::http::StatusCode; use clusterizer_common::errors::{ - FetchTasksError, Infallible, NotFound, RegisterError, SubmitResultError, ValidateFetchError, - ValidateSubmitError, + CreateFileError, FetchTasksError, Infallible, NotFound, RegisterError, SubmitResultError, + ValidateFetchError, ValidateSubmitError, }; pub trait Status { @@ -55,3 +55,12 @@ impl Status for ValidateSubmitError { } } } + +impl Status for CreateFileError { + fn status(&self) -> StatusCode { + match self { + Self::Forbidden => StatusCode::FORBIDDEN, + _ => StatusCode::BAD_REQUEST, + } + } +} diff --git a/server/src/routes/create_file.rs b/server/src/routes/create_file.rs new file mode 100644 index 0000000..4a3c4e7 --- /dev/null +++ b/server/src/routes/create_file.rs @@ -0,0 +1,38 @@ +use axum::{Json, extract::State}; +use clusterizer_common::{ + errors::CreateFileError, + records::{File, FileBuilder, Insert, Select}, + requests::CreateFileRequest, + types::Id, +}; +use url::Url; + +use crate::{ + auth::Auth, + result::{AppError, AppResult}, + state::AppState, +}; + +pub async fn create_file( + State(state): State, + Auth(user_id): Auth, + Json(request): Json, +) -> AppResult>, CreateFileError> { + let user = user_id.select().fetch_one(&state.pool).await?; + + if !user.is_admin { + Err(AppError::Specific(CreateFileError::Forbidden))?; + } + + Url::parse(&request.url).map_err(|_| AppError::Specific(CreateFileError::InvalidUrl))?; + + let file_id = FileBuilder { + url: request.url, + hash: request.hash, + } + .insert() + .fetch_one(&state.pool) + .await?; + + Ok(Json(file_id)) +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 133dad7..ec92c68 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -14,12 +14,14 @@ use crate::{ state::AppState, }; +pub mod create_file; pub mod fetch_tasks; pub mod register; pub mod submit_result; pub mod validate_fetch; pub mod validate_submit; +pub use create_file::create_file; pub use fetch_tasks::fetch_tasks; pub use register::register; pub use submit_result::submit_result;