1
0
Fork 0
mirror of https://gitlab.com/famedly/conduit.git synced 2025-09-05 18:41:00 +00:00

feat(media): retention policies

This commit is contained in:
Matthias Ahouansou 2025-04-16 13:15:01 +01:00
parent 594fe5f98f
commit c3fb1b0456
No known key found for this signature in database
11 changed files with 698 additions and 61 deletions

View file

@ -1,5 +1,5 @@
mod data;
use std::{fs, io::Cursor};
use std::{fs, io::Cursor, sync::Arc};
pub use data::Data;
use ruma::{
@ -8,10 +8,10 @@ use ruma::{
OwnedServerName, ServerName, UserId,
};
use sha2::{digest::Output, Digest, Sha256};
use tracing::error;
use tracing::{error, info};
use crate::{
config::{DirectoryStructure, MediaConfig},
config::{DirectoryStructure, MediaBackendConfig},
services, utils, Error, Result,
};
use image::imageops::FilterType;
@ -34,6 +34,29 @@ pub struct FileMeta {
pub file: Vec<u8>,
}
pub enum MediaType {
LocalMedia { thumbnail: bool },
RemoteMedia { thumbnail: bool },
}
impl MediaType {
pub fn new(server_name: &ServerName, thumbnail: bool) -> Self {
if server_name == services().globals.server_name() {
Self::LocalMedia { thumbnail }
} else {
Self::RemoteMedia { thumbnail }
}
}
pub fn is_thumb(&self) -> bool {
match self {
MediaType::LocalMedia { thumbnail } | MediaType::RemoteMedia { thumbnail } => {
*thumbnail
}
}
}
}
pub struct Service {
pub db: &'static dyn Data,
}
@ -47,6 +70,34 @@ pub struct BlockedMediaInfo {
}
impl Service {
pub fn start_time_retention_checker(self: &Arc<Self>) {
let self2 = Arc::clone(self);
if let Some(cleanup_interval) = services().globals.config.media.retention.cleanup_interval()
{
tokio::spawn(async move {
let mut i = cleanup_interval;
loop {
i.tick().await;
let _ = self2.try_purge_time_retention().await;
}
});
}
}
async fn try_purge_time_retention(&self) -> Result<()> {
info!("Checking if any media should be deleted due to time-based retention policies");
let files = self
.db
.cleanup_time_retention(&services().globals.config.media.retention);
let count = files.iter().filter(|res| res.is_ok()).count();
info!("Found {count} media files to delete");
purge_files(files);
Ok(())
}
/// Uploads a file.
pub async fn create(
&self,
@ -59,6 +110,16 @@ impl Service {
) -> Result<()> {
let (sha256_digest, sha256_hex) = generate_digests(file);
for error in self.clear_required_space(
&sha256_digest,
MediaType::new(servername, false),
size(file)?,
)? {
error!(
"Error deleting file to clear space when downloading/creating new media file: {error}"
)
}
self.db.create_file_metadata(
sha256_digest,
size(file)?,
@ -93,6 +154,12 @@ impl Service {
) -> Result<()> {
let (sha256_digest, sha256_hex) = generate_digests(file);
self.clear_required_space(
&sha256_digest,
MediaType::new(servername, true),
size(file)?,
)?;
self.db.create_thumbnail_metadata(
sha256_digest,
size(file)?,
@ -125,7 +192,7 @@ impl Service {
return Ok(None);
}
let file = get_file(&hex::encode(sha256_digest)).await?;
let file = self.get_file(&sha256_digest, None).await?;
Ok(Some(FileMeta {
content_disposition: content_disposition(filename, &content_type),
@ -180,7 +247,9 @@ impl Service {
}
// Using saved thumbnail
let file = get_file(&hex::encode(sha256_digest)).await?;
let file = self
.get_file(&sha256_digest, Some((servername, media_id)))
.await?;
Ok(Some(FileMeta {
content_disposition: content_disposition(filename, &content_type),
@ -202,7 +271,7 @@ impl Service {
let content_disposition = content_disposition(filename.clone(), &content_type);
// Generate a thumbnail
let file = get_file(&hex::encode(sha256_digest)).await?;
let file = self.get_file(&sha256_digest, None).await?;
if let Ok(image) = image::load_from_memory(&file) {
let original_width = image.width();
@ -303,7 +372,7 @@ impl Service {
return Ok(None);
}
let file = get_file(&hex::encode(sha256_digest)).await?;
let file = self.get_file(&sha256_digest, None).await?;
Ok(Some(FileMeta {
content_disposition: content_disposition(filename, &content_type),
@ -416,14 +485,73 @@ impl Service {
pub fn list_blocked(&self) -> Vec<Result<BlockedMediaInfo>> {
self.db.list_blocked()
}
pub fn clear_required_space(
&self,
sha256_digest: &[u8],
media_type: MediaType,
new_size: u64,
) -> Result<Vec<Error>> {
let files = self.db.files_to_delete(
sha256_digest,
&services().globals.config.media.retention,
media_type,
new_size,
)?;
let count = files.iter().filter(|r| r.is_ok()).count();
if count != 0 {
info!("Deleting {} files to clear space for new media file", count);
}
Ok(purge_files(files))
}
/// Fetches the file from the configured media backend, as well as updating the "last accessed"
/// part of the metadata of the file
///
/// If specified, the original file will also have it's last accessed time updated, if present
/// (use when accessing thumbnails)
async fn get_file(
&self,
sha256_digest: &[u8],
original_file_id: Option<(&ServerName, &str)>,
) -> Result<Vec<u8>> {
let file = match &services().globals.config.media.backend {
MediaBackendConfig::FileSystem {
path,
directory_structure,
} => {
let path = services().globals.get_media_path(
path,
directory_structure,
&hex::encode(sha256_digest),
)?;
let mut file = Vec::new();
File::open(path).await?.read_to_end(&mut file).await?;
file
}
};
if let Some((server_name, media_id)) = original_file_id {
self.db.update_last_accessed(server_name, media_id)?;
}
self.db
.update_last_accessed_filehash(sha256_digest)
.map(|_| file)
}
}
/// Creates the media file, using the configured media backend
///
/// Note: this function does NOT set the metadata related to the file
pub async fn create_file(sha256_hex: &str, file: &[u8]) -> Result<()> {
match &services().globals.config.media {
MediaConfig::FileSystem {
match &services().globals.config.media.backend {
MediaBackendConfig::FileSystem {
path,
directory_structure,
} => {
@ -439,25 +567,6 @@ pub async fn create_file(sha256_hex: &str, file: &[u8]) -> Result<()> {
Ok(())
}
/// Fetches the file from the configured media backend
async fn get_file(sha256_hex: &str) -> Result<Vec<u8>> {
Ok(match &services().globals.config.media {
MediaConfig::FileSystem {
path,
directory_structure,
} => {
let path = services()
.globals
.get_media_path(path, directory_structure, sha256_hex)?;
let mut file = Vec::new();
File::open(path).await?.read_to_end(&mut file).await?;
file
}
})
}
/// Purges the given files from the media backend
/// Returns a `Vec` of errors that occurred when attempting to delete the files
///
@ -477,8 +586,8 @@ fn purge_files(hashes: Vec<Result<String>>) -> Vec<Error> {
///
/// Note: this does NOT remove the related metadata from the database
fn delete_file(sha256_hex: &str) -> Result<()> {
match &services().globals.config.media {
MediaConfig::FileSystem {
match &services().globals.config.media.backend {
MediaBackendConfig::FileSystem {
path,
directory_structure,
} => {