Daniel García
4 years ago
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