Goal

The goal for today is to implement API endpoints to upload and download chunks.

Plan

  • Add an endpoint for PUT /chunks/ID?label=LABEL.
  • Add an endpoint for GET /chunks/ID
  • Add verification scenario or scenarios for the endpoints.

Notes

  • Preparation: existing code builds and tests pass.
    • in a checkout of main, run make
    • it succeeds
    • all good
  • Create branch chunks.
  • In obnam-server/src/api.rs add a route for PUT /chunks/ID, which just prints out that it's been called.
         let app = Router::new()
             .route("/validate", get(validate))
+            .route("/chunks", put(new_chunk))
             .with_state(self.api_state.clone());


+#[debug_handler]
+async fn new_chunk(
+    _: State<ApiState>,
+    TokenExtractor(token): TokenExtractor,
+    Path(chunk_id): Path<String>,
+) -> &'static str {
+    eprintln!("new_chunk: chunk_id={chunk_id:?} {token:?}");
+    "OK"
+}
+
  • Initialize server.
    • cargo run --bin obnam-server -- --config obnam-server-config.yaml init[]
  • Generate token.
    • cargo run --bin obnam-server -- --config obnam-server-config.yaml token --allow append --output token.txt
  • Run server.
    • cargo run --bin obnam-server -- --config obnam-server-config.yaml serve
  • Call API endpoint.
    • curl -sv -H "Authorization: Bearer $(cat token.txt)" -X PUT http://localhost:8000/chunks/1
    • failed: 404 not found
  • There's something about the way the route is set up that fails. I'll simplify so no chunk id is extracted.
+#[debug_handler]
+async fn new_chunk() -> &'static str {
+    eprintln!("new_chunk");
+    "new_chunk OK"
+}
+
  • This works. It doesn't do anythin useful, but the route is set up correctly. Commit.
  • Aha! I need to annotate the route with the id.
-            .route("/chunks", put(new_chunk))
+            .route("/chunks/{id}", put(new_chunk))
  • PUT with query parameters seem to be more complicated than I thought. I'll put the label in the path as well. There's certainly a way to this with query parameters, but I want simple and clear.
  • API design thinking: I need to decide which endpoints to implement:
    • GET /chunks/{id} -- I find this simplest and most elegant
    • GET /chunks?id=ID would be easily complemented by GET /chunks?label=LABEL but then the function implemting that route would need to handle query parameters with more complicated logic
  • After a quick ponder, I'm landing on the following, at least for now:
    • upload chunk: PUT /chunks/{id}/{label}
    • download chunk: GET /chunks/{id}
    • search for chunks: GET /chunks?label=LABEL
    • delete chunk: DELETE /chunks/{id}
  • Principles:
    • when operating on a specific chunk, the path always starts with /chunks/{id}
    • I'd like to set label when uploading with a query parameter
  • Implemented routes:
-            .route("/chunks/{id}/{label}", put(new_chunk))
-            .route("/chunks/{id}", get(get_chunk))
+            .route("/chunks/{id}", put(new_chunk).get(get_chunk))

-async fn new_chunk(Path((id, label)): Path<(String, String)>) -> &'static str {
+async fn new_chunk(Path(id): Path<String>, label: Query<LabelQuery>) -> &'static str {
  • Added a route for DELETE /chunks/{id} as well.
  • To implement actually managing chunks I'll need to implement something like the local store in the client. Ideally, use the same code. However, I will first implement an in-memory mockup of that, for simplicity. This will let me implement most of the verification scenarios needed. I can then think about how to implement this persistentl later on, with more time.
  • Did that using a HashMap.
  • Manual testing indicated that this works. Time to automate that.
  • Immediately realize I don't actually do anything with chunk contents. Oops.
  • Also interesting to think about status code for the deletion route.
    • if chunk doesn't exist, 404
    • if exists, 204, not 200, because no body needed in response
  • I haven't even done anything about authorization yet.
  • Fixed storing chunk contents in store.
  • Added authorization checks, and also to the verification scenario.

Summary

Got everything done that I planned, and also searching by label and deleting chunks. This turned out to be easy, because I had laid down some groundwork to make it easy. Well done, past me.

Comments?

If you have feedback on this, please use the following fediverse thread: https://toot.liw.fi/@liw/116193242847864394.