Author:
Hash:
Timestamp:
+344 -139 +/-13 browse
Kevin Schoon [me@kevinschoon.com]
0b4403d8386006dc18aede446e6656ba8c207b5e
Fri, 28 Mar 2025 21:01:39 +0000 (7 months ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index 8d37510..8f700ae 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -27,6 +27,17 @@ dependencies = [ |
| 6 | ] |
| 7 | |
| 8 | [[package]] |
| 9 | + name = "async-trait" |
| 10 | + version = "0.1.88" |
| 11 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 12 | + checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" |
| 13 | + dependencies = [ |
| 14 | + "proc-macro2", |
| 15 | + "quote", |
| 16 | + "syn", |
| 17 | + ] |
| 18 | + |
| 19 | + [[package]] |
| 20 | name = "axum" |
| 21 | version = "0.8.1" |
| 22 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 23 | @@ -420,18 +431,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 24 | checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" |
| 25 | |
| 26 | [[package]] |
| 27 | - name = "papyri-axum" |
| 28 | + name = "papyri" |
| 29 | version = "0.1.0" |
| 30 | dependencies = [ |
| 31 | + "async-trait", |
| 32 | "axum", |
| 33 | "oci-spec", |
| 34 | + "regex", |
| 35 | + "serde", |
| 36 | + "serde_json", |
| 37 | + "thiserror", |
| 38 | + "tracing", |
| 39 | ] |
| 40 | |
| 41 | [[package]] |
| 42 | - name = "papyri-common" |
| 43 | - version = "0.1.0" |
| 44 | - |
| 45 | - [[package]] |
| 46 | name = "percent-encoding" |
| 47 | version = "2.3.1" |
| 48 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 49 | @@ -730,10 +743,22 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" |
| 50 | dependencies = [ |
| 51 | "log", |
| 52 | "pin-project-lite", |
| 53 | + "tracing-attributes", |
| 54 | "tracing-core", |
| 55 | ] |
| 56 | |
| 57 | [[package]] |
| 58 | + name = "tracing-attributes" |
| 59 | + version = "0.1.28" |
| 60 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 61 | + checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" |
| 62 | + dependencies = [ |
| 63 | + "proc-macro2", |
| 64 | + "quote", |
| 65 | + "syn", |
| 66 | + ] |
| 67 | + |
| 68 | + [[package]] |
| 69 | name = "tracing-core" |
| 70 | version = "0.1.33" |
| 71 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 72 | diff --git a/Cargo.toml b/Cargo.toml |
| 73 | index 05a59c6..7a6e130 100644 |
| 74 | --- a/Cargo.toml |
| 75 | +++ b/Cargo.toml |
| 76 | @@ -1,26 +1,14 @@ |
| 77 | - [workspace] |
| 78 | - resolver = "2" |
| 79 | - members = [ |
| 80 | - "papyri-common", |
| 81 | - "papyri-axum", |
| 82 | - ] |
| 83 | + [package] |
| 84 | + name = "papyri" |
| 85 | + version = "0.1.0" |
| 86 | + edition = "2024" |
| 87 | |
| 88 | - # [workspace.dependencies] |
| 89 | - # async-trait = "0.1.86" |
| 90 | - # bytes = "1.10.0" |
| 91 | - # clap = { version = "4.5.20", features = ["derive"] } |
| 92 | - # clap_complete = { version = "4.4.10" } |
| 93 | - # serde = { version = "1.0", features = ["derive"] } |
| 94 | - # git2 = "0.19.0" |
| 95 | - # rand = "0.8.5" |
| 96 | - # thiserror = "2.0.11" |
| 97 | - # tracing = { version = "0.1.41", features=["log"] } |
| 98 | - # toml = "0.8.20" |
| 99 | - # futures = "0.3.31" |
| 100 | - # futures-util = "0.3.31" |
| 101 | - # sqlx = { version = "0.8.3", features = [ "runtime-tokio-rustls", "sqlite", "macros", "time" ] } |
| 102 | - # tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } |
| 103 | - # |
| 104 | - # tokio = { version = "1.43.0", features = ["full"] } |
| 105 | - # tokio-util = { version = "0.7.13", features = ["io", "compat"] } |
| 106 | - # tokio-stream = "0.1.17" |
| 107 | + [dependencies] |
| 108 | + axum = "0.8.1" |
| 109 | + oci-spec = "0.7.1" |
| 110 | + serde = "1.0.219" |
| 111 | + serde_json = "1.0.140" |
| 112 | + async-trait = "0.1.88" |
| 113 | + regex = "1.11.1" |
| 114 | + tracing = { version = "0.1.41", features = ["log"] } |
| 115 | + thiserror = "2.0.12" |
| 116 | diff --git a/papyri-axum/Cargo.toml b/papyri-axum/Cargo.toml |
| 117 | deleted file mode 100644 |
| 118 | index 4b83733..0000000 |
| 119 | --- a/papyri-axum/Cargo.toml |
| 120 | +++ /dev/null |
| 121 | @@ -1,8 +0,0 @@ |
| 122 | - [package] |
| 123 | - name = "papyri-axum" |
| 124 | - version = "0.1.0" |
| 125 | - edition = "2024" |
| 126 | - |
| 127 | - [dependencies] |
| 128 | - axum = "0.8.1" |
| 129 | - oci-spec = "0.7.1" |
| 130 | diff --git a/papyri-axum/src/lib.rs b/papyri-axum/src/lib.rs |
| 131 | deleted file mode 100644 |
| 132 | index 6a664ab..0000000 |
| 133 | --- a/papyri-axum/src/lib.rs |
| 134 | +++ /dev/null |
| 135 | @@ -1 +0,0 @@ |
| 136 | - pub mod routes; |
| 137 | diff --git a/papyri-axum/src/routes.rs b/papyri-axum/src/routes.rs |
| 138 | deleted file mode 100644 |
| 139 | index e5509a8..0000000 |
| 140 | --- a/papyri-axum/src/routes.rs |
| 141 | +++ /dev/null |
| 142 | @@ -1,91 +0,0 @@ |
| 143 | - use axum::Json; |
| 144 | - use axum::extract::{MatchedPath, Request}; |
| 145 | - use axum::{Router, routing::get}; |
| 146 | - use oci_spec::image::{ImageManifest, ImageIndexBuilder}; |
| 147 | - use serde_json::json; |
| 148 | - use tower_http::trace::TraceLayer; |
| 149 | - use tracing::info_span; |
| 150 | - use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; |
| 151 | - |
| 152 | - pub async fn handler_index(req: Request) -> Json<serde_json::Value> { |
| 153 | - req.headers().iter().for_each(|h| { |
| 154 | - let header_value = h.1.to_str().ok(); |
| 155 | - tracing::info!(key = h.0.as_str(), value = header_value); |
| 156 | - }); |
| 157 | - Json(json!({})) |
| 158 | - } |
| 159 | - |
| 160 | - pub async fn handler_manifest_index() -> Json<serde_json::Value> { |
| 161 | - } |
| 162 | - |
| 163 | - // async fn serve() { |
| 164 | - // let app = Router::new() |
| 165 | - // .route("/", get(|| async { "Hello, World!" })) |
| 166 | - // .route("/v2", get(handler_index)) |
| 167 | - // .route("/v2/", get(handler_index)) |
| 168 | - // .layer( |
| 169 | - // TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { |
| 170 | - // // Log the matched route's path (with placeholders not filled in). |
| 171 | - // // Use request.uri() or OriginalUri if you want the real path. |
| 172 | - // let matched_path = request |
| 173 | - // .extensions() |
| 174 | - // .get::<MatchedPath>() |
| 175 | - // .map(MatchedPath::as_str); |
| 176 | - // |
| 177 | - // let actual_path = request.uri().path(); |
| 178 | - // |
| 179 | - // info_span!( |
| 180 | - // "http_request", |
| 181 | - // method = ?request.method(), |
| 182 | - // matched_path, |
| 183 | - // actual_path, |
| 184 | - // some_other_field = tracing::field::Empty, |
| 185 | - // ) |
| 186 | - // }), // .on_request(|_request: &Request<_>, _span: &Span| { |
| 187 | - // // // You can use `_span.record("some_other_field", value)` in one of these |
| 188 | - // // // closures to attach a value to the initially empty field in the info_span |
| 189 | - // // // created above. |
| 190 | - // // }) |
| 191 | - // // .on_response(|_response: &Response, _latency: Duration, _span: &Span| { |
| 192 | - // // // ... |
| 193 | - // // }) |
| 194 | - // // .on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| { |
| 195 | - // // // ... |
| 196 | - // // }) |
| 197 | - // // .on_eos( |
| 198 | - // // |_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| { |
| 199 | - // // // ... |
| 200 | - // // }, |
| 201 | - // // ) |
| 202 | - // // .on_failure( |
| 203 | - // // |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { |
| 204 | - // // // ... |
| 205 | - // // }, |
| 206 | - // // ), |
| 207 | - // ); |
| 208 | - // |
| 209 | - // // run our app with hyper, listening globally on port 3000 |
| 210 | - // tracing::info!("Listening @ 0.0.0.0:3000"); |
| 211 | - // let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); |
| 212 | - // axum::serve(listener, app).await.unwrap(); |
| 213 | - // } |
| 214 | - // |
| 215 | - // #[tokio::main] |
| 216 | - // async fn main() { |
| 217 | - // tracing_subscriber::registry() |
| 218 | - // .with( |
| 219 | - // tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { |
| 220 | - // // axum logs rejections from built-in extractors with the `axum::rejection` |
| 221 | - // // target, at `TRACE` level. `axum::rejection=trace` enables showing those events |
| 222 | - // format!( |
| 223 | - // "{}=debug,tower_http=debug,axum::rejection=trace", |
| 224 | - // env!("CARGO_CRATE_NAME") |
| 225 | - // ) |
| 226 | - // .into() |
| 227 | - // }), |
| 228 | - // ) |
| 229 | - // .with(tracing_subscriber::fmt::layer()) |
| 230 | - // .init(); |
| 231 | - // serve().await; |
| 232 | - // } |
| 233 | - |
| 234 | diff --git a/papyri-axum/src/storage.rs b/papyri-axum/src/storage.rs |
| 235 | deleted file mode 100644 |
| 236 | index e69de29..0000000 |
| 237 | --- a/papyri-axum/src/storage.rs |
| 238 | +++ /dev/null |
| 239 | diff --git a/papyri-common/Cargo.toml b/papyri-common/Cargo.toml |
| 240 | deleted file mode 100644 |
| 241 | index 6e65a6c..0000000 |
| 242 | --- a/papyri-common/Cargo.toml |
| 243 | +++ /dev/null |
| 244 | @@ -1,6 +0,0 @@ |
| 245 | - [package] |
| 246 | - name = "papyri-common" |
| 247 | - version = "0.1.0" |
| 248 | - edition = "2024" |
| 249 | - |
| 250 | - [dependencies] |
| 251 | diff --git a/papyri-common/src/main.rs b/papyri-common/src/main.rs |
| 252 | deleted file mode 100644 |
| 253 | index e7a11a9..0000000 |
| 254 | --- a/papyri-common/src/main.rs |
| 255 | +++ /dev/null |
| 256 | @@ -1,3 +0,0 @@ |
| 257 | - fn main() { |
| 258 | - println!("Hello, world!"); |
| 259 | - } |
| 260 | diff --git a/src/error.rs b/src/error.rs |
| 261 | new file mode 100644 |
| 262 | index 0000000..45f1daa |
| 263 | --- /dev/null |
| 264 | +++ b/src/error.rs |
| 265 | @@ -0,0 +1,98 @@ |
| 266 | + use axum::{Json, response::IntoResponse}; |
| 267 | + |
| 268 | + use crate::storage::Error as StorageError; |
| 269 | + |
| 270 | + // | ID | Code | Description | |
| 271 | + // |-------- | ------------------------|------------------------------------------------------------| |
| 272 | + // | code-1 | `BLOB_UNKNOWN` | blob unknown to registry | |
| 273 | + // | code-2 | `BLOB_UPLOAD_INVALID` | blob upload invalid | |
| 274 | + // | code-3 | `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | |
| 275 | + // | code-4 | `DIGEST_INVALID` | provided digest did not match uploaded content | |
| 276 | + // | code-5 | `MANIFEST_BLOB_UNKNOWN` | manifest references a manifest or blob unknown to registry | |
| 277 | + // | code-6 | `MANIFEST_INVALID` | manifest invalid | |
| 278 | + // | code-7 | `MANIFEST_UNKNOWN` | manifest unknown to registry | |
| 279 | + // | code-8 | `NAME_INVALID` | invalid repository name | |
| 280 | + // | code-9 | `NAME_UNKNOWN` | repository name not known to registry | |
| 281 | + // | code-10 | `SIZE_INVALID` | provided length did not match content length | |
| 282 | + // | code-11 | `UNAUTHORIZED` | authentication required | |
| 283 | + // | code-12 | `DENIED` | requested access to the resource is denied | |
| 284 | + // | code-13 | `UNSUPPORTED` | the operation is unsupported | |
| 285 | + // | code-14 | `TOOMANYREQUESTS` | too many requests | |
| 286 | + #[derive(Debug)] |
| 287 | + pub enum Code { |
| 288 | + BlobUnknown, |
| 289 | + BlobUploadInvalid, |
| 290 | + BlobUploadUnknown, |
| 291 | + DigestInvalid, |
| 292 | + ManifestBlobUnknown, |
| 293 | + ManifestInvalid, |
| 294 | + ManifestUnknown, |
| 295 | + NameInvalid, |
| 296 | + NameUnknown, |
| 297 | + SizeInvalid, |
| 298 | + Unathorized, |
| 299 | + Denied, |
| 300 | + Unsupported, |
| 301 | + TooManyRequests, |
| 302 | + } |
| 303 | + |
| 304 | + #[derive(serde::Serialize, Debug)] |
| 305 | + pub struct Message { |
| 306 | + pub code: String, |
| 307 | + pub message: String, |
| 308 | + pub detail: Option<String>, |
| 309 | + } |
| 310 | + |
| 311 | + #[derive(thiserror::Error, Debug)] |
| 312 | + pub enum Error { |
| 313 | + Code(Code), |
| 314 | + Storage(StorageError), |
| 315 | + } |
| 316 | + |
| 317 | + impl std::fmt::Display for Error { |
| 318 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 319 | + let msg: Message = self.into(); |
| 320 | + f.write_str(&format!("{:?}", msg)) |
| 321 | + } |
| 322 | + } |
| 323 | + |
| 324 | + impl From<&Error> for Message { |
| 325 | + fn from(val: &Error) -> Self { |
| 326 | + match val { |
| 327 | + Error::Code(code) => match code { |
| 328 | + Code::BlobUnknown => Message { |
| 329 | + code: String::from("BLOB_UNKNOWN"), |
| 330 | + message: String::from("blob unknown to registry"), |
| 331 | + detail: None, |
| 332 | + }, |
| 333 | + Code::BlobUploadInvalid => todo!(), |
| 334 | + Code::BlobUploadUnknown => todo!(), |
| 335 | + Code::DigestInvalid => todo!(), |
| 336 | + Code::ManifestBlobUnknown => todo!(), |
| 337 | + Code::ManifestInvalid => todo!(), |
| 338 | + Code::ManifestUnknown => todo!(), |
| 339 | + Code::NameInvalid => todo!(), |
| 340 | + Code::NameUnknown => todo!(), |
| 341 | + Code::SizeInvalid => todo!(), |
| 342 | + Code::Unathorized => todo!(), |
| 343 | + Code::Denied => todo!(), |
| 344 | + Code::Unsupported => todo!(), |
| 345 | + Code::TooManyRequests => todo!(), |
| 346 | + }, |
| 347 | + Error::Storage(error) => { |
| 348 | + match error { |
| 349 | + StorageError::Unspecified => todo!(), |
| 350 | + StorageError::IO => todo!(), |
| 351 | + StorageError::NotFound => todo!(), |
| 352 | + } |
| 353 | + }, |
| 354 | + } |
| 355 | + } |
| 356 | + } |
| 357 | + |
| 358 | + impl IntoResponse for Error { |
| 359 | + fn into_response(self) -> axum::response::Response { |
| 360 | + let message = Message::from(&self); |
| 361 | + Json(message).into_response() |
| 362 | + } |
| 363 | + } |
| 364 | diff --git a/src/lib.rs b/src/lib.rs |
| 365 | new file mode 100644 |
| 366 | index 0000000..c3e1100 |
| 367 | --- /dev/null |
| 368 | +++ b/src/lib.rs |
| 369 | @@ -0,0 +1,3 @@ |
| 370 | + pub mod routes; |
| 371 | + pub mod error; |
| 372 | + pub mod storage; |
| 373 | diff --git a/src/routes.rs b/src/routes.rs |
| 374 | new file mode 100644 |
| 375 | index 0000000..acc33b5 |
| 376 | --- /dev/null |
| 377 | +++ b/src/routes.rs |
| 378 | @@ -0,0 +1,179 @@ |
| 379 | + use std::cell::OnceCell; |
| 380 | + use std::str::FromStr; |
| 381 | + |
| 382 | + use axum::Json; |
| 383 | + use axum::extract::{MatchedPath, Path, Request}; |
| 384 | + use axum::response::IntoResponse; |
| 385 | + use axum::routing::{delete, head, patch, post, put}; |
| 386 | + use axum::{Router, routing::get}; |
| 387 | + use core::result::Result as StdResult; |
| 388 | + use oci_spec::image::{ImageIndexBuilder, ImageManifest}; |
| 389 | + use regex::Regex; |
| 390 | + use serde::Deserialize; |
| 391 | + use serde_json::json; |
| 392 | + use tracing::info_span; |
| 393 | + |
| 394 | + use crate::error::Error; |
| 395 | + |
| 396 | + pub type Result<T = Json<serde_json::Value>, E = Error> = std::result::Result<T, E>; |
| 397 | + |
| 398 | + const NAME_REGEXP_MATCH: &str = |
| 399 | + r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*"; |
| 400 | + |
| 401 | + #[derive(Deserialize)] |
| 402 | + pub struct Namespace(String); |
| 403 | + |
| 404 | + impl Namespace { |
| 405 | + pub fn validate(&self) -> Result<(), Error> { |
| 406 | + let regexp = Regex::new(NAME_REGEXP_MATCH).unwrap(); |
| 407 | + if regexp.is_match(&self.0) { |
| 408 | + Ok(()) |
| 409 | + } else { |
| 410 | + Err(Error::Code(crate::error::Code::NameInvalid)) |
| 411 | + } |
| 412 | + } |
| 413 | + } |
| 414 | + |
| 415 | + // Registry conformance applies to the following workflow categories: |
| 416 | + // |
| 417 | + // 1. **Pull** - Clients are able to pull from the registry |
| 418 | + // 2. **Push** - Clients are able to push to the registry |
| 419 | + // 3. **Content Discovery** - Clients are able to list or otherwise query the content stored in the registry |
| 420 | + // 4. **Content Management** - Clients are able to control the full life-cycle of the content stored in the registry |
| 421 | + // |
| 422 | + |
| 423 | + pub async fn index() -> Result { |
| 424 | + Ok(Json(json!({}))) |
| 425 | + } |
| 426 | + |
| 427 | + pub mod pull { |
| 428 | + pub async fn read_manifest() {} |
| 429 | + pub async fn read_blob() {} |
| 430 | + } |
| 431 | + |
| 432 | + pub mod push { |
| 433 | + pub async fn write_manifest() {} |
| 434 | + pub async fn write_blob() {} |
| 435 | + } |
| 436 | + |
| 437 | + pub mod discovery { |
| 438 | + use super::*; |
| 439 | + |
| 440 | + pub async fn read_tags(Path(name): Path<Namespace>) -> Result { |
| 441 | + name.validate()?; |
| 442 | + todo!() |
| 443 | + } |
| 444 | + |
| 445 | + pub async fn read_referrers() {} |
| 446 | + } |
| 447 | + |
| 448 | + pub mod management { |
| 449 | + pub async fn delete_manifest() {} |
| 450 | + pub async fn delete_tag() {} |
| 451 | + pub async fn delete_blob() {} |
| 452 | + } |
| 453 | + |
| 454 | + pub fn router() -> Router { |
| 455 | + Router::new() |
| 456 | + .route("/v2", get(index)) |
| 457 | + .route("/v2/", get(index)) |
| 458 | + .route("/v2/:name/blobs/:digest", get(pull::read_blob)) |
| 459 | + .route("/v2/:name/blobs/:digest", head(pull::read_blob)) |
| 460 | + .route("/v2/:name/manifests/:reference", get(pull::read_manifest)) |
| 461 | + .route("/v2/:name/manifests/:reference", head(pull::read_manifest)) |
| 462 | + .route("/v2/:name/blobs/uploads", post(push::write_blob)) |
| 463 | + .route( |
| 464 | + "/v2/:name/blobs/uploads/:reference", |
| 465 | + patch(push::write_blob), |
| 466 | + ) |
| 467 | + .route("/v2/:name/manifests/:reference", put(push::write_manifest)) |
| 468 | + .route("/v2/:name/tags/list", get(discovery::read_tags)) |
| 469 | + .route( |
| 470 | + "/v2/:name/manifests/:reference", |
| 471 | + delete(management::delete_manifest), |
| 472 | + ) |
| 473 | + .route("/v2/:name/blobs/:digest", delete(management::delete_blob)) |
| 474 | + // .route("/v2/:name/blobs/uploads", post()) |
| 475 | + } |
| 476 | + |
| 477 | + // pub async fn handler_index(req: Request) -> Json<serde_json::Value> { |
| 478 | + // req.headers().iter().for_each(|h| { |
| 479 | + // let header_value = h.1.to_str().ok(); |
| 480 | + // tracing::info!(key = h.0.as_str(), value = header_value); |
| 481 | + // }); |
| 482 | + // Json(json!({})) |
| 483 | + // } |
| 484 | + // |
| 485 | + // pub async fn handler_manifest_index() -> Json<serde_json::Value> { |
| 486 | + // } |
| 487 | + |
| 488 | + // async fn serve() { |
| 489 | + // let app = Router::new() |
| 490 | + // .route("/", get(|| async { "Hello, World!" })) |
| 491 | + // .route("/v2", get(handler_index)) |
| 492 | + // .route("/v2/", get(handler_index)) |
| 493 | + // .layer( |
| 494 | + // TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { |
| 495 | + // // Log the matched route's path (with placeholders not filled in). |
| 496 | + // // Use request.uri() or OriginalUri if you want the real path. |
| 497 | + // let matched_path = request |
| 498 | + // .extensions() |
| 499 | + // .get::<MatchedPath>() |
| 500 | + // .map(MatchedPath::as_str); |
| 501 | + // |
| 502 | + // let actual_path = request.uri().path(); |
| 503 | + // |
| 504 | + // info_span!( |
| 505 | + // "http_request", |
| 506 | + // method = ?request.method(), |
| 507 | + // matched_path, |
| 508 | + // actual_path, |
| 509 | + // some_other_field = tracing::field::Empty, |
| 510 | + // ) |
| 511 | + // }), // .on_request(|_request: &Request<_>, _span: &Span| { |
| 512 | + // // // You can use `_span.record("some_other_field", value)` in one of these |
| 513 | + // // // closures to attach a value to the initially empty field in the info_span |
| 514 | + // // // created above. |
| 515 | + // // }) |
| 516 | + // // .on_response(|_response: &Response, _latency: Duration, _span: &Span| { |
| 517 | + // // // ... |
| 518 | + // // }) |
| 519 | + // // .on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| { |
| 520 | + // // // ... |
| 521 | + // // }) |
| 522 | + // // .on_eos( |
| 523 | + // // |_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| { |
| 524 | + // // // ... |
| 525 | + // // }, |
| 526 | + // // ) |
| 527 | + // // .on_failure( |
| 528 | + // // |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { |
| 529 | + // // // ... |
| 530 | + // // }, |
| 531 | + // // ), |
| 532 | + // ); |
| 533 | + // |
| 534 | + // // run our app with hyper, listening globally on port 3000 |
| 535 | + // tracing::info!("Listening @ 0.0.0.0:3000"); |
| 536 | + // let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); |
| 537 | + // axum::serve(listener, app).await.unwrap(); |
| 538 | + // } |
| 539 | + // |
| 540 | + // #[tokio::main] |
| 541 | + // async fn main() { |
| 542 | + // tracing_subscriber::registry() |
| 543 | + // .with( |
| 544 | + // tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { |
| 545 | + // // axum logs rejections from built-in extractors with the `axum::rejection` |
| 546 | + // // target, at `TRACE` level. `axum::rejection=trace` enables showing those events |
| 547 | + // format!( |
| 548 | + // "{}=debug,tower_http=debug,axum::rejection=trace", |
| 549 | + // env!("CARGO_CRATE_NAME") |
| 550 | + // ) |
| 551 | + // .into() |
| 552 | + // }), |
| 553 | + // ) |
| 554 | + // .with(tracing_subscriber::fmt::layer()) |
| 555 | + // .init(); |
| 556 | + // serve().await; |
| 557 | + // } |
| 558 | diff --git a/src/storage.rs b/src/storage.rs |
| 559 | new file mode 100644 |
| 560 | index 0000000..65cb4c0 |
| 561 | --- /dev/null |
| 562 | +++ b/src/storage.rs |
| 563 | @@ -0,0 +1,21 @@ |
| 564 | + #[derive(thiserror::Error, Debug)] |
| 565 | + pub enum Error { |
| 566 | + Unspecified, |
| 567 | + IO, |
| 568 | + NotFound, |
| 569 | + } |
| 570 | + |
| 571 | + impl std::fmt::Display for Error { |
| 572 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 573 | + match self { |
| 574 | + Error::Unspecified => f.write_str("An unspecified storage error occurred"), |
| 575 | + Error::IO => f.write_str("A storage IO error occurred"), |
| 576 | + Error::NotFound => f.write_str("Storage resource not found"), |
| 577 | + } |
| 578 | + } |
| 579 | + } |
| 580 | + |
| 581 | + #[async_trait::async_trait] |
| 582 | + pub trait Storage: Sync + Send { |
| 583 | + async fn read_tags() -> Result<Vec<String>, Error>; |
| 584 | + } |
| 585 | diff --git a/src/storage_fs.rs b/src/storage_fs.rs |
| 586 | new file mode 100644 |
| 587 | index 0000000..e69de29 |
| 588 | --- /dev/null |
| 589 | +++ b/src/storage_fs.rs |