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_core::error::RepoResult;
|
||||||
use mediarepo_database::entities::hash;
|
use mediarepo_core::fs::thumbnail_store::Dimensions;
|
||||||
use mediarepo_database::entities::thumbnail;
|
use std::path::PathBuf;
|
||||||
use sea_orm::prelude::*;
|
use tokio::fs::{File, OpenOptions};
|
||||||
use sea_orm::{DatabaseConnection, Set};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::io::BufReader;
|
use tokio::io::BufReader;
|
||||||
|
|
||||||
pub struct Thumbnail {
|
pub struct Thumbnail {
|
||||||
db: DatabaseConnection,
|
pub file_hash: String,
|
||||||
model: thumbnail::Model,
|
pub path: PathBuf,
|
||||||
hash: hash::Model,
|
pub size: Dimensions,
|
||||||
|
pub mime_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Thumbnail {
|
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
|
/// Returns the reader of the thumbnail file
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
#[tracing::instrument(level = "debug", skip(self))]
|
||||||
pub async fn get_reader(&self) -> RepoResult<BufReader<File>> {
|
pub async fn get_reader(&self) -> RepoResult<BufReader<File>> {
|
||||||
let storage = self.storage().await?;
|
let file = OpenOptions::new().read(true).open(&self.path).await?;
|
||||||
storage.get_file_reader(self.hash()).await
|
Ok(BufReader::new(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue