1use std::sync::Arc;
12
13use axum::Router;
14use axum::routing::{delete, get, post};
15
16use crate::handlers::{base, catalog};
17use crate::registry::RegistryMeta;
18use ferro_blob_store::SharedBlobStore;
19
20pub struct AppState {
22 pub blob_store: SharedBlobStore,
24 pub registry: Arc<dyn RegistryMeta>,
26}
27
28pub fn router(state: Arc<AppState>) -> Router {
30 Router::new()
31 .route("/v2/", get(base::version_check))
33 .route("/v2", get(base::version_check))
34 .route("/v2/_catalog", get(catalog::list_catalog))
36 .route("/v2/{*rest}", get(dispatch::dispatch_get))
37 .route("/v2/{*rest}", axum::routing::head(dispatch::dispatch_head))
38 .route("/v2/{*rest}", delete(dispatch::dispatch_delete))
39 .route(
41 "/v2/{*rest}",
42 post(dispatch::dispatch_post)
43 .patch(dispatch::dispatch_patch_inner)
44 .put(dispatch::dispatch_put_inner),
45 )
46 .with_state(state)
47}
48
49pub mod dispatch {
58 use std::sync::Arc;
59
60 use axum::body::Bytes;
61 use axum::extract::{Path, Query, State};
62 use axum::http::{HeaderMap, Method, StatusCode};
63 use axum::response::{IntoResponse, Response};
64
65 use super::AppState;
66 use crate::error::{OciError, OciErrorCode};
67 use crate::handlers::{blob, blob_upload, manifest as manifest_h, referrers, tags};
68
69 fn split_rest(rest: &str) -> Option<(&str, &str)> {
72 let keywords = ["blobs/", "manifests/", "tags/list", "referrers/"];
76 for kw in keywords {
77 if let Some(idx) = rest.rfind(kw) {
78 if idx == 0 {
81 return None;
82 }
83 if &rest[idx - 1..idx] != "/" {
84 continue;
85 }
86 let name = &rest[..idx - 1];
87 let suffix = &rest[idx..];
88 return Some((name, suffix));
89 }
90 }
91 None
92 }
93
94 fn decode(rest: &str) -> Result<(String, String), OciError> {
96 let (name, suffix) = split_rest(rest).ok_or_else(|| {
97 OciError::new(OciErrorCode::NameUnknown, format!("cannot route `{rest}`"))
98 })?;
99 Ok((name.to_owned(), suffix.to_owned()))
100 }
101
102 pub async fn dispatch_get(
104 State(state): State<Arc<AppState>>,
105 Path(rest): Path<String>,
106 Query(params): Query<std::collections::BTreeMap<String, String>>,
107 headers: HeaderMap,
108 ) -> Response {
109 let (name, suffix) = match decode(&rest) {
110 Ok(v) => v,
111 Err(e) => return e.into_response(),
112 };
113 dispatch_inner(
114 state,
115 name,
116 suffix,
117 Method::GET,
118 headers,
119 params,
120 Bytes::new(),
121 )
122 .await
123 }
124
125 pub async fn dispatch_head(
127 State(state): State<Arc<AppState>>,
128 Path(rest): Path<String>,
129 headers: HeaderMap,
130 ) -> Response {
131 let (name, suffix) = match decode(&rest) {
132 Ok(v) => v,
133 Err(e) => return e.into_response(),
134 };
135 dispatch_inner(
136 state,
137 name,
138 suffix,
139 Method::HEAD,
140 headers,
141 std::collections::BTreeMap::default(),
142 Bytes::new(),
143 )
144 .await
145 }
146
147 pub async fn dispatch_delete(
149 State(state): State<Arc<AppState>>,
150 Path(rest): Path<String>,
151 headers: HeaderMap,
152 ) -> Response {
153 let (name, suffix) = match decode(&rest) {
154 Ok(v) => v,
155 Err(e) => return e.into_response(),
156 };
157 dispatch_inner(
158 state,
159 name,
160 suffix,
161 Method::DELETE,
162 headers,
163 std::collections::BTreeMap::default(),
164 Bytes::new(),
165 )
166 .await
167 }
168
169 pub async fn dispatch_post(
171 State(state): State<Arc<AppState>>,
172 Path(rest): Path<String>,
173 Query(params): Query<std::collections::BTreeMap<String, String>>,
174 headers: HeaderMap,
175 body: Bytes,
176 ) -> Response {
177 let (name, suffix) = match decode(&rest) {
178 Ok(v) => v,
179 Err(e) => return e.into_response(),
180 };
181 dispatch_inner(state, name, suffix, Method::POST, headers, params, body).await
182 }
183
184 pub async fn dispatch_patch_inner(
186 State(state): State<Arc<AppState>>,
187 Path(rest): Path<String>,
188 headers: HeaderMap,
189 body: Bytes,
190 ) -> Response {
191 let (name, suffix) = match decode(&rest) {
192 Ok(v) => v,
193 Err(e) => return e.into_response(),
194 };
195 dispatch_inner(
196 state,
197 name,
198 suffix,
199 Method::PATCH,
200 headers,
201 std::collections::BTreeMap::default(),
202 body,
203 )
204 .await
205 }
206
207 pub async fn dispatch_put_inner(
209 State(state): State<Arc<AppState>>,
210 Path(rest): Path<String>,
211 Query(params): Query<std::collections::BTreeMap<String, String>>,
212 headers: HeaderMap,
213 body: Bytes,
214 ) -> Response {
215 let (name, suffix) = match decode(&rest) {
216 Ok(v) => v,
217 Err(e) => return e.into_response(),
218 };
219 dispatch_inner(state, name, suffix, Method::PUT, headers, params, body).await
220 }
221
222 #[allow(clippy::too_many_arguments)]
223 async fn dispatch_inner(
224 state: Arc<AppState>,
225 name: String,
226 suffix: String,
227 method: Method,
228 headers: HeaderMap,
229 params: std::collections::BTreeMap<String, String>,
230 body: Bytes,
231 ) -> Response {
232 if suffix == "tags/list" {
234 return if method == Method::GET {
235 tags::list_tags(&state, &name, ¶ms)
236 .await
237 .into_response()
238 } else {
239 OciError::new(OciErrorCode::Unsupported, "unsupported method")
240 .with_status(StatusCode::METHOD_NOT_ALLOWED)
241 .into_response()
242 };
243 }
244 if let Some(rest) = suffix.strip_prefix("referrers/") {
246 return if method == Method::GET {
247 referrers::get_referrers(&state, &name, rest, ¶ms)
248 .await
249 .into_response()
250 } else {
251 OciError::new(OciErrorCode::Unsupported, "unsupported method")
252 .with_status(StatusCode::METHOD_NOT_ALLOWED)
253 .into_response()
254 };
255 }
256 if let Some(rest) = suffix.strip_prefix("manifests/") {
258 return match method {
259 Method::GET => manifest_h::get_manifest(&state, &name, rest, &headers)
260 .await
261 .into_response(),
262 Method::HEAD => manifest_h::head_manifest(&state, &name, rest)
263 .await
264 .into_response(),
265 Method::PUT => manifest_h::put_manifest(&state, &name, rest, &headers, body)
266 .await
267 .into_response(),
268 Method::DELETE => manifest_h::delete_manifest(&state, &name, rest)
269 .await
270 .into_response(),
271 _ => OciError::new(OciErrorCode::Unsupported, "unsupported method")
272 .with_status(StatusCode::METHOD_NOT_ALLOWED)
273 .into_response(),
274 };
275 }
276 if let Some(rest) = suffix.strip_prefix("blobs/uploads/") {
278 let uuid = rest.trim_end_matches('/');
279 return match method {
280 Method::POST => {
281 blob_upload::init_upload(&state, &name, &headers, ¶ms, body)
283 .await
284 .into_response()
285 }
286 Method::PATCH => blob_upload::patch_upload(&state, &name, uuid, &headers, body)
287 .await
288 .into_response(),
289 Method::PUT => blob_upload::finish_upload(&state, &name, uuid, ¶ms, body)
290 .await
291 .into_response(),
292 Method::GET => blob_upload::get_upload_status(&state, &name, uuid)
293 .await
294 .into_response(),
295 Method::DELETE => blob_upload::cancel_upload(&state, &name, uuid)
296 .await
297 .into_response(),
298 _ => OciError::new(OciErrorCode::Unsupported, "unsupported method")
299 .with_status(StatusCode::METHOD_NOT_ALLOWED)
300 .into_response(),
301 };
302 }
303 if let Some(rest) = suffix.strip_prefix("blobs/") {
305 return match method {
306 Method::GET => blob::get_blob(&state, &name, rest).await.into_response(),
307 Method::HEAD => blob::head_blob(&state, &name, rest).await.into_response(),
308 Method::DELETE => blob::delete_blob(&state, &name, rest).await.into_response(),
309 _ => OciError::new(OciErrorCode::Unsupported, "unsupported method")
310 .with_status(StatusCode::METHOD_NOT_ALLOWED)
311 .into_response(),
312 };
313 }
314 OciError::new(
315 OciErrorCode::NameUnknown,
316 format!("cannot route `{name}/{suffix}`"),
317 )
318 .into_response()
319 }
320
321 #[cfg(test)]
322 mod tests {
323 use super::split_rest;
324
325 #[test]
326 fn split_simple_manifest_path() {
327 let (name, suffix) = split_rest("alpine/manifests/latest").expect("split");
328 assert_eq!(name, "alpine");
329 assert_eq!(suffix, "manifests/latest");
330 }
331
332 #[test]
333 fn split_nested_blob_path() {
334 let (name, suffix) = split_rest("my-org/lib/alpine/blobs/uploads/abc").expect("split");
335 assert_eq!(name, "my-org/lib/alpine");
336 assert_eq!(suffix, "blobs/uploads/abc");
337 }
338
339 #[test]
340 fn split_tags_list() {
341 let (name, suffix) = split_rest("lib/alpine/tags/list").expect("split");
342 assert_eq!(name, "lib/alpine");
343 assert_eq!(suffix, "tags/list");
344 }
345
346 #[test]
347 fn split_referrers() {
348 let (name, suffix) = split_rest("lib/alpine/referrers/sha256:abcd").expect("split");
349 assert_eq!(name, "lib/alpine");
350 assert_eq!(suffix, "referrers/sha256:abcd");
351 }
352
353 #[test]
354 fn split_none_for_bare_name() {
355 assert!(split_rest("alpine").is_none());
356 }
357 }
358}