2022-09-07 13:25:51 +02:00
mod data ;
2022-10-09 15:34:36 +02:00
use std ::io ::Cursor ;
2022-09-07 13:25:51 +02:00
pub use data ::Data ;
2020-10-19 15:29:36 +02:00
2022-10-05 20:41:05 +02:00
use crate ::{ services , Result };
2022-10-09 15:34:36 +02:00
use image ::imageops ::FilterType ;
2022-10-08 13:04:55 +02:00
2021-09-13 19:45:56 +02:00
use tokio ::{
fs ::File ,
2023-06-25 19:31:40 +02:00
io ::{ AsyncReadExt , AsyncWriteExt , BufReader },
2021-09-13 19:45:56 +02:00
};
2020-05-18 17:53:34 +02:00
2020-07-28 08:59:30 -04:00
pub struct FileMeta {
2021-05-30 21:55:43 +02:00
pub content_disposition : Option < String > ,
2020-11-18 08:36:12 -05:00
pub content_type : Option < String > ,
2020-07-28 08:59:30 -04:00
pub file : Vec < u8 > ,
}
2020-07-25 23:56:50 -04:00
2022-10-05 12:45:54 +02:00
pub struct Service {
2022-10-08 13:02:52 +02:00
pub db : & 'static dyn Data ,
2020-05-18 17:53:34 +02:00
}
2022-10-05 12:45:54 +02:00
impl Service {
2021-06-04 08:06:12 +04:30
/// Uploads a file.
pub async fn create (
2020-05-18 17:53:34 +02:00
& self ,
mxc : String ,
2022-10-05 15:33:57 +02:00
content_disposition : Option <& str > ,
content_type : Option <& str > ,
2020-05-18 17:53:34 +02:00
file : & [ u8 ],
) -> Result < () > {
2022-09-07 13:25:51 +02:00
// Width, Height = 0 if it's not a thumbnail
2022-10-05 20:34:31 +02:00
let key = self
. db
. create_file_metadata ( mxc , 0 , 0 , content_disposition , content_type ) ? ;
2020-05-18 17:53:34 +02:00
2023-11-25 02:11:41 -05:00
let path : std ::path ::PathBuf ;
if cfg! ( feature = "sha256_media" ) {
path = services (). globals . get_media_file_new ( & key );
} else {
path = services (). globals . get_media_file ( & key );
}
2021-06-04 08:06:12 +04:30
let mut f = File ::create ( path ). await ? ;
f . write_all ( file ). await ? ;
2020-05-18 17:53:34 +02:00
Ok (())
}
2020-09-14 14:20:38 +02:00
/// Uploads or replaces a file thumbnail.
2021-07-14 12:31:38 +02:00
#[allow(clippy::too_many_arguments)]
2021-06-04 08:06:12 +04:30
pub async fn upload_thumbnail (
2020-09-14 14:20:38 +02:00
& self ,
mxc : String ,
2022-10-05 15:33:57 +02:00
content_disposition : Option <& str > ,
content_type : Option <& str > ,
2020-09-14 14:20:38 +02:00
width : u32 ,
height : u32 ,
file : & [ u8 ],
) -> Result < () > {
2022-10-05 20:34:31 +02:00
let key =
self . db
. create_file_metadata ( mxc , width , height , content_disposition , content_type ) ? ;
2020-09-14 14:20:38 +02:00
2023-11-25 02:11:41 -05:00
let path : std ::path ::PathBuf ;
if cfg! ( feature = "sha256_media" ) {
path = services (). globals . get_media_file_new ( & key );
} else {
path = services (). globals . get_media_file ( & key );
}
2021-06-04 08:06:12 +04:30
let mut f = File ::create ( path ). await ? ;
f . write_all ( file ). await ? ;
2020-09-14 14:20:38 +02:00
Ok (())
}
2020-05-18 17:53:34 +02:00
/// Downloads a file.
2022-09-07 13:25:51 +02:00
pub async fn get ( & self , mxc : String ) -> Result < Option < FileMeta >> {
2022-10-05 20:34:31 +02:00
if let Ok (( content_disposition , content_type , key )) =
self . db . search_file_metadata ( mxc , 0 , 0 )
{
2023-11-25 02:11:41 -05:00
let path : std ::path ::PathBuf ;
if cfg! ( feature = "sha256_media" ) {
path = services (). globals . get_media_file_new ( & key );
} else {
path = services (). globals . get_media_file ( & key );
}
2021-06-08 20:53:24 +04:30
let mut file = Vec ::new ();
2023-06-25 19:31:40 +02:00
BufReader ::new ( File ::open ( path ). await ? )
. read_to_end ( & mut file )
. await ? ;
2020-05-18 17:53:34 +02:00
2020-07-28 08:59:30 -04:00
Ok ( Some ( FileMeta {
2021-05-30 21:55:43 +02:00
content_disposition ,
2020-07-28 08:59:30 -04:00
content_type ,
2021-06-04 08:06:12 +04:30
file ,
2020-07-28 08:59:30 -04:00
}))
2020-05-19 18:31:34 +02:00
} else {
Ok ( None )
}
}
2020-10-19 15:29:36 +02:00
/// Returns width, height of the thumbnail and whether it should be cropped. Returns None when
/// the server should send the original file.
pub fn thumbnail_properties ( & self , width : u32 , height : u32 ) -> Option < ( u32 , u32 , bool ) > {
match ( width , height ) {
( 0 ..= 32 , 0 ..= 32 ) => Some (( 32 , 32 , true )),
( 0 ..= 96 , 0 ..= 96 ) => Some (( 96 , 96 , true )),
( 0 ..= 320 , 0 ..= 240 ) => Some (( 320 , 240 , false )),
( 0 ..= 640 , 0 ..= 480 ) => Some (( 640 , 480 , false )),
( 0 ..= 800 , 0 ..= 600 ) => Some (( 800 , 600 , false )),
_ => None ,
}
}
2020-05-19 18:31:34 +02:00
/// Downloads a file's thumbnail.
2020-10-19 15:29:36 +02:00
///
/// Here's an example on how it works:
///
/// - Client requests an image with width=567, height=567
/// - Server rounds that up to (800, 600), so it doesn't have to save too many thumbnails
/// - Server rounds that up again to (958, 600) to fix the aspect ratio (only for width,height>96)
/// - Server creates the thumbnail and sends it to the user
///
/// For width,height <= 96 the server uses another thumbnailing algorithm which crops the image afterwards.
2021-06-06 16:58:32 +04:30
pub async fn get_thumbnail (
& self ,
2022-09-07 13:25:51 +02:00
mxc : String ,
2021-06-06 16:58:32 +04:30
width : u32 ,
height : u32 ,
) -> Result < Option < FileMeta >> {
2020-10-19 15:29:36 +02:00
let ( width , height , crop ) = self
. thumbnail_properties ( width , height )
. unwrap_or (( 0 , 0 , false )); // 0, 0 because that's the original file
2022-10-05 20:34:31 +02:00
if let Ok (( content_disposition , content_type , key )) =
self . db . search_file_metadata ( mxc . clone (), width , height )
{
2020-05-19 18:31:34 +02:00
// Using saved thumbnail
2023-11-25 02:11:41 -05:00
let path : std ::path ::PathBuf ;
if cfg! ( feature = "sha256_media" ) {
path = services (). globals . get_media_file_new ( & key );
} else {
path = services (). globals . get_media_file ( & key );
}
2021-06-08 20:53:24 +04:30
let mut file = Vec ::new ();
2021-06-04 08:06:12 +04:30
File ::open ( path ). await ? . read_to_end ( & mut file ). await ? ;
2020-05-19 18:31:34 +02:00
2020-07-28 08:59:30 -04:00
Ok ( Some ( FileMeta {
2021-05-30 21:55:43 +02:00
content_disposition ,
2020-07-28 08:59:30 -04:00
content_type ,
file : file . to_vec (),
}))
2022-10-05 20:34:31 +02:00
} else if let Ok (( content_disposition , content_type , key )) =
self . db . search_file_metadata ( mxc . clone (), 0 , 0 )
{
2020-05-19 18:31:34 +02:00
// Generate a thumbnail
2023-11-25 02:11:41 -05:00
let path : std ::path ::PathBuf ;
if cfg! ( feature = "sha256_media" ) {
path = services (). globals . get_media_file_new ( & key );
} else {
path = services (). globals . get_media_file ( & key );
}
2021-06-08 20:53:24 +04:30
let mut file = Vec ::new ();
2021-06-04 08:06:12 +04:30
File ::open ( path ). await ? . read_to_end ( & mut file ). await ? ;
2021-06-06 16:58:32 +04:30
2020-05-19 18:31:34 +02:00
if let Ok ( image ) = image ::load_from_memory ( & file ) {
2020-10-19 15:29:36 +02:00
let original_width = image . width ();
let original_height = image . height ();
if width > original_width || height > original_height {
return Ok ( Some ( FileMeta {
2021-05-30 21:55:43 +02:00
content_disposition ,
2020-10-19 15:29:36 +02:00
content_type ,
file : file . to_vec (),
}));
}
let thumbnail = if crop {
2021-03-23 19:46:54 +01:00
image . resize_to_fill ( width , height , FilterType ::CatmullRom )
2020-10-19 15:29:36 +02:00
} else {
let ( exact_width , exact_height ) = {
// Copied from image::dynimage::resize_dimensions
let ratio = u64 ::from ( original_width ) * u64 ::from ( height );
let nratio = u64 ::from ( width ) * u64 ::from ( original_height );
2021-03-23 19:46:54 +01:00
let use_width = nratio <= ratio ;
2020-10-19 15:29:36 +02:00
let intermediate = if use_width {
2021-03-23 19:46:54 +01:00
u64 ::from ( original_height ) * u64 ::from ( width )
/ u64 ::from ( original_width )
2020-10-19 15:29:36 +02:00
} else {
u64 ::from ( original_width ) * u64 ::from ( height )
/ u64 ::from ( original_height )
};
if use_width {
if intermediate <= u64 ::from ( ::std ::u32 ::MAX ) {
( width , intermediate as u32 )
} else {
(
( u64 ::from ( width ) * u64 ::from ( ::std ::u32 ::MAX ) / intermediate )
as u32 ,
::std ::u32 ::MAX ,
)
}
} else if intermediate <= u64 ::from ( ::std ::u32 ::MAX ) {
( intermediate as u32 , height )
} else {
(
::std ::u32 ::MAX ,
( u64 ::from ( height ) * u64 ::from ( ::std ::u32 ::MAX ) / intermediate )
as u32 ,
)
}
};
2021-03-24 11:52:10 +01:00
image . thumbnail_exact ( exact_width , exact_height )
2020-10-19 15:29:36 +02:00
};
2020-05-19 18:31:34 +02:00
let mut thumbnail_bytes = Vec ::new ();
2022-10-09 15:34:36 +02:00
thumbnail . write_to (
& mut Cursor ::new ( & mut thumbnail_bytes ),
image ::ImageOutputFormat ::Png ,
) ? ;
2020-05-19 18:31:34 +02:00
// Save thumbnail in database so we don't have to generate it again next time
2022-10-05 20:34:31 +02:00
let thumbnail_key = self . db . create_file_metadata (
mxc ,
width ,
height ,
content_disposition . as_deref (),
content_type . as_deref (),
) ? ;
2020-05-19 18:31:34 +02:00
2023-11-25 02:11:41 -05:00
let path : std ::path ::PathBuf ;
if cfg! ( feature = "sha256_media" ) {
2023-11-25 15:46:03 -05:00
path = services (). globals . get_media_file_new ( & thumbnail_key );
2023-11-25 02:11:41 -05:00
} else {
2023-11-25 15:46:03 -05:00
path = services (). globals . get_media_file ( & thumbnail_key );
2023-11-25 02:11:41 -05:00
}
2021-06-04 08:06:12 +04:30
let mut f = File ::create ( path ). await ? ;
f . write_all ( & thumbnail_bytes ). await ? ;
2021-06-06 16:58:32 +04:30
2020-07-28 08:59:30 -04:00
Ok ( Some ( FileMeta {
2021-05-30 21:55:43 +02:00
content_disposition ,
2020-07-28 08:59:30 -04:00
content_type ,
2021-06-06 16:58:32 +04:30
file : thumbnail_bytes . to_vec (),
2020-07-28 08:59:30 -04:00
}))
2020-05-19 18:31:34 +02:00
} else {
2020-12-08 10:33:44 +01:00
// Couldn't parse file to generate thumbnail, send original
Ok ( Some ( FileMeta {
2021-05-30 21:55:43 +02:00
content_disposition ,
2020-12-08 10:33:44 +01:00
content_type ,
2021-06-06 16:58:32 +04:30
file : file . to_vec (),
2020-12-08 10:33:44 +01:00
}))
2020-05-19 18:31:34 +02:00
}
2020-05-18 17:53:34 +02:00
} else {
Ok ( None )
}
}
}
2023-11-25 02:11:41 -05:00
#[cfg(test)]
mod tests {
use std ::path ::PathBuf ;
use sha2 ::Digest ;
2023-11-25 15:53:33 -05:00
use base64 ::{ engine ::general_purpose , Engine as _ };
2023-11-25 02:11:41 -05:00
use super ::* ;
struct MockedKVDatabase ;
impl Data for MockedKVDatabase {
fn create_file_metadata (
& self ,
mxc : String ,
width : u32 ,
height : u32 ,
content_disposition : Option <& str > ,
content_type : Option <& str > ,
) -> Result < Vec < u8 >> {
// copied from src/database/key_value/media.rs
let mut key = mxc . as_bytes (). to_vec ();
key . push ( 0xff );
key . extend_from_slice ( & width . to_be_bytes ());
key . extend_from_slice ( & height . to_be_bytes ());
key . push ( 0xff );
key . extend_from_slice (
content_disposition
. as_ref ()
. map ( | f | f . as_bytes ())
. unwrap_or_default (),
);
key . push ( 0xff );
key . extend_from_slice (
content_type
. as_ref ()
. map ( | c | c . as_bytes ())
. unwrap_or_default (),
);
Ok ( key )
}
fn search_file_metadata (
& self ,
_mxc : String ,
_width : u32 ,
_height : u32 ,
) -> Result < ( Option < String > , Option < String > , Vec < u8 > ) > {
todo! ()
}
}
#[tokio::test]
async fn long_file_names_works () {
static DB : MockedKVDatabase = MockedKVDatabase ;
let media = Service { db : & DB };
let mxc = "mxc://example.com/ascERGshawAWawugaAcauga" . to_owned ();
let width = 100 ;
let height = 100 ;
let content_disposition = "attachment; filename= \" this is a very long file name with spaces and special characters like äöüß and even emoji like 🦀.png \" " ;
let content_type = "image/png" ;
let key = media
. db
. create_file_metadata (
mxc ,
width ,
height ,
Some ( content_disposition ),
Some ( content_type ),
)
. unwrap ();
let mut r = PathBuf ::new ();
r . push ( "/tmp" );
r . push ( "media" );
// r.push(base64::encode_config(key, base64::URL_SAFE_NO_PAD));
// use the sha256 hash of the key as the file name instead of the key itself
// this is because the base64 encoded key can be longer than 255 characters.
r . push ( general_purpose ::URL_SAFE_NO_PAD . encode ( sha2 ::Sha256 ::digest ( & key )));
// Check that the file path is not longer than 255 characters
// (255 is the maximum length of a file path on most file systems)
assert! (
r . to_str (). unwrap (). len () <= 255 ,
"File path is too long: {}" ,
r . to_str (). unwrap (). len ()
);
}
}