1 use std::io::ErrorKind; 2 3 use serde_json::Value; 4 5 use super::Cipher; 6 use crate::CONFIG; 7 8 db_object! { 9 #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] 10 #[table_name = "attachments"] 11 #[changeset_options(treat_none_as_null="true")] 12 #[belongs_to(super::Cipher, foreign_key = "cipher_uuid")] 13 #[primary_key(id)] 14 pub struct Attachment { 15 pub id: String, 16 pub cipher_uuid: String, 17 pub file_name: String, // encrypted 18 pub file_size: i32, 19 pub akey: Option<String>, 20 } 21 } 22 23 /// Local methods 24 impl Attachment { new(id: String, cipher_uuid: String, file_name: String, file_size: i32, akey: Option<String>) -> Self25 pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i32, akey: Option<String>) -> Self { 26 Self { 27 id, 28 cipher_uuid, 29 file_name, 30 file_size, 31 akey, 32 } 33 } 34 get_file_path(&self) -> String35 pub fn get_file_path(&self) -> String { 36 format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id) 37 } 38 get_url(&self, host: &str) -> String39 pub fn get_url(&self, host: &str) -> String { 40 format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id) 41 } 42 to_json(&self, host: &str) -> Value43 pub fn to_json(&self, host: &str) -> Value { 44 json!({ 45 "Id": self.id, 46 "Url": self.get_url(host), 47 "FileName": self.file_name, 48 "Size": self.file_size.to_string(), 49 "SizeName": crate::util::get_display_size(self.file_size), 50 "Key": self.akey, 51 "Object": "attachment" 52 }) 53 } 54 } 55 56 use crate::db::DbConn; 57 58 use crate::api::EmptyResult; 59 use crate::error::MapResult; 60 61 /// Database methods 62 impl Attachment { save(&self, conn: &DbConn) -> EmptyResult63 pub fn save(&self, conn: &DbConn) -> EmptyResult { 64 db_run! { conn: 65 sqlite, mysql { 66 match diesel::replace_into(attachments::table) 67 .values(AttachmentDb::to_db(self)) 68 .execute(conn) 69 { 70 Ok(_) => Ok(()), 71 // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. 72 Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { 73 diesel::update(attachments::table) 74 .filter(attachments::id.eq(&self.id)) 75 .set(AttachmentDb::to_db(self)) 76 .execute(conn) 77 .map_res("Error saving attachment") 78 } 79 Err(e) => Err(e.into()), 80 }.map_res("Error saving attachment") 81 } 82 postgresql { 83 let value = AttachmentDb::to_db(self); 84 diesel::insert_into(attachments::table) 85 .values(&value) 86 .on_conflict(attachments::id) 87 .do_update() 88 .set(&value) 89 .execute(conn) 90 .map_res("Error saving attachment") 91 } 92 } 93 } 94 delete(&self, conn: &DbConn) -> EmptyResult95 pub fn delete(&self, conn: &DbConn) -> EmptyResult { 96 db_run! { conn: { 97 crate::util::retry( 98 || diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn), 99 10, 100 ) 101 .map_res("Error deleting attachment")?; 102 103 let file_path = &self.get_file_path(); 104 105 match crate::util::delete_file(file_path) { 106 // Ignore "file not found" errors. This can happen when the 107 // upstream caller has already cleaned up the file as part of 108 // its own error handling. 109 Err(e) if e.kind() == ErrorKind::NotFound => { 110 debug!("File '{}' already deleted.", file_path); 111 Ok(()) 112 } 113 Err(e) => Err(e.into()), 114 _ => Ok(()), 115 } 116 }} 117 } 118 delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult119 pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { 120 for attachment in Attachment::find_by_cipher(cipher_uuid, conn) { 121 attachment.delete(conn)?; 122 } 123 Ok(()) 124 } 125 find_by_id(id: &str, conn: &DbConn) -> Option<Self>126 pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> { 127 db_run! { conn: { 128 attachments::table 129 .filter(attachments::id.eq(id.to_lowercase())) 130 .first::<AttachmentDb>(conn) 131 .ok() 132 .from_db() 133 }} 134 } 135 find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self>136 pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> { 137 db_run! { conn: { 138 attachments::table 139 .filter(attachments::cipher_uuid.eq(cipher_uuid)) 140 .load::<AttachmentDb>(conn) 141 .expect("Error loading attachments") 142 .from_db() 143 }} 144 } 145 size_by_user(user_uuid: &str, conn: &DbConn) -> i64146 pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 { 147 db_run! { conn: { 148 let result: Option<i64> = attachments::table 149 .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) 150 .filter(ciphers::user_uuid.eq(user_uuid)) 151 .select(diesel::dsl::sum(attachments::file_size)) 152 .first(conn) 153 .expect("Error loading user attachment total size"); 154 result.unwrap_or(0) 155 }} 156 } 157 count_by_user(user_uuid: &str, conn: &DbConn) -> i64158 pub fn count_by_user(user_uuid: &str, conn: &DbConn) -> i64 { 159 db_run! { conn: { 160 attachments::table 161 .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) 162 .filter(ciphers::user_uuid.eq(user_uuid)) 163 .count() 164 .first(conn) 165 .unwrap_or(0) 166 }} 167 } 168 size_by_org(org_uuid: &str, conn: &DbConn) -> i64169 pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 { 170 db_run! { conn: { 171 let result: Option<i64> = attachments::table 172 .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) 173 .filter(ciphers::organization_uuid.eq(org_uuid)) 174 .select(diesel::dsl::sum(attachments::file_size)) 175 .first(conn) 176 .expect("Error loading user attachment total size"); 177 result.unwrap_or(0) 178 }} 179 } 180 count_by_org(org_uuid: &str, conn: &DbConn) -> i64181 pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { 182 db_run! { conn: { 183 attachments::table 184 .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) 185 .filter(ciphers::organization_uuid.eq(org_uuid)) 186 .count() 187 .first(conn) 188 .unwrap_or(0) 189 }} 190 } 191 } 192