Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
Commit 6a88302e authored by Pierre Jarriges's avatar Pierre Jarriges
Browse files

Merge branch 'upload_files' into 'master'

Upload files

See merge request !9
parents e7a243b2 d68e0cef
No related branches found
No related tags found
1 merge request!9Upload files
Showing
with 1066 additions and 66 deletions
...@@ -8,4 +8,6 @@ target ...@@ -8,4 +8,6 @@ target
public/**/*.js public/**/*.js
public/**/view/* public/**/view/*
public/standard/test_sitemap.xml public/standard/test_sitemap.xml
public/standard/dyn_sitemap.xml public/standard/dyn_sitemap.xml
\ No newline at end of file public/assets/uploads
testing_static
\ No newline at end of file
...@@ -101,7 +101,7 @@ dependencies = [ ...@@ -101,7 +101,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"indexmap", "indexmap",
"itoa", "itoa 0.4.8",
"language-tags", "language-tags",
"lazy_static", "lazy_static",
"log", "log",
...@@ -128,6 +128,24 @@ dependencies = [ ...@@ -128,6 +128,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "actix-multipart"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"bytes 0.5.6",
"derive_more",
"futures-util",
"httparse",
"log",
"mime",
"twoway",
]
[[package]] [[package]]
name = "actix-router" name = "actix-router"
version = "0.2.7" version = "0.2.7"
...@@ -1111,13 +1129,13 @@ dependencies = [ ...@@ -1111,13 +1129,13 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.4" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [ dependencies = [
"bytes 1.1.0", "bytes 1.1.0",
"fnv", "fnv",
"itoa", "itoa 1.0.2",
] ]
[[package]] [[package]]
...@@ -1163,7 +1181,7 @@ dependencies = [ ...@@ -1163,7 +1181,7 @@ dependencies = [
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa 0.4.8",
"pin-project 1.0.8", "pin-project 1.0.8",
"socket2", "socket2",
"tokio", "tokio",
...@@ -1257,6 +1275,12 @@ version = "0.4.8" ...@@ -1257,6 +1275,12 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.55" version = "0.3.55"
...@@ -1281,6 +1305,7 @@ name = "kuadrado_server" ...@@ -1281,6 +1305,7 @@ name = "kuadrado_server"
version = "2.0.2" version = "2.0.2"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-multipart",
"actix-web", "actix-web",
"actix-web-middleware-redirect-https", "actix-web-middleware-redirect-https",
"chrono", "chrono",
...@@ -1313,9 +1338,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" ...@@ -1313,9 +1338,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.102" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
...@@ -1325,9 +1350,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" ...@@ -1325,9 +1350,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.5" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
] ]
...@@ -1588,9 +1613,9 @@ checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" ...@@ -1588,9 +1613,9 @@ checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.8.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
...@@ -2107,7 +2132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" ...@@ -2107,7 +2132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa 0.4.8",
"ryu", "ryu",
"serde", "serde",
] ]
...@@ -2119,7 +2144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" ...@@ -2119,7 +2144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa", "itoa 0.4.8",
"ryu", "ryu",
"serde", "serde",
] ]
...@@ -2537,9 +2562,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" ...@@ -2537,9 +2562,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.28" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"log", "log",
...@@ -2549,11 +2574,11 @@ dependencies = [ ...@@ -2549,11 +2574,11 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.20" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf" checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
dependencies = [ dependencies = [
"lazy_static", "once_cell",
] ]
[[package]] [[package]]
...@@ -2612,6 +2637,16 @@ version = "0.2.3" ...@@ -2612,6 +2637,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "twoway"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
dependencies = [
"memchr",
"unchecked-index",
]
[[package]] [[package]]
name = "typed-builder" name = "typed-builder"
version = "0.4.1" version = "0.4.1"
...@@ -2635,6 +2670,12 @@ version = "0.1.3" ...@@ -2635,6 +2670,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.6.0" version = "2.6.0"
......
...@@ -25,3 +25,4 @@ time = "0.2.7" ...@@ -25,3 +25,4 @@ time = "0.2.7"
regex = "1.5" regex = "1.5"
tokio = { version = "0.2", features = ["full"] } tokio = { version = "0.2", features = ["full"] }
sitemap = "0.4.1" sitemap = "0.4.1"
actix-multipart = "0.3"
"use strict"; "use strict";
const Article = require("../article"); const Article = require("../article");
const { images_url } = require("../constants");
const { fetch_post_article, fetch_article, fetch_update_article } = require("../xhr"); const { fetch_post_article, fetch_article, fetch_update_article } = require("../xhr");
class CreateArticleForm { class CreateArticleForm {
...@@ -23,6 +22,7 @@ class CreateArticleForm { ...@@ -23,6 +22,7 @@ class CreateArticleForm {
const metadata = Object.assign(this.state.output.metadata, { const metadata = Object.assign(this.state.output.metadata, {
[field]: e.target.value, [field]: e.target.value,
}); });
this.state.output.metadata = metadata; this.state.output.metadata = metadata;
} }
...@@ -166,22 +166,26 @@ class CreateArticleForm { ...@@ -166,22 +166,26 @@ class CreateArticleForm {
{ {
tag: "img", tag: "img",
style_rules: { minWidth: "100%", minHeight: "100%" }, style_rules: { minWidth: "100%", minHeight: "100%" },
src: img ? `${images_url}/${img}` : "", src: img || "",
} }
], ],
}, },
{ {
tag: "input", tag: "select",
type: "text", contents: [
placeholder: "image file name", { tag: "option", value: "", contents: "choose", disabled: true, selected: !this.state.output.images[i] }
value: img, ].concat((this.params.files_index.images || []).map(url => {
oninput: e => { return {
tag: "option",
value: url,
selected: this.state.output.images[i] && this.state.output.images[i] === url,
contents: url.split("/").reverse()[0],
}
})),
onchange: e => {
this.state.output.images[i] = e.target.value; this.state.output.images[i] = e.target.value;
} this.refresh_images();
}, },
{
tag: "button", contents: "OK",
onclick: this.refresh_images.bind(this)
}, },
{ {
tag: "button", contents: "DEL", tag: "button", contents: "DEL",
...@@ -234,7 +238,7 @@ class CreateArticleForm { ...@@ -234,7 +238,7 @@ class CreateArticleForm {
return { return {
tag: "img", tag: "img",
style_rules: { height: "100px", width: "auto" }, style_rules: { height: "100px", width: "auto" },
src: `${images_url}/${img}` src: img
} }
}) })
}, },
......
"use strict";
const { fetch_post_file, fetch_delete_static_file } = require("../xhr");
class FilesIndexView {
constructor(params) {
this.params = params;
this.show_category = Object.keys(this.params.get_files_index())[0];
this.id = "files-index-view";
}
get_files_list_style(category) {
const base_style = {
listStyleType: "none",
margin: 0,
padding: 0,
overflow: "auto",
height: "300px",
};
switch (category) {
case "images":
return {
display: "flex",
flexWrap: "wrap",
gap: "5px",
...base_style,
}
case "sounds":
return base_style // TMP
default:
return base_style
}
}
get_item_view(item) {
const get_link = () => {
switch (this.show_category) {
case "images":
return {
tag: "a",
href: item,
target: "_blank",
style_rules: {
backgroundImage: `url(${item})`,
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
backgroundPosition: "center",
width: "100px",
height: "100px",
display: "block"
}
}
case "sounds":
return {
tag: "div",
contents: [
{
tag: "audio",
controls: true,
contents: [
{
tag: "source",
src: item
}]
},
{
tag: "a",
href: item,
target: "_blank",
contents: item
},
]
}
default:
return {
tag: "a",
href: item,
target: "_blank",
contents: item
}
}
}
return {
tag: "div",
contents: [
get_link(),
{
tag: "button",
contents: "DEL",
onclick: () => {
const rev_split = item.split("/").reverse();
const filename = rev_split.shift();
const category = rev_split.shift();
const dir = rev_split[0] === "uploads" ? "uploads" : "base";
fetch_delete_static_file(dir, category, filename).then(res => {
this.params.refresh_index(this.refresh.bind(this));
}).catch(err => console.log(err))
}
}
]
}
}
refresh() {
obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" })
}
render() {
const files_index = this.params.get_files_index();
return {
tag: "div",
id: this.id,
contents: [
{
tag: "ul",
style_rules: {
display: "flex",
listStyleType: "none",
margin: 0,
padding: 0,
},
contents: Object.keys(files_index).map(cat => {
return {
tag: "li",
style_rules: {
cursor: "pointer",
fontWeight: "bold",
padding: "20px",
border: cat === this.show_category ? "1px solid black" : "none"
},
contents: cat,
onclick: () => {
this.show_category = cat;
this.refresh();
}
}
})
},
{
tag: "ul",
style_rules: this.get_files_list_style(this.show_category),
contents: files_index[this.show_category].map(item => {
return {
tag: "li",
contents: [
this.get_item_view(item)
]
}
}),
}
]
}
}
}
class ManageFilesForm {
constructor(params) {
this.params = params;
this.id = "static-file-manager-view";
}
render_index_view() {
return new FilesIndexView(this.params).render()
}
refresh() {
obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" })
}
render() {
return {
tag: "div",
id: this.id,
contents: [
this.render_index_view(),
{
tag: "form",
style_rules: { border: "1px solid black" },
enctype: "multipart/form-data",
onsubmit: e => {
e.preventDefault();
fetch_post_file(e.target).then(() => {
this.params.refresh_index(this.refresh.bind(this))
}).catch(err => console.log(err));
},
contents: [
{ tag: "input", name: "file", type: "file", multiple: true },
{ tag: "input", type: "submit" }
]
}
]
}
}
}
module.exports = ManageFilesForm;
\ No newline at end of file
const { fetch_static_files_index } = require("../xhr");
const CreateArticleForm = require("./create-article-form"); const CreateArticleForm = require("./create-article-form");
const ManageFilesForm = require("./manage-files-form");
const UpdateArticleForm = require("./update-article-form"); const UpdateArticleForm = require("./update-article-form");
class RootComponent { class RootComponent {
constructor() { constructor() {
this.state = { this.state = {
selected_tab: "" selected_tab: "",
loading_index: true,
static_files_index: {}
}; };
this.fetch_files_index(obj2htm.renderCycle.bind(obj2htm));
}
build_index_object(files_urls) {
return files_urls.reduce((o, url) => {
const split_url = url.split("/");
const cat = split_url[(split_url[2] === "uploads") ? 3 : 2];
o[cat] = o[cat] ? [...o[cat], url] : [url];
return o;
}, {});
}
fetch_files_index(cb) {
fetch_static_files_index()
.then(files =>
this.state.static_files_index = this.build_index_object(files)
)
.catch(err => console.log(err))
.finally(() => {
this.state.loading_index = false;
cb();
});
} }
handle_nav_click(e) { handle_nav_click(e) {
...@@ -15,11 +42,14 @@ class RootComponent { ...@@ -15,11 +42,14 @@ class RootComponent {
} }
render_state() { render_state() {
const files_index = this.state.static_files_index;
switch (this.state.selected_tab) { switch (this.state.selected_tab) {
case "create": case "create":
return new CreateArticleForm().render(); return new CreateArticleForm({ files_index }).render();
case "update": case "update":
return new UpdateArticleForm().render(); return new UpdateArticleForm({ files_index }).render();
case "files":
return new ManageFilesForm({ get_files_index: () => this.state.static_files_index, refresh_index: this.fetch_files_index.bind(this) }).render()
default: default:
return undefined; return undefined;
} }
...@@ -43,6 +73,11 @@ class RootComponent { ...@@ -43,6 +73,11 @@ class RootComponent {
class: this.state.selected_tab === "update" ? "selected" : "", class: this.state.selected_tab === "update" ? "selected" : "",
onclick: this.handle_nav_click.bind(this), onclick: this.handle_nav_click.bind(this),
}, },
{
tag: "span", contents: "Manage files", tab_name: "files",
class: this.state.selected_tab === "files" ? "selected" : "",
onclick: this.handle_nav_click.bind(this),
},
], ],
}, },
this.render_state(), this.render_state(),
......
...@@ -5,7 +5,8 @@ const ArticleList = require("./articles-list"); ...@@ -5,7 +5,8 @@ const ArticleList = require("./articles-list");
const CreateArticleForm = require("./create-article-form"); const CreateArticleForm = require("./create-article-form");
class UpdateArticleForm { class UpdateArticleForm {
constructor() { constructor(params) {
this.params = params;
this.state = { this.state = {
search_article_title: "", search_article_title: "",
article_to_update: {}, article_to_update: {},
...@@ -63,7 +64,8 @@ class UpdateArticleForm { ...@@ -63,7 +64,8 @@ class UpdateArticleForm {
on_article_sent: () => { on_article_sent: () => {
this.reset(); this.reset();
this.articles_list.fetch_list(); this.articles_list.fetch_list();
} },
...this.params
}).render()] }).render()]
: [] : []
} }
......
...@@ -104,6 +104,49 @@ function fetch_all_articles() { ...@@ -104,6 +104,49 @@ function fetch_all_articles() {
}); });
} }
function fetch_post_file(form) {
return new Promise((resolve, reject) => {
fetch("/post-files", {
method: "POST",
body: new FormData(form),
}).then(async res => {
if (res.status >= 400 && res.status < 600) {
reject((await res.text()))
} else {
resolve((await res.json()));
}
}).catch(err => reject(err))
})
}
function fetch_static_files_index() {
return new Promise((resolve, reject) => {
fetch("/static-files-index").then(async res => {
if (res.status >= 400 && res.status < 600) {
reject((await res.text()))
} else {
resolve((await res.json()));
}
}).catch(err => reject(err))
})
}
function fetch_delete_static_file(dir, category, filename) {
return new Promise((resolve, reject) => {
fetch(`/delete-file/${dir}/${category}/${filename}`, {
credentials: 'include',
method: "DELETE",
}).then(async res => {
const text = await res.text();
if (res.status >= 400 && res.status < 600) {
reject(text)
} else {
resolve(text);
}
}).catch(err => reject(err))
})
}
module.exports = { module.exports = {
fetch_article, fetch_article,
...@@ -112,4 +155,7 @@ module.exports = { ...@@ -112,4 +155,7 @@ module.exports = {
fetch_update_article, fetch_update_article,
fetch_delete_article, fetch_delete_article,
fetch_all_articles, fetch_all_articles,
fetch_post_file,
fetch_static_files_index,
fetch_delete_static_file,
} }
\ No newline at end of file
...@@ -7,12 +7,7 @@ db.auth(adminname, adminpwd); ...@@ -7,12 +7,7 @@ db.auth(adminname, adminpwd);
articles = db.getCollection("articles"); articles = db.getCollection("articles");
articles.update({}, articles.find().forEach(art => {
{ art.images = art.images.map(img => "/assets/images/" + img);
$set: { articles.save(art);
"metadata": { "description": "" }, });
"with_static_view": false \ No newline at end of file
}
},
{ upsert: false, multi: true }
);
\ No newline at end of file
pub mod static_files;
use crate::env::Env;
use std::fs::create_dir_all;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct StaticFilesIndex(pub Vec<String>);
impl StaticFilesIndex {
fn rec_read_dir(root: &Path, files: &mut Vec<String>, strip_from: &Path) {
for entry in root.read_dir().unwrap() {
if let Ok(entry) = entry {
if entry.path().is_dir() {
StaticFilesIndex::rec_read_dir(&entry.path(), files, strip_from);
} else {
StaticFilesIndex::_push_path(&entry.path(), files, strip_from);
}
}
}
}
pub fn get_public_dir(env: &Env) -> std::path::PathBuf {
match std::env::var("CONTEXT") {
Ok(v) => match &v[..] {
"testing" => StaticFilesIndex::create_if_missing_testing_static(),
_ => env.public_dir.clone(),
},
Err(_) => env.public_dir.clone(),
}
}
fn create_if_missing_testing_static() -> std::path::PathBuf {
let path = std::env::current_dir().unwrap().join("testing_static");
if !path.exists() {
std::fs::create_dir_all(path.join("assets")).unwrap();
}
path
}
fn _push_path(path: &Path, files: &mut Vec<String>, strip_from: &Path) {
let push_path = path.strip_prefix(strip_from).unwrap();
files.push(format!("/{}", push_path.to_str().unwrap().to_owned()));
}
pub fn rebuild(&mut self, env: &Env) {
let public_dir = StaticFilesIndex::get_public_dir(env);
let root = public_dir.join("assets");
self.0 = Vec::new();
StaticFilesIndex::rec_read_dir(&root, &mut self.0, &public_dir);
}
pub fn push_path(&mut self, path: &Path, env: &Env) {
let strip_from = StaticFilesIndex::get_public_dir(env);
StaticFilesIndex::_push_path(path, &mut self.0, &strip_from);
}
pub fn remove_path(&mut self, strpath: String) {
self.0 = self
.0
.iter()
.filter(|url| strpath != **url)
.map(|s| s.to_owned())
.collect();
}
pub fn get_index(&self) -> Vec<String> {
self.0.clone()
}
}
#[derive(Debug, PartialEq)]
pub enum UploadType {
Image,
Sound,
Video,
Doc,
}
pub struct UploadData {
pub up_type: UploadType,
pub filename: String,
}
pub fn create_dir_if_missing(path: std::path::PathBuf) -> std::path::PathBuf {
if !path.exists() {
create_dir_all(&path).unwrap();
}
path
}
pub fn get_uploads_dir(public_dir: std::path::PathBuf) -> std::path::PathBuf {
create_dir_if_missing(public_dir.join("assets/uploads"))
}
pub fn dirname_from_type(upload_type: &UploadType) -> String {
match upload_type {
UploadType::Image => String::from("images"),
UploadType::Sound => String::from("sounds"),
UploadType::Video => String::from("videos"),
UploadType::Doc => String::from("docs"),
}
}
pub fn upload_type_from_file_ext(ext: &String) -> UploadType {
match &ext[..] {
"webp" | "jpg" | "png" | "jpeg" | "bmp" | "gif" => UploadType::Image,
"mp3" | "ogg" | "wav" | "opus" => UploadType::Sound,
"mp4" | "webm" | "ogv" => UploadType::Video,
_ => UploadType::Doc,
}
}
pub fn file_ext(file_name: &String) -> Result<String, String> {
let parts = file_name.split(".").collect::<Vec<&str>>();
let err_msg = format!("Couldn't get extension from filename : {}", file_name);
if parts.len() < 2 {
return Err(err_msg);
}
match parts.last() {
Some(ext) => Ok(ext.to_string()),
None => Err(err_msg),
}
}
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
* _______ ______ ______ _______ *@@
* |__ __@ | ____@ / ____@ |__ __@ *@@
* | @ | @__ \_ @_ | @ *@@
* | @ | __@ \ @_ | @ *@@
* | @ | @___ ____\ @ | @ *@@
* |__@ |______@ \______@ |__@ *@@
* *@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/
#[cfg(test)]
mod test_static_files {
use super::*;
use std::{fs::remove_dir_all, path::PathBuf};
#[test]
fn should_create_missing_directory() {
let missing_path = PathBuf::from("./some_nested/missing_dir");
let created_path = create_dir_if_missing(missing_path);
assert!(created_path.exists());
remove_dir_all(PathBuf::from("./some_nested")).unwrap();
}
#[test]
fn uploads_subdirs_should_be_consistent() {
assert_eq!(
upload_type_from_file_ext(&"jpg".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"jpeg".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"png".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"bmp".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"gif".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"webp".to_string()),
UploadType::Image
);
assert_eq!(
upload_type_from_file_ext(&"mp3".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"ogg".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"wav".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"opus".to_string()),
UploadType::Sound
);
assert_eq!(
upload_type_from_file_ext(&"mp4".to_string()),
UploadType::Video
);
assert_eq!(
upload_type_from_file_ext(&"webm".to_string()),
UploadType::Video
);
assert_eq!(
upload_type_from_file_ext(&"ogv".to_string()),
UploadType::Video
);
assert_eq!(
upload_type_from_file_ext(&"any".to_string()),
UploadType::Doc
);
assert_eq!(
upload_type_from_file_ext(&"jszaj".to_string()),
UploadType::Doc
);
}
#[test]
fn should_get_filename_extension() {
fn valid_ext(ext: Result<String, String>) -> String {
assert!(ext.is_ok());
ext.unwrap()
}
let filename = String::from("somefilename.png");
let ext = valid_ext(file_ext(&filename));
assert_eq!(ext, "png");
let filename = String::from("aïe aïe aïe.wav");
let ext = valid_ext(file_ext(&filename));
assert_eq!(ext, "wav");
let filename = String::from("salut Ça va.machin.jpg");
let ext = valid_ext(file_ext(&filename));
assert_eq!(ext, "jpg");
let filename = String::from("no extension");
let ext = file_ext(&filename);
assert!(ext.is_err());
}
}
//! # WEB SERVER FOR THE KUADRADO SOFTWARE WEBSITE //! # WEB SERVER FOR THE KUADRADO SOFTWARE WEBSITE
mod app_state; mod app_state;
mod core;
mod crypto; mod crypto;
mod env; mod env;
mod init_admin; mod init_admin;
...@@ -11,6 +12,7 @@ mod static_view; ...@@ -11,6 +12,7 @@ mod static_view;
mod tls; mod tls;
mod view; mod view;
mod view_resource; mod view_resource;
use crate::core::static_files::StaticFilesIndex;
use actix_files::Files; use actix_files::Files;
use actix_web::{ use actix_web::{
middleware::{normalize::TrailingSlash, Logger, NormalizePath}, middleware::{normalize::TrailingSlash, Logger, NormalizePath},
...@@ -37,6 +39,10 @@ async fn main() -> std::io::Result<()> { ...@@ -37,6 +39,10 @@ async fn main() -> std::io::Result<()> {
let server_port_tls = env_var("SERVER_PORT_TLS").expect("SERVER_PORT_TLS is not defined."); let server_port_tls = env_var("SERVER_PORT_TLS").expect("SERVER_PORT_TLS is not defined.");
let app_state = AppState::with_default_admin_user().await; let app_state = AppState::with_default_admin_user().await;
let mut static_files_index = StaticFilesIndex(Vec::new());
static_files_index.rebuild(&app_state.env);
let static_files_index = Data::new(std::sync::Mutex::new(static_files_index));
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(Logger::default()) .wrap(Logger::default())
...@@ -47,6 +53,7 @@ async fn main() -> std::io::Result<()> { ...@@ -47,6 +53,7 @@ async fn main() -> std::io::Result<()> {
)])) )]))
.wrap(actix_web::middleware::Compress::default()) .wrap(actix_web::middleware::Compress::default())
.app_data(Data::new(app_state.clone())) .app_data(Data::new(app_state.clone()))
.app_data(Data::clone(&static_files_index))
.app_data(Data::new(AuthenticatedAdminMiddleware::new( .app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth", "kuadrado-admin-auth",
))) )))
...@@ -87,6 +94,9 @@ async fn main() -> std::io::Result<()> { ...@@ -87,6 +94,9 @@ async fn main() -> std::io::Result<()> {
.service(get_articles_by_category) .service(get_articles_by_category)
.service(get_article) .service(get_article)
.service(get_all_articles) .service(get_all_articles)
.service(post_files)
.service(get_static_files_index)
.service(delete_static_file)
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////
// STANDARD FILES /////////////////////////////////////////////////////////////////////////////////////////// // STANDARD FILES ///////////////////////////////////////////////////////////////////////////////////////////
.service(resource("/favicon.ico").route(get().to(favicon))) .service(resource("/favicon.ico").route(get().to(favicon)))
......
mod admin_auth; mod admin_auth;
mod articles; mod articles;
mod static_files;
pub use admin_auth::*; pub use admin_auth::*;
pub use articles::*; pub use articles::*;
pub use static_files::*;
...@@ -364,7 +364,7 @@ mod test_articles { ...@@ -364,7 +364,7 @@ mod test_articles {
if path.exists() && path.is_dir() { if path.exists() && path.is_dir() {
let res = remove_dir_all(path); let res = remove_dir_all(path);
if let Err(e) = res { if let Err(e) = res {
return Err(format!("Error removing testing stativ views {}", e)); return Err(format!("Error removing testing static views {}", e));
} }
} }
Ok(()) Ok(())
...@@ -431,6 +431,7 @@ mod test_articles { ...@@ -431,6 +431,7 @@ mod test_articles {
// Static view tests // Static view tests
let static_view_path = find_inserted.metadata.static_resource_path; let static_view_path = find_inserted.metadata.static_resource_path;
assert!(static_view_path.is_some()); assert!(static_view_path.is_some());
let static_view_path = static_view_path.unwrap(); let static_view_path = static_view_path.unwrap();
assert!(static_view_path.exists()); assert!(static_view_path.exists());
......
use crate::{core::static_files::*, middleware::AuthenticatedAdminMiddleware, AppState};
use actix_multipart::Multipart;
use actix_web::{
delete, get, post,
web::{Data, Path},
HttpRequest, HttpResponse, Responder,
};
use futures::StreamExt;
use std::{
fs::{remove_file, File},
io::Write,
};
fn upload_data_from_multipart_field(field: &actix_multipart::Field) -> Result<UploadData, String> {
match field.content_disposition() {
Some(content_disposition) => match content_disposition.get_filename() {
Some(fname) => match file_ext(&fname.to_string()) {
Ok(ext) => Ok(UploadData {
up_type: upload_type_from_file_ext(&ext),
filename: fname.to_owned(),
}),
Err(msg) => return Err(msg),
},
None => Err("Couldn't retrieve file extension".to_string()),
},
None => Err("Missing content disposition".to_string()),
}
}
async fn write_uploaded_file(
app_state: &AppState,
field: &mut actix_multipart::Field,
filename: &String,
upload_type: UploadType,
) -> Result<String, String> {
let uploads_dir = get_uploads_dir(StaticFilesIndex::get_public_dir(&app_state.env));
let sub_dir = dirname_from_type(&upload_type);
let filepath = create_dir_if_missing(uploads_dir.join(&sub_dir)).join(&filename);
match File::create(&filepath) {
Err(e) => Err(format!("Error creating file {:?} : {:?}", filepath, e)),
Ok(mut f) => {
// Field in turn is stream of *Bytes* object
while let Some(chunk) = field.next().await {
match chunk {
Ok(chunk) => {
if f.write_all(&chunk).is_err() {
remove_file(&filepath).unwrap();
return Err("Error writing chunk".to_string());
}
}
Err(e) => {
return Err(format!("Error writing file {} : {:?}", filename, e));
}
}
}
Ok(filepath.into_os_string().into_string().unwrap())
}
}
}
#[post("/post-files")]
pub async fn post_files(
app_state: Data<AppState>,
static_files_index: Data<std::sync::Mutex<StaticFilesIndex>>,
mut payload: Multipart,
middleware: Data<AuthenticatedAdminMiddleware<'_>>,
req: HttpRequest,
) -> impl Responder {
if middleware.exec(&app_state, &req, None).await.is_err() {
return HttpResponse::Unauthorized().finish();
}
let mut uploaded_filepathes = Vec::new();
let mut files_index = static_files_index.lock().unwrap();
while let Some(item) = payload.next().await {
match item {
Ok(mut field) => {
let up_data = upload_data_from_multipart_field(&field);
if let Err(msg) = up_data {
return HttpResponse::InternalServerError().body(msg);
}
let up_data = up_data.unwrap();
match write_uploaded_file(
&app_state,
&mut field,
&up_data.filename,
up_data.up_type,
)
.await
{
Err(msg) => return HttpResponse::InternalServerError().body(msg),
Ok(filepath) => {
files_index.push_path(std::path::Path::new(&filepath), &app_state.env);
uploaded_filepathes.push(filepath);
}
}
}
Err(e) => {
return HttpResponse::InternalServerError().body(format!("FIELD ERR {:?}", e))
}
}
}
HttpResponse::Ok().json(uploaded_filepathes)
}
#[get("/static-files-index")]
async fn get_static_files_index(
static_files_index: Data<std::sync::Mutex<StaticFilesIndex>>,
) -> impl Responder {
HttpResponse::Ok().json(
static_files_index
.lock()
.expect("Couldn't lock files index")
.get_index(),
)
}
#[delete("/delete-file/{dir}/{category}/{filename}")]
async fn delete_static_file(
app_state: Data<AppState>,
static_files_index: Data<std::sync::Mutex<StaticFilesIndex>>,
fileinfo: Path<(String, String, String)>,
middleware: Data<AuthenticatedAdminMiddleware<'_>>,
req: HttpRequest,
) -> impl Responder {
if middleware.exec(&app_state, &req, None).await.is_err() {
return HttpResponse::Unauthorized().finish();
}
let (dir, cat, fname) = fileinfo.into_inner();
let fpath = std::path::PathBuf::from(cat).join(fname);
let fbasedir = {
let p = std::path::PathBuf::from("assets");
if dir == "uploads" {
p.join("uploads")
} else {
p
}
};
match remove_file(
StaticFilesIndex::get_public_dir(&app_state.env)
.join(&fbasedir)
.join(&fpath),
) {
Ok(_) => {
let indexed_url = std::path::PathBuf::from("/").join(&fbasedir).join(&fpath);
let mut files_index = static_files_index.lock().unwrap();
files_index.remove_path(indexed_url.to_str().unwrap().to_owned());
HttpResponse::Accepted().body("File was deleted")
}
Err(e) => HttpResponse::InternalServerError().body(format!("Error deleting file {:?}", e)),
}
}
// EXAMPLE FROM ACTIX REPO (using threadpool)
// use futures::TryStreamExt;
// use std::io::Write;
// pub async fn save_file(mut payload: Multipart) -> impl Responder {
// println!("SAVE FILE");
// // iterate over multipart stream
// while let Some(mut field) = payload.try_next().await.unwrap() {
// // A multipart/form-data stream has to contain `content_disposition`
// let content_disposition = field.content_disposition().unwrap();
// let filename = content_disposition.get_filename().unwrap();
// let filepath = format!("./tmp/{filename}");
// // File::create is blocking operation, use threadpool
// let mut f = actix_web::web::block(|| std::fs::File::create(filepath))
// .await
// .unwrap();
// // Field in turn is stream of *Bytes* object
// while let Some(chunk) = field.try_next().await.unwrap() {
// // filesystem operations are blocking, we have to use threadpool
// f = actix_web::web::block(move || f.write_all(&chunk).map(|_| f))
// .await
// .unwrap();
// }
// }
// HttpResponse::Ok().body("sucess")
// }
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
* _______ ______ ______ _______ *@@
* |__ __@ | ____@ / ____@ |__ __@ *@@
* | @ | @__ \_ @_ | @ *@@
* | @ | __@ \ @_ | @ *@@
* | @ | @___ ____\ @ | @ *@@
* |__@ |______@ \______@ |__@ *@@
* *@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/
#[cfg(test)]
mod test_static_files {
use super::*;
use crate::{
core::static_files::StaticFilesIndex,
middleware::get_auth_cookie,
model::{AdminAuthCredentials, Administrator},
};
use actix_web::{
http::{Method, StatusCode},
test,
web::Bytes,
App,
};
fn create_simple_request() -> Bytes {
Bytes::from(
"--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"data.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
)
}
fn create_files_index(app_state: &AppState) -> Data<std::sync::Mutex<StaticFilesIndex>> {
let mut static_files_index = StaticFilesIndex(Vec::new());
static_files_index.rebuild(&app_state.env);
Data::new(std::sync::Mutex::new(static_files_index))
}
fn clear_testing_static() {
std::fs::remove_dir_all(std::env::current_dir().unwrap().join("testing_static")).unwrap();
}
#[tokio::test]
async fn post_files_unauthenticated_should_be_unauthorized() {
let app_state = AppState::for_test().await;
let static_files_index = create_files_index(&app_state);
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::clone(&static_files_index))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(post_files),
)
.await;
let req = test::TestRequest::with_uri("/post-files")
.method(Method::POST)
.header(
"Content-Type",
"multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
)
.cookie(get_auth_cookie(
"wrong-cookie",
app_state.encryption.random_ascii_lc_string(32),
))
.set_payload(create_simple_request())
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_post_files() {
let app_state = AppState::for_test().await;
let admin_user = Administrator::authenticated(
&app_state,
AdminAuthCredentials {
username: app_state.env.default_admin_username.to_owned(),
password: app_state.env.default_admin_password.to_owned(),
},
)
.await
.unwrap();
let static_files_index = create_files_index(&app_state);
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::clone(&static_files_index))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(post_files),
)
.await;
let req = test::TestRequest::with_uri("/post-files")
.method(Method::POST)
.header(
"Content-Type",
"multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
)
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state
.encryption
.decrypt(&admin_user.auth_token.unwrap())
.to_owned(),
))
.set_payload(create_simple_request())
.to_request();
let resp = test::call_service(&mut app, req).await;
let status = resp.status();
assert_eq!(status, StatusCode::OK);
let pathes: Vec<String> = test::read_body_json(resp).await;
let public_dir = StaticFilesIndex::get_public_dir(&app_state.env);
let pathes_from_public = pathes
.iter()
.map(|p| {
format!(
"/{}",
std::path::Path::new(p)
.strip_prefix(&public_dir)
.unwrap()
.to_str()
.unwrap()
)
})
.collect::<Vec<String>>();
let index = static_files_index.lock().unwrap();
assert_eq!(pathes_from_public, index.0);
let mut iter_pathes = pathes.iter();
let f = std::fs::read_to_string(iter_pathes.next().unwrap()).unwrap();
assert_eq!(f, "test");
let f = std::fs::read_to_string(iter_pathes.next().unwrap()).unwrap();
assert_eq!(f, "data");
clear_testing_static();
}
#[tokio::test]
async fn test_delete_file() {
let app_state = AppState::for_test().await;
let admin_user = Administrator::authenticated(
&app_state,
AdminAuthCredentials {
username: app_state.env.default_admin_username.to_owned(),
password: app_state.env.default_admin_password.to_owned(),
},
)
.await
.unwrap();
let static_files_index = create_files_index(&app_state);
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::clone(&static_files_index))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(post_files)
.service(delete_static_file),
)
.await;
let auth_token = admin_user.auth_token.unwrap();
let req = test::TestRequest::with_uri("/post-files")
.method(Method::POST)
.header(
"Content-Type",
"multipart/form-data; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
)
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state.encryption.decrypt(&auth_token).to_owned(),
))
.set_payload(create_simple_request())
.to_request();
let resp = test::call_service(&mut app, req).await;
let status = resp.status();
assert_eq!(status, StatusCode::OK);
let req = test::TestRequest::with_uri("/delete-file/uploads/docs/test.txt")
.method(Method::DELETE)
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state.encryption.decrypt(&auth_token).to_owned(),
))
.to_request();
let resp = test::call_service(&mut app, req).await;
let status = resp.status();
assert_eq!(status, StatusCode::ACCEPTED);
clear_testing_static();
}
}
{ {
"name": "kuadrado-website", "name": "kuadrado-website",
"version": "1.0.4", "version": "1.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kuadrado-website", "name": "kuadrado-website",
"version": "1.0.4", "version": "1.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ks-cheap-translator": "^0.1.0", "ks-cheap-translator": "^0.1.0",
......
...@@ -4,7 +4,6 @@ const ImageCarousel = require("../generic-components/image-carousel"); ...@@ -4,7 +4,6 @@ const ImageCarousel = require("../generic-components/image-carousel");
const { getArticleBody } = require("../lib/article-utils"); const { getArticleBody } = require("../lib/article-utils");
const { fetch_json_or_error_text } = require("../lib/fetch"); const { fetch_json_or_error_text } = require("../lib/fetch");
const { MentaloEngine } = require("mentalo-engine"); const { MentaloEngine } = require("mentalo-engine");
const { images_url, data_url } = require("../../constants");
class GameArticle { class GameArticle {
...@@ -41,16 +40,16 @@ class GameArticle { ...@@ -41,16 +40,16 @@ class GameArticle {
tag: "button", tag: "button",
class: "play-button", class: "play-button",
contents: "▶️&nbsp;&nbsp;" + t("Jouer"), contents: "▶️&nbsp;&nbsp;" + t("Jouer"),
onclick: this.handle_click_play.bind(this, button_data.filename, button_data.engine) onclick: this.handle_click_play.bind(this, button_data.fileurl, button_data.engine)
}; };
} }
load_and_run_mentalo_game(filename, button_element) { load_and_run_mentalo_game(fileurl, button_element) {
const button_text = button_element.innerHTML; const button_text = button_element.innerHTML;
button_element.innerHTML = "Loading ..."; button_element.innerHTML = "Loading ...";
button_element.style.pointerEvents = "none"; button_element.style.pointerEvents = "none";
fetch_json_or_error_text(`${data_url}/${filename}`) fetch_json_or_error_text(fileurl)
.then(game_data => { .then(game_data => {
const container = document.createElement("div"); const container = document.createElement("div");
container.style.position = "fixed"; container.style.position = "fixed";
...@@ -88,10 +87,10 @@ class GameArticle { ...@@ -88,10 +87,10 @@ class GameArticle {
}); });
} }
handle_click_play(filename, engine, e) { handle_click_play(fileurl, engine, e) {
switch (engine) { switch (engine) {
case "mentalo": case "mentalo":
this.load_and_run_mentalo_game(filename, e.target); this.load_and_run_mentalo_game(fileurl, e.target);
break; break;
default: default:
console.log("Error, unkown engine") console.log("Error, unkown engine")
...@@ -116,7 +115,7 @@ class GameArticle { ...@@ -116,7 +115,7 @@ class GameArticle {
tag: "div", tag: "div",
id: "article-banner", id: "article-banner",
style_rules: { style_rules: {
backgroundImage: `url(${images_url}/${images[0]})`, backgroundImage: `url(${images[0]})`,
}, },
contents: [ contents: [
{ {
...@@ -158,7 +157,7 @@ class GameArticle { ...@@ -158,7 +157,7 @@ class GameArticle {
}, },
], ],
}, },
trad_ready && new ImageCarousel({ images: images.map(img => `${images_url}/${img}`) }).render(), trad_ready && new ImageCarousel({ images }).render(),
this.details.length > 0 && { this.details.length > 0 && {
tag: "div", tag: "div",
class: "article-details", class: "article-details",
......
"use strict"; "use strict";
const ImageCarousel = require("../generic-components/image-carousel"); const ImageCarousel = require("../generic-components/image-carousel");
const { images_url } = require("../../constants");
const { getArticleBody } = require("../lib/article-utils"); const { getArticleBody } = require("../lib/article-utils");
...@@ -35,7 +34,7 @@ class SoftwareArticle { ...@@ -35,7 +34,7 @@ class SoftwareArticle {
contents: [ contents: [
{ {
tag: "img", tag: "img",
src: `${images_url}/${logo}` src: logo
}, },
] ]
}, },
...@@ -76,7 +75,7 @@ class SoftwareArticle { ...@@ -76,7 +75,7 @@ class SoftwareArticle {
tag: "div", tag: "div",
class: "article-more", class: "article-more",
contents: [ contents: [
trad_ready && screens.length > 0 && new ImageCarousel({ images: screens.map(img => `${images_url}/${img}`) }).render(), trad_ready && screens.length > 0 && new ImageCarousel({ images: screens }).render(),
details.length > 0 && { details.length > 0 && {
tag: "div", tag: "div",
class: "article-details", class: "article-details",
......
"use strict"; "use strict";
const { images_url } = require("../../constants");
class ThemeCard { class ThemeCard {
constructor(props) { constructor(props) {
this.props = props; this.props = props;
...@@ -16,7 +14,12 @@ class ThemeCard { ...@@ -16,7 +14,12 @@ class ThemeCard {
{ {
tag: "div", tag: "div",
class: "card-img", class: "card-img",
contents: [{ tag: "img", alt: `thematic image ${this.props.img.replace(/\.[A-Za-z]+/, "")}`, src: `${images_url}/${this.props.img}` }], contents: [
{
tag: "img",
alt: `thematic image ${this.props.img.split("/").reverse()[0].replace(/\.[A-Za-z]+/, "")}`,
src: this.props.img
}],
}, },
{ {
tag: "div", tag: "div",
......
...@@ -59,20 +59,20 @@ class HomePage extends WebPage { ...@@ -59,20 +59,20 @@ class HomePage extends WebPage {
contents: [ contents: [
{ {
title: t("Jeux"), title: t("Jeux"),
img: "game_controller.svg", img: images_url + "/game_controller.svg",
href: "/games/", href: "/games/",
description: description:
t("games-description"), t("games-description"),
}, },
{ {
title: t("Pédagogie"), title: t("Pédagogie"),
img: "brain.svg", img: images_url + "/brain.svg",
href: "/education/", href: "/education/",
description: t("education-description"), description: t("education-description"),
}, },
{ {
title: "Software", title: "Software",
img: "meca_proc.svg", img: images_url + "/meca_proc.svg",
href: "/software-development/", href: "/software-development/",
description: t("software-description"), description: t("software-description"),
}, },
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment