Ready to implement photos management

This commit is contained in:
Pierre HUBERT 2023-08-05 19:15:52 +02:00
parent 4cd7519890
commit 02da973dd8
11 changed files with 195 additions and 20 deletions

View File

@ -83,6 +83,44 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "actix-multipart"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee489e3c01eae4d1c35b03c4493f71cb40d93f66b14558feb1b1a807671cc4e"
dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
"bytes",
"derive_more",
"futures-core",
"futures-util",
"httparse",
"local-waker",
"log",
"memchr",
"mime",
"serde",
"serde_json",
"serde_plain",
"tempfile",
"tokio",
]
[[package]]
name = "actix-multipart-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7"
dependencies = [
"darling 0.14.4",
"parse-size",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "actix-remote-ip" name = "actix-remote-ip"
version = "0.1.0" version = "0.1.0"
@ -700,14 +738,38 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core 0.14.4",
"darling_macro 0.14.4",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.20.3" version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.3",
"darling_macro", "darling_macro 0.20.3",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 1.0.109",
] ]
[[package]] [[package]]
@ -724,13 +786,24 @@ dependencies = [
"syn 2.0.23", "syn 2.0.23",
] ]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core 0.14.4",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.3" version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.3",
"quote", "quote",
"syn 2.0.23", "syn 2.0.23",
] ]
@ -1057,6 +1130,7 @@ name = "geneit_backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-multipart",
"actix-remote-ip", "actix-remote-ip",
"actix-web", "actix-web",
"anyhow", "anyhow",
@ -1701,6 +1775,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "parse-size"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.13" version = "1.0.13"
@ -2166,6 +2246,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_plain"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -2200,7 +2289,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
dependencies = [ dependencies = [
"darling", "darling 0.20.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.23", "syn 2.0.23",

View File

@ -13,11 +13,12 @@ lazy_static = "1.4.0"
anyhow = "1.0.71" anyhow = "1.0.71"
actix-web = "4.3.1" actix-web = "4.3.1"
actix-cors = "0.6.4" actix-cors = "0.6.4"
actix-multipart = "0.6.0"
actix-remote-ip = "0.1.0"
futures-util = "0.3.28" futures-util = "0.3.28"
diesel = { version = "2.0.4", features = ["postgres"] } diesel = { version = "2.0.4", features = ["postgres"] }
serde = { version = "1.0.163", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96" serde_json = "1.0.96"
actix-remote-ip = "0.1.0"
mailchecker = "5.0.9" mailchecker = "5.0.9"
redis = "0.23.0" redis = "0.23.0"
lettre = "0.10.4" lettre = "0.10.4"

View File

@ -2,6 +2,7 @@
DROP view if EXISTS families_memberships ; DROP view if EXISTS families_memberships ;
DROP table IF EXISTS couples; DROP table IF EXISTS couples;
DROP table IF EXISTS members; DROP table IF EXISTS members;
DROP table IF EXISTS photos;
DROP table IF EXISTS memberships ; DROP table IF EXISTS memberships ;
DROP table IF EXISTS families; DROP table IF EXISTS families;
DROP table IF EXISTS users; DROP table IF EXISTS users;

View File

@ -30,13 +30,22 @@ CREATE TABLE memberships (
PRIMARY KEY(user_id, family_id) PRIMARY KEY(user_id, family_id)
); );
CREATE TABLE photos (
id SERIAL PRIMARY KEY,
time_create VARCHAR(130) NOT NULL,
mime_type VARCHAR(150) NOT NULL,
sha512 VARCHAR(130) NOT NULL,
file_size INTEGER NOT NULL,
thumb_sha512 VARCHAR(130) NOT NULL
);
CREATE TABLE members ( CREATE TABLE members (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families, family_id integer NOT NULL REFERENCES families,
first_name VARCHAR(30) NULL, first_name VARCHAR(30) NULL,
last_name VARCHAR(30) NULL, last_name VARCHAR(30) NULL,
birth_last_name VARCHAR(30) NULL, birth_last_name VARCHAR(30) NULL,
photo_id VARCHAR(255) NULL, photo_id INTEGER NULL REFERENCES photos ON DELETE SET NULL,
email VARCHAR(255) NULL, email VARCHAR(255) NULL,
phone VARCHAR(30) NULL, phone VARCHAR(30) NULL,
address VARCHAR (155) NULL, address VARCHAR (155) NULL,
@ -60,7 +69,7 @@ CREATE TABLE members (
CREATE TABLE couples ( CREATE TABLE couples (
wife integer NOT NULL REFERENCES members, wife integer NOT NULL REFERENCES members,
husband integer NOT NULL REFERENCES members, husband integer NOT NULL REFERENCES members,
photo_id VARCHAR(255) NULL, photo_id INTEGER NULL REFERENCES photos ON DELETE SET NULL,
wedding_year smallint NULL, wedding_year smallint NULL,
wedding_month smallint NULL, wedding_month smallint NULL,
wedding_day smallint NULL, wedding_day smallint NULL,

View File

@ -141,6 +141,10 @@ pub struct AppConfig {
/// S3 skip auto create bucket if not existing /// S3 skip auto create bucket if not existing
#[arg(long, env)] #[arg(long, env)]
pub s3_skip_auto_create_bucket: bool, pub s3_skip_auto_create_bucket: bool,
/// Directory where temporary files are stored
#[arg(long, env, default_value = "/tmp")]
pub temp_dir: String,
} }
lazy_static::lazy_static! { lazy_static::lazy_static! {

View File

@ -36,6 +36,13 @@ impl NumberValueConstraint {
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
pub struct StaticConstraints { pub struct StaticConstraints {
pub date_year: NumberValueConstraint,
pub date_month: NumberValueConstraint,
pub date_day: NumberValueConstraint,
pub photo_allowed_types: &'static [&'static str],
pub photo_max_size: usize,
pub mail_len: SizeConstraint, pub mail_len: SizeConstraint,
pub user_name_len: SizeConstraint, pub user_name_len: SizeConstraint,
pub password_len: SizeConstraint, pub password_len: SizeConstraint,
@ -53,15 +60,18 @@ pub struct StaticConstraints {
pub member_country: SizeConstraint, pub member_country: SizeConstraint,
pub member_sex: SizeConstraint, pub member_sex: SizeConstraint,
pub member_note: SizeConstraint, pub member_note: SizeConstraint,
pub date_year: NumberValueConstraint,
pub date_month: NumberValueConstraint,
pub date_day: NumberValueConstraint,
} }
impl Default for StaticConstraints { impl Default for StaticConstraints {
fn default() -> Self { fn default() -> Self {
Self { Self {
date_year: NumberValueConstraint::new(1, 2050),
date_month: NumberValueConstraint::new(1, 12),
date_day: NumberValueConstraint::new(1, 31),
photo_allowed_types: &ALLOWED_PHOTOS_MIMETYPES,
photo_max_size: PHOTOS_MAX_SIZE,
mail_len: SizeConstraint::new(5, 255), mail_len: SizeConstraint::new(5, 255),
user_name_len: SizeConstraint::new(3, 30), user_name_len: SizeConstraint::new(3, 30),
password_len: SizeConstraint::new(8, 255), password_len: SizeConstraint::new(8, 255),
@ -81,9 +91,6 @@ impl Default for StaticConstraints {
member_country: SizeConstraint::new(0, 2), member_country: SizeConstraint::new(0, 2),
member_sex: SizeConstraint::new(0, 1), member_sex: SizeConstraint::new(0, 1),
member_note: SizeConstraint::new(0, 35000), member_note: SizeConstraint::new(0, 35000),
date_year: NumberValueConstraint::new(1, 2050),
date_month: NumberValueConstraint::new(1, 12),
date_day: NumberValueConstraint::new(1, 31),
} }
} }
} }
@ -108,3 +115,16 @@ pub const AUTH_TOKEN_MAX_INACTIVITY: Duration = Duration::from_secs(3600);
/// Length of family invitation code /// Length of family invitation code
pub const FAMILY_INVITATION_CODE_LEN: usize = 7; pub const FAMILY_INVITATION_CODE_LEN: usize = 7;
/// Allowed photos mimetypes
pub const ALLOWED_PHOTOS_MIMETYPES: [&str; 6] = [
"image/jpeg",
"image/png",
"image/gif",
"image/bmp",
"image/webp",
"image/vnd.microsoft.icon",
];
/// Uploaded photos max size
pub const PHOTOS_MAX_SIZE: usize = 10 * 1000 * 1000;

View File

@ -5,6 +5,8 @@ use crate::extractors::member_extractor::FamilyAndMemberInPath;
use crate::models::{Member, MemberID, Sex}; use crate::models::{Member, MemberID, Sex};
use crate::services::members_service; use crate::services::members_service;
use crate::utils::countries_utils; use crate::utils::countries_utils;
use actix_multipart::form::tempfile::TempFile;
use actix_multipart::form::MultipartForm;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
serde_with::with_prefix!(prefix_birth "birth_"); serde_with::with_prefix!(prefix_birth "birth_");
@ -292,3 +294,17 @@ pub async fn delete(m: FamilyAndMemberInPath) -> HttpResult {
members_service::delete(&m).await?; members_service::delete(&m).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
#[derive(Debug, MultipartForm)]
pub struct UploadPhotoForm {
#[multipart(rename = "photo")]
photo: TempFile,
}
/// Upload a new photo for a user
pub async fn set_photo(
_m: FamilyAndMemberInPath,
MultipartForm(_form): MultipartForm<UploadPhotoForm>,
) -> HttpResult {
todo!()
}

View File

@ -1,4 +1,5 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_multipart::form::tempfile::TempFileConfig;
use actix_remote_ip::RemoteIPConfig; use actix_remote_ip::RemoteIPConfig;
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
@ -35,6 +36,8 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(RemoteIPConfig { .app_data(web::Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(), proxy: AppConfig::get().proxy_ip.clone(),
})) }))
// Uploaded files
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
// Config controller // Config controller
.route("/", web::get().to(server_controller::home)) .route("/", web::get().to(server_controller::home))
.route( .route(
@ -150,6 +153,10 @@ async fn main() -> std::io::Result<()> {
"/family/{id}/member/{member_id}", "/family/{id}/member/{member_id}",
web::delete().to(members_controller::delete), web::delete().to(members_controller::delete),
) )
.route(
"/family/{id}/member/{member_id}/photo",
web::put().to(members_controller::set_photo),
)
}) })
.bind(AppConfig::get().listen_address.as_str())? .bind(AppConfig::get().listen_address.as_str())?
.run() .run()

View File

@ -117,6 +117,10 @@ pub struct FamilyMembership {
pub count_admins: i64, pub count_admins: i64,
} }
/// Photo ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct PhotoID(pub i32);
/// Member ID holder /// Member ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct MemberID(pub i32); pub struct MemberID(pub i32);
@ -153,7 +157,7 @@ pub struct Member {
pub first_name: Option<String>, pub first_name: Option<String>,
pub last_name: Option<String>, pub last_name: Option<String>,
pub birth_last_name: Option<String>, pub birth_last_name: Option<String>,
pub photo_id: Option<String>, photo_id: Option<i32>,
pub email: Option<String>, pub email: Option<String>,
pub phone: Option<String>, pub phone: Option<String>,
pub address: Option<String>, pub address: Option<String>,
@ -183,6 +187,10 @@ impl Member {
FamilyID(self.family_id) FamilyID(self.family_id)
} }
pub fn photo_id(&self) -> Option<PhotoID> {
self.photo_id.map(PhotoID)
}
pub fn sex(&self) -> Option<Sex> { pub fn sex(&self) -> Option<Sex> {
self.sex.as_deref().map(Sex::parse_str).unwrap_or_default() self.sex.as_deref().map(Sex::parse_str).unwrap_or_default()
} }

View File

@ -4,7 +4,7 @@ diesel::table! {
couples (wife, husband) { couples (wife, husband) {
wife -> Int4, wife -> Int4,
husband -> Int4, husband -> Int4,
photo_id -> Nullable<Varchar>, photo_id -> Nullable<Int4>,
wedding_year -> Nullable<Int2>, wedding_year -> Nullable<Int2>,
wedding_month -> Nullable<Int2>, wedding_month -> Nullable<Int2>,
wedding_day -> Nullable<Int2>, wedding_day -> Nullable<Int2>,
@ -30,7 +30,7 @@ diesel::table! {
first_name -> Nullable<Varchar>, first_name -> Nullable<Varchar>,
last_name -> Nullable<Varchar>, last_name -> Nullable<Varchar>,
birth_last_name -> Nullable<Varchar>, birth_last_name -> Nullable<Varchar>,
photo_id -> Nullable<Varchar>, photo_id -> Nullable<Int4>,
email -> Nullable<Varchar>, email -> Nullable<Varchar>,
phone -> Nullable<Varchar>, phone -> Nullable<Varchar>,
address -> Nullable<Varchar>, address -> Nullable<Varchar>,
@ -61,6 +61,17 @@ diesel::table! {
} }
} }
diesel::table! {
photos (id) {
id -> Int4,
time_create -> Varchar,
mime_type -> Varchar,
sha512 -> Varchar,
file_size -> Int4,
thumb_sha512 -> Varchar,
}
}
diesel::table! { diesel::table! {
users (id) { users (id) {
id -> Int4, id -> Int4,
@ -78,8 +89,17 @@ diesel::table! {
} }
} }
diesel::joinable!(couples -> photos (photo_id));
diesel::joinable!(members -> families (family_id)); diesel::joinable!(members -> families (family_id));
diesel::joinable!(members -> photos (photo_id));
diesel::joinable!(memberships -> families (family_id)); diesel::joinable!(memberships -> families (family_id));
diesel::joinable!(memberships -> users (user_id)); diesel::joinable!(memberships -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(couples, families, members, memberships, users,); diesel::allow_tables_to_appear_in_same_query!(
couples,
families,
members,
memberships,
photos,
users,
);

View File

@ -60,7 +60,7 @@ pub async fn update(member: &mut Member) -> anyhow::Result<()> {
members::dsl::first_name.eq(member.first_name.clone()), members::dsl::first_name.eq(member.first_name.clone()),
members::dsl::last_name.eq(member.last_name.clone()), members::dsl::last_name.eq(member.last_name.clone()),
members::dsl::birth_last_name.eq(member.birth_last_name.clone()), members::dsl::birth_last_name.eq(member.birth_last_name.clone()),
members::dsl::photo_id.eq(member.photo_id.clone()), members::dsl::photo_id.eq(member.photo_id().map(|p| p.0)),
members::dsl::email.eq(member.email.clone()), members::dsl::email.eq(member.email.clone()),
members::dsl::phone.eq(member.phone.clone()), members::dsl::phone.eq(member.phone.clone()),
members::dsl::address.eq(member.address.clone()), members::dsl::address.eq(member.address.clone()),