mirror of https://gitlab.com/famedly/conduit.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1036 lines
36 KiB
1036 lines
36 KiB
use std::{collections::BTreeMap, ops::Range, slice::Split}; |
|
|
|
use ruma::{api::client::error::ErrorKind, OwnedServerName, ServerName, UserId}; |
|
use sha2::{digest::Output, Sha256}; |
|
use tracing::error; |
|
|
|
use crate::{ |
|
database::KeyValueDatabase, |
|
service::{ |
|
self, |
|
media::{BlockedMediaInfo, DbFileMeta}, |
|
}, |
|
utils, Error, Result, |
|
}; |
|
|
|
impl service::media::Data for KeyValueDatabase { |
|
fn create_file_metadata( |
|
&self, |
|
sha256_digest: Output<Sha256>, |
|
file_size: u64, |
|
servername: &ServerName, |
|
media_id: &str, |
|
filename: Option<&str>, |
|
content_type: Option<&str>, |
|
user_id: Option<&UserId>, |
|
is_blocked_filehash: bool, |
|
) -> Result<()> { |
|
if !is_blocked_filehash { |
|
let metadata = FilehashMetadata::new(file_size); |
|
|
|
self.filehash_metadata |
|
.insert(&sha256_digest, metadata.value())?; |
|
}; |
|
|
|
let mut key = sha256_digest.to_vec(); |
|
key.extend_from_slice(servername.as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
self.filehash_servername_mediaid.insert(&key, &[])?; |
|
|
|
let mut key = servername.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
let mut value = sha256_digest.to_vec(); |
|
value.extend_from_slice(filename.map(|f| f.as_bytes()).unwrap_or_default()); |
|
value.push(0xff); |
|
value.extend_from_slice(content_type.map(|f| f.as_bytes()).unwrap_or_default()); |
|
|
|
self.servernamemediaid_metadata.insert(&key, &value)?; |
|
|
|
if let Some(user_id) = user_id { |
|
let mut key = servername.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(user_id.localpart().as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
self.servername_userlocalpart_mediaid.insert(&key, &[])?; |
|
|
|
let mut key = servername.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
self.servernamemediaid_userlocalpart |
|
.insert(&key, user_id.localpart().as_bytes())?; |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
fn search_file_metadata(&self, servername: &ServerName, media_id: &str) -> Result<DbFileMeta> { |
|
let mut key = servername.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
let value = self |
|
.servernamemediaid_metadata |
|
.get(&key)? |
|
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Media not found."))?; |
|
|
|
let metadata = parse_metadata(&value).inspect_err(|e| { |
|
error!("Error parsing metadata for \"mxc://{servername}/{media_id}\" from servernamemediaid_metadata: {e}"); |
|
})?; |
|
|
|
// Only assume file is available if there is metadata about the filehash itself |
|
self.filehash_metadata |
|
.get(&metadata.sha256_digest)? |
|
.map(|_| metadata) |
|
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Media not found.")) |
|
} |
|
|
|
fn create_thumbnail_metadata( |
|
&self, |
|
sha256_digest: Output<Sha256>, |
|
file_size: u64, |
|
servername: &ServerName, |
|
media_id: &str, |
|
width: u32, |
|
height: u32, |
|
filename: Option<&str>, |
|
content_type: Option<&str>, |
|
) -> Result<()> { |
|
let metadata = FilehashMetadata::new(file_size); |
|
|
|
self.filehash_metadata |
|
.insert(&sha256_digest, metadata.value())?; |
|
|
|
let mut key = sha256_digest.to_vec(); |
|
key.extend_from_slice(servername.as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(&width.to_be_bytes()); |
|
key.extend_from_slice(&height.to_be_bytes()); |
|
|
|
self.filehash_thumbnailid.insert(&key, &[])?; |
|
|
|
let mut key = servername.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(&width.to_be_bytes()); |
|
key.extend_from_slice(&height.to_be_bytes()); |
|
|
|
let mut value = sha256_digest.to_vec(); |
|
value.extend_from_slice(filename.map(|f| f.as_bytes()).unwrap_or_default()); |
|
value.push(0xff); |
|
value.extend_from_slice(content_type.map(|f| f.as_bytes()).unwrap_or_default()); |
|
|
|
self.thumbnailid_metadata.insert(&key, &value) |
|
} |
|
|
|
fn search_thumbnail_metadata( |
|
&self, |
|
servername: &ServerName, |
|
media_id: &str, |
|
width: u32, |
|
height: u32, |
|
) -> Result<DbFileMeta> { |
|
let mut key = servername.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(&width.to_be_bytes()); |
|
key.extend_from_slice(&height.to_be_bytes()); |
|
|
|
let value = self |
|
.thumbnailid_metadata |
|
.get(&key)? |
|
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Media not found."))?; |
|
|
|
let metadata = parse_metadata(&value).inspect_err(|e| { |
|
error!("Error parsing metadata for thumbnail \"mxc://{servername}/{media_id}\" with dimensions {width}x{height} from thumbnailid_metadata: {e}"); |
|
})?; |
|
|
|
// Only assume file is available if there is metadata about the filehash itself |
|
self.filehash_metadata |
|
.get(&metadata.sha256_digest)? |
|
.map(|_| metadata) |
|
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Media not found.")) |
|
} |
|
|
|
fn purge_and_get_hashes( |
|
&self, |
|
media: &[(OwnedServerName, String)], |
|
force_filehash: bool, |
|
) -> Vec<Result<String>> { |
|
let mut files = Vec::new(); |
|
|
|
let purge = |mut value: Vec<u8>| { |
|
value.truncate(32); |
|
let sha256_digest = value; |
|
|
|
let is_blocked = self.is_blocked_filehash(&sha256_digest)?; |
|
let sha256_hex = hex::encode(&sha256_digest); |
|
|
|
// If the file is blocked, we want to keep the metadata about it so it can be viewed, |
|
// as well as filehashes blocked |
|
self.purge_filehash(sha256_digest, is_blocked)?; |
|
|
|
Ok(sha256_hex) |
|
}; |
|
|
|
for (server_name, media_id) in media { |
|
if force_filehash { |
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
match self.servernamemediaid_metadata.get(&key) { |
|
Ok(Some(value)) => { |
|
files.push(purge(value)); |
|
} |
|
Ok(None) => (), |
|
Err(e) => { |
|
files.push(Err(e)); |
|
} |
|
} |
|
|
|
key.push(0xff); |
|
for (_, value) in self.thumbnailid_metadata.scan_prefix(key) { |
|
files.push(purge(value)); |
|
} |
|
} else { |
|
match self |
|
.is_blocked(server_name, media_id) |
|
.map(|is_blocked| self.purge_mediaid(server_name, media_id, is_blocked)) |
|
{ |
|
Ok(Ok(f)) => { |
|
files.append(&mut f.into_iter().map(Ok).collect()); |
|
} |
|
Ok(Err(e)) | Err(e) => files.push(Err(e)), |
|
} |
|
} |
|
} |
|
|
|
files |
|
} |
|
|
|
fn purge_and_get_hashes_from_user( |
|
&self, |
|
user_id: &UserId, |
|
force_filehash: bool, |
|
after: Option<u64>, |
|
) -> Vec<Result<String>> { |
|
let mut files = Vec::new(); |
|
let mut prefix = user_id.server_name().as_bytes().to_vec(); |
|
prefix.push(0xff); |
|
prefix.extend_from_slice(user_id.localpart().as_bytes()); |
|
prefix.push(0xff); |
|
|
|
let purge_filehash = |sha256_digest: Vec<u8>| { |
|
let sha256_hex = hex::encode(&sha256_digest); |
|
let is_blocked = self.is_blocked_filehash(&sha256_digest)?; |
|
|
|
// If the file is blocked, we want to keep the metadata about it so it can be viewed, |
|
// as well as filehashes blocked |
|
self.purge_filehash(sha256_digest, is_blocked)?; |
|
|
|
Ok(sha256_hex) |
|
}; |
|
|
|
for (k, _) in self.servername_userlocalpart_mediaid.scan_prefix(prefix) { |
|
let metadata = || { |
|
let mut parts = k.rsplit(|&b| b == 0xff); |
|
let media_id_bytes = parts.next().ok_or_else(|| { |
|
Error::bad_database( |
|
"Invalid format for key of servername_userlocalpart_mediaid", |
|
) |
|
})?; |
|
|
|
let media_id = utils::string_from_bytes(media_id_bytes).map_err(|_| { |
|
Error::bad_database( |
|
"Invalid media_id string in servername_userlocalpart_mediaid", |
|
) |
|
})?; |
|
|
|
let mut key = user_id.server_name().as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
Ok(( |
|
self.servernamemediaid_metadata.get(&key)?.ok_or_else(|| { |
|
error!( |
|
"Missing metadata for \"mxc://{}/{media_id}\", despite storing it's uploader", |
|
user_id.server_name() |
|
); |
|
Error::BadDatabase("Missing metadata for media id and server_name") |
|
})?, |
|
media_id, |
|
)) |
|
}; |
|
|
|
let (mut metadata, media_id) = match metadata() { |
|
Ok(v) => v, |
|
Err(e) => { |
|
files.push(Err(e)); |
|
continue; |
|
} |
|
}; |
|
|
|
metadata.truncate(32); |
|
let sha256_digest = metadata; |
|
|
|
if let Some(after) = after { |
|
let metadata = match self |
|
.filehash_metadata |
|
.get(&sha256_digest) |
|
.map(|opt| opt.map(FilehashMetadata::from_vec)) |
|
{ |
|
Ok(Some(metadata)) => metadata, |
|
// If the media has already been deleted, we shouldn't treat that as an error |
|
Ok(None) => continue, |
|
Err(e) => { |
|
files.push(Err(e)); |
|
continue; |
|
} |
|
}; |
|
|
|
let creation = match metadata.creation(&sha256_digest) { |
|
Ok(c) => c, |
|
Err(e) => { |
|
files.push(Err(e)); |
|
continue; |
|
} |
|
}; |
|
|
|
if creation < after { |
|
continue; |
|
} |
|
} |
|
|
|
if force_filehash { |
|
files.push(purge_filehash(sha256_digest)); |
|
|
|
let mut prefix = user_id.server_name().as_bytes().to_vec(); |
|
prefix.push(0xff); |
|
prefix.extend_from_slice(media_id.as_bytes()); |
|
prefix.push(0xff); |
|
for (_, mut metadata) in self.thumbnailid_metadata.scan_prefix(prefix) { |
|
metadata.truncate(32); |
|
let sha256_digest = metadata; |
|
files.push(purge_filehash(sha256_digest)); |
|
} |
|
} else { |
|
match self |
|
.is_blocked(user_id.server_name(), &media_id) |
|
.map(|is_blocked| { |
|
self.purge_mediaid(user_id.server_name(), &media_id, is_blocked) |
|
}) { |
|
Ok(Ok(f)) => { |
|
files.append(&mut f.into_iter().map(Ok).collect()); |
|
} |
|
Ok(Err(e)) | Err(e) => files.push(Err(e)), |
|
} |
|
} |
|
} |
|
|
|
files |
|
} |
|
|
|
fn purge_and_get_hashes_from_server( |
|
&self, |
|
server_name: &ServerName, |
|
force_filehash: bool, |
|
after: Option<u64>, |
|
) -> Vec<Result<String>> { |
|
let mut prefix = server_name.as_bytes().to_vec(); |
|
prefix.push(0xff); |
|
|
|
let mut files = Vec::new(); |
|
|
|
// Purges all references to the given media in the database, |
|
// returning a Vec of hex sha256 digests |
|
let purge_sha256 = |files: &mut Vec<Result<String>>, mut metadata: Vec<u8>| { |
|
metadata.truncate(32); |
|
let sha256_digest = metadata; |
|
|
|
if let Some(after) = after { |
|
let Some(metadata) = self |
|
.filehash_metadata |
|
.get(&sha256_digest)? |
|
.map(FilehashMetadata::from_vec) |
|
else { |
|
// If the media has already been deleted, we shouldn't treat that as an error |
|
return Ok(()); |
|
}; |
|
|
|
if metadata.creation(&sha256_digest)? < after { |
|
return Ok(()); |
|
} |
|
} |
|
|
|
let sha256_hex = hex::encode(&sha256_digest); |
|
let is_blocked = self.is_blocked_filehash(&sha256_digest)?; |
|
|
|
// If the file is blocked, we want to keep the metadata about it so it can be viewed, |
|
// as well as filehashes blocked |
|
self.purge_filehash(sha256_digest, is_blocked)?; |
|
|
|
files.push(Ok(sha256_hex)); |
|
Ok(()) |
|
}; |
|
|
|
let purge_mediaid = |files: &mut Vec<Result<String>>, key: Vec<u8>| { |
|
let mut parts = key.split(|&b| b == 0xff); |
|
|
|
let server_name = parts |
|
.next() |
|
.ok_or_else(|| Error::bad_database("Invalid format of metadata key")) |
|
.map(utils::string_from_bytes)? |
|
.map_err(|_| Error::bad_database("Invalid ServerName String in metadata key")) |
|
.map(OwnedServerName::try_from)? |
|
.map_err(|_| Error::bad_database("Invalid ServerName String in metadata key"))?; |
|
|
|
let media_id = parts |
|
.next() |
|
.ok_or_else(|| Error::bad_database("Invalid format of metadata key")) |
|
.map(utils::string_from_bytes)? |
|
.map_err(|_| Error::bad_database("Invalid Media ID String in metadata key"))?; |
|
|
|
let is_blocked = self.is_blocked(&server_name, &media_id)?; |
|
|
|
files.append( |
|
&mut self |
|
.purge_mediaid(&server_name, &media_id, is_blocked)? |
|
.into_iter() |
|
.map(Ok) |
|
.collect(), |
|
); |
|
|
|
Ok(()) |
|
}; |
|
|
|
for (key, value) in self |
|
.servernamemediaid_metadata |
|
.scan_prefix(prefix.clone()) |
|
.chain(self.thumbnailid_metadata.scan_prefix(prefix.clone())) |
|
{ |
|
if let Err(e) = if force_filehash { |
|
purge_sha256(&mut files, value) |
|
} else { |
|
purge_mediaid(&mut files, key) |
|
} { |
|
files.push(Err(e)); |
|
} |
|
} |
|
|
|
files |
|
} |
|
|
|
fn is_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool> { |
|
let blocked_via_hash = || { |
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
let Some(metadata) = self.servernamemediaid_metadata.get(&key)? else { |
|
return Ok(false); |
|
}; |
|
|
|
let sha256_digest = parse_metadata(&metadata).inspect_err(|e| { |
|
error!("Error parsing metadata for \"mxc://{server_name}/{media_id}\" from servernamemediaid_metadata: {e}"); |
|
})?.sha256_digest; |
|
|
|
self.is_blocked_filehash(&sha256_digest) |
|
}; |
|
|
|
Ok(self.is_directly_blocked(server_name, media_id)? || blocked_via_hash()?) |
|
} |
|
|
|
fn block( |
|
&self, |
|
media: &[(OwnedServerName, String)], |
|
unix_secs: u64, |
|
reason: Option<String>, |
|
) -> Vec<Error> { |
|
let reason = reason.unwrap_or_default(); |
|
let unix_secs = unix_secs.to_be_bytes(); |
|
|
|
let mut errors = Vec::new(); |
|
|
|
for (server_name, media_id) in media { |
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
let mut value = unix_secs.to_vec(); |
|
value.extend_from_slice(reason.as_bytes()); |
|
|
|
if let Err(e) = self.blocked_servername_mediaid.insert(&key, &value) { |
|
errors.push(e); |
|
} |
|
} |
|
|
|
errors |
|
} |
|
|
|
fn block_from_user( |
|
&self, |
|
user_id: &UserId, |
|
now: u64, |
|
reason: &str, |
|
after: Option<u64>, |
|
) -> Vec<Error> { |
|
let mut prefix = user_id.server_name().as_bytes().to_vec(); |
|
prefix.push(0xff); |
|
prefix.extend_from_slice(user_id.localpart().as_bytes()); |
|
prefix.push(0xff); |
|
|
|
let mut value = now.to_be_bytes().to_vec(); |
|
value.extend_from_slice(reason.as_bytes()); |
|
|
|
self.servername_userlocalpart_mediaid |
|
.scan_prefix(prefix) |
|
.map(|(k, _)| { |
|
let parts = k.split(|&b| b == 0xff); |
|
|
|
let media_id = parts.last().ok_or_else(|| { |
|
Error::bad_database("Invalid format of key in blocked_servername_mediaid") |
|
})?; |
|
|
|
let mut key = user_id.server_name().as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id); |
|
|
|
let Some(mut meta) = self.servernamemediaid_metadata.get(&key)? else { |
|
return Err(Error::bad_database( |
|
"Invalid format of metadata in servernamemediaid_metadata", |
|
)); |
|
}; |
|
meta.truncate(32); |
|
let sha256_digest = meta; |
|
|
|
let Some(metadata) = self |
|
.filehash_metadata |
|
.get(&sha256_digest)? |
|
.map(FilehashMetadata::from_vec) |
|
else { |
|
return Ok(()); |
|
}; |
|
|
|
if after |
|
.map(|after| Ok::<bool, Error>(metadata.creation(&sha256_digest)? > after)) |
|
.transpose()? |
|
.unwrap_or(true) |
|
{ |
|
self.blocked_servername_mediaid.insert(&key, &value) |
|
} else { |
|
Ok(()) |
|
} |
|
}) |
|
.filter_map(Result::err) |
|
.collect() |
|
} |
|
|
|
fn unblock(&self, media: &[(OwnedServerName, String)]) -> Vec<Error> { |
|
let maybe_remove_remaining_metadata = |metadata: &DbFileMeta, errors: &mut Vec<Error>| { |
|
for (k, _) in self |
|
.filehash_servername_mediaid |
|
.scan_prefix(metadata.sha256_digest.clone()) |
|
{ |
|
if let Some(servername_mediaid) = k.get(32..) { |
|
if let Err(e) = self.blocked_servername_mediaid.remove(servername_mediaid) { |
|
errors.push(e); |
|
} |
|
} else { |
|
error!( |
|
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}", |
|
hex::encode(&metadata.sha256_digest) |
|
); |
|
errors.push(Error::BadDatabase( |
|
"Invalid format of key in filehash_servername_mediaid", |
|
)); |
|
} |
|
} |
|
|
|
let thumbnail_id_error = || { |
|
error!( |
|
"Invalid format of key in filehash_thumbnail_id for media with sha256 content hash of {}", |
|
hex::encode(&metadata.sha256_digest) |
|
); |
|
Error::BadDatabase("Invalid format of value in filehash_thumbnailid") |
|
}; |
|
|
|
for (k, _) in self |
|
.filehash_thumbnailid |
|
.scan_prefix(metadata.sha256_digest.clone()) |
|
{ |
|
if let Some(end) = k.len().checked_sub(9) { |
|
if let Some(servername_mediaid) = k.get(32..end) { |
|
if let Err(e) = self.blocked_servername_mediaid.remove(servername_mediaid) { |
|
errors.push(e); |
|
} |
|
} else { |
|
errors.push(thumbnail_id_error()); |
|
} |
|
errors.push(thumbnail_id_error()); |
|
}; |
|
} |
|
|
|
// If we don't have the actual file downloaded anymore, remove the remaining |
|
// metadata of the file |
|
match self |
|
.filehash_metadata |
|
.get(&metadata.sha256_digest) |
|
.map(|opt| opt.is_none()) |
|
{ |
|
Err(e) => errors.push(e), |
|
Ok(true) => { |
|
if let Err(e) = self.purge_filehash(metadata.sha256_digest.clone(), false) { |
|
errors.push(e); |
|
} |
|
} |
|
Ok(false) => (), |
|
} |
|
}; |
|
|
|
let mut errors = Vec::new(); |
|
|
|
for (server_name, media_id) in media { |
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
match self |
|
.servernamemediaid_metadata |
|
.get(&key) |
|
.map(|opt| opt.as_deref().map(parse_metadata)) |
|
{ |
|
Err(e) => { |
|
errors.push(e); |
|
continue; |
|
} |
|
Ok(None) => (), |
|
Ok(Some(Err(e))) => { |
|
error!("Error parsing metadata for \"mxc://{server_name}/{media_id}\" from servernamemediaid_metadata: {e}"); |
|
errors.push(e); |
|
continue; |
|
} |
|
Ok(Some(Ok(metadata))) => { |
|
maybe_remove_remaining_metadata(&metadata, &mut errors); |
|
} |
|
} |
|
|
|
key.push(0xff); |
|
for (_, v) in self.thumbnailid_metadata.scan_prefix(key) { |
|
match parse_metadata(&v) { |
|
Ok(metadata) => { |
|
maybe_remove_remaining_metadata(&metadata, &mut errors); |
|
} |
|
Err(e) => { |
|
error!("Error parsing metadata for thumbnail of \"mxc://{server_name}/{media_id}\" from thumbnailid_metadata: {e}"); |
|
errors.push(e); |
|
} |
|
} |
|
} |
|
} |
|
|
|
errors |
|
} |
|
|
|
fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>> { |
|
let parse_servername = |parts: &mut Split<_, _>| { |
|
OwnedServerName::try_from( |
|
utils::string_from_bytes(parts.next().ok_or_else(|| { |
|
Error::BadDatabase("Invalid format of metadata of blocked media") |
|
})?) |
|
.map_err(|_| Error::BadDatabase("Invalid server_name String of blocked data"))?, |
|
) |
|
.map_err(|_| Error::BadDatabase("Invalid ServerName in blocked_servername_mediaid")) |
|
}; |
|
|
|
let parse_string = |
|
|parts: &mut Split<_, _>| { |
|
utils::string_from_bytes(parts.next().ok_or_else(|| { |
|
Error::BadDatabase("Invalid format of metadata of blocked media") |
|
})?) |
|
.map_err(|_| Error::BadDatabase("Invalid string in blocked media metadata")) |
|
}; |
|
|
|
let splitter = |b: &u8| *b == 0xff; |
|
|
|
self.blocked_servername_mediaid |
|
.iter() |
|
.map(|(k, v)| { |
|
let mut parts = k.split(splitter); |
|
|
|
// Using map_err, as inspect_err causes lifetime issues |
|
// "implementation of `FnOnce` is not general enough" |
|
let log_error = |e| { |
|
error!("Error parsing key of blocked media: {e}"); |
|
e |
|
}; |
|
|
|
let server_name = parse_servername(&mut parts).map_err(log_error)?; |
|
|
|
let media_id = parse_string(&mut parts).map_err(log_error)?; |
|
|
|
let (unix_secs, reason) = v |
|
.split_at_checked(8) |
|
.map(|(secs, reason)| -> Result<(u64, Option<String>)> { |
|
Ok(( |
|
secs.try_into() |
|
.map_err(|_| { |
|
Error::bad_database( |
|
"Invalid block time in blocked_servername_mediaid ", |
|
) |
|
}) |
|
.map(u64::from_be_bytes)?, |
|
if reason.is_empty() { |
|
None |
|
} else { |
|
Some(utils::string_from_bytes(reason).map_err(|_| { |
|
Error::bad_database("Invalid string in blocked media metadata") |
|
})?) |
|
}, |
|
)) |
|
}) |
|
.ok_or_else(|| { |
|
Error::bad_database("Invalid format of value in blocked_servername_mediaid") |
|
})??; |
|
|
|
let sha256_hex = self.servernamemediaid_metadata.get(&k)?.map(|mut meta| { |
|
meta.truncate(32); |
|
hex::encode(meta) |
|
}); |
|
|
|
Ok(BlockedMediaInfo { |
|
server_name, |
|
media_id, |
|
unix_secs, |
|
reason, |
|
sha256_hex, |
|
}) |
|
}) |
|
.collect() |
|
} |
|
|
|
fn is_blocked_filehash(&self, sha256_digest: &[u8]) -> Result<bool> { |
|
for (filehash_servername_mediaid, _) in self |
|
.filehash_servername_mediaid |
|
.scan_prefix(sha256_digest.to_owned()) |
|
{ |
|
let servername_mediaid = filehash_servername_mediaid.get(32..).ok_or_else(|| { |
|
error!( |
|
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}", |
|
hex::encode(sha256_digest) |
|
); |
|
Error::BadDatabase("Invalid format of key in filehash_servername_mediaid") |
|
})?; |
|
|
|
if self |
|
.blocked_servername_mediaid |
|
.get(servername_mediaid)? |
|
.is_some() |
|
{ |
|
return Ok(true); |
|
} |
|
} |
|
|
|
let thumbnail_id_error = || { |
|
error!( |
|
"Invalid format of key in filehash_thumbnail_id for media with sha256 content hash of {}", |
|
hex::encode(sha256_digest) |
|
); |
|
Error::BadDatabase("Invalid format of value in filehash_thumbnailid") |
|
}; |
|
|
|
for (thumbnail_id, _) in self |
|
.filehash_thumbnailid |
|
.scan_prefix(sha256_digest.to_owned()) |
|
{ |
|
let servername_mediaid = thumbnail_id |
|
.get( |
|
32..thumbnail_id |
|
.len() |
|
.checked_sub(9) |
|
.ok_or_else(thumbnail_id_error)?, |
|
) |
|
.ok_or_else(thumbnail_id_error)?; |
|
|
|
if self |
|
.blocked_servername_mediaid |
|
.get(servername_mediaid)? |
|
.is_some() |
|
{ |
|
return Ok(true); |
|
} |
|
} |
|
|
|
Ok(false) |
|
} |
|
} |
|
|
|
impl KeyValueDatabase { |
|
/// Only checks whether the media id itself is blocked, and not associated filehashes |
|
fn is_directly_blocked(&self, server_name: &ServerName, media_id: &str) -> Result<bool> { |
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
self.blocked_servername_mediaid |
|
.get(&key) |
|
.map(|x| x.is_some()) |
|
} |
|
|
|
fn purge_mediaid( |
|
&self, |
|
server_name: &ServerName, |
|
media_id: &str, |
|
only_filehash_metadata: bool, |
|
) -> Result<Vec<String>> { |
|
let mut files = Vec::new(); |
|
|
|
let count_required_to_purge = if only_filehash_metadata { 1 } else { 0 }; |
|
|
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
if let Some(sha256_digest) = self.servernamemediaid_metadata.get(&key)?.map(|mut value| { |
|
value.truncate(32); |
|
value |
|
}) { |
|
if !only_filehash_metadata { |
|
if let Some(localpart) = self.servernamemediaid_userlocalpart.get(&key)? { |
|
self.servernamemediaid_userlocalpart.remove(&key)?; |
|
|
|
let mut key = server_name.as_bytes().to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(&localpart); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
self.servername_userlocalpart_mediaid.remove(&key)?; |
|
}; |
|
|
|
self.servernamemediaid_metadata.remove(&key)?; |
|
|
|
let mut key = sha256_digest.clone(); |
|
key.extend_from_slice(server_name.as_bytes()); |
|
key.push(0xff); |
|
key.extend_from_slice(media_id.as_bytes()); |
|
|
|
self.filehash_servername_mediaid.remove(&key)?; |
|
} |
|
|
|
if self |
|
.filehash_servername_mediaid |
|
.scan_prefix(sha256_digest.clone()) |
|
.count() |
|
<= count_required_to_purge |
|
&& self |
|
.filehash_thumbnailid |
|
.scan_prefix(sha256_digest.clone()) |
|
.next() |
|
.is_none() |
|
{ |
|
self.filehash_metadata.remove(&sha256_digest)?; |
|
files.push(hex::encode(sha256_digest)); |
|
} |
|
} |
|
|
|
key.push(0xff); |
|
|
|
let mut thumbnails = BTreeMap::new(); |
|
|
|
for (thumbnail_id, mut value) in self.thumbnailid_metadata.scan_prefix(key) { |
|
value.truncate(32); |
|
let sha256_digest = value; |
|
|
|
let entry = thumbnails |
|
.entry(sha256_digest.clone()) |
|
.and_modify(|v| *v += 1) |
|
.or_insert(1); |
|
|
|
if !only_filehash_metadata { |
|
self.filehash_thumbnailid.remove(&sha256_digest)?; |
|
self.thumbnailid_metadata.remove(&thumbnail_id)?; |
|
} |
|
|
|
// Basically, if this is the only media pointing to the filehash, get rid of it. |
|
// It's a little complicated due to how blocking works. |
|
if self |
|
.filehash_servername_mediaid |
|
.scan_prefix(sha256_digest.clone()) |
|
.count() |
|
<= count_required_to_purge |
|
&& self |
|
.filehash_thumbnailid |
|
.scan_prefix(sha256_digest.clone()) |
|
.count() |
|
<= if only_filehash_metadata { *entry } else { 0 } |
|
{ |
|
self.filehash_metadata.remove(&sha256_digest)?; |
|
files.push(hex::encode(sha256_digest)); |
|
} |
|
} |
|
|
|
Ok(files) |
|
} |
|
|
|
fn purge_filehash(&self, sha256_digest: Vec<u8>, only_filehash_metadata: bool) -> Result<()> { |
|
let handle_error = || { |
|
error!( |
|
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}", |
|
hex::encode(&sha256_digest) |
|
); |
|
Error::BadDatabase("Invalid format of key in filehash_servername_mediaid") |
|
}; |
|
|
|
if !only_filehash_metadata { |
|
for (key, _) in self.filehash_thumbnailid.scan_prefix(sha256_digest.clone()) { |
|
self.filehash_thumbnailid.remove(&key)?; |
|
let (_, key) = key.split_at(32); |
|
self.thumbnailid_metadata.remove(key)?; |
|
} |
|
|
|
for (k, _) in self |
|
.filehash_servername_mediaid |
|
.scan_prefix(sha256_digest.clone()) |
|
{ |
|
let (_, servername_mediaid) = k.split_at_checked(32).ok_or_else(handle_error)?; |
|
|
|
self.servernamemediaid_metadata.remove(servername_mediaid)?; |
|
self.filehash_servername_mediaid.remove(&k)?; |
|
|
|
if let Some(localpart) = self |
|
.servernamemediaid_userlocalpart |
|
.get(servername_mediaid)? |
|
{ |
|
self.servernamemediaid_userlocalpart |
|
.remove(servername_mediaid)?; |
|
|
|
let mut parts = servername_mediaid.split(|b: &u8| *b == 0xff); |
|
|
|
let mut key = parts.next().ok_or_else(handle_error)?.to_vec(); |
|
key.push(0xff); |
|
key.extend_from_slice(&localpart); |
|
key.push(0xff); |
|
key.extend_from_slice(parts.next().ok_or_else(handle_error)?); |
|
|
|
self.servername_userlocalpart_mediaid.remove(&key)?; |
|
}; |
|
} |
|
} |
|
|
|
self.filehash_metadata.remove(&sha256_digest) |
|
} |
|
} |
|
|
|
fn parse_metadata(value: &[u8]) -> Result<DbFileMeta> { |
|
let (sha256_digest, mut parts) = value |
|
.split_at_checked(32) |
|
.map(|(digest, value)| (digest.to_vec(), value.split(|&b| b == 0xff))) |
|
.ok_or_else(|| Error::BadDatabase("Invalid format for media metadata"))?; |
|
|
|
let filename = parts |
|
.next() |
|
.map(|bytes| { |
|
utils::string_from_bytes(bytes) |
|
.map_err(|_| Error::BadDatabase("filename in media metadata is invalid unicode")) |
|
}) |
|
.transpose()? |
|
.and_then(|s| (!s.is_empty()).then_some(s)); |
|
|
|
let content_type = parts |
|
.next() |
|
.map(|bytes| { |
|
utils::string_from_bytes(bytes).map_err(|_| { |
|
Error::BadDatabase("content type in media metadata is invalid unicode") |
|
}) |
|
}) |
|
.transpose()? |
|
.and_then(|s| (!s.is_empty()).then_some(s)); |
|
|
|
let unauthenticated_access_permitted = parts.next().is_some_and(|v| v.is_empty()); |
|
|
|
Ok(DbFileMeta { |
|
sha256_digest, |
|
filename, |
|
content_type, |
|
unauthenticated_access_permitted, |
|
}) |
|
} |
|
|
|
pub struct FilehashMetadata { |
|
value: Vec<u8>, |
|
} |
|
|
|
impl FilehashMetadata { |
|
pub fn new_with_times(size: u64, creation: u64, last_access: u64) -> Self { |
|
let mut value = size.to_be_bytes().to_vec(); |
|
value.extend_from_slice(&creation.to_be_bytes()); |
|
value.extend_from_slice(&last_access.to_be_bytes()); |
|
|
|
Self { value } |
|
} |
|
|
|
pub fn new(size: u64) -> Self { |
|
let now = utils::secs_since_unix_epoch(); |
|
|
|
let mut value = size.to_be_bytes().to_vec(); |
|
value.extend_from_slice(&now.to_be_bytes()); |
|
value.extend_from_slice(&now.to_be_bytes()); |
|
|
|
Self { value } |
|
} |
|
|
|
pub fn from_vec(vec: Vec<u8>) -> Self { |
|
Self { value: vec } |
|
} |
|
|
|
pub fn value(&self) -> &[u8] { |
|
&self.value |
|
} |
|
|
|
fn get_u64_val( |
|
&self, |
|
range: Range<usize>, |
|
name: &str, |
|
sha256_digest: &[u8], |
|
invalid_error: &'static str, |
|
) -> Result<u64> { |
|
self.value |
|
.get(range) |
|
.ok_or_else(|| { |
|
error!( |
|
"Invalid format of metadata for media with sha256 content hash of {}", |
|
hex::encode(sha256_digest) |
|
); |
|
Error::BadDatabase("Invalid format of metadata in filehash_metadata") |
|
})? |
|
.try_into() |
|
.map(u64::from_be_bytes) |
|
.map_err(|_| { |
|
error!( |
|
"Invalid {name} for media with sha256 content hash of {}", |
|
hex::encode(sha256_digest) |
|
); |
|
Error::BadDatabase(invalid_error) |
|
}) |
|
} |
|
|
|
pub fn creation(&self, sha256_digest: &[u8]) -> Result<u64> { |
|
self.get_u64_val( |
|
8..16, |
|
"creation time", |
|
sha256_digest, |
|
"Invalid creation time in filehash_metadata", |
|
) |
|
} |
|
}
|
|
|