Add image handler
This commit is contained in:
parent
4ce82d15fb
commit
25986ec594
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
/target
|
||||
node_modules
|
||||
uploads
|
||||
dist
|
||||
|
35
Cargo.lock
generated
35
Cargo.lock
generated
@ -143,6 +143,24 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9edfb0e7663d7fe18c8d5b668c9c1bcf79176b1dcc9d4da9592503209a6bfb0"
|
||||
dependencies = [
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"futures-core",
|
||||
"httparse",
|
||||
"local-waker",
|
||||
"log",
|
||||
"mime",
|
||||
"twoway",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.0"
|
||||
@ -1282,6 +1300,7 @@ dependencies = [
|
||||
"actix-files",
|
||||
"actix-http",
|
||||
"actix-identity",
|
||||
"actix-multipart",
|
||||
"actix-rt",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
@ -2080,12 +2099,28 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twoway"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"unchecked-index",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unchecked-index"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
|
29
Cargo.toml
29
Cargo.toml
@ -5,26 +5,27 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "*" }
|
||||
actix-web = { version = "*" }
|
||||
actix-http = { version = "3.2.1" }
|
||||
actix-cors = { version = "*" }
|
||||
actix-files = { version = "*" }
|
||||
actix-http = { version = "3.2.1" }
|
||||
actix-identity = { version = "0.4.0" }
|
||||
actix-multipart = { version = "0.4.0" }
|
||||
actix-rt = { version = "*" }
|
||||
actix-utils = { version = "3.0.0" }
|
||||
actix-files = { version = "*" }
|
||||
actix-identity = { version = "0.4.0" }
|
||||
actix-web = { version = "*" }
|
||||
argon2 = { version = "0.4.1" }
|
||||
askama = { version = "*" }
|
||||
validator = { version = "0.14", features = ["derive"] }
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
futures = { version = "0.3.21", features = ["async-await", "std"] }
|
||||
futures-util = { version = "0.3.21", features = [] }
|
||||
gumdrop = { version = "*" }
|
||||
password-hash = { version = "0.4.2" }
|
||||
rand = { version = "0.8.5", features = [] }
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = { version = "*" }
|
||||
sqlx = { version = "*", features = ["runtime-actix-rustls", "postgres", "uuid", "chrono"] }
|
||||
uuid = { version = "*", features = ["serde"] }
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
gumdrop = { version = "*" }
|
||||
tracing = { version = "*" }
|
||||
tracing-subscriber = { version = "*" }
|
||||
tracing-actix-web = { version = "*" }
|
||||
argon2 = { version = "0.4.1" }
|
||||
password-hash = { version = "0.4.2" }
|
||||
rand = { version = "0.8.5", features = [] }
|
||||
futures = { version = "0.3.21", features = ["async-await", "std"] }
|
||||
futures-util = { version = "0.3.21", features = [] }
|
||||
tracing-subscriber = { version = "*" }
|
||||
uuid = { version = "*", features = ["serde"] }
|
||||
validator = { version = "0.14", features = ["derive"] }
|
||||
|
@ -2,7 +2,11 @@
|
||||
{% block content %}
|
||||
<business-items>
|
||||
{% for item in items %}
|
||||
<business-item name="{{item.name}}" price="{{item.price}}">
|
||||
<business-item
|
||||
name="{{item.name}}"
|
||||
price="{{item.price}}"
|
||||
url="{{item.picture_url}}"
|
||||
>
|
||||
</business-item>
|
||||
{% endfor %}
|
||||
|
||||
|
88
client/dist/app.js
vendored
88
client/dist/app.js
vendored
@ -1266,6 +1266,14 @@ customElements.define("register-form", class extends HTMLElement {
|
||||
}
|
||||
});
|
||||
customElements.define("image-input", class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return [
|
||||
"width",
|
||||
"height",
|
||||
"account-id",
|
||||
"url"
|
||||
];
|
||||
}
|
||||
constructor(){
|
||||
super();
|
||||
let b = this[S] = this.attachShadow({
|
||||
@ -1275,7 +1283,7 @@ customElements.define("image-input", class extends HTMLElement {
|
||||
<style>
|
||||
:host { display: block; border: 1px solid black; }
|
||||
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
||||
input { position: absolute; top: -10px; left: -10px; display: none; }
|
||||
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
|
||||
#view { width: 200px; height: 200px; cursor: pointer; }
|
||||
canvas { width: 200px; height: 200px; }
|
||||
</style>
|
||||
@ -1284,13 +1292,31 @@ customElements.define("image-input", class extends HTMLElement {
|
||||
<input id="file" type="file" accept="image/*" />
|
||||
<img alt="" src="" />
|
||||
</section>
|
||||
<div id="view"><canvas width="200" height="200"></canvas></div>
|
||||
<div id="view">
|
||||
<canvas width="200" height="200"></canvas>
|
||||
</div>
|
||||
<div>
|
||||
<input id="save" type="button" value="Zapisz" />
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
`, b.querySelector("#save").addEventListener("click", (a)=>{
|
||||
a.preventDefault(), a.stopPropagation();
|
||||
let b = atob(g.toDataURL("image/webp", 1.0).split(",")[1]), c = [];
|
||||
for(let d = 0; d < b.length; d++)c.push(b.charCodeAt(d));
|
||||
let e = new Blob([
|
||||
new Uint8Array(c)
|
||||
], {
|
||||
type: "image/webp"
|
||||
}), f = new FormData;
|
||||
f.append(`${crypto.randomUUID()}.webp`, e), fetch("/upload", {
|
||||
method: "POST",
|
||||
body: f
|
||||
}).then((a)=>a.json()).then(({ path: a })=>this.url = a);
|
||||
});
|
||||
let c = new FileReader(), d = b.querySelector("#file"), e = b.querySelector("#view"), f = b.querySelector("img"), g = b.querySelector("canvas"), h = g.getContext("2d");
|
||||
f.addEventListener("load", ()=>{
|
||||
let a, b;
|
||||
f.width > f.height ? (a = 200, b = 200 * f.height / f.width) : (a = 200 * f.width / f.height, b = 200), console.log(f.width, f.height), console.log(a, b), f.width = a, f.height = b, h.fillStyle = "#F00", h.rect(0, 0, 200, 200), h.drawImage(f, 0, 0, a, b);
|
||||
f.width > f.height ? (a = 200, b = 200 * f.height / f.width) : (a = 200 * f.width / f.height, b = 200), this.setAttribute("width", a), this.setAttribute("height", b), f.width = a, f.height = b, h.clearRect(0, 0, 200, 200), h.drawImage(f, 0, 0, a, b);
|
||||
}), d.addEventListener("change", (a)=>{
|
||||
a.stopPropagation(), c.addEventListener("loadend", (a)=>{
|
||||
a.total === a.loaded && (f.src = a.target.result || "");
|
||||
@ -1299,6 +1325,37 @@ customElements.define("image-input", class extends HTMLElement {
|
||||
a.stopPropagation(), d.click();
|
||||
});
|
||||
}
|
||||
connectedCallback() {
|
||||
this.account_id = this.account_id, this.url = this.url;
|
||||
}
|
||||
attributeChangedCallback(a, b, c) {
|
||||
if (b !== c) switch(a){
|
||||
case "account-id":
|
||||
return this.account_id = c;
|
||||
case "url":
|
||||
return this.url = c;
|
||||
}
|
||||
}
|
||||
get account_id() {
|
||||
return this.getAttribute("account-id");
|
||||
}
|
||||
set account_id(a) {
|
||||
this.setAttribute("account-id", a);
|
||||
}
|
||||
get width() {
|
||||
let a = parseInt(this.getAttribute("width"));
|
||||
return isNaN(a) ? 0 : a;
|
||||
}
|
||||
get height() {
|
||||
let a = parseInt(this.getAttribute("height"));
|
||||
return isNaN(a) ? 0 : a;
|
||||
}
|
||||
get url() {
|
||||
return this.getAttribute("url");
|
||||
}
|
||||
set url(b) {
|
||||
this.setAttribute("url", b), this[S].querySelector("img").src = b;
|
||||
}
|
||||
});
|
||||
customElements.define("business-item", class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
@ -1326,28 +1383,35 @@ customElements.define("business-item", class extends HTMLElement {
|
||||
`;
|
||||
}
|
||||
connectedCallback() {
|
||||
this.filter = this.getAttribute("filter");
|
||||
this.name = this.name, this.price = this.price, this.picture_url = this.picture_url;
|
||||
}
|
||||
attributeChangedCallback(a, b, c) {
|
||||
if (b !== c && "filter" === a) return this.filter = c;
|
||||
if (b !== c) switch(a){
|
||||
case "name":
|
||||
return this.name = c;
|
||||
case "price":
|
||||
return this.price = c / 100.0;
|
||||
case "picture-url":
|
||||
return this.picture_url = c;
|
||||
}
|
||||
}
|
||||
get name() {
|
||||
return this.getAttribute("name");
|
||||
}
|
||||
set name(a) {
|
||||
this.setAttribute("name", a), this.querySelector("#name").textContent = a;
|
||||
set name(b) {
|
||||
this.setAttribute("name", b), this[S].querySelector("#name").textContent = b;
|
||||
}
|
||||
get price() {
|
||||
return this.getAttribute("price");
|
||||
}
|
||||
set price(a) {
|
||||
this.setAttribute("price", a), this.querySelector("price-input").value = a;
|
||||
set price(b) {
|
||||
this.setAttribute("price", b), this[S].querySelector("price-input").value = b;
|
||||
}
|
||||
get picture_url() {
|
||||
return this.getAttribute("picture-url");
|
||||
}
|
||||
set picture_url(a) {
|
||||
this.setAttribute("picture-url", a), this.querySelector("image-input").src = a;
|
||||
set picture_url(b) {
|
||||
this.setAttribute("picture-url", b), this[S].querySelector("image-input").src = b;
|
||||
}
|
||||
});
|
||||
customElements.define("business-items", class extends HTMLElement {
|
||||
|
2
client/dist/app.js.map
vendored
2
client/dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
@ -25,14 +25,17 @@ customElements.define('business-item', class extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.filter = this.getAttribute('filter');
|
||||
this.name = this.name;
|
||||
this.price = this.price;
|
||||
this.picture_url = this.picture_url;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV === newV) return;
|
||||
switch (name) {
|
||||
case 'filter':
|
||||
return this.filter = newV;
|
||||
case 'name': return this.name = newV;
|
||||
case 'price': return this.price = newV / 100.0;
|
||||
case 'picture-url': return this.picture_url = newV;
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +44,7 @@ customElements.define('business-item', class extends HTMLElement {
|
||||
}
|
||||
set name(v) {
|
||||
this.setAttribute('name', v);
|
||||
this.querySelector('#name').textContent = v;
|
||||
this[S].querySelector('#name').textContent = v;
|
||||
}
|
||||
|
||||
|
||||
@ -50,7 +53,7 @@ customElements.define('business-item', class extends HTMLElement {
|
||||
}
|
||||
set price(v) {
|
||||
this.setAttribute('price', v);
|
||||
this.querySelector('price-input').value = v;
|
||||
this[S].querySelector('price-input').value = v;
|
||||
}
|
||||
|
||||
|
||||
@ -59,6 +62,6 @@ customElements.define('business-item', class extends HTMLElement {
|
||||
}
|
||||
set picture_url(v) {
|
||||
this.setAttribute('picture-url', v);
|
||||
this.querySelector('image-input').src = v;
|
||||
this[S].querySelector('image-input').src = v;
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { S } from "../shared.js";
|
||||
|
||||
customElements.define('image-input', class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['width', 'height', "account-id", "url"]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -10,7 +14,7 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
<style>
|
||||
:host { display: block; border: 1px solid black; }
|
||||
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
||||
input { position: absolute; top: -10px; left: -10px; display: none; }
|
||||
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
|
||||
#view { width: 200px; height: 200px; cursor: pointer; }
|
||||
canvas { width: 200px; height: 200px; }
|
||||
</style>
|
||||
@ -19,10 +23,32 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
<input id="file" type="file" accept="image/*" />
|
||||
<img alt="" src="" />
|
||||
</section>
|
||||
<div id="view"><canvas width="200" height="200"></canvas></div>
|
||||
<div id="view">
|
||||
<canvas width="200" height="200"></canvas>
|
||||
</div>
|
||||
<div>
|
||||
<input id="save" type="button" value="Zapisz" />
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
shadow.querySelector('#save').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const blobBin = atob(canvas.toDataURL("image/webp", 1.0).split(',')[1]);
|
||||
const array = [];
|
||||
for (let i = 0; i < blobBin.length; i++) {
|
||||
array.push(blobBin.charCodeAt(i));
|
||||
}
|
||||
const file = new Blob([new Uint8Array(array)], { type: 'image/webp' });
|
||||
const form = new FormData;
|
||||
form.append(`${ crypto.randomUUID() }.webp`, file);
|
||||
fetch("/upload", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
}).then(res => res.json()).then(({ path }) => this.url = path);
|
||||
});
|
||||
|
||||
const f = new FileReader();
|
||||
const input = shadow.querySelector('#file');
|
||||
const view = shadow.querySelector('#view');
|
||||
@ -39,13 +65,12 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
width = (img.width * 200) / img.height;
|
||||
height = 200;
|
||||
}
|
||||
console.log(img.width, img.height);
|
||||
console.log(width, height);
|
||||
this.setAttribute('width', width);
|
||||
this.setAttribute('height', height);
|
||||
|
||||
img.width = width;
|
||||
img.height = height;
|
||||
// ctx.drawImage(img, 0, 0);
|
||||
ctx.fillStyle = '#F00';
|
||||
ctx.rect(0, 0, 200, 200);
|
||||
ctx.clearRect(0, 0, 200, 200);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
});
|
||||
input.addEventListener('change', ev => {
|
||||
@ -64,4 +89,46 @@ customElements.define('image-input', class extends HTMLElement {
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.account_id = this.account_id;
|
||||
this.url = this.url;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV === newV) return;
|
||||
switch (name) {
|
||||
case 'account-id':
|
||||
return this.account_id = newV;
|
||||
case 'url':
|
||||
return this.url = newV;
|
||||
}
|
||||
}
|
||||
|
||||
get account_id() {
|
||||
return this.getAttribute('account-id');
|
||||
}
|
||||
|
||||
set account_id(v) {
|
||||
this.setAttribute('account-id', v);
|
||||
}
|
||||
|
||||
get width() {
|
||||
const v = parseInt(this.getAttribute('width'));
|
||||
return isNaN(v) ? 0 : v;
|
||||
}
|
||||
|
||||
get height() {
|
||||
const v = parseInt(this.getAttribute('height'));
|
||||
return isNaN(v) ? 0 : v;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.getAttribute('url');
|
||||
}
|
||||
|
||||
set url(v) {
|
||||
this.setAttribute('url', v);
|
||||
this[S].querySelector('img').src = v;
|
||||
}
|
||||
});
|
||||
|
2
migrations/20220707203500_add_picture_url.sql
Normal file
2
migrations/20220707203500_add_picture_url.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE local_business_items
|
||||
ADD COLUMN picture_url TEXT NOT NULL UNIQUE;
|
@ -78,4 +78,5 @@ pub struct LocalBusinessItem {
|
||||
pub name: String,
|
||||
pub price: i64,
|
||||
pub item_order: i32,
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
@ -49,19 +49,21 @@ impl Page {
|
||||
pub struct BusinessItemInput {
|
||||
pub name: String,
|
||||
pub price: u32,
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
impl BusinessItemInput {
|
||||
pub fn new<S: Into<String>>(name: S, price: u32) -> Self {
|
||||
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
price,
|
||||
picture_url: picture_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LocalService {
|
||||
pub struct LocalBusiness {
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub name: String,
|
||||
@ -70,7 +72,7 @@ pub struct LocalService {
|
||||
pub items: Vec<db::LocalBusinessItem>,
|
||||
}
|
||||
|
||||
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalService {
|
||||
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalBusiness {
|
||||
fn from((service, items): (db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)) -> Self {
|
||||
Self {
|
||||
id: service.id,
|
||||
|
@ -6,14 +6,17 @@ use actix_files::Files;
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::*;
|
||||
use askama::Template;
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tracing::*;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
pub struct IndexTemplate {
|
||||
services: Vec<view::LocalService>,
|
||||
services: Vec<view::LocalBusiness>,
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
@ -68,7 +71,8 @@ SELECT
|
||||
local_business_id,
|
||||
name,
|
||||
price,
|
||||
item_order
|
||||
item_order,
|
||||
picture_url
|
||||
FROM local_business_items
|
||||
ORDER BY item_order DESC
|
||||
"#,
|
||||
@ -88,7 +92,7 @@ ORDER BY item_order DESC
|
||||
use crate::model::view::*;
|
||||
services
|
||||
.into_iter()
|
||||
.map(|service| LocalService::from((service, &mut items)))
|
||||
.map(|service| LocalBusiness::from((service, &mut items)))
|
||||
.collect()
|
||||
};
|
||||
|
||||
@ -160,7 +164,7 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
|
||||
.map(|s| s.strip_suffix(']').unwrap_or(s));
|
||||
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
|
||||
match name.next() {
|
||||
Some(s @ ("name" | "price")) => Some((idx, s.to_string(), value)),
|
||||
Some(s @ ("name" | "price" | "picture_url")) => Some((idx, s.to_string(), value)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
@ -177,6 +181,9 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
|
||||
"price" => {
|
||||
item.price = value.parse().unwrap_or_default();
|
||||
}
|
||||
"picture_url" => {
|
||||
item.picture_url = value;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
memo
|
||||
@ -304,15 +311,16 @@ RETURNING id, owner_id, name, description, state
|
||||
for (idx, item) in form.items.unwrap_or_default().iter().enumerate() {
|
||||
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_business_items (local_business_id, name, price, item_order)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, local_business_id, name, price, item_order
|
||||
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
"#,
|
||||
)
|
||||
.bind(business.id)
|
||||
.bind(&item.name)
|
||||
.bind(item.price as i32)
|
||||
.bind(idx as i32)
|
||||
.bind(item.picture_url)
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
match res {
|
||||
@ -427,8 +435,47 @@ WHERE email = $1
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UploadResponse {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn upload(
|
||||
mut payload: actix_multipart::Multipart,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let path = PathBuf::new().join(
|
||||
id.identity()
|
||||
.map(|id| format!("./uploads/{id}"))
|
||||
.unwrap_or_else(|| "./uploads/tmp".into()),
|
||||
);
|
||||
std::fs::create_dir_all(&path)?;
|
||||
|
||||
if let Some(item) = payload.next().await {
|
||||
let mut field = item?;
|
||||
let name = field.name();
|
||||
tracing::info!("Writing file {:?}", name);
|
||||
let path = path.join(name);
|
||||
|
||||
while let Some(chunk) = field.next().await {
|
||||
let chunk = chunk?;
|
||||
std::fs::write(&path, chunk)?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(UploadResponse {
|
||||
path: path.to_str().unwrap_or_default().into(),
|
||||
}))
|
||||
} else {
|
||||
Ok(HttpResponse::BadRequest().finish())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
std::fs::create_dir_all("./uploads").expect("Failed to create ./uploads directory");
|
||||
|
||||
config
|
||||
.service(Files::new("/uploads", "./uploads"))
|
||||
.service(Files::new("/assets/images", "./assets/images"))
|
||||
.service(Files::new("/assets/css", "./assets/css"))
|
||||
.service(
|
||||
@ -441,7 +488,8 @@ pub fn configure(config: &mut ServiceConfig) {
|
||||
.service(account_page)
|
||||
.service(register)
|
||||
.service(logout)
|
||||
.service(login);
|
||||
.service(login)
|
||||
.service(upload);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
Loading…
Reference in New Issue
Block a user