Change implementation to store thumbnails independent from the database
Signed-off-by: trivernis <trivernis@protonmail.com>pull/4/head
parent
de41cb3a9b
commit
c3b744ed02
@ -0,0 +1,2 @@
|
||||
pub mod file_hash_store;
|
||||
pub mod thumbnail_store;
|
@ -0,0 +1,77 @@
|
||||
use std::io::Result;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThumbnailStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Dimensions {
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
impl ThumbnailStore {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
/// Adds a thumbnail to be stored for a parent id
|
||||
/// if the thumbnail already exists it will be recreated without warning
|
||||
pub async fn add_thumbnail<S: ToString>(
|
||||
&self,
|
||||
parent_id: S,
|
||||
size: Dimensions,
|
||||
data: &[u8],
|
||||
) -> Result<PathBuf> {
|
||||
let parent_dir = self.path.join(parent_id.to_string());
|
||||
let entry_path = parent_dir.join(format!("{}-{}", size.height, size.width));
|
||||
|
||||
if !parent_dir.exists() {
|
||||
fs::create_dir_all(parent_dir).await?;
|
||||
}
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&entry_path)
|
||||
.await?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
writer.write_all(data).await?;
|
||||
writer.flush().await?;
|
||||
|
||||
Ok(entry_path)
|
||||
}
|
||||
|
||||
/// Returns all thumbnails for a parent id
|
||||
pub async fn get_thumbnails<S: ToString>(
|
||||
&self,
|
||||
parent_id: S,
|
||||
) -> Result<Vec<(Dimensions, PathBuf)>> {
|
||||
let mut entries = Vec::new();
|
||||
let parent_dir = self.path.join(parent_id.to_string());
|
||||
if !parent_dir.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let mut dir = fs::read_dir(parent_dir).await?;
|
||||
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy();
|
||||
|
||||
let (height, width) = name
|
||||
.split_once("-")
|
||||
.and_then(|(height, width)| {
|
||||
Some((height.parse::<u32>().ok()?, width.parse::<u32>().ok()?))
|
||||
})
|
||||
.unwrap_or((255, 255));
|
||||
entries.push((Dimensions { height, width }, entry.path()))
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
@ -1,140 +1,21 @@
|
||||
use crate::storage::Storage;
|
||||
use mediarepo_core::error::RepoResult;
|
||||
use mediarepo_database::entities::hash;
|
||||
use mediarepo_database::entities::thumbnail;
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::{DatabaseConnection, Set};
|
||||
use std::fmt::Debug;
|
||||
use tokio::fs::File;
|
||||
use mediarepo_core::fs::thumbnail_store::Dimensions;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs::{File, OpenOptions};
|
||||
use tokio::io::BufReader;
|
||||
|
||||
pub struct Thumbnail {
|
||||
db: DatabaseConnection,
|
||||
model: thumbnail::Model,
|
||||
hash: hash::Model,
|
||||
pub file_hash: String,
|
||||
pub path: PathBuf,
|
||||
pub size: Dimensions,
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
impl Thumbnail {
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub(crate) fn new(db: DatabaseConnection, model: thumbnail::Model, hash: hash::Model) -> Self {
|
||||
Self { db, model, hash }
|
||||
}
|
||||
|
||||
/// Returns the thumbnail by id
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult<Option<Self>> {
|
||||
let model: Option<(thumbnail::Model, Option<hash::Model>)> =
|
||||
thumbnail::Entity::find_by_id(id)
|
||||
.find_also_related(hash::Entity)
|
||||
.one(&db)
|
||||
.await?;
|
||||
|
||||
if let Some((model, Some(hash))) = model {
|
||||
Ok(Some(Self::new(db, model, hash)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a thumbnail by hash
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub async fn by_hash<S: AsRef<str> + Debug>(
|
||||
db: DatabaseConnection,
|
||||
hash: S,
|
||||
) -> RepoResult<Option<Self>> {
|
||||
let result: Option<(hash::Model, Option<thumbnail::Model>)> = hash::Entity::find()
|
||||
.filter(hash::Column::Value.eq(hash.as_ref()))
|
||||
.find_also_related(thumbnail::Entity)
|
||||
.one(&db)
|
||||
.await?;
|
||||
if let Some((hash, Some(model))) = result {
|
||||
Ok(Some(Self::new(db, model, hash)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a thumbnail into the database
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub async fn add(
|
||||
db: DatabaseConnection,
|
||||
hash_id: i64,
|
||||
file_id: i64,
|
||||
storage_id: i64,
|
||||
height: i32,
|
||||
width: i32,
|
||||
mime: Option<String>,
|
||||
) -> RepoResult<Self> {
|
||||
let active_model = thumbnail::ActiveModel {
|
||||
storage_id: Set(storage_id),
|
||||
hash_id: Set(hash_id),
|
||||
file_id: Set(file_id),
|
||||
height: Set(height),
|
||||
width: Set(width),
|
||||
mime: Set(mime),
|
||||
..Default::default()
|
||||
};
|
||||
let active_model: thumbnail::ActiveModel = active_model.insert(&db).await?;
|
||||
let thumbnail = Self::by_id(db, active_model.id.unwrap())
|
||||
.await?
|
||||
.expect("Inserted thumbnail does not exist");
|
||||
|
||||
Ok(thumbnail)
|
||||
}
|
||||
|
||||
/// Returns all thumbnails for a given file
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub async fn for_file_id(db: DatabaseConnection, file_id: i64) -> RepoResult<Vec<Self>> {
|
||||
let thumb_models: Vec<(thumbnail::Model, Option<hash::Model>)> = thumbnail::Entity::find()
|
||||
.filter(thumbnail::Column::FileId.eq(file_id))
|
||||
.find_also_related(hash::Entity)
|
||||
.all(&db)
|
||||
.await?;
|
||||
|
||||
Ok(thumb_models
|
||||
.into_iter()
|
||||
.filter_map(|(m, h)| Some(Self::new(db.clone(), m, h?)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn id(&self) -> i64 {
|
||||
self.model.id
|
||||
}
|
||||
|
||||
pub fn file_id(&self) -> i64 {
|
||||
self.model.file_id
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &String {
|
||||
&self.hash.value
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i32 {
|
||||
self.model.height
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i32 {
|
||||
self.model.width
|
||||
}
|
||||
|
||||
pub fn mime_type(&self) -> &Option<String> {
|
||||
&self.model.mime
|
||||
}
|
||||
|
||||
/// Returns the storage for the thumbnail
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub async fn storage(&self) -> RepoResult<Storage> {
|
||||
let storage = Storage::by_id(self.db.clone(), self.model.storage_id)
|
||||
.await?
|
||||
.expect("The FK storage_id doesn't exist?!");
|
||||
|
||||
Ok(storage)
|
||||
}
|
||||
|
||||
/// Returns the reader of the thumbnail file
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub async fn get_reader(&self) -> RepoResult<BufReader<File>> {
|
||||
let storage = self.storage().await?;
|
||||
storage.get_file_reader(self.hash()).await
|
||||
let file = OpenOptions::new().read(true).open(&self.path).await?;
|
||||
Ok(BufReader::new(file))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue