diff options
Diffstat (limited to '')
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | src/args.rs | 10 | ||||
-rw-r--r-- | src/errors.rs | 20 | ||||
-rw-r--r-- | src/file_upload.rs | 152 | ||||
-rw-r--r-- | src/listing.rs | 9 | ||||
-rw-r--r-- | src/main.rs | 30 | ||||
-rw-r--r-- | src/renderer.rs | 145 | ||||
-rw-r--r-- | src/themes.rs | 50 |
10 files changed, 411 insertions, 22 deletions
@@ -87,7 +87,7 @@ dependencies = [ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -778,7 +778,7 @@ dependencies = [ "maud 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", "nanoid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", "tar 0.4.22 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1219,7 +1219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1242,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1252,7 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2013,7 +2013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560" +"checksum serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "aa5f7c20820475babd2c077c3ab5f8c77a31c15e16ea38687b4c02d3e48680f4" "checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c" "checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" "checksum serde_urlencoded 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d48f9f99cd749a2de71d29da5f948de7f2764cc5a9d7f3c97e3514d4ee6eabf2" @@ -36,10 +36,10 @@ structopt = "0.2.15" chrono = "0.4.6" chrono-humanize = "0.0.11" maud = { version = "0.20.0", features = ["actix-web"] } -serde = { version = "1.0.89", features = ["derive"] } +serde = { version = "1.0.90", features = ["derive"] } tar = "0.4.22" bytes = "0.4.12" futures = "0.1.26" libflate = "0.1.21" failure = "0.1.5" -log = "0.4.6"
\ No newline at end of file +log = "0.4.6" @@ -45,6 +45,7 @@ Sometimes this is just a more practical and quick way than doing things properly - Authentication support with username and password - Mega fast and highly parallel (thanks to [Rust](https://www.rust-lang.org/) and [Actix](https://actix.rs/)) - Folder download (compressed in .tar.gz) +- File uploading ## Known limitations diff --git a/src/args.rs b/src/args.rs index bcee0bc..3e54d08 100644 --- a/src/args.rs +++ b/src/args.rs @@ -60,6 +60,14 @@ struct CLIArgs { ) )] color_scheme: themes::ColorScheme, + + /// Enable file uploading + #[structopt(short = "u", long = "upload-files")] + file_upload: bool, + + /// Enable overriding existing files during file upload + #[structopt(short = "o", long = "overwrite-files")] + overwrite_files: bool, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -116,5 +124,7 @@ pub fn parse_args() -> crate::MiniserveConfig { no_symlinks: args.no_symlinks, random_route, default_color_scheme, + overwrite_files: args.overwrite_files, + file_upload: args.file_upload, } } diff --git a/src/errors.rs b/src/errors.rs index 2aa5f58..21d9e07 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,6 +1,26 @@ use failure::{Backtrace, Context, Fail}; use std::fmt::{self, Debug, Display}; +/// Kinds of errors which might happen during file upload +#[derive(Debug, Fail)] +pub enum FileUploadErrorKind { + /// This error will occur when file overriding is off and a file with same name already exists + #[fail(display = "File with this name already exists")] + FileExist, + /// This error will occur when the server fails to process the HTTP header during file upload + #[fail(display = "Failed to parse incoming request")] + ParseError, + /// This error will occur when we fail to process the multipart request + #[fail(display = "Failed to process multipart request")] + MultipartError(actix_web::error::MultipartError), + /// This error may occur when trying to write the incoming file to disk + #[fail(display = "Failed to create or write to file")] + IOError(std::io::Error), + /// This error will occur when we he have insuffictent permissions to create new file + #[fail(display = "Insufficient permissions to create file")] + InsufficientPermissions, +} + /// Kinds of errors which might happen during the generation of an archive #[derive(Debug, Fail)] pub enum CompressionErrorKind { diff --git a/src/file_upload.rs b/src/file_upload.rs new file mode 100644 index 0000000..273d12c --- /dev/null +++ b/src/file_upload.rs @@ -0,0 +1,152 @@ +use crate::errors::FileUploadErrorKind; +use crate::renderer::file_upload_error; +use actix_web::{ + dev, http::header, multipart, FromRequest, FutureResponse, HttpMessage, HttpRequest, + HttpResponse, Query, +}; +use futures::{future, Future, Stream}; +use serde::Deserialize; +use std::{ + fs, + io::Write, + path::{Component, PathBuf}, +}; + +/// Query parameters +#[derive(Debug, Deserialize)] +struct QueryParameters { + path: PathBuf, +} + +/// Create future to save file. +fn save_file( + field: multipart::Field<dev::Payload>, + file_path: PathBuf, + overwrite_files: bool, +) -> Box<Future<Item = i64, Error = FileUploadErrorKind>> { + if !overwrite_files && file_path.exists() { + return Box::new(future::err(FileUploadErrorKind::FileExist)); + } + let mut file = match std::fs::File::create(file_path) { + Ok(file) => file, + Err(e) => { + return Box::new(future::err(FileUploadErrorKind::IOError(e))); + } + }; + Box::new( + field + .map_err(FileUploadErrorKind::MultipartError) + .fold(0i64, move |acc, bytes| { + let rt = file + .write_all(bytes.as_ref()) + .map(|_| acc + bytes.len() as i64) + .map_err(FileUploadErrorKind::IOError); + future::result(rt) + }), + ) +} + +/// Create new future to handle file as multipart data. +fn handle_multipart( + item: multipart::MultipartItem<dev::Payload>, + mut file_path: PathBuf, + overwrite_files: bool, +) -> Box<Stream<Item = i64, Error = FileUploadErrorKind>> { + match item { + multipart::MultipartItem::Field(field) => { + let filename = field + .headers() + .get(header::CONTENT_DISPOSITION) + .ok_or(FileUploadErrorKind::ParseError) + .and_then(|cd| { + header::ContentDisposition::from_raw(cd) + .map_err(|_| FileUploadErrorKind::ParseError) + }) + .and_then(|content_disposition| { + content_disposition + .get_filename() + .ok_or(FileUploadErrorKind::ParseError) + .map(String::from) + }); + let err = |e: FileUploadErrorKind| Box::new(future::err(e).into_stream()); + match filename { + Ok(f) => { + match fs::metadata(&file_path) { + Ok(metadata) => { + if !metadata.is_dir() || metadata.permissions().readonly() { + return err(FileUploadErrorKind::InsufficientPermissions); + } + } + Err(_) => { + return err(FileUploadErrorKind::InsufficientPermissions); + } + } + file_path = file_path.join(f); + Box::new(save_file(field, file_path, overwrite_files).into_stream()) + } + Err(e) => err(e), + } + } + multipart::MultipartItem::Nested(mp) => Box::new( + mp.map_err(FileUploadErrorKind::MultipartError) + .map(move |item| handle_multipart(item, file_path.clone(), overwrite_files)) + .flatten(), + ), + } +} + +/// Handle incoming request to upload file. +/// Target file path is expected as path parameter in URI and is interpreted as relative from +/// server root directory. Any path which will go outside of this directory is considered +/// invalid. +/// This method returns future. +pub fn upload_file(req: &HttpRequest<crate::MiniserveConfig>) -> FutureResponse<HttpResponse> { + let app_root_dir = req.state().path.clone().canonicalize().unwrap(); + let path = match Query::<QueryParameters>::extract(req) { + Ok(query) => { + if let Ok(stripped_path) = query.path.strip_prefix(Component::RootDir) { + stripped_path.to_owned() + } else { + query.path.clone() + } + } + Err(_) => { + return Box::new(future::ok( + HttpResponse::BadRequest().body("Unspecified parameter path"), + )) + } + }; + // this is really ugly I will try to think about something smarter + let return_path: String = req.headers()[header::REFERER] + .clone() + .to_str() + .unwrap_or("/") + .to_owned(); + let r_p2 = return_path.clone(); + + // If the target path is under the app root directory, save the file. + let target_dir = match &app_root_dir.clone().join(path.clone()).canonicalize() { + Ok(path) if path.starts_with(&app_root_dir) => path.clone(), + _ => return Box::new(future::ok(HttpResponse::BadRequest().body("Invalid path"))), + }; + let overwrite_files = req.state().overwrite_files; + Box::new( + req.multipart() + .map_err(FileUploadErrorKind::MultipartError) + .map(move |item| handle_multipart(item, target_dir.clone(), overwrite_files)) + .flatten() + .collect() + .map(move |_| { + HttpResponse::TemporaryRedirect() + .header(header::LOCATION, return_path.to_string()) + .finish() + }) + .or_else(move |e| { + let error_description = format!("{}", e); + future::ok( + HttpResponse::BadRequest() + .body(file_upload_error(&error_description, &r_p2.clone()).into_string()), + ) + }), + ) +} diff --git a/src/listing.rs b/src/listing.rs index de84dc6..b7070f3 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -144,14 +144,20 @@ pub fn directory_listing<S>( dir: &fs::Directory, req: &HttpRequest<S>, skip_symlinks: bool, + file_upload: bool, random_route: Option<String>, default_color_scheme: themes::ColorScheme, + upload_route: String, ) -> Result<HttpResponse, io::Error> { let title = format!("Index of {}", req.path()); let base = Path::new(req.path()); let random_route = format!("/{}", random_route.unwrap_or_default()); let is_root = base.parent().is_none() || req.path() == random_route; let page_parent = base.parent().map(|p| p.display().to_string()); + let current_dir = match base.strip_prefix(random_route) { + Ok(c_d) => Path::new("/").join(c_d), + Err(_) => base.to_path_buf(), + }; let (sort_method, sort_order, download, color_scheme) = if let Ok(query) = Query::<QueryParameters>::extract(req) { @@ -292,6 +298,9 @@ pub fn directory_listing<S>( sort_method, sort_order, color_scheme, + file_upload, + &upload_route, + ¤t_dir.display().to_string(), ) .into_string(), )) diff --git a/src/main.rs b/src/main.rs index 79a5db2..64de8d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![feature(proc_macro_hygiene)] +use actix_web::http::Method; use actix_web::{fs, middleware, server, App}; use clap::crate_version; use simplelog::{Config, LevelFilter, TermLogger}; @@ -13,6 +14,7 @@ mod archive; mod args; mod auth; mod errors; +mod file_upload; mod listing; mod renderer; mod themes; @@ -46,6 +48,12 @@ pub struct MiniserveConfig { /// Default color scheme pub default_color_scheme: themes::ColorScheme, + + /// Enable file upload + pub file_upload: bool, + + /// Enable upload to override existing files + pub overwrite_files: bool, } fn main() { @@ -178,14 +186,21 @@ fn main() { /// Configures the Actix application fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { + let upload_route; let s = { let path = &app.state().path; let no_symlinks = app.state().no_symlinks; let random_route = app.state().random_route.clone(); let default_color_scheme = app.state().default_color_scheme.clone(); + let file_upload = app.state().file_upload; + upload_route = match app.state().random_route.clone() { + Some(random_route) => format!("/{}/upload", random_route), + None => "/upload".to_string(), + }; if path.is_file() { None } else { + let u_r = upload_route.clone(); Some( fs::StaticFiles::new(path) .expect("Couldn't create path") @@ -195,8 +210,10 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { dir, req, no_symlinks, + file_upload, random_route.clone(), default_color_scheme.clone(), + u_r.clone(), ) }), ) @@ -207,8 +224,17 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { let full_route = format!("/{}", random_route); if let Some(s) = s { - // Handle directories - app.handler(&full_route, s) + if app.state().file_upload { + // Allow file upload + app.resource(&upload_route, |r| { + r.method(Method::POST).f(file_upload::upload_file) + }) + // Handle directories + .handler(&full_route, s) + } else { + // Handle directories + app.handler(&full_route, s) + } } else { // Handle single files app.resource(&full_route, |r| r.f(listing::file_handler)) diff --git a/src/renderer.rs b/src/renderer.rs index abb8730..bd7b272 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -16,19 +16,38 @@ pub fn page( sort_method: Option<listing::SortingMethod>, sort_order: Option<listing::SortingOrder>, color_scheme: themes::ColorScheme, + file_upload: bool, + upload_route: &str, + current_dir: &str, ) -> Markup { html! { (page_header(page_title, &color_scheme)) - body { + body#drop-container { + div.drag-form { + div.drag-title { + h1 { "Drop your file here to upload it" } + } + } (color_scheme_selector(&sort_method, &sort_order, &color_scheme)) div.container { - span #top { } + span#top { } h1.title { (page_title) } div.download { - @for compression_method in archive::CompressionMethod::get_compression_methods() { + @for compression_method in archive::CompressionMethod::get_compression_methods() { (archive_button(compression_method)) } } + @if file_upload { + div.upload { + form id="file_submit" action={(upload_route) "?path=" (current_dir)} method="POST" enctype="multipart/form-data" { + p { "Select a file to upload or drag it anywhere into the window" } + div { + input#file-input type="file" name="file_to_upload" {} + button type="submit" { "Upload file" } + } + } + } + } table { thead { th { (build_link("name", "Name", &sort_method, &sort_order, &color_scheme)) } @@ -269,6 +288,7 @@ fn css(color_scheme: &themes::ColorScheme) -> Markup { font-weight: 300; color: {text_color}; background: {background}; + position: relative; }} .container {{ padding: 1.5rem 5rem; @@ -488,6 +508,50 @@ fn css(color_scheme: &themes::ColorScheme) -> Markup { .download a:not(:last-of-type) {{ margin-right: 1rem; }} + .upload {{ + display: flex; + justify-content: flex-end; + margin-top: 1rem; + }} + .upload p {{ + font-size: 0.8rem; + margin-bottom: 1rem; + color: {upload_text_color}; + }} + .upload form {{ + padding: 1rem; + border: 1px solid {upload_form_border_color}; + background: {upload_form_background}; + }} + .upload button {{ + background: {upload_button_background}; + padding: 0.5rem; + border-radius: 0.2rem; + color: {upload_button_text_color}; + border: none; + }} + .upload div {{ + display: flex; + align-items: baseline; + justify-content: space-between; + }} + .drag-form {{ + display: none; + background: {drag_background}; + position: absolute; + border: 0.5rem dashed {drag_border_color}; + width: calc(100% - 1rem); + height: calc(100% - 1rem); + text-align: center; + z-index: 2; + }} + .drag-title {{ + position: fixed; + color: {drag_text_color}; + top: 50%; + width: 100%; + text-align: center; + }} @media (max-width: 760px) {{ nav {{ padding: 0 2.5rem; @@ -496,7 +560,7 @@ fn css(color_scheme: &themes::ColorScheme) -> Markup { padding: 1.5rem 2.5rem; }} h1 {{ - font-size: 1.375em; + font-size: 1.4em; }} td:not(:nth-child(1)), th:not(:nth-child(1)){{ display: none; @@ -508,10 +572,21 @@ fn css(color_scheme: &themes::ColorScheme) -> Markup { padding-bottom: 1rem; }} .back {{ - right: 1.5rem; + display: initial; + }} + .upload {{ + margin-top: 2rem; + }} + .upload form {{ + width: 100%; }} .back {{ - display: initial; + right: 1.5rem; + }} + }} + @media (max-width: 600px) {{ + h1 {{ + font-size: 1.375em; }} }} @media (max-width: 400px) {{ @@ -557,7 +632,15 @@ fn css(color_scheme: &themes::ColorScheme) -> Markup { switch_theme_border = theme.switch_theme_border, change_theme_link_color = theme.change_theme_link_color, change_theme_link_color_hover = theme.change_theme_link_color_hover, - field_color = theme.field_color); + field_color = theme.field_color, + upload_text_color = theme.upload_text_color, + upload_form_border_color = theme.upload_form_border_color, + upload_form_background = theme.upload_form_background, + upload_button_background = theme.upload_button_background, + upload_button_text_color = theme.upload_button_text_color, + drag_background = theme.drag_background, + drag_border_color = theme.drag_border_color, + drag_text_color = theme.drag_text_color); (PreEscaped(css)) } @@ -596,6 +679,43 @@ fn page_header(page_title: &str, color_scheme: &themes::ColorScheme) -> Markup { meta name="viewport" content="width=device-width, initial-scale=1"; title { (page_title) } style { (css(&color_scheme)) } + (PreEscaped(r#" + <script> + window.onload = function() { + const dropContainer = document.querySelector('#drop-container'); + const dragForm = document.querySelector('.drag-form'); + const fileInput = document.querySelector('#file-input'); + const collection = []; + + dropContainer.ondragover = function(e) { + e.preventDefault(); + } + + dropContainer.ondragenter = function(e) { + e.preventDefault(); + if (collection.length === 0) { + dragForm.style.display = 'initial'; + } + collection.push(e.target); + }; + + dropContainer.ondragleave = function(e) { + e.preventDefault(); + collection.splice(collection.indexOf(e.target), 1); + if (collection.length === 0) { + dragForm.style.display = 'none'; + } + }; + + dropContainer.ondrop = function(e) { + e.preventDefault(); + fileInput.files = e.dataTransfer.files; + file_submit.submit(); + dragForm.style.display = 'none'; + }; + } + </script> + "#)) } } } @@ -621,3 +741,14 @@ fn humanize_systemtime(src_time: Option<SystemTime>) -> Option<String> { .and_then(|from_now| Duration::from_std(from_now).ok()) .map(|duration| HumanTime::from(duration).to_text_en(Accuracy::Rough, Tense::Past)) } + +/// Renders error page when file uploading fails +pub fn file_upload_error(error_description: &str, return_address: &str) -> Markup { + html! { + h1 { "File uploading failed" } + p { (error_description) } + a href=(return_address) { + "back" + } + } +} diff --git a/src/themes.rs b/src/themes.rs index d04eab2..917e56e 100644 --- a/src/themes.rs +++ b/src/themes.rs @@ -90,6 +90,14 @@ impl ColorScheme { change_theme_link_color: "#fefefe".to_string(), change_theme_link_color_hover: "#fefefe".to_string(), field_color: "#859cb9".to_string(), + upload_text_color: "#fefefe".to_string(), + upload_form_border_color: "#353946".to_string(), + upload_form_background: "#4b5162".to_string(), + upload_button_background: "#ea95ff".to_string(), + upload_button_text_color: "#ffffff".to_string(), + drag_background: "#3333338f".to_string(), + drag_border_color: "#fefefe".to_string(), + drag_text_color: "#fefefe".to_string(), }, ColorScheme::Zenburn => Theme { background: "#3f3f3f".to_string(), @@ -123,6 +131,14 @@ impl ColorScheme { change_theme_link_color: "#efefef".to_string(), change_theme_link_color_hover: "#efefef".to_string(), field_color: "#9fc3a1".to_string(), + upload_text_color: "#efefef".to_string(), + upload_form_border_color: "#4a4949".to_string(), + upload_form_background: "#777777".to_string(), + upload_button_background: "#cc9393".to_string(), + upload_button_text_color: "#efefef".to_string(), + drag_background: "#3333338f".to_string(), + drag_border_color: "#efefef".to_string(), + drag_text_color: "#efefef".to_string(), }, ColorScheme::Monokai => Theme { background: "#272822".to_string(), @@ -156,6 +172,14 @@ impl ColorScheme { change_theme_link_color: "#F8F8F2".to_string(), change_theme_link_color_hover: "#F8F8F2".to_string(), field_color: "#ccc7a7".to_string(), + upload_text_color: "#F8F8F2".to_string(), + upload_form_border_color: "#3B3A32".to_string(), + upload_form_background: "#49483E".to_string(), + upload_button_background: "#AE81FF".to_string(), + upload_button_text_color: "#F8F8F0".to_string(), + drag_background: "#3333338f".to_string(), + drag_border_color: "#F8F8F2".to_string(), + drag_text_color: "#F8F8F2".to_string(), }, ColorScheme::Squirrel => Theme { background: "#FFFFFF".to_string(), @@ -163,7 +187,7 @@ impl ColorScheme { directory_link_color: "#d02474".to_string(), file_link_color: "#0086B3".to_string(), symlink_link_color: "#ED6A43".to_string(), - table_background: "#F5F5F5".to_string(), + table_background: "#ffffff".to_string(), table_text_color: "#323232".to_string(), table_header_background: "#323232".to_string(), table_header_text_color: "#F5F5F5".to_string(), @@ -174,12 +198,12 @@ impl ColorScheme { root_link_color: "#323232".to_string(), download_button_background: "#d02474".to_string(), download_button_background_hover: "#f52d8a".to_string(), - download_button_link_color: "#F8F8F0".to_string(), - download_button_link_color_hover: "#F8F8F0".to_string(), + download_button_link_color: "#FFFFFF".to_string(), + download_button_link_color_hover: "#FFFFFF".to_string(), back_button_background: "#d02474".to_string(), back_button_background_hover: "#d02474".to_string(), - back_button_link_color: "#F8F8F0".to_string(), - back_button_link_color_hover: "#F8F8F0".to_string(), + back_button_link_color: "#FFFFFF".to_string(), + back_button_link_color_hover: "#FFFFFF".to_string(), date_text_color: "#797979".to_string(), at_color: "#797979".to_string(), switch_theme_background: "#323232".to_string(), @@ -189,6 +213,14 @@ impl ColorScheme { change_theme_link_color: "#F5F5F5".to_string(), change_theme_link_color_hover: "#F5F5F5".to_string(), field_color: "#797979".to_string(), + upload_text_color: "#323232".to_string(), + upload_form_border_color: "#d2d2d2".to_string(), + upload_form_background: "#f2f2f2".to_string(), + upload_button_background: "#d02474".to_string(), + upload_button_text_color: "#FFFFFF".to_string(), + drag_background: "#3333338f".to_string(), + drag_border_color: "#ffffff".to_string(), + drag_text_color: "#ffffff".to_string(), }, } } @@ -227,4 +259,12 @@ pub struct Theme { pub change_theme_link_color: String, pub change_theme_link_color_hover: String, pub field_color: String, + pub upload_text_color: String, + pub upload_form_border_color: String, + pub upload_form_background: String, + pub upload_button_background: String, + pub upload_button_text_color: String, + pub drag_background: String, + pub drag_border_color: String, + pub drag_text_color: String, } |