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