diff --git a/Cargo.lock b/Cargo.lock index 41e07ec..97675fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2025,6 +2025,7 @@ dependencies = [ "tower 0.5.1", "tracing", "tracing-subscriber", + "uuid", "walkdir", ] @@ -2731,6 +2732,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index fd812dc..3be1dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ tokio-util = { version = "0.7", features = ["io", "io-util"] } toml = "0.8" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.11.0", features = ["v4"] } walkdir = "2" [dependencies.abscissa_core] diff --git a/maskfile.md b/maskfile.md index 8d96b67..25af371 100644 --- a/maskfile.md +++ b/maskfile.md @@ -328,9 +328,11 @@ export RESTIC_REPOSITORY=rest:http://127.0.0.1:8000/ci_repo export RESTIC_PASSWORD=restic export RESTIC_REST_USERNAME=restic export RESTIC_REST_PASSWORD=restic - restic init -restic backup src +restic backup tests/fixtures/test_data/test_repo_source +restic backup tests/fixtures/test_data/test_repo_source +restic check +restic forget --keep-last 1 --prune ``` PowerShell: @@ -340,8 +342,11 @@ $env:RESTIC_REPOSITORY = "rest:http://127.0.0.1:8000/ci_repo"; $env:RESTIC_PASSWORD = "restic"; $env:RESTIC_REST_USERNAME = "restic"; $env:RESTIC_REST_PASSWORD = "restic"; -# restic init +restic init +restic backup tests/fixtures/test_data/test_repo_source restic backup tests/fixtures/test_data/test_repo_source +restic check +restic forget --keep-last 1 --prune ``` ## test-server @@ -385,5 +390,21 @@ PowerShell: PowerShell: ```powershell -watchexec --stop-signal "CTRL+C" -r -w src -- "cargo run -- serve -c tests/fixtures/test_data/rustic_server.toml -v" +watchexec --stop-signal "CTRL+C" -r -w src -w tests -- "cargo run -- serve -c tests/fixtures/test_data/rustic_server.toml -v" +``` + +## hurl + +> Run a hurl test against the server + +Bash: + +```bash +hurl -i tests/fixtures/hurl/endpoints.hurl +``` + +PowerShell: + +```powershell +hurl -i tests/fixtures/hurl/endpoints.hurl ``` diff --git a/src/auth.rs b/src/auth.rs index 0a110a3..29d92c8 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -85,7 +85,7 @@ impl FromRequestParts for AuthFromRequest { let auth_result = AuthBasic::from_request_parts(parts, state).await; - tracing::debug!(?auth_result, "[auth]"); + tracing::debug!(?auth_result, "[AUTH]"); return match auth_result { Ok(auth) => { diff --git a/src/handlers/file_config.rs b/src/handlers/file_config.rs index de4c475..5cc0f99 100644 --- a/src/handlers/file_config.rs +++ b/src/handlers/file_config.rs @@ -101,6 +101,7 @@ pub(crate) async fn add_config( /// delete_config /// Interface: DELETE {repo}/config +#[allow(dead_code)] pub(crate) async fn delete_config( path: P, auth: AuthFromRequest, diff --git a/src/handlers/file_length.rs b/src/handlers/file_length.rs index c04620e..12b7b68 100644 --- a/src/handlers/file_length.rs +++ b/src/handlers/file_length.rs @@ -67,8 +67,6 @@ pub(crate) async fn file_length( #[cfg(test)] mod test { - use std::path::PathBuf; - use axum::{ http::{header, Method, StatusCode}, middleware, Router, diff --git a/src/handlers/files_list.rs b/src/handlers/files_list.rs index 6cdad35..d6ba917 100644 --- a/src/handlers/files_list.rs +++ b/src/handlers/files_list.rs @@ -143,8 +143,6 @@ pub(crate) async fn list_files( #[cfg(test)] mod test { - use std::path::PathBuf; - use axum::{ body::Body, http::{ diff --git a/src/handlers/repository.rs b/src/handlers/repository.rs index eb91ffe..ed9337d 100644 --- a/src/handlers/repository.rs +++ b/src/handlers/repository.rs @@ -95,7 +95,6 @@ mod test { }; use axum::{middleware, Router}; use axum_extra::routing::RouterExt; - use std::env; use std::path::PathBuf; use tokio::fs; use tower::ServiceExt; diff --git a/src/log.rs b/src/log.rs index ceba5e0..6f7976f 100644 --- a/src/log.rs +++ b/src/log.rs @@ -6,7 +6,6 @@ use axum::{ }; use axum_macros::debug_middleware; use http_body_util::BodyExt; -use tracing::Instrument; use crate::error::ApiErrorKind; @@ -18,7 +17,7 @@ use crate::error::ApiErrorKind; /// /// ```rust /// use axum::Router; -/// +/// /// app = Router::new() /// .layer(middleware::from_fn(print_request_response)) /// ``` @@ -28,41 +27,38 @@ pub async fn print_request_response( next: Next, ) -> Result { let (parts, body) = req.into_parts(); + let uuid = uuid::Uuid::new_v4(); - let span = tracing::span!( - tracing::Level::DEBUG, - "request", + tracing::debug!( + id = %uuid, method = %parts.method, uri = %parts.uri, + "[REQUEST]", ); - let _enter = span.enter(); - - tracing::debug!(headers = ?parts.headers, "[new request]"); + tracing::debug!(id = %uuid, headers = ?parts.headers, "[HEADERS]"); - let bytes = buffer_and_print(body).instrument(span.clone()).await?; + let bytes = buffer_and_print(&uuid, body).await?; let req = Request::from_parts(parts, Body::from(bytes)); - let res = next.run(req).instrument(span.clone()).await; + let res = next.run(req).await; let (parts, body) = res.into_parts(); - let span = tracing::span!( - tracing::Level::DEBUG, - "response", + tracing::debug!( + id = %uuid, headers = ?parts.headers, status = %parts.status, + "[RESPONSE]", ); - let _enter = span.enter(); - - let bytes = buffer_and_print(body).instrument(span.clone()).await?; + let bytes = buffer_and_print(&uuid, body).await?; let res = Response::from_parts(parts, Body::from(bytes)); Ok(res) } -async fn buffer_and_print(body: B) -> Result +async fn buffer_and_print(uuid: &uuid::Uuid, body: B) -> Result where B: axum::body::HttpBody, B::Error: std::fmt::Display, @@ -77,7 +73,7 @@ where }; if let Ok(body) = std::str::from_utf8(&bytes) { - tracing::debug!(body = %body); + tracing::debug!(id = %uuid, body = %body, "[BODY]"); } Ok(bytes) diff --git a/src/typed_path.rs b/src/typed_path.rs index 758699d..1e2dc99 100644 --- a/src/typed_path.rs +++ b/src/typed_path.rs @@ -67,9 +67,9 @@ impl PathParts for RepositoryConfigPath { } } -// A type safe route with `"/:repo"` as its associated path. +// A type safe route with `"/:repo/"` as its associated path. #[derive(TypedPath, Deserialize, Debug)] -#[typed_path("/:repo")] +#[typed_path("/:repo/")] pub struct RepositoryPath { pub repo: String, } @@ -93,9 +93,9 @@ impl PathParts for TpePath { } } -// A type safe route with `"/:repo/:tpe"` as its associated path. +// A type safe route with `"/:repo/:tpe/"` as its associated path. #[derive(TypedPath, Deserialize, Debug)] -#[typed_path("/:repo/:tpe")] +#[typed_path("/:repo/:tpe/")] pub struct RepositoryTpePath { pub repo: String, pub tpe: TpeKind, diff --git a/src/web.rs b/src/web.rs index 69ac6b3..e5c592a 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,10 +1,7 @@ use std::net::SocketAddr; -use axum::{ - middleware, - routing::{delete, get, head, post}, - Router, -}; +use axum::{middleware, Router}; +use axum_extra::routing::RouterExt; use axum_server::tls_rustls::RustlsConfig; use tokio::net::TcpListener; use tracing::{info, level_filters::LevelFilter}; @@ -23,31 +20,13 @@ use crate::{ }, log::print_request_response, storage::{init_storage, Storage}, - typed_path::{RepositoryTpeNamePath, RepositoryTpePath, TpeNamePath, TpePath}, + typed_path::{RepositoryTpeNamePath, RepositoryTpePath}, }; use crate::{ config::TlsSettings, typed_path::{RepositoryConfigPath, RepositoryPath}, }; -pub(crate) mod constants { - // TPE_CONFIG is defined, but outside the types[] array. - // This allows us to loop over the types[] when generating "routes" - - pub(crate) const TPE_DATA: &str = "data"; - - pub(crate) const TPE_KEYS: &str = "keys"; - - pub(crate) const TPE_LOCKS: &str = "locks"; - - pub(crate) const TPE_SNAPSHOTS: &str = "snapshots"; - - pub(crate) const TPE_INDEX: &str = "index"; - - pub(crate) const _TPE_CONFIG: &str = "config"; - - pub(crate) const TYPES: [&str; 5] = [TPE_DATA, TPE_KEYS, TPE_LOCKS, TPE_SNAPSHOTS, TPE_INDEX]; -} /// Start the web server /// /// # Arguments @@ -71,59 +50,119 @@ pub async fn start_web_server( init_auth(auth)?; init_storage(storage)?; - // ------------------------------------- - // Create routing structure - // ------------------------------------- let mut app = Router::new(); - // /:repo/config + // /:repo/:tpe/:name app = app - .route("/:repo/config", head(has_config)) - .route("/:repo/config", post(add_config::)) - .route("/:repo/config", get(get_config::)) - .route( - "/:repo/config", - delete(delete_config::), - ); - - // /:tpe --> note: NO trailing slash - // we loop here over explicit types, to prevent the conflict with paths "/:repo/" - for tpe in constants::TYPES.into_iter() { - let path = format!("/{}", &tpe); - app = app.route(path.as_str(), get(list_files::)); - } + // Returns “200 OK” if the blob with the given name and type is stored in the repository, + // “404 not found” otherwise. If the blob exists, the HTTP header Content-Length + // is set to the file size. + .typed_head(file_length::) + // Returns the content of the blob with the given name and type if it is stored + // in the repository, “404 not found” otherwise. + // If the request specifies a partial read with a Range header field, then the + // status code of the response is 206 instead of 200 and the response only contains + // the specified range. + // + // Response format: binary/octet-stream + .typed_get(get_file::) + // Saves the content of the request body as a blob with the given name and type, + // an HTTP error otherwise. + // + // Request format: binary/octet-stream + .typed_post(add_file::) + // Returns “200 OK” if the blob with the given name and type has been deleted from + // the repository, an HTTP error otherwise. + .typed_delete(delete_file::); - // /:repo/ --> note: trailing slash + // /:repo/config app = app - .route("/:repo/", post(create_repository::)) - .route("/:repo/", delete(delete_repository::)); - - // /:tpe/:name - // we loop here over explicit types, to prevent conflict with paths "/:repo/:tpe" - for tpe in constants::TYPES.into_iter() { - let path = format!("/{}:name", &tpe); - app = app - .route(path.as_str(), head(file_length::)) - .route(path.as_str(), get(get_file::)) - .route(path.as_str(), post(add_file::)) - .route(path.as_str(), delete(delete_file::)); - } + // Returns “200 OK” if the repository has a configuration, an HTTP error otherwise. + .typed_head(has_config) + // Returns the content of the configuration file if the repository has a configuration, + // an HTTP error otherwise. + // + // Response format: binary/octet-stream + .typed_get(get_config::) + // Returns “200 OK” if the configuration of the request body has been saved, + // an HTTP error otherwise. + .typed_post(add_config::) + // Returns “200 OK” if the configuration of the repository has been deleted, + // an HTTP error otherwise. + // Note: This is not part of the API documentation, but it is implemented + // to allow for the deletion of the configuration file during testing. + .typed_delete(delete_config::); + + // /:repo/:tpe/ + // # API version 1 + // + // Returns a JSON array containing the names of all the blobs stored for a given type, example: + // + // ```json + // [ + // "245bc4c430d393f74fbe7b13325e30dbde9fb0745e50caad57c446c93d20096b", + // "85b420239efa1132c41cea0065452a40ebc20c6f8e0b132a5b2f5848360973ec", + // "8e2006bb5931a520f3c7009fe278d1ebb87eb72c3ff92a50c30e90f1b8cf3e60", + // "e75c8c407ea31ba399ab4109f28dd18c4c68303d8d86cc275432820c42ce3649" + // ] + // ``` + // + // # API version 2 + // + // Returns a JSON array containing an object for each file of the given type. + // The objects have two keys: name for the file name, and size for the size in bytes. + // + // [ + // { + // "name": "245bc4c430d393f74fbe7b13325e30dbde9fb0745e50caad57c446c93d20096b", + // "size": 2341058 + // }, + // { + // "name": "85b420239efa1132c41cea0065452a40ebc20c6f8e0b132a5b2f5848360973ec", + // "size": 2908900 + // }, + // { + // "name": "8e2006bb5931a520f3c7009fe278d1ebb87eb72c3ff92a50c30e90f1b8cf3e60", + // "size": 3030712 + // }, + // { + // "name": "e75c8c407ea31ba399ab4109f28dd18c4c68303d8d86cc275432820c42ce3649", + // "size": 2804 + // } + // ] + app = app.typed_get(list_files::); - // /:repo/:tpe - app = app.route("/:repo/:tpe", get(list_files::)); - - // /:repo/:tpe/:name + // /:repo/ --> note: trailing slash app = app - .route( - "/:repo/:tpe/:name", - head(file_length::), - ) - .route("/:repo/:tpe/:name", get(get_file::)) - .route("/:repo/:tpe/:name", post(add_file::)) - .route( - "/:repo/:tpe/:name", - delete(delete_file::), - ); + // This request is used to initially create a new repository. + // The server responds with “200 OK” if the repository structure was created + // successfully or already exists, otherwise an error is returned. + .typed_post(create_repository::) + // Deletes the repository on the server side. The server responds with “200 OK” + // if the repository was successfully removed. If this function is not implemented + // the server returns “501 Not Implemented”, if this it is denied by the server it + // returns “403 Forbidden”. + .typed_delete(delete_repository::); + + // TODO: This is not reflected in the API documentation? + // TODO: Decide if we want to keep this or not! + // // /:tpe/:name + // // we loop here over explicit types, to prevent conflict with paths "/:repo/:tpe" + // for tpe in constants::TYPES.into_iter() { + // let path = format!("/{}:name", &tpe); + // app = app + // .route(path.as_str(), head(file_length::)) + // .route(path.as_str(), get(get_file::)) + // .route(path.as_str(), post(add_file::)) + // .route(path.as_str(), delete(delete_file::)); + // } + // + // /:tpe --> note: NO trailing slash + // we loop here over explicit types, to prevent the conflict with paths "/:repo/" + // for tpe in constants::TYPES.into_iter() { + // let path = format!("/{}", &tpe); + // app = app.route(path.as_str(), get(list_files::)); + // } // Extra logging requested. Handlers will log too // TODO: Use LogSettings here, this should be set from the cli by `--log` diff --git a/tests/endpoints.hurl b/tests/endpoints.hurl deleted file mode 100644 index bc502bf..0000000 --- a/tests/endpoints.hurl +++ /dev/null @@ -1,27 +0,0 @@ -# No auth -HEAD http://127.0.0.1:8000/ci_repo/config -HTTP 403 - -# Login -HEAD http://127.0.0.1:8000/ci_repo/config -[BasicAuth] -restic: restic -HTTP 200 - -# Access to keys -GET http://127.0.0.1:8000/ci_repo/keys -[BasicAuth] -restic: restic -HTTP 200 -Content-Type: application/vnd.x.restic.rest.v1 - -GET http://127.0.0.1:8000/ci_repo/keys/92c489eaca950f3a9d860da1bef0af845dd8c74a9a34c860bd8e6a54bd2326d2 -RANGE: bytes=0-230 -[BasicAuth] -restic: restic -HTTP 206 - -# GET http://127.0.0.1:8000/ci_repo/keys/92c489eaca950f3a9d860da1bef0af845dd8c74a9a34c860bd8e6a54bd2326d2 -# [BasicAuth] -# restic: restic -# HTTP 200 \ No newline at end of file diff --git a/tests/fixtures/hurl/endpoints.hurl b/tests/fixtures/hurl/endpoints.hurl new file mode 100644 index 0000000..27a59da --- /dev/null +++ b/tests/fixtures/hurl/endpoints.hurl @@ -0,0 +1,83 @@ +# No auth +HEAD http://127.0.0.1:8000/ci_repo/config +HTTP 403 + +# Login +HEAD http://127.0.0.1:8000/ci_repo/config +[BasicAuth] +hurl: hurl +HTTP 200 + +# Access to keys +GET http://127.0.0.1:8000/ci_repo/keys/ +[BasicAuth] +hurl: hurl +HTTP 200 +Content-Type: application/vnd.x.restic.rest.v1 + +GET http://127.0.0.1:8000/ci_repo/keys/92c489eaca950f3a9d860da1bef0af845dd8c74a9a34c860bd8e6a54bd2326d2 +RANGE: bytes=0-230 +[BasicAuth] +hurl: hurl +HTTP 206 +content-length: 231 + +GET http://127.0.0.1:8000/ci_repo/keys/92c489eaca950f3a9d860da1bef0af845dd8c74a9a34c860bd8e6a54bd2326d2 +[BasicAuth] +hurl: hurl +HTTP 200 + +GET http://127.0.0.1:8000/ci_repo/config +[BasicAuth] +hurl: hurl +HTTP 200 + +GET http://127.0.0.1:8000/ci_repo/locks/ +[BasicAuth] +hurl: hurl +HTTP 200 + +POST http://127.0.0.1:8000/ci_repo/locks/ac4ff62472b009cf71c81199f4fc635152639909cb1143911150db467ca86544 +[BasicAuth] +hurl: hurl +HTTP 200 + +GET http://127.0.0.1:8000/ci_repo/locks/ +[BasicAuth] +hurl: hurl +HTTP 200 + +GET http://127.0.0.1:8000/ci_repo/snapshots/ +[BasicAuth] +hurl: hurl +HTTP 200 + +GET http://127.0.0.1:8000/ci_repo/index/ +[BasicAuth] +hurl: hurl +HTTP 200 + +POST http://127.0.0.1:8000/ci_repo/data/3b013253cd72fa7e98f9dcd6106f9565933556f1c80a720e1e44dbf3b57af446 +[BasicAuth] +hurl: hurl +HTTP 200 + +POST http://127.0.0.1:8000/ci_repo/data/89923722810777f3026a7cf9b246eb9613c7e3f64e77e6ccecb4001774c38acf +[BasicAuth] +hurl: hurl +HTTP 200 + +POST http://127.0.0.1:8000/ci_repo/index/004ebe81e7927131b6dde40bd4595ebf95a355bf26509de83fb9d12b4ab280b4 +[BasicAuth] +hurl: hurl +HTTP 200 + +POST http://127.0.0.1:8000/ci_repo/snapshots/ddd75013f2d2470d910adadf728c5c8cd6e91cb591bdfbf1cfd7f3af7e32c7eb +[BasicAuth] +hurl: hurl +HTTP 200 + +DELETE http://127.0.0.1:8000/ci_repo/locks/ac4ff62472b009cf71c81199f4fc635152639909cb1143911150db467ca86544 +[BasicAuth] +hurl: hurl +HTTP 200 diff --git a/tests/fixtures/test_data/.htpasswd b/tests/fixtures/test_data/.htpasswd index 46076f9..512c419 100644 --- a/tests/fixtures/test_data/.htpasswd +++ b/tests/fixtures/test_data/.htpasswd @@ -1 +1,2 @@ restic:$2y$05$iKXd4X4AKOpBPufMhlSfwOQqrl/nu1A9yAFbKYG742cJz325qeB/a +hurl:$2y$05$63hF2CpYPDYuM3Jlm04hH.TYIxGo6nk1eFjVBHd06X7LLRcTFyMz2 diff --git a/tests/fixtures/test_data/acl.toml b/tests/fixtures/test_data/acl.toml index c6584e0..735fd7b 100644 --- a/tests/fixtures/test_data/acl.toml +++ b/tests/fixtures/test_data/acl.toml @@ -1,5 +1,6 @@ [test_repo] restic = "Append" +hurl = "Append" [repo_remove_me] restic = "Modify" @@ -9,3 +10,4 @@ restic = "Modify" [ci_repo] restic = "Modify" +hurl = "Modify"