20 changed files with 812 additions and 2 deletions
			
			
		| @ -0,0 +1 @@ | |||
| DROP TABLE sends; | |||
| @ -0,0 +1,25 @@ | |||
| CREATE TABLE sends ( | |||
|   uuid              CHAR(36) NOT NULL   PRIMARY KEY, | |||
|   user_uuid         CHAR(36)            REFERENCES users (uuid), | |||
|   organization_uuid CHAR(36)            REFERENCES organizations (uuid), | |||
| 
 | |||
|   name              TEXT    NOT NULL, | |||
|   notes             TEXT, | |||
| 
 | |||
|   atype             INTEGER NOT NULL, | |||
|   data              TEXT    NOT NULL, | |||
|   key               TEXT    NOT NULL, | |||
|   password_hash     BLOB, | |||
|   password_salt     BLOB, | |||
|   password_iter     INTEGER, | |||
| 
 | |||
|   max_access_count  INTEGER, | |||
|   access_count      INTEGER NOT NULL, | |||
| 
 | |||
|   creation_date     DATETIME NOT NULL, | |||
|   revision_date     DATETIME NOT NULL, | |||
|   expiration_date   DATETIME, | |||
|   deletion_date     DATETIME NOT NULL, | |||
| 
 | |||
|   disabled          BOOLEAN NOT NULL | |||
| ); | |||
| @ -0,0 +1 @@ | |||
| DROP TABLE sends; | |||
| @ -0,0 +1,25 @@ | |||
| CREATE TABLE sends ( | |||
|   uuid              CHAR(36) NOT NULL   PRIMARY KEY, | |||
|   user_uuid         CHAR(36)            REFERENCES users (uuid), | |||
|   organization_uuid CHAR(36)            REFERENCES organizations (uuid), | |||
| 
 | |||
|   name              TEXT    NOT NULL, | |||
|   notes             TEXT, | |||
| 
 | |||
|   atype             INTEGER NOT NULL, | |||
|   data              TEXT    NOT NULL, | |||
|   key               TEXT    NOT NULL, | |||
|   password_hash     BYTEA, | |||
|   password_salt     BYTEA, | |||
|   password_iter     INTEGER, | |||
| 
 | |||
|   max_access_count  INTEGER, | |||
|   access_count      INTEGER NOT NULL, | |||
| 
 | |||
|   creation_date     TIMESTAMP NOT NULL, | |||
|   revision_date     TIMESTAMP NOT NULL, | |||
|   expiration_date   TIMESTAMP, | |||
|   deletion_date     TIMESTAMP NOT NULL, | |||
| 
 | |||
|   disabled          BOOLEAN NOT NULL | |||
| ); | |||
| @ -0,0 +1 @@ | |||
| DROP TABLE sends; | |||
| @ -0,0 +1,25 @@ | |||
| CREATE TABLE sends ( | |||
|   uuid              TEXT NOT NULL   PRIMARY KEY, | |||
|   user_uuid         TEXT            REFERENCES users (uuid), | |||
|   organization_uuid TEXT            REFERENCES organizations (uuid), | |||
| 
 | |||
|   name              TEXT    NOT NULL, | |||
|   notes             TEXT, | |||
| 
 | |||
|   atype             INTEGER NOT NULL, | |||
|   data              TEXT    NOT NULL, | |||
|   key               TEXT    NOT NULL, | |||
|   password_hash     BLOB, | |||
|   password_salt     BLOB, | |||
|   password_iter     INTEGER, | |||
| 
 | |||
|   max_access_count  INTEGER, | |||
|   access_count      INTEGER NOT NULL, | |||
| 
 | |||
|   creation_date     DATETIME NOT NULL, | |||
|   revision_date     DATETIME NOT NULL, | |||
|   expiration_date   DATETIME, | |||
|   deletion_date     DATETIME NOT NULL, | |||
| 
 | |||
|   disabled          BOOLEAN NOT NULL | |||
| ); | |||
| @ -0,0 +1,383 @@ | |||
| use std::{io::Read, path::Path}; | |||
| 
 | |||
| use chrono::{DateTime, Duration, Utc}; | |||
| use multipart::server::{save::SavedData, Multipart, SaveResult}; | |||
| use rocket::{http::ContentType, Data}; | |||
| use rocket_contrib::json::Json; | |||
| use serde_json::Value; | |||
| 
 | |||
| use crate::{ | |||
|     api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, | |||
|     auth::{Headers, Host}, | |||
|     db::{models::*, DbConn}, | |||
|     CONFIG, | |||
| }; | |||
| 
 | |||
| pub fn routes() -> Vec<rocket::Route> { | |||
|     routes![ | |||
|         post_send, | |||
|         post_send_file, | |||
|         post_access, | |||
|         post_access_file, | |||
|         put_send, | |||
|         delete_send, | |||
|         put_remove_password | |||
|     ] | |||
| } | |||
| 
 | |||
| #[derive(Deserialize)] | |||
| #[allow(non_snake_case)] | |||
| pub struct SendData { | |||
|     pub Type: i32, | |||
|     pub Key: String, | |||
|     pub Password: Option<String>, | |||
|     pub MaxAccessCount: Option<i32>, | |||
|     pub ExpirationDate: Option<DateTime<Utc>>, | |||
|     pub DeletionDate: DateTime<Utc>, | |||
|     pub Disabled: bool, | |||
| 
 | |||
|     // Data field
 | |||
|     pub Name: String, | |||
|     pub Notes: Option<String>, | |||
|     pub Text: Option<Value>, | |||
|     pub File: Option<Value>, | |||
| } | |||
| 
 | |||
| fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> { | |||
|     let data_val = if data.Type == SendType::Text as i32 { | |||
|         data.Text | |||
|     } else if data.Type == SendType::File as i32 { | |||
|         data.File | |||
|     } else { | |||
|         err!("Invalid Send type") | |||
|     }; | |||
| 
 | |||
|     let data_str = if let Some(mut d) = data_val { | |||
|         d.as_object_mut().and_then(|o| o.remove("Response")); | |||
|         serde_json::to_string(&d)? | |||
|     } else { | |||
|         err!("Send data not provided"); | |||
|     }; | |||
| 
 | |||
|     if data.DeletionDate > Utc::now() + Duration::days(31) { | |||
|         err!( | |||
|             "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." | |||
|         ); | |||
|     } | |||
| 
 | |||
|     let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc()); | |||
|     send.user_uuid = Some(user_uuid); | |||
|     send.notes = data.Notes; | |||
|     send.max_access_count = data.MaxAccessCount; | |||
|     send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); | |||
|     send.disabled = data.Disabled; | |||
|     send.atype = data.Type; | |||
| 
 | |||
|     send.set_password(data.Password.as_deref()); | |||
| 
 | |||
|     Ok(send) | |||
| } | |||
| 
 | |||
| #[post("/sends", data = "<data>")] | |||
| fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | |||
|     let data: SendData = data.into_inner().data; | |||
| 
 | |||
|     if data.Type == SendType::File as i32 { | |||
|         err!("File sends should use /api/sends/file") | |||
|     } | |||
| 
 | |||
|     let mut send = create_send(data, headers.user.uuid.clone())?; | |||
|     send.save(&conn)?; | |||
|     nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); | |||
| 
 | |||
|     Ok(Json(send.to_json())) | |||
| } | |||
| 
 | |||
| #[post("/sends/file", format = "multipart/form-data", data = "<data>")] | |||
| fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | |||
|     let boundary = content_type.params().next().expect("No boundary provided").1; | |||
| 
 | |||
|     let mut mpart = Multipart::with_body(data.open(), boundary); | |||
| 
 | |||
|     // First entry is the SendData JSON
 | |||
|     let mut model_entry = match mpart.read_entry()? { | |||
|         Some(e) if &*e.headers.name == "model" => e, | |||
|         Some(_) => err!("Invalid entry name"), | |||
|         None => err!("No model entry present"), | |||
|     }; | |||
| 
 | |||
|     let mut buf = String::new(); | |||
|     model_entry.data.read_to_string(&mut buf)?; | |||
|     let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?; | |||
| 
 | |||
|     // Get the file length and add an extra 10% to avoid issues
 | |||
|     const SIZE_110_MB: u64 = 115_343_360; | |||
| 
 | |||
|     let size_limit = match CONFIG.user_attachment_limit() { | |||
|         Some(0) => err!("File uploads are disabled"), | |||
|         Some(limit_kb) => { | |||
|             let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn); | |||
|             if left <= 0 { | |||
|                 err!("Attachment size limit reached! Delete some files to open space") | |||
|             } | |||
|             std::cmp::Ord::max(left as u64, SIZE_110_MB) | |||
|         } | |||
|         None => SIZE_110_MB, | |||
|     }; | |||
| 
 | |||
|     // Create the Send
 | |||
|     let mut send = create_send(data.data, headers.user.uuid.clone())?; | |||
|     let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32])); | |||
| 
 | |||
|     if send.atype != SendType::File as i32 { | |||
|         err!("Send content is not a file"); | |||
|     } | |||
| 
 | |||
|     let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id); | |||
| 
 | |||
|     // Read the data entry and save the file
 | |||
|     let mut data_entry = match mpart.read_entry()? { | |||
|         Some(e) if &*e.headers.name == "data" => e, | |||
|         Some(_) => err!("Invalid entry name"), | |||
|         None => err!("No model entry present"), | |||
|     }; | |||
| 
 | |||
|     let size = match data_entry | |||
|         .data | |||
|         .save() | |||
|         .memory_threshold(0) | |||
|         .size_limit(size_limit) | |||
|         .with_path(&file_path) | |||
|     { | |||
|         SaveResult::Full(SavedData::File(_, size)) => size as i32, | |||
|         SaveResult::Full(other) => { | |||
|             std::fs::remove_file(&file_path).ok(); | |||
|             err!(format!("Attachment is not a file: {:?}", other)); | |||
|         } | |||
|         SaveResult::Partial(_, reason) => { | |||
|             std::fs::remove_file(&file_path).ok(); | |||
|             err!(format!("Attachment size limit exceeded with this file: {:?}", reason)); | |||
|         } | |||
|         SaveResult::Error(e) => { | |||
|             std::fs::remove_file(&file_path).ok(); | |||
|             err!(format!("Error: {:?}", e)); | |||
|         } | |||
|     }; | |||
| 
 | |||
|     // Set ID and sizes
 | |||
|     let mut data_value: Value = serde_json::from_str(&send.data)?; | |||
|     if let Some(o) = data_value.as_object_mut() { | |||
|         o.insert(String::from("Id"), Value::String(file_id)); | |||
|         o.insert(String::from("Size"), Value::Number(size.into())); | |||
|         o.insert( | |||
|             String::from("SizeName"), | |||
|             Value::String(crate::util::get_display_size(size)), | |||
|         ); | |||
|     } | |||
|     send.data = serde_json::to_string(&data_value)?; | |||
| 
 | |||
|     // Save the changes in the database
 | |||
|     send.save(&conn)?; | |||
|     nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); | |||
| 
 | |||
|     Ok(Json(send.to_json())) | |||
| } | |||
| 
 | |||
| #[derive(Deserialize)] | |||
| #[allow(non_snake_case)] | |||
| pub struct SendAccessData { | |||
|     pub Password: Option<String>, | |||
| } | |||
| 
 | |||
| #[post("/sends/access/<access_id>", data = "<data>")] | |||
| fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn) -> JsonResult { | |||
|     let mut send = match Send::find_by_access_id(&access_id, &conn) { | |||
|         Some(s) => s, | |||
|         None => err_code!("Send not found", 404), | |||
|     }; | |||
| 
 | |||
|     if let Some(max_access_count) = send.max_access_count { | |||
|         if send.access_count > max_access_count { | |||
|             err_code!("Max access count reached", 404); | |||
|         } | |||
|     } | |||
| 
 | |||
|     if let Some(expiration) = send.expiration_date { | |||
|         if Utc::now().naive_utc() > expiration { | |||
|             err_code!("Send has expired", 404) | |||
|         } | |||
|     } | |||
| 
 | |||
|     if Utc::now().naive_utc() > send.deletion_date { | |||
|         err_code!("Send has been deleted", 404) | |||
|     } | |||
| 
 | |||
|     if send.disabled { | |||
|         err_code!("Send has been disabled", 404) | |||
|     } | |||
| 
 | |||
|     if send.password_hash.is_some() { | |||
|         match data.into_inner().data.Password { | |||
|             Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } | |||
|             Some(_) => err!("Invalid password."), | |||
|             None => err_code!("Password not provided", 401), | |||
|         } | |||
|     } | |||
| 
 | |||
|     // Files are incremented during the download
 | |||
|     if send.atype == SendType::Text as i32 { | |||
|         send.access_count += 1; | |||
|     } | |||
| 
 | |||
|     send.save(&conn)?; | |||
| 
 | |||
|     Ok(Json(send.to_json())) | |||
| } | |||
| 
 | |||
| #[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")] | |||
| fn post_access_file( | |||
|     send_id: String, | |||
|     file_id: String, | |||
|     data: JsonUpcase<SendAccessData>, | |||
|     host: Host, | |||
|     conn: DbConn, | |||
| ) -> JsonResult { | |||
|     let mut send = match Send::find_by_uuid(&send_id, &conn) { | |||
|         Some(s) => s, | |||
|         None => err_code!("Send not found", 404), | |||
|     }; | |||
| 
 | |||
|     if let Some(max_access_count) = send.max_access_count { | |||
|         if send.access_count > max_access_count { | |||
|             err_code!("Max access count reached", 404); | |||
|         } | |||
|     } | |||
| 
 | |||
|     if let Some(expiration) = send.expiration_date { | |||
|         if Utc::now().naive_utc() > expiration { | |||
|             err_code!("Send has expired", 404) | |||
|         } | |||
|     } | |||
| 
 | |||
|     if Utc::now().naive_utc() > send.deletion_date { | |||
|         err_code!("Send has been deleted", 404) | |||
|     } | |||
| 
 | |||
|     if send.disabled { | |||
|         err_code!("Send has been disabled", 404) | |||
|     } | |||
| 
 | |||
|     if send.password_hash.is_some() { | |||
|         match data.into_inner().data.Password { | |||
|             Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } | |||
|             Some(_) => err!("Invalid password."), | |||
|             None => err_code!("Password not provided", 401), | |||
|         } | |||
|     } | |||
| 
 | |||
|     send.access_count += 1; | |||
| 
 | |||
|     send.save(&conn)?; | |||
| 
 | |||
|     Ok(Json(json!({ | |||
|         "Object": "send-fileDownload", | |||
|         "Id": file_id, | |||
|         "Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id) | |||
|     }))) | |||
| } | |||
| 
 | |||
| #[put("/sends/<id>", data = "<data>")] | |||
| fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | |||
|     let data: SendData = data.into_inner().data; | |||
| 
 | |||
|     let mut send = match Send::find_by_uuid(&id, &conn) { | |||
|         Some(s) => s, | |||
|         None => err!("Send not found"), | |||
|     }; | |||
| 
 | |||
|     if send.user_uuid.as_ref() != Some(&headers.user.uuid) { | |||
|         err!("Send is not owned by user") | |||
|     } | |||
| 
 | |||
|     if send.atype != data.Type { | |||
|         err!("Sends can't change type") | |||
|     } | |||
| 
 | |||
|     let data_val = if data.Type == SendType::Text as i32 { | |||
|         data.Text | |||
|     } else if data.Type == SendType::File as i32 { | |||
|         data.File | |||
|     } else { | |||
|         err!("Invalid Send type") | |||
|     }; | |||
| 
 | |||
|     let data_str = if let Some(mut d) = data_val { | |||
|         d.as_object_mut().and_then(|d| d.remove("Response")); | |||
|         serde_json::to_string(&d)? | |||
|     } else { | |||
|         err!("Send data not provided"); | |||
|     }; | |||
| 
 | |||
|     if data.DeletionDate > Utc::now() + Duration::days(31) { | |||
|         err!( | |||
|             "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." | |||
|         ); | |||
|     } | |||
|     send.data = data_str; | |||
|     send.name = data.Name; | |||
|     send.key = data.Key; | |||
|     send.deletion_date = data.DeletionDate.naive_utc(); | |||
|     send.notes = data.Notes; | |||
|     send.max_access_count = data.MaxAccessCount; | |||
|     send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); | |||
|     send.disabled = data.Disabled; | |||
| 
 | |||
|     // Only change the value if it's present
 | |||
|     if let Some(password) = data.Password { | |||
|         send.set_password(Some(&password)); | |||
|     } | |||
| 
 | |||
|     send.save(&conn)?; | |||
|     nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); | |||
| 
 | |||
|     Ok(Json(send.to_json())) | |||
| } | |||
| 
 | |||
| #[delete("/sends/<id>")] | |||
| fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { | |||
|     let send = match Send::find_by_uuid(&id, &conn) { | |||
|         Some(s) => s, | |||
|         None => err!("Send not found"), | |||
|     }; | |||
| 
 | |||
|     if send.user_uuid.as_ref() != Some(&headers.user.uuid) { | |||
|         err!("Send is not owned by user") | |||
|     } | |||
| 
 | |||
|     if send.atype == SendType::File as i32 { | |||
|         std::fs::remove_dir_all(Path::new(&CONFIG.sends_folder()).join(&send.uuid)).ok(); | |||
|     } | |||
| 
 | |||
|     send.delete(&conn)?; | |||
|     nt.send_user_update(UpdateType::SyncSendDelete, &headers.user); | |||
| 
 | |||
|     Ok(()) | |||
| } | |||
| 
 | |||
| #[put("/sends/<id>/remove-password")] | |||
| fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { | |||
|     let mut send = match Send::find_by_uuid(&id, &conn) { | |||
|         Some(s) => s, | |||
|         None => err!("Send not found"), | |||
|     }; | |||
| 
 | |||
|     if send.user_uuid.as_ref() != Some(&headers.user.uuid) { | |||
|         err!("Send is not owned by user") | |||
|     } | |||
| 
 | |||
|     send.set_password(None); | |||
|     send.save(&conn)?; | |||
|     nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); | |||
| 
 | |||
|     Ok(Json(send.to_json())) | |||
| } | |||
| @ -0,0 +1,235 @@ | |||
| use chrono::{NaiveDateTime, Utc}; | |||
| use serde_json::Value; | |||
| 
 | |||
| use super::{Organization, User}; | |||
| 
 | |||
| db_object! { | |||
|     #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] | |||
|     #[table_name = "sends"] | |||
|     #[changeset_options(treat_none_as_null="true")] | |||
|     #[belongs_to(User, foreign_key = "user_uuid")] | |||
|     #[belongs_to(Organization, foreign_key = "organization_uuid")] | |||
|     #[primary_key(uuid)] | |||
|     pub struct Send { | |||
|         pub uuid: String, | |||
| 
 | |||
|         pub user_uuid: Option<String>, | |||
|         pub organization_uuid: Option<String>, | |||
| 
 | |||
| 
 | |||
|         pub name: String, | |||
|         pub notes: Option<String>, | |||
| 
 | |||
|         pub atype: i32, | |||
|         pub data: String, | |||
|         pub key: String, | |||
|         pub password_hash: Option<Vec<u8>>, | |||
|         password_salt: Option<Vec<u8>>, | |||
|         password_iter: Option<i32>, | |||
| 
 | |||
|         pub max_access_count: Option<i32>, | |||
|         pub access_count: i32, | |||
| 
 | |||
|         pub creation_date: NaiveDateTime, | |||
|         pub revision_date: NaiveDateTime, | |||
|         pub expiration_date: Option<NaiveDateTime>, | |||
|         pub deletion_date: NaiveDateTime, | |||
| 
 | |||
|         pub disabled: bool, | |||
|     } | |||
| } | |||
| 
 | |||
| #[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] | |||
| pub enum SendType { | |||
|     Text = 0, | |||
|     File = 1, | |||
| } | |||
| 
 | |||
| impl Send { | |||
|     pub fn new(atype: i32, name: String, data: String, key: String, deletion_date: NaiveDateTime) -> Self { | |||
|         let now = Utc::now().naive_utc(); | |||
| 
 | |||
|         Self { | |||
|             uuid: crate::util::get_uuid(), | |||
|             user_uuid: None, | |||
|             organization_uuid: None, | |||
| 
 | |||
|             name, | |||
|             notes: None, | |||
| 
 | |||
|             atype, | |||
|             data, | |||
|             key, | |||
|             password_hash: None, | |||
|             password_salt: None, | |||
|             password_iter: None, | |||
| 
 | |||
|             max_access_count: None, | |||
|             access_count: 0, | |||
| 
 | |||
|             creation_date: now, | |||
|             revision_date: now, | |||
|             expiration_date: None, | |||
|             deletion_date, | |||
| 
 | |||
|             disabled: false, | |||
|         } | |||
|     } | |||
|     
 | |||
|     pub fn set_password(&mut self, password: Option<&str>) { | |||
|         const PASSWORD_ITER: i32 = 100_000; | |||
| 
 | |||
|         if let Some(password) = password { | |||
|             self.password_iter = Some(PASSWORD_ITER); | |||
|             let salt = crate::crypto::get_random_64(); | |||
|             let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32); | |||
|             self.password_salt = Some(salt); | |||
|             self.password_hash = Some(hash); | |||
|         } else { | |||
|             self.password_iter = None; | |||
|             self.password_salt = None; | |||
|             self.password_hash = None; | |||
|         } | |||
|     } | |||
| 
 | |||
|     pub fn check_password(&self, password: &str) -> bool { | |||
|         match (&self.password_hash, &self.password_salt, self.password_iter) { | |||
|             (Some(hash), Some(salt), Some(iter)) => { | |||
|                 crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32) | |||
|             } | |||
|             _ => false, | |||
|         } | |||
|     } | |||
| 
 | |||
|     pub fn to_json(&self) -> Value { | |||
|         use crate::util::format_date; | |||
|         use data_encoding::BASE64URL_NOPAD; | |||
|         use uuid::Uuid; | |||
| 
 | |||
|         let data: Value = serde_json::from_str(&self.data).unwrap_or_default(); | |||
| 
 | |||
|         json!({ | |||
|             "Id": self.uuid, | |||
|             "AccessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()), | |||
|             "Type": self.atype, | |||
| 
 | |||
|             "Name": self.name, | |||
|             "Notes": self.notes, | |||
|             "Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None }, | |||
|             "File": if self.atype == SendType::File as i32 { Some(&data) } else { None }, | |||
| 
 | |||
|             "Key": self.key, | |||
|             "MaxAccessCount": self.max_access_count, | |||
|             "AccessCount": self.access_count, | |||
|             "Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)), | |||
|             "Disabled": self.disabled, | |||
| 
 | |||
|             "RevisionDate": format_date(&self.revision_date), | |||
|             "ExpirationDate": self.expiration_date.as_ref().map(format_date), | |||
|             "DeletionDate": format_date(&self.deletion_date), | |||
|             "Object": "send", | |||
|         }) | |||
|     } | |||
| } | |||
| 
 | |||
| use crate::db::DbConn; | |||
| 
 | |||
| use crate::api::EmptyResult; | |||
| use crate::error::MapResult; | |||
| 
 | |||
| impl Send { | |||
|     pub fn save(&mut self, conn: &DbConn) -> EmptyResult { | |||
|         // self.update_users_revision(conn);
 | |||
|         self.revision_date = Utc::now().naive_utc(); | |||
| 
 | |||
|         db_run! { conn: | |||
|             sqlite, mysql { | |||
|                 match diesel::replace_into(sends::table) | |||
|                     .values(SendDb::to_db(self)) | |||
|                     .execute(conn) | |||
|                 { | |||
|                     Ok(_) => Ok(()), | |||
|                     // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
 | |||
|                     Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { | |||
|                         diesel::update(sends::table) | |||
|                             .filter(sends::uuid.eq(&self.uuid)) | |||
|                             .set(SendDb::to_db(self)) | |||
|                             .execute(conn) | |||
|                             .map_res("Error saving send") | |||
|                     } | |||
|                     Err(e) => Err(e.into()), | |||
|                 }.map_res("Error saving send") | |||
|             } | |||
|             postgresql { | |||
|                 let value = SendDb::to_db(self); | |||
|                 diesel::insert_into(sends::table) | |||
|                     .values(&value) | |||
|                     .on_conflict(sends::uuid) | |||
|                     .do_update() | |||
|                     .set(&value) | |||
|                     .execute(conn) | |||
|                     .map_res("Error saving send") | |||
|             } | |||
|         } | |||
|     } | |||
| 
 | |||
|     pub fn delete(&self, conn: &DbConn) -> EmptyResult { | |||
|         // self.update_users_revision(conn);
 | |||
| 
 | |||
|         db_run! { conn: { | |||
|             diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid))) | |||
|                 .execute(conn) | |||
|                 .map_res("Error deleting send") | |||
|         }} | |||
|     } | |||
| 
 | |||
|     pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { | |||
|         for send in Self::find_by_user(user_uuid, &conn) { | |||
|             send.delete(&conn)?; | |||
|         } | |||
|         Ok(()) | |||
|     } | |||
| 
 | |||
|     pub fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> { | |||
|         use data_encoding::BASE64URL_NOPAD; | |||
|         use uuid::Uuid; | |||
| 
 | |||
|         let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) { | |||
|             Ok(v) => v, | |||
|             Err(_) => return None, | |||
|         }; | |||
| 
 | |||
|         let uuid = match Uuid::from_slice(&uuid_vec) { | |||
|             Ok(u) => u.to_string(), | |||
|             Err(_) => return None, | |||
|         }; | |||
| 
 | |||
|         Self::find_by_uuid(&uuid, conn) | |||
|     } | |||
| 
 | |||
|     pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { | |||
|         db_run! {conn: { | |||
|             sends::table | |||
|                 .filter(sends::uuid.eq(uuid)) | |||
|                 .first::<SendDb>(conn) | |||
|                 .ok() | |||
|                 .from_db() | |||
|         }} | |||
|     } | |||
| 
 | |||
|     pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { | |||
|         db_run! {conn: { | |||
|             sends::table | |||
|                 .filter(sends::user_uuid.eq(user_uuid)) | |||
|                 .load::<SendDb>(conn).expect("Error loading sends").from_db() | |||
|         }} | |||
|     } | |||
| 
 | |||
|     pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { | |||
|         db_run! {conn: { | |||
|             sends::table | |||
|                 .filter(sends::organization_uuid.eq(org_uuid)) | |||
|                 .load::<SendDb>(conn).expect("Error loading sends").from_db() | |||
|         }} | |||
|     } | |||
| } | |||
					Loading…
					
					
				
		Reference in new issue