mirror of
https://github.com/Mercantec-GHC/h4-projekt-gruppe-0-sm.git
synced 2025-04-27 16:24:07 +02:00
product images
This commit is contained in:
parent
e494277978
commit
a7591a96ff
@ -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);
|
||||||
|
|
||||||
|
9
backend/public/deno.jsonc
Normal file
9
backend/public/deno.jsonc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"checkJs": false,
|
||||||
|
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"indentWidth": 4
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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());
|
||||||
|
});
|
||||||
|
BIN
backend/public/product_fallback_256x256.png
Normal file
BIN
backend/public/product_fallback_256x256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
backend/public/product_fallback_512x512.png
Normal file
BIN
backend/public/product_fallback_512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
@ -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");
|
||||||
|
@ -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, \
|
||||||
|
@ -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);
|
||||||
|
@ -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");
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user