Author:
Hash:
Timestamp:
+344 -139 +/-13 browse
Kevin Schoon [me@kevinschoon.com]
0b4403d8386006dc18aede446e6656ba8c207b5e
Fri, 28 Mar 2025 21:01:39 +0000 (2 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 |