diff --git a/backend/prepare.sql b/backend/prepare.sql index de14924..8592e0a 100644 --- a/backend/prepare.sql +++ b/backend/prepare.sql @@ -51,6 +51,14 @@ CREATE TABLE IF NOT EXISTS receipt_products ( FOREIGN KEY(product_price) REFERENCES product_prices(id) ); +CREATE TABLE IF NOT EXISTS product_images ( + id INTEGER PRIMARY KEY, + product INTEGER NOT NULL UNIQUE, + data BLOB NOT NULL, + + FOREIGN KEY(product) REFERENCES products(id) +); + INSERT OR REPLACE INTO users (name, email, password_hash, balance_dkk_cent) VALUES ('User','test@email.com','08ce0220f6d63d85c3ac313e308f4fca35ecfb850baa8ddb924cfab98137b6b18b4a8e027067cb98802757df1337246a0f3aa25c44c2b788517a871086419dcf',10000); diff --git a/backend/public/deno.jsonc b/backend/public/deno.jsonc new file mode 100644 index 0000000..4d62fa6 --- /dev/null +++ b/backend/public/deno.jsonc @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "checkJs": false, + "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"] + }, + "fmt": { + "indentWidth": 4 + } +} diff --git a/backend/public/product_editor.html b/backend/public/product_editor.html index 3db5a9e..b5858a6 100644 --- a/backend/public/product_editor.html +++ b/backend/public/product_editor.html @@ -59,51 +59,97 @@ #editor input, #editor textarea { width: 250px; } + #wrapper { + display: flex; + justify-content: center; + gap: 2rem; + } + #wrapper form { + min-width: 500px; + } + #image-uploader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + height: 100%; + } + #image-uploader #preview { + height: 150px; + background-color: #fff; + }

Products

-
- Editor +
+
+ Editor -
-
- + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ +
+
+ Image uploader + +
+ + + + + + + + + +
+ +
+ +
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
+ +
+ diff --git a/backend/public/product_editor.js b/backend/public/product_editor.js index b130570..f295dc5 100644 --- a/backend/public/product_editor.js +++ b/backend/public/product_editor.js @@ -1,16 +1,22 @@ - const productList = document.querySelector("#product-list"); const editor = { form: document.querySelector("#editor"), loadButton: document.querySelector("#editor #load"), saveButton: document.querySelector("#editor #save"), - newButton: document.querySelector("#editor #new"), - idInput: document.querySelector("#editor #product-id"), - nameInput: document.querySelector("#editor #product-name"), - priceInput: document.querySelector("#editor #product-price"), - descriptionTextarea: document.querySelector("#editor #product-description"), - coordInput: document.querySelector("#editor #product-coord"), + newButton: document.querySelector("#editor #new"), + idInput: document.querySelector("#editor #product-id"), + nameInput: document.querySelector("#editor #product-name"), + priceInput: document.querySelector("#editor #product-price"), + coordInput: document.querySelector("#editor #product-coord"), barcodeInput: document.querySelector("#editor #product-barcode"), + descriptionTextarea: document.querySelector("#editor #product-description"), +}; +const imageUploader = { + form: document.querySelector("#image-uploader"), + idInput: document.querySelector("#image-uploader #product-id"), + saveButton: document.querySelector("#image-uploader #save"), + preview: document.querySelector("#image-uploader #preview"), + fileInput: document.querySelector("#image-uploader #file"), }; let products = []; @@ -28,11 +34,13 @@ function selectProduct(product) { editor.barcodeInput.value = product.barcode.toString(); } -async function loadProduct() { +function loadProduct() { selectedProductId = parseInt(editor.idInput.value); - const product = products.find(product => product.id === selectedProductId); - if (!product){ + const product = products.find((product) => + product.id === selectedProductId + ); + if (!product) { alert(`no product with id ${selectedProductId}`); return; } @@ -48,35 +56,34 @@ function productFromForm() { price_dkk_cent: Math.floor(parseFloat(editor.priceInput.value) * 100), coord_id: parseInt(editor.coordInput.value), barcode: editor.barcodeInput.value, - } + }; } async function saveProduct() { const product = productFromForm(); await fetch("/api/products/update", { method: "POST", - headers: {"Content-Type": "application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(product), - }).then(res => res.json()); + }).then((res) => res.json()); await updateProductList(); - } async function newProduct() { const product = productFromForm(); await fetch("/api/products/create", { method: "POST", - headers: {"Content-Type": "application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(product), - }).then(res => res.json()); + }).then((res) => res.json()); await updateProductList(); } async function updateProductList() { const res = await fetch("/api/products/all") - .then(res => res.json()); + .then((res) => res.json()); products = res.products; @@ -92,7 +99,7 @@ async function updateProductList() { `; productList.innerHTML += products - .map(product => ` + .map((product) => ` ${product.id} ${product.name} @@ -121,15 +128,45 @@ editor.form e.preventDefault(); }); editor.loadButton - .addEventListener("click", (e) => { + .addEventListener("click", (_e) => { loadProduct(); }); editor.saveButton - .addEventListener("click", (e) => { + .addEventListener("click", (_e) => { saveProduct(); }); editor.newButton - .addEventListener("click", (e) => { + .addEventListener("click", (_e) => { newProduct(); }); +imageUploader.form + .addEventListener("submit", (e) => { + e.preventDefault(); + }); +imageUploader.fileInput + .addEventListener("input", (e) => { + console.log(e); + const image = imageUploader.fileInput.files[0]; + const data = URL.createObjectURL(image); + imageUploader.preview.src = data; + }); +imageUploader.saveButton + .addEventListener("click", async (_e) => { + const id = parseInt(imageUploader.idInput.value); + const image = imageUploader.fileInput.files[0]; + + const buffer = await new Promise((resolve) => { + const reader = new FileReader(); + reader.addEventListener("loadend", () => { + resolve(reader.result); + }); + reader.readAsArrayBuffer(image); + }); + + await fetch(`/api/products/set-image?product_id=${id}`, { + method: "post", + headers: { "Content-Type": image.type }, + body: buffer, + }).then((res) => res.json()); + }); diff --git a/backend/public/product_fallback_256x256.png b/backend/public/product_fallback_256x256.png new file mode 100644 index 0000000..568f4f7 Binary files /dev/null and b/backend/public/product_fallback_256x256.png differ diff --git a/backend/public/product_fallback_512x512.png b/backend/public/product_fallback_512x512.png new file mode 100644 index 0000000..10f1888 Binary files /dev/null and b/backend/public/product_fallback_512x512.png differ diff --git a/backend/src/controllers/carts.c b/backend/src/controllers/carts.c index 374c49b..9fe467c 100644 --- a/backend/src/controllers/carts.c +++ b/backend/src/controllers/carts.c @@ -10,7 +10,7 @@ void route_post_carts_purchase(HttpCtx* ctx) if (!session) return; - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); if (!body_json) { RESPOND_BAD_REQUEST(ctx, "bad request"); diff --git a/backend/src/controllers/controllers.h b/backend/src/controllers/controllers.h index bbcd296..9f89a93 100644 --- a/backend/src/controllers/controllers.h +++ b/backend/src/controllers/controllers.h @@ -38,6 +38,8 @@ void route_get_not_found(HttpCtx* ctx); void route_get_products_all(HttpCtx* ctx); void route_post_products_create(HttpCtx* ctx); void route_post_products_update(HttpCtx* ctx); +void route_post_products_set_image(HttpCtx* ctx); +void route_get_products_image_png(HttpCtx* ctx); void route_get_product_editor_html(HttpCtx* ctx); void route_get_product_editor_js(HttpCtx* ctx); @@ -68,9 +70,8 @@ const Session* middleware_session(HttpCtx* ctx); snprintf(content_length, 24 - 1, "%ld", strlen(_body)); \ \ http_ctx_res_headers_set(_ctx, "Content-Type", MIME_TYPE); \ - http_ctx_res_headers_set(_ctx, "Content-Length", content_length); \ \ - http_ctx_respond(_ctx, (STATUS), _body); \ + http_ctx_respond_str(_ctx, (STATUS), _body); \ free(_body); \ } @@ -84,6 +85,14 @@ const Session* middleware_session(HttpCtx* ctx); #define RESPOND_SERVER_ERROR(HTTP_CTX) \ RESPOND_JSON(HTTP_CTX, 500, "{\"ok\":false,\"msg\":\"server error\"}") +#define RESPOND_HTML_BAD_REQUEST(CTX, ...) \ + RESPOND_HTML(CTX, \ + 500, \ + "

400 Bad " \ + "Request

%s

", \ + __VA_ARGS__); + #define RESPOND_HTML_SERVER_ERROR(CTX) \ RESPOND_HTML(CTX, \ 500, \ diff --git a/backend/src/controllers/general.c b/backend/src/controllers/general.c index 0d33d70..8485aef 100644 --- a/backend/src/controllers/general.c +++ b/backend/src/controllers/general.c @@ -18,7 +18,7 @@ void route_post_set_number(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_text = http_ctx_req_body(ctx); + const char* body_text = http_ctx_req_body_str(ctx); JsonParser parser; json_parser_construct(&parser, body_text, strlen(body_text)); JsonValue* body = json_parser_parse(&parser); diff --git a/backend/src/controllers/products.c b/backend/src/controllers/products.c index cbb356a..d9318d3 100644 --- a/backend/src/controllers/products.c +++ b/backend/src/controllers/products.c @@ -40,7 +40,7 @@ void route_post_products_create(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); if (!body_json) { RESPOND_BAD_REQUEST(ctx, "bad request"); @@ -81,7 +81,7 @@ void route_post_products_update(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); printf("body_str = '%s'\n", body_str); JsonValue* body_json = json_parse(body_str, strlen(body_str)); @@ -113,10 +113,126 @@ l0_return: product_destroy(&product); } -static inline int read_and_send_file(HttpCtx* ctx, - const char* filepath, - size_t max_file_size, - const char* mime_type) +void route_post_products_set_image(HttpCtx* ctx) +{ + Cx* cx = http_ctx_user_ctx(ctx); + + const char* query = http_ctx_req_query(ctx); + if (!query) { + RESPOND_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + HttpQueryParams* params = http_parse_query_params(query); + char* product_id_str = http_query_params_get(params, "product_id"); + http_query_params_free(params); + if (!product_id_str) { + RESPOND_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + + int64_t product_id = strtol(product_id_str, NULL, 10); + free(product_id_str); + + const uint8_t* body = http_ctx_req_body(ctx); + size_t body_size = http_ctx_req_body_size(ctx); + + DbRes db_res = db_product_image_insert(cx->db, product_id, body, body_size); + if (db_res != DbRes_Ok) { + RESPOND_SERVER_ERROR(ctx); + return; + } + + RESPOND_JSON(ctx, 200, "{\"ok\":true}"); +} + +static inline int read_fallback_image(uint8_t** buffer, size_t* buffer_size) +{ + int res; + + const char* filepath = PUBLIC_DIR_PATH "/product_fallback_256x256.png"; + + FILE* fp = fopen(filepath, "r"); + if (!fp) { + return -1; + } + fseek(fp, 0L, SEEK_END); + size_t file_size = (size_t)ftell(fp); + rewind(fp); + + const size_t max_file_size = 16777216; + if (file_size >= max_file_size) { + fprintf(stderr, + "error: file too large '%s' >= %ld\n", + filepath, + max_file_size); + res = -1; + goto l0_return; + } + + uint8_t* temp_buffer = malloc(file_size); + size_t bytes_read = fread(temp_buffer, sizeof(char), file_size, fp); + if (bytes_read != file_size) { + fprintf(stderr, "error: could not read file '%s'\n", filepath); + res = -1; + goto l1_return; + } + + *buffer = temp_buffer; + *buffer_size = file_size; + temp_buffer = NULL; + + res = 0; +l1_return: + if (temp_buffer) + free(temp_buffer); +l0_return: + fclose(fp); + return res; +} + +void route_get_products_image_png(HttpCtx* ctx) +{ + Cx* cx = http_ctx_user_ctx(ctx); + + const char* query = http_ctx_req_query(ctx); + if (!query) { + RESPOND_HTML_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + HttpQueryParams* params = http_parse_query_params(query); + char* product_id_str = http_query_params_get(params, "product_id"); + http_query_params_free(params); + if (!product_id_str) { + RESPOND_HTML_BAD_REQUEST(ctx, "no product_id parameter"); + return; + } + + int64_t product_id = strtol(product_id_str, NULL, 10); + free(product_id_str); + + uint8_t* buffer; + size_t buffer_size; + + DbRes db_res = db_product_image_with_product_id( + cx->db, &buffer, &buffer_size, product_id); + if (db_res == DbRes_NotFound) { + int res = read_fallback_image(&buffer, &buffer_size); + if (res != 0) { + RESPOND_HTML_SERVER_ERROR(ctx); + return; + } + } else if (db_res != DbRes_Ok) { + RESPOND_HTML_SERVER_ERROR(ctx); + return; + } + + http_ctx_res_headers_set(ctx, "Content-Type", "image/png"); + + http_ctx_respond(ctx, 200, buffer, buffer_size); +} + +static inline int read_and_send_file( + HttpCtx* ctx, const char* filepath, const char* mime_type) { int res; @@ -125,14 +241,12 @@ static inline int read_and_send_file(HttpCtx* ctx, RESPOND_HTML_SERVER_ERROR(ctx); return -1; } + fseek(fp, 0L, SEEK_END); + size_t file_size = (size_t)ftell(fp); + rewind(fp); - char* buf = calloc(max_file_size + 1, sizeof(char)); - size_t bytes_read = fread(buf, sizeof(char), max_file_size, fp); - if (bytes_read == 0) { - RESPOND_HTML_SERVER_ERROR(ctx); - res = -1; - goto l0_return; - } else if (bytes_read >= max_file_size) { + const size_t max_file_size = 16777216; + if (file_size >= max_file_size) { fprintf(stderr, "error: file too large '%s' >= %ld\n", filepath, @@ -142,13 +256,18 @@ static inline int read_and_send_file(HttpCtx* ctx, goto l0_return; } - char content_length[24] = { 0 }; - snprintf(content_length, 24 - 1, "%ld", bytes_read); + char* buf = calloc(file_size + 1, sizeof(char)); + size_t bytes_read = fread(buf, sizeof(char), file_size, fp); + if (bytes_read != file_size) { + fprintf(stderr, "error: could not read file '%s'\n", filepath); + RESPOND_HTML_SERVER_ERROR(ctx); + res = -1; + goto l0_return; + } http_ctx_res_headers_set(ctx, "Content-Type", mime_type); - http_ctx_res_headers_set(ctx, "Content-Length", content_length); - http_ctx_respond(ctx, 200, buf); + http_ctx_respond_str(ctx, 200, buf); res = 0; l0_return: @@ -159,13 +278,11 @@ l0_return: void route_get_product_editor_html(HttpCtx* ctx) { read_and_send_file( - ctx, PUBLIC_DIR_PATH "/product_editor.html", 16384 - 1, "text/html"); + ctx, PUBLIC_DIR_PATH "/product_editor.html", "text/html"); } void route_get_product_editor_js(HttpCtx* ctx) { - read_and_send_file(ctx, - PUBLIC_DIR_PATH "/product_editor.js", - 16384 - 1, - "application/javascript"); + read_and_send_file( + ctx, PUBLIC_DIR_PATH "/product_editor.js", "application/javascript"); } diff --git a/backend/src/controllers/receipts.c b/backend/src/controllers/receipts.c index 7846436..25c9eb1 100644 --- a/backend/src/controllers/receipts.c +++ b/backend/src/controllers/receipts.c @@ -12,6 +12,10 @@ void route_get_receipts_one(HttpCtx* ctx) return; const char* query = http_ctx_req_query(ctx); + if (!query) { + RESPOND_BAD_REQUEST(ctx, "no receipt_id parameter"); + return; + } HttpQueryParams* params = http_parse_query_params(query); char* receipt_id_str = http_query_params_get(params, "receipt_id"); http_query_params_free(params); diff --git a/backend/src/controllers/sessions.c b/backend/src/controllers/sessions.c index 6edc744..ce29ea3 100644 --- a/backend/src/controllers/sessions.c +++ b/backend/src/controllers/sessions.c @@ -8,7 +8,7 @@ void route_post_sessions_login(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); diff --git a/backend/src/controllers/users.c b/backend/src/controllers/users.c index e7b1b39..a475d43 100644 --- a/backend/src/controllers/users.c +++ b/backend/src/controllers/users.c @@ -8,7 +8,7 @@ void route_post_users_register(HttpCtx* ctx) { Cx* cx = http_ctx_user_ctx(ctx); - const char* body_str = http_ctx_req_body(ctx); + const char* body_str = http_ctx_req_body_str(ctx); JsonValue* body_json = json_parse(body_str, strlen(body_str)); diff --git a/backend/src/db/db.h b/backend/src/db/db.h index dbb25a3..2a4f9d4 100644 --- a/backend/src/db/db.h +++ b/backend/src/db/db.h @@ -67,3 +67,10 @@ DbRes db_receipt_prices( /// `products` field is an out parameter. /// Expects `products` to be constructed. DbRes db_receipt_products(Db* db, ProductVec* products, int64_t receipt_id); + +DbRes db_product_image_insert( + Db* db, int64_t product_id, const uint8_t* data, size_t data_size); +/// `data` and `data_size` are out parameters. +/// `*data` should be freed. +DbRes db_product_image_with_product_id( + Db* db, uint8_t** data, size_t* data_size, int64_t product_id); diff --git a/backend/src/db/db_sqlite.c b/backend/src/db/db_sqlite.c index 547fa97..2d0f747 100644 --- a/backend/src/db/db_sqlite.c +++ b/backend/src/db/db_sqlite.c @@ -6,6 +6,8 @@ #include #include #include +#include +#include #define REPORT_SQLITE3_ERROR() \ fprintf(stderr, \ @@ -925,3 +927,86 @@ l0_return: DISCONNECT; return res; } + +DbRes db_product_image_insert( + Db* db, int64_t product_id, const uint8_t* data, size_t data_size) +{ + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt; + int prepare_res = sqlite3_prepare_v2(connection, + "INSERT INTO product_images (product, data) " + "VALUES (?, ?)", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + sqlite3_bind_int64(stmt, 1, product_id); + sqlite3_bind_blob64(stmt, 2, data, data_size, NULL); + + int step_res = sqlite3_step(stmt); + if (step_res != SQLITE_DONE) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} + +DbRes db_product_image_with_product_id( + Db* db, uint8_t** data, size_t* data_size, int64_t product_id) +{ + + sqlite3* connection; + CONNECT; + DbRes res; + + sqlite3_stmt* stmt; + int prepare_res = sqlite3_prepare_v2(connection, + "SELECT data" + " FROM product_images WHERE product = ?", + -1, + &stmt, + NULL); + if (prepare_res != SQLITE_OK) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + sqlite3_bind_int64(stmt, 1, product_id); + + int step_res = sqlite3_step(stmt); + if (step_res == SQLITE_DONE) { + res = DbRes_NotFound; + goto l0_return; + } else if (step_res != SQLITE_ROW) { + REPORT_SQLITE3_ERROR(); + res = DbRes_Error; + goto l0_return; + } + + *data_size = (size_t)sqlite3_column_bytes(stmt, 0); + *data = malloc(*data_size); + const void* db_data = sqlite3_column_blob(stmt, 0); + memcpy(*data, db_data, *data_size); + + res = DbRes_Ok; +l0_return: + if (stmt) + sqlite3_finalize(stmt); + DISCONNECT; + return res; +} diff --git a/backend/src/http/http.h b/backend/src/http/http.h index b7a42c5..3ca0613 100644 --- a/backend/src/http/http.h +++ b/backend/src/http/http.h @@ -34,9 +34,13 @@ const char* http_ctx_req_path(HttpCtx* ctx); bool http_ctx_req_headers_has(HttpCtx* ctx, const char* key); const char* http_ctx_req_headers_get(HttpCtx* ctx, const char* key); const char* http_ctx_req_query(HttpCtx* ctx); -const char* http_ctx_req_body(HttpCtx* ctx); +const char* http_ctx_req_body_str(HttpCtx* ctx); +const uint8_t* http_ctx_req_body(HttpCtx* ctx); +size_t http_ctx_req_body_size(HttpCtx* ctx); void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value); -void http_ctx_respond(HttpCtx* ctx, int status, const char* body); +void http_ctx_respond_str(HttpCtx* ctx, int status, const char* body); +void http_ctx_respond( + HttpCtx* ctx, int status, const uint8_t* body, size_t body_size); typedef struct HttpQueryParams HttpQueryParams; diff --git a/backend/src/http/server.c b/backend/src/http/server.c index fd257d1..fc2583a 100644 --- a/backend/src/http/server.c +++ b/backend/src/http/server.c @@ -160,11 +160,21 @@ const char* http_ctx_req_query(HttpCtx* ctx) return ctx->req->query; } -const char* http_ctx_req_body(HttpCtx* ctx) +const char* http_ctx_req_body_str(HttpCtx* ctx) +{ + return (char*)ctx->req_body; +} + +const uint8_t* http_ctx_req_body(HttpCtx* ctx) { return ctx->req_body; } +size_t http_ctx_req_body_size(HttpCtx* ctx) +{ + return ctx->req_body_size; +} + void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value) { char* key_copy = malloc(strlen(key) + 1); @@ -175,11 +185,21 @@ void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value) header_vec_push(&ctx->res_headers, (Header) { key_copy, value_copy }); } -void http_ctx_respond(HttpCtx* ctx, int status, const char* body) +void http_ctx_respond_str(HttpCtx* ctx, int status, const char* body) +{ + http_ctx_respond(ctx, status, (const uint8_t*)body, strlen(body)); +} + +void http_ctx_respond( + HttpCtx* ctx, int status, const uint8_t* body, size_t body_size) { // https://httpwg.org/specs/rfc9112.html#persistent.tear-down http_ctx_res_headers_set(ctx, "Connection", "close"); + char content_length[24] = { 0 }; + snprintf(content_length, 24 - 1, "%ld", body_size); + http_ctx_res_headers_set(ctx, "Content-Length", content_length); + String res; string_construct(&res); @@ -199,12 +219,14 @@ void http_ctx_respond(HttpCtx* ctx, int status, const char* body) } string_push_str(&res, "\r\n"); - string_push_str(&res, body); - ssize_t bytes_written = write(ctx->client->file, res.data, res.size); if (bytes_written != (ssize_t)res.size) { - fprintf(stderr, "error: could not send response\n"); + fprintf(stderr, "error: could not send response header\n"); } - string_destroy(&res); + + bytes_written = write(ctx->client->file, body, body_size); + if (bytes_written != (ssize_t)body_size) { + fprintf(stderr, "error: could not send response body\n"); + } } diff --git a/backend/src/http/server.h b/backend/src/http/server.h index b4e0056..c41da00 100644 --- a/backend/src/http/server.h +++ b/backend/src/http/server.h @@ -37,7 +37,8 @@ struct HttpServer { struct HttpCtx { ClientConnection* client; const Request* req; - const char* req_body; + const uint8_t* req_body; + size_t req_body_size; HeaderVec res_headers; void* user_ctx; }; diff --git a/backend/src/http/worker.c b/backend/src/http/worker.c index 6fd4063..5ff5e3f 100644 --- a/backend/src/http/worker.c +++ b/backend/src/http/worker.c @@ -96,7 +96,8 @@ void http_worker_handle_connection(Worker* worker, ClientConnection connection) HttpCtx handler_ctx = { .client = &client->connection, .req = &request, - .req_body = (char*)request.body, + .req_body = request.body, + .req_body_size = request.body_size, .res_headers = { 0 }, .user_ctx = worker->ctx->server->user_ctx, }; diff --git a/backend/src/main.c b/backend/src/main.c index cc4a240..5d45ceb 100644 --- a/backend/src/main.c +++ b/backend/src/main.c @@ -42,6 +42,10 @@ int main(void) server, "/api/products/create", route_post_products_create); http_server_post( server, "/api/products/update", route_post_products_update); + http_server_post( + server, "/api/products/set-image", route_post_products_set_image); + http_server_get( + server, "/api/products/image.png", route_get_products_image_png); http_server_get( server, "/product_editor/index.html", route_get_product_editor_html);