product images

This commit is contained in:
SimonFJ20 2025-03-19 17:26:36 +01:00
parent e494277978
commit a7591a96ff
20 changed files with 451 additions and 97 deletions

View File

@ -51,6 +51,14 @@ CREATE TABLE IF NOT EXISTS receipt_products (
FOREIGN KEY(product_price) REFERENCES product_prices(id) 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) INSERT OR REPLACE INTO users (name, email, password_hash, balance_dkk_cent)
VALUES ('User','test@email.com','08ce0220f6d63d85c3ac313e308f4fca35ecfb850baa8ddb924cfab98137b6b18b4a8e027067cb98802757df1337246a0f3aa25c44c2b788517a871086419dcf',10000); VALUES ('User','test@email.com','08ce0220f6d63d85c3ac313e308f4fca35ecfb850baa8ddb924cfab98137b6b18b4a8e027067cb98802757df1337246a0f3aa25c44c2b788517a871086419dcf',10000);

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"checkJs": false,
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
},
"fmt": {
"indentWidth": 4
}
}

View File

@ -59,51 +59,97 @@
#editor input, #editor textarea { #editor input, #editor textarea {
width: 250px; 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;
}
</style> </style>
</head> </head>
<body> <body>
<h2>Products</h2> <h2>Products</h2>
<table id="product-list"> <table id="product-list">
</table> </table>
<fieldset> <div id="wrapper">
<legend>Editor</legend> <fieldset>
<legend>Editor</legend>
<form id="editor"> <form id="editor">
<center> <table>
<button id="load">Load</button> <tr>
<td><label for="product-id">id: </label></td>
<td>
<input id="product-id" type="text">
</td>
</tr>
<tr>
<td><label for="product-name">name: </label></td>
<td><input id="product-name" type="text"></td>
</tr>
<tr>
<td><label for="product-price">price (dkk): </label></td>
<td><input id="product-price" type="number" step="0.01"></td>
</tr>
<tr>
<td><label for="product-description">description: </label></td>
<td><textarea id="product-description"></textarea></td>
</tr>
<tr>
<td><label for="product-coord">coord_id: </label></td>
<td><input id="product-coord" type="text"></td>
</tr>
<tr>
<td><label for="product-barcode">barcode: </label></td>
<td><input id="product-barcode" type="text"></td>
</tr>
</table>
<center>
<button id="load">Load</button>
<button id="save">Save</button>
<button id="new">New</button>
</center>
</form>
</fieldset>
<fieldset>
<legend>Image uploader</legend>
<form id="image-uploader">
<table>
<tr>
<td><label for="product-id">id: </label></td>
<td>
<input id="product-id" type="text">
</td>
</tr>
<tr>
<td><label for="image-file">image: </label></td>
<td>
<input
id="file"
type="file"
accept="image/png" />
</td>
</tr>
</table>
<div><img id="preview" src="" alt=""></div>
<button id="save">Save</button> <button id="save">Save</button>
<button id="new">New</button> </form>
</center> </fieldset>
</div>
<table>
<tr>
<td><label for="product-id">id: </label></td>
<td>
<input id="product-id" type="text">
</td>
</tr>
<tr>
<td><label for="product-name">name: </label></td>
<td><input id="product-name" type="text"></td>
</tr>
<tr>
<td><label for="product-price">price (dkk): </label></td>
<td><input id="product-price" type="number" step="0.01"></td>
</tr>
<tr>
<td><label for="product-description">description: </label></td>
<td><textarea id="product-description"></textarea></td>
</tr>
<tr>
<td><label for="product-coord">coord_id: </label></td>
<td><input id="product-coord" type="text"></td>
</tr>
<tr>
<td><label for="product-barcode">barcode: </label></td>
<td><input id="product-barcode" type="text"></td>
</tr>
</table>
</form>
</fieldset>
</body> </body>
</html> </html>

View File

@ -1,16 +1,22 @@
const productList = document.querySelector("#product-list"); const productList = document.querySelector("#product-list");
const editor = { const editor = {
form: document.querySelector("#editor"), form: document.querySelector("#editor"),
loadButton: document.querySelector("#editor #load"), loadButton: document.querySelector("#editor #load"),
saveButton: document.querySelector("#editor #save"), saveButton: document.querySelector("#editor #save"),
newButton: document.querySelector("#editor #new"), newButton: document.querySelector("#editor #new"),
idInput: document.querySelector("#editor #product-id"), idInput: document.querySelector("#editor #product-id"),
nameInput: document.querySelector("#editor #product-name"), nameInput: document.querySelector("#editor #product-name"),
priceInput: document.querySelector("#editor #product-price"), priceInput: document.querySelector("#editor #product-price"),
descriptionTextarea: document.querySelector("#editor #product-description"), coordInput: document.querySelector("#editor #product-coord"),
coordInput: document.querySelector("#editor #product-coord"),
barcodeInput: document.querySelector("#editor #product-barcode"), 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 = []; let products = [];
@ -28,11 +34,13 @@ function selectProduct(product) {
editor.barcodeInput.value = product.barcode.toString(); editor.barcodeInput.value = product.barcode.toString();
} }
async function loadProduct() { function loadProduct() {
selectedProductId = parseInt(editor.idInput.value); selectedProductId = parseInt(editor.idInput.value);
const product = products.find(product => product.id === selectedProductId); const product = products.find((product) =>
if (!product){ product.id === selectedProductId
);
if (!product) {
alert(`no product with id ${selectedProductId}`); alert(`no product with id ${selectedProductId}`);
return; return;
} }
@ -48,35 +56,34 @@ function productFromForm() {
price_dkk_cent: Math.floor(parseFloat(editor.priceInput.value) * 100), price_dkk_cent: Math.floor(parseFloat(editor.priceInput.value) * 100),
coord_id: parseInt(editor.coordInput.value), coord_id: parseInt(editor.coordInput.value),
barcode: editor.barcodeInput.value, barcode: editor.barcodeInput.value,
} };
} }
async function saveProduct() { async function saveProduct() {
const product = productFromForm(); const product = productFromForm();
await fetch("/api/products/update", { await fetch("/api/products/update", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify(product), body: JSON.stringify(product),
}).then(res => res.json()); }).then((res) => res.json());
await updateProductList(); await updateProductList();
} }
async function newProduct() { async function newProduct() {
const product = productFromForm(); const product = productFromForm();
await fetch("/api/products/create", { await fetch("/api/products/create", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify(product), body: JSON.stringify(product),
}).then(res => res.json()); }).then((res) => res.json());
await updateProductList(); await updateProductList();
} }
async function updateProductList() { async function updateProductList() {
const res = await fetch("/api/products/all") const res = await fetch("/api/products/all")
.then(res => res.json()); .then((res) => res.json());
products = res.products; products = res.products;
@ -92,7 +99,7 @@ async function updateProductList() {
</tr> </tr>
`; `;
productList.innerHTML += products productList.innerHTML += products
.map(product => ` .map((product) => `
<tr> <tr>
<td><code>${product.id}</code></td> <td><code>${product.id}</code></td>
<td><strong>${product.name}</strong></td> <td><strong>${product.name}</strong></td>
@ -121,15 +128,45 @@ editor.form
e.preventDefault(); e.preventDefault();
}); });
editor.loadButton editor.loadButton
.addEventListener("click", (e) => { .addEventListener("click", (_e) => {
loadProduct(); loadProduct();
}); });
editor.saveButton editor.saveButton
.addEventListener("click", (e) => { .addEventListener("click", (_e) => {
saveProduct(); saveProduct();
}); });
editor.newButton editor.newButton
.addEventListener("click", (e) => { .addEventListener("click", (_e) => {
newProduct(); 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());
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -10,7 +10,7 @@ void route_post_carts_purchase(HttpCtx* ctx)
if (!session) if (!session)
return; 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)); JsonValue* body_json = json_parse(body_str, strlen(body_str));
if (!body_json) { if (!body_json) {
RESPOND_BAD_REQUEST(ctx, "bad request"); RESPOND_BAD_REQUEST(ctx, "bad request");

View File

@ -38,6 +38,8 @@ void route_get_not_found(HttpCtx* ctx);
void route_get_products_all(HttpCtx* ctx); void route_get_products_all(HttpCtx* ctx);
void route_post_products_create(HttpCtx* ctx); void route_post_products_create(HttpCtx* ctx);
void route_post_products_update(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_html(HttpCtx* ctx);
void route_get_product_editor_js(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)); \ 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-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); \ free(_body); \
} }
@ -84,6 +85,14 @@ const Session* middleware_session(HttpCtx* ctx);
#define RESPOND_SERVER_ERROR(HTTP_CTX) \ #define RESPOND_SERVER_ERROR(HTTP_CTX) \
RESPOND_JSON(HTTP_CTX, 500, "{\"ok\":false,\"msg\":\"server error\"}") RESPOND_JSON(HTTP_CTX, 500, "{\"ok\":false,\"msg\":\"server error\"}")
#define RESPOND_HTML_BAD_REQUEST(CTX, ...) \
RESPOND_HTML(CTX, \
500, \
"<!DOCTYPE html><html><head><meta " \
"charset=\"utf-8\"></head><body><center><h1>400 Bad " \
"Request</h1><p>%s</p></body></html>", \
__VA_ARGS__);
#define RESPOND_HTML_SERVER_ERROR(CTX) \ #define RESPOND_HTML_SERVER_ERROR(CTX) \
RESPOND_HTML(CTX, \ RESPOND_HTML(CTX, \
500, \ 500, \

View File

@ -18,7 +18,7 @@ void route_post_set_number(HttpCtx* ctx)
{ {
Cx* cx = http_ctx_user_ctx(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; JsonParser parser;
json_parser_construct(&parser, body_text, strlen(body_text)); json_parser_construct(&parser, body_text, strlen(body_text));
JsonValue* body = json_parser_parse(&parser); JsonValue* body = json_parser_parse(&parser);

View File

@ -40,7 +40,7 @@ void route_post_products_create(HttpCtx* ctx)
{ {
Cx* cx = http_ctx_user_ctx(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)); JsonValue* body_json = json_parse(body_str, strlen(body_str));
if (!body_json) { if (!body_json) {
RESPOND_BAD_REQUEST(ctx, "bad request"); RESPOND_BAD_REQUEST(ctx, "bad request");
@ -81,7 +81,7 @@ void route_post_products_update(HttpCtx* ctx)
{ {
Cx* cx = http_ctx_user_ctx(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); printf("body_str = '%s'\n", body_str);
JsonValue* body_json = json_parse(body_str, strlen(body_str)); JsonValue* body_json = json_parse(body_str, strlen(body_str));
@ -113,10 +113,126 @@ l0_return:
product_destroy(&product); product_destroy(&product);
} }
static inline int read_and_send_file(HttpCtx* ctx, void route_post_products_set_image(HttpCtx* ctx)
const char* filepath, {
size_t max_file_size, Cx* cx = http_ctx_user_ctx(ctx);
const char* mime_type)
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; int res;
@ -125,14 +241,12 @@ static inline int read_and_send_file(HttpCtx* ctx,
RESPOND_HTML_SERVER_ERROR(ctx); RESPOND_HTML_SERVER_ERROR(ctx);
return -1; 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)); const size_t max_file_size = 16777216;
size_t bytes_read = fread(buf, sizeof(char), max_file_size, fp); if (file_size >= max_file_size) {
if (bytes_read == 0) {
RESPOND_HTML_SERVER_ERROR(ctx);
res = -1;
goto l0_return;
} else if (bytes_read >= max_file_size) {
fprintf(stderr, fprintf(stderr,
"error: file too large '%s' >= %ld\n", "error: file too large '%s' >= %ld\n",
filepath, filepath,
@ -142,13 +256,18 @@ static inline int read_and_send_file(HttpCtx* ctx,
goto l0_return; goto l0_return;
} }
char content_length[24] = { 0 }; char* buf = calloc(file_size + 1, sizeof(char));
snprintf(content_length, 24 - 1, "%ld", bytes_read); 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-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; res = 0;
l0_return: l0_return:
@ -159,13 +278,11 @@ l0_return:
void route_get_product_editor_html(HttpCtx* ctx) void route_get_product_editor_html(HttpCtx* ctx)
{ {
read_and_send_file( 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) void route_get_product_editor_js(HttpCtx* ctx)
{ {
read_and_send_file(ctx, read_and_send_file(
PUBLIC_DIR_PATH "/product_editor.js", ctx, PUBLIC_DIR_PATH "/product_editor.js", "application/javascript");
16384 - 1,
"application/javascript");
} }

View File

@ -12,6 +12,10 @@ void route_get_receipts_one(HttpCtx* ctx)
return; return;
const char* query = http_ctx_req_query(ctx); 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); HttpQueryParams* params = http_parse_query_params(query);
char* receipt_id_str = http_query_params_get(params, "receipt_id"); char* receipt_id_str = http_query_params_get(params, "receipt_id");
http_query_params_free(params); http_query_params_free(params);

View File

@ -8,7 +8,7 @@ void route_post_sessions_login(HttpCtx* ctx)
{ {
Cx* cx = http_ctx_user_ctx(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)); JsonValue* body_json = json_parse(body_str, strlen(body_str));

View File

@ -8,7 +8,7 @@ void route_post_users_register(HttpCtx* ctx)
{ {
Cx* cx = http_ctx_user_ctx(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)); JsonValue* body_json = json_parse(body_str, strlen(body_str));

View File

@ -67,3 +67,10 @@ DbRes db_receipt_prices(
/// `products` field is an out parameter. /// `products` field is an out parameter.
/// Expects `products` to be constructed. /// Expects `products` to be constructed.
DbRes db_receipt_products(Db* db, ProductVec* products, int64_t receipt_id); 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);

View File

@ -6,6 +6,8 @@
#include <sqlite3.h> #include <sqlite3.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define REPORT_SQLITE3_ERROR() \ #define REPORT_SQLITE3_ERROR() \
fprintf(stderr, \ fprintf(stderr, \
@ -925,3 +927,86 @@ l0_return:
DISCONNECT; DISCONNECT;
return res; 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;
}

View File

@ -34,9 +34,13 @@ const char* http_ctx_req_path(HttpCtx* ctx);
bool http_ctx_req_headers_has(HttpCtx* ctx, const char* key); 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_headers_get(HttpCtx* ctx, const char* key);
const char* http_ctx_req_query(HttpCtx* ctx); 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_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; typedef struct HttpQueryParams HttpQueryParams;

View File

@ -160,11 +160,21 @@ const char* http_ctx_req_query(HttpCtx* ctx)
return ctx->req->query; 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; 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) void http_ctx_res_headers_set(HttpCtx* ctx, const char* key, const char* value)
{ {
char* key_copy = malloc(strlen(key) + 1); 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 }); 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 // https://httpwg.org/specs/rfc9112.html#persistent.tear-down
http_ctx_res_headers_set(ctx, "Connection", "close"); 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 res;
string_construct(&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, "\r\n");
string_push_str(&res, body);
ssize_t bytes_written = write(ctx->client->file, res.data, res.size); ssize_t bytes_written = write(ctx->client->file, res.data, res.size);
if (bytes_written != (ssize_t)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); 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");
}
} }

View File

@ -37,7 +37,8 @@ struct HttpServer {
struct HttpCtx { struct HttpCtx {
ClientConnection* client; ClientConnection* client;
const Request* req; const Request* req;
const char* req_body; const uint8_t* req_body;
size_t req_body_size;
HeaderVec res_headers; HeaderVec res_headers;
void* user_ctx; void* user_ctx;
}; };

View File

@ -96,7 +96,8 @@ void http_worker_handle_connection(Worker* worker, ClientConnection connection)
HttpCtx handler_ctx = { HttpCtx handler_ctx = {
.client = &client->connection, .client = &client->connection,
.req = &request, .req = &request,
.req_body = (char*)request.body, .req_body = request.body,
.req_body_size = request.body_size,
.res_headers = { 0 }, .res_headers = { 0 },
.user_ctx = worker->ctx->server->user_ctx, .user_ctx = worker->ctx->server->user_ctx,
}; };

View File

@ -42,6 +42,10 @@ int main(void)
server, "/api/products/create", route_post_products_create); server, "/api/products/create", route_post_products_create);
http_server_post( http_server_post(
server, "/api/products/update", route_post_products_update); 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( http_server_get(
server, "/product_editor/index.html", route_get_product_editor_html); server, "/product_editor/index.html", route_get_product_editor_html);