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
2023-11-25 02:11:41 -05:00
use base64 ::{ engine ::general_purpose , Engine as _ } ;
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 ;
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 ( )
) ;
}
}