Add file dao and tag dao

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/5/head
trivernis 2 years ago
parent a6da2b9e1e
commit cb493b4651
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -2499,9 +2499,9 @@ dependencies = [
[[package]]
name = "thumbnailer"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8981b60fe29b8213c829340b7d5bea9d0bfb145fc460450ea3db01cf54d8643a"
checksum = "6017341c89a0c406e38801119f67dd0b67d045ff0e50aa2cf8fc1de4a1b48c3b"
dependencies = [
"ffmpeg-next",
"image",

@ -22,7 +22,7 @@ data-encoding = "^2.3.2"
tokio-graceful-shutdown = "^0.4.3"
[dependencies.thumbnailer]
version = "^0.2.4"
version = "^0.2.5"
default-features = false
[dependencies.sea-orm]

@ -0,0 +1,71 @@
use crate::dto::{AddFileDto, FileDto};
use chrono::{Local, NaiveDateTime};
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{content_descriptor, file, file_metadata};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseTransaction};
use std::io::Cursor;
use crate::dao::file::FileDao;
impl FileDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add(&self, add_dto: AddFileDto) -> RepoResult<FileDto> {
let trx = self.ctx.db.begin().await?;
let file_size = add_dto.content.len();
let cd_bin = self
.ctx
.main_storage
.add_file(Cursor::new(add_dto.content), None)
.await?;
let cd_model = content_descriptor::ActiveModel {
descriptor: Set(cd_bin),
..Default::default()
};
let cd = cd_model.insert(&trx).await?;
let model = file::ActiveModel {
cd_id: Set(cd.id),
mime_type: Set(add_dto.mime_type),
..Default::default()
};
let file: file::Model = model.insert(&trx).await?;
let metadata = add_file_metadata(
&trx,
file.id,
file_size as i64,
add_dto.creation_time,
add_dto.change_time,
add_dto.name,
)
.await?;
trx.commit().await?;
Ok(FileDto::new(file, cd, Some(metadata)))
}
}
async fn add_file_metadata(
trx: &DatabaseTransaction,
file_id: i64,
size: i64,
creation_time: NaiveDateTime,
change_time: NaiveDateTime,
name: Option<String>,
) -> RepoResult<file_metadata::Model> {
let metadata_model = file_metadata::ActiveModel {
file_id: Set(file_id),
size: Set(size),
import_time: Set(Local::now().naive_local()),
creation_time: Set(creation_time),
change_time: Set(change_time),
name: Set(name),
..Default::default()
};
let metadata = metadata_model.insert(trx).await?;
Ok(metadata)
}

@ -0,0 +1,41 @@
use crate::dao::file::{map_file_and_cd, FileDao};
use crate::dto::FileDto;
use mediarepo_core::error::{RepoError, RepoResult};
use mediarepo_database::entities::{
content_descriptor, content_descriptor_tag, file, file_metadata,
};
use sea_orm::prelude::*;
use sea_orm::ConnectionTrait;
impl FileDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete(&self, file: FileDto) -> RepoResult<()> {
let trx = self.ctx.db.begin().await?;
file_metadata::Entity::delete_many()
.filter(file_metadata::Column::FileId.eq(file.id()))
.exec(&trx)
.await?;
file::Entity::delete_many()
.filter(file::Column::Id.eq(file.id()))
.exec(&trx)
.await?;
content_descriptor_tag::Entity::delete_many()
.filter(content_descriptor_tag::Column::CdId.eq(file.cd_id()))
.exec(&trx)
.await?;
content_descriptor::Entity::delete_many()
.filter(content_descriptor::Column::Id.eq(file.cd_id()))
.exec(&trx)
.await?;
self.ctx
.thumbnail_storage
.delete_parent(&file.encoded_cd())
.await?;
self.ctx.main_storage.delete_file(file.cd()).await?;
trx.commit().await?;
Ok(())
}
}

@ -1,11 +1,14 @@
use crate::dao::file::{map_cd_and_file, FileDao};
use crate::dto::FileDto;
use chrono::NaiveDateTime;
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::content_descriptor;
use mediarepo_database::entities::content_descriptor_tag;
use mediarepo_database::entities::file;
use mediarepo_database::entities::file_metadata;
use sea_orm::sea_query::{Alias, Expr, Query, SimpleExpr};
use sea_orm::ColumnTrait;
use sea_orm::Condition;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
macro_rules! apply_ordering_comparator {
($column:expr, $filter:expr) => {
@ -53,8 +56,28 @@ pub enum NegatableComparator<T> {
IsNot(T),
}
impl FileDao {
/// Finds files by filters
#[tracing::instrument(level = "debug", skip(self))]
pub async fn find(&self, filters: Vec<Vec<FilterProperty>>) -> RepoResult<Vec<FileDto>> {
let main_condition = build_find_filter_conditions(filters);
let files = content_descriptor::Entity::find()
.find_also_related(file::Entity)
.filter(main_condition)
.group_by(file::Column::Id)
.all(&self.ctx.db)
.await?
.into_iter()
.filter_map(map_cd_and_file)
.collect();
Ok(files)
}
}
#[tracing::instrument(level = "debug")]
pub fn build_find_filter_conditions(filters: Vec<Vec<FilterProperty>>) -> Condition {
fn build_find_filter_conditions(filters: Vec<Vec<FilterProperty>>) -> Condition {
filters
.into_iter()
.fold(Condition::all(), |all_cond, mut expression| {

@ -0,0 +1,149 @@
use crate::dao::{DaoContext, DaoProvider};
use crate::dto::{FileDto, FileMetadataDto, ThumbnailDto};
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{content_descriptor, file, file_metadata};
use sea_orm::prelude::*;
use tokio::io::AsyncReadExt;
pub mod add;
pub mod delete;
pub mod find;
pub mod update;
pub struct FileDao {
ctx: DaoContext,
}
impl DaoProvider for FileDao {
fn dao_ctx(&self) -> DaoContext {
self.ctx.clone()
}
}
impl FileDao {
pub fn new(ctx: DaoContext) -> Self {
Self { ctx }
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all(&self) -> RepoResult<Vec<FileDto>> {
let files = file::Entity::find()
.find_also_related(content_descriptor::Entity)
.all(&self.ctx.db)
.await?
.into_iter()
.filter_map(map_file_and_cd)
.collect();
Ok(files)
}
#[tracing::instrument(level = "debug", skip(self))]
#[inline]
pub async fn by_id(&self, id: i64) -> RepoResult<Option<FileDto>> {
self.all_by_id(vec![id]).await.map(|f| f.into_iter().next())
}
#[tracing::instrument(level = "debug", skip(self))]
#[inline]
pub async fn by_cd(&self, cd: Vec<u8>) -> RepoResult<Option<FileDto>> {
self.all_by_cd(vec![cd]).await.map(|f| f.into_iter().next())
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all_by_cd(&self, cds: Vec<Vec<u8>>) -> RepoResult<Vec<FileDto>> {
if cds.is_empty() {
return Ok(vec![]);
}
let files = file::Entity::find()
.find_also_related(content_descriptor::Entity)
.filter(content_descriptor::Column::Descriptor.is_in(cds))
.all(&self.ctx.db)
.await?
.into_iter()
.filter_map(map_file_and_cd)
.collect();
Ok(files)
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all_by_id(&self, ids: Vec<i64>) -> RepoResult<Vec<FileDto>> {
if ids.is_empty() {
return Ok(vec![]);
}
let files = file::Entity::find()
.find_also_related(content_descriptor::Entity)
.filter(file::Column::Id.is_in(ids))
.all(&self.ctx.db)
.await?
.into_iter()
.filter_map(map_file_and_cd)
.collect();
Ok(files)
}
pub async fn metadata(&self, file_id: i64) -> RepoResult<Option<FileMetadataDto>> {
self.all_metadata(vec![file_id])
.await
.map(|m| m.into_iter().next())
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all_metadata(&self, file_ids: Vec<i64>) -> RepoResult<Vec<FileMetadataDto>> {
if file_ids.is_empty() {
return Ok(vec![]);
}
let metadata = file_metadata::Entity::find()
.filter(file_metadata::Column::FileId.is_in(file_ids))
.all(&self.ctx.db)
.await?
.into_iter()
.map(|m| FileMetadataDto::new(m))
.collect();
Ok(metadata)
}
/// Returns all thumbnails for a cd
#[tracing::instrument(level = "debug", skip(self))]
pub async fn thumbnails(&self, encoded_cd: String) -> RepoResult<Vec<ThumbnailDto>> {
let thumbnails = self
.ctx
.thumbnail_storage
.get_thumbnails(&encoded_cd)
.await?
.into_iter()
.map(|(size, path)| {
ThumbnailDto::new(path, encoded_cd.clone(), size, String::from("image/png"))
})
.collect();
Ok(thumbnails)
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_bytes(&self, cd: &[u8]) -> RepoResult<Vec<u8>> {
let mut buf = Vec::new();
let mut reader = self.ctx.main_storage.get_file(cd).await?.1;
reader.read_to_end(&mut buf).await?;
Ok(buf)
}
}
fn map_file_and_cd(
(file, cd): (file::Model, Option<content_descriptor::Model>),
) -> Option<FileDto> {
cd.map(|c| FileDto::new(file, c, None))
}
fn map_cd_and_file(
(cd, file): (content_descriptor::Model, Option<file::Model>),
) -> Option<FileDto> {
file.map(|f| FileDto::new(f, cd, None))
}

@ -0,0 +1,93 @@
use crate::dto::{FileDto, FileMetadataDto, ThumbnailDto, UpdateFileDto, UpdateFileMetadataDto};
use mediarepo_core::error::{RepoError, RepoResult};
use mediarepo_core::fs::thumbnail_store::Dimensions;
use mediarepo_core::thumbnailer;
use mediarepo_core::thumbnailer::{Thumbnail, ThumbnailSize};
use mediarepo_database::entities::{content_descriptor, file, file_metadata};
use sea_orm::prelude::*;
use sea_orm::ActiveValue::{Set, Unchanged};
use sea_orm::{ConnectionTrait, NotSet};
use std::fmt::Debug;
use std::io::Cursor;
use std::str::FromStr;
use crate::dao::file::FileDao;
use crate::dao::opt_to_active_val;
impl FileDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn update(&self, update_dto: UpdateFileDto) -> RepoResult<FileDto> {
let trx = self.ctx.db.begin().await?;
let model = file::ActiveModel {
id: Set(update_dto.id),
cd_id: update_dto.cd_id.map(|v| Set(v)).unwrap_or(NotSet),
mime_type: update_dto.mime_type.map(|v| Set(v)).unwrap_or(NotSet),
status: update_dto.status.map(|v| Set(v as i32)).unwrap_or(NotSet),
};
let file_model = model.update(&trx).await?;
let cd = file_model
.find_related(content_descriptor::Entity)
.one(&trx)
.await?
.ok_or_else(|| RepoError::from("Content descriptor not found"))?;
trx.commit().await?;
Ok(FileDto::new(file_model, cd, None))
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn update_metadata(
&self,
update_dto: UpdateFileMetadataDto,
) -> RepoResult<FileMetadataDto> {
let model = file_metadata::ActiveModel {
file_id: Unchanged(update_dto.file_id),
name: opt_to_active_val(update_dto.name),
comment: opt_to_active_val(update_dto.comment),
size: opt_to_active_val(update_dto.size),
change_time: opt_to_active_val(update_dto.change_time),
..Default::default()
};
let metadata = model.update(&self.ctx.db).await?;
Ok(FileMetadataDto::new(metadata))
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn create_thumbnails<I: IntoIterator<Item = ThumbnailSize> + Debug>(
&self,
file: FileDto,
sizes: I,
) -> RepoResult<Vec<ThumbnailDto>> {
let bytes = self.get_bytes(file.cd()).await?;
let mime_type = mime::Mime::from_str(file.mime_type())
.unwrap_or_else(|_| mime::APPLICATION_OCTET_STREAM);
let thumbnails =
thumbnailer::create_thumbnails(Cursor::new(bytes), mime_type.clone(), sizes)?;
let mut dtos = Vec::new();
for thumbnail in thumbnails {
let mut buf = Vec::new();
let size = thumbnail.size();
let size = Dimensions {
height: size.1,
width: size.0,
};
thumbnail.write_png(&mut buf)?;
let path = self
.ctx
.thumbnail_storage
.add_thumbnail(file.encoded_cd(), size.clone(), &buf)
.await?;
dtos.push(ThumbnailDto::new(
path,
file.encoded_cd(),
size,
mime_type.to_string(),
))
}
Ok(dtos)
}
}

@ -0,0 +1,33 @@
pub mod file;
pub mod repo;
pub mod tag;
use crate::dao::file::FileDao;
use crate::dao::tag::TagDao;
use mediarepo_core::fs::file_hash_store::FileHashStore;
use mediarepo_core::fs::thumbnail_store::ThumbnailStore;
use sea_orm::{ActiveValue, DatabaseConnection};
#[derive(Clone)]
pub struct DaoContext {
pub db: DatabaseConnection,
pub main_storage: FileHashStore,
pub thumbnail_storage: ThumbnailStore,
}
pub trait DaoProvider {
fn dao_ctx(&self) -> DaoContext;
fn file(&self) -> FileDao {
FileDao::new(self.dao_ctx())
}
fn tag(&self) -> TagDao {
TagDao::new(self.dao_ctx())
}
}
fn opt_to_active_val<T: Into<sea_orm::Value>>(opt: Option<T>) -> ActiveValue<T> {
opt.map(|v| ActiveValue::Set(v))
.unwrap_or(ActiveValue::NotSet)
}

@ -1,5 +1,5 @@
use crate::content_descriptor::ContentDescriptor;
use crate::file::filter::FilterProperty;
use crate::dao::{DaoContext, DaoProvider};
use crate::file::File;
use crate::file_metadata::FileMetadata;
use crate::namespace::Namespace;
@ -32,6 +32,16 @@ pub struct Repo {
thumbnail_storage: ThumbnailStore,
}
impl DaoProvider for Repo {
fn dao_ctx(&self) -> DaoContext {
DaoContext {
db: self.db.clone(),
main_storage: self.main_storage.clone(),
thumbnail_storage: self.thumbnail_storage.clone(),
}
}
}
impl Repo {
pub(crate) fn new(
db: DatabaseConnection,
@ -61,33 +71,6 @@ impl Repo {
&self.db
}
/// Returns a file by its mapped hash
#[tracing::instrument(level = "debug", skip(self))]
pub async fn file_by_cd(&self, cd: &[u8]) -> RepoResult<Option<File>> {
File::by_cd(self.db.clone(), cd).await
}
/// Returns a file by id
#[tracing::instrument(level = "debug", skip(self))]
pub async fn file_by_id(&self, id: i64) -> RepoResult<Option<File>> {
File::by_id(self.db.clone(), id).await
}
/// Returns a list of all stored files
#[tracing::instrument(level = "debug", skip(self))]
pub async fn files(&self) -> RepoResult<Vec<File>> {
File::all(self.db.clone()).await
}
/// Finds all files by a list of tags
#[tracing::instrument(level = "debug", skip(self))]
pub async fn find_files_by_filters(
&self,
filters: Vec<Vec<FilterProperty>>,
) -> RepoResult<Vec<File>> {
File::find_by_filters(self.db.clone(), filters).await
}
/// Returns all file metadata entries for the given file ids
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_file_metadata_for_ids(&self, ids: Vec<i64>) -> RepoResult<Vec<FileMetadata>> {

@ -0,0 +1,64 @@
use crate::dao::tag::TagDao;
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::content_descriptor_tag;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
use sea_orm::{ConnectionTrait, DatabaseTransaction};
impl TagDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn upsert_mappings(&self, cd_ids: Vec<i64>, tag_ids: Vec<i64>) -> RepoResult<()> {
let trx = self.ctx.db.begin().await?;
let existing_mappings = get_existing_mappings(&trx, &cd_ids, &tag_ids).await?;
let active_models: Vec<content_descriptor_tag::ActiveModel> = cd_ids
.into_iter()
.flat_map(|cd_id: i64| {
tag_ids
.iter()
.filter(|tag_id| !existing_mappings.contains(&(cd_id, **tag_id)))
.map(move |tag_id| content_descriptor_tag::ActiveModel {
cd_id: Set(cd_id),
tag_id: Set(*tag_id),
})
.collect::<Vec<content_descriptor_tag::ActiveModel>>()
})
.collect();
content_descriptor_tag::Entity::insert_many(active_models)
.exec(&trx)
.await?;
trx.commit().await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn remove_mappings(&self, cd_ids: Vec<i64>, tag_ids: Vec<i64>) -> RepoResult<()> {
content_descriptor_tag::Entity::delete_many()
.filter(content_descriptor_tag::Column::CdId.is_in(cd_ids))
.filter(content_descriptor_tag::Column::TagId.is_in(tag_ids))
.exec(&self.ctx.db)
.await?;
Ok(())
}
}
async fn get_existing_mappings(
trx: &DatabaseTransaction,
cd_ids: &Vec<i64>,
tag_ids: &Vec<i64>,
) -> RepoResult<Vec<(i64, i64)>> {
let existing_mappings: Vec<(i64, i64)> = content_descriptor_tag::Entity::find()
.filter(content_descriptor_tag::Column::CdId.is_in(cd_ids.clone()))
.filter(content_descriptor_tag::Column::TagId.is_in(tag_ids.clone()))
.all(trx)
.await?
.into_iter()
.map(|model: content_descriptor_tag::Model| (model.tag_id, model.cd_id))
.collect();
Ok(existing_mappings)
}

@ -0,0 +1,47 @@
pub mod mappings;
use crate::dao::{DaoContext, DaoProvider};
use crate::dto::TagDto;
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{content_descriptor, content_descriptor_tag, namespace, tag};
use sea_orm::prelude::*;
use sea_orm::QuerySelect;
use sea_orm::{DatabaseConnection, JoinType};
pub struct TagDao {
ctx: DaoContext,
}
impl DaoProvider for TagDao {
fn dao_ctx(&self) -> DaoContext {
self.ctx.clone()
}
}
impl TagDao {
pub fn new(ctx: DaoContext) -> Self {
Self { ctx }
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn tags_for_cd(&self, cd_id: i64) -> RepoResult<Vec<TagDto>> {
let tags = tag::Entity::find()
.find_also_related(namespace::Entity)
.join(
JoinType::LeftJoin,
content_descriptor_tag::Relation::Tag.def().rev(),
)
.join(
JoinType::InnerJoin,
content_descriptor_tag::Relation::ContentDescriptorId.def(),
)
.filter(content_descriptor::Column::Id.eq(cd_id))
.all(&self.ctx.db)
.await?
.into_iter()
.map(|(t, n)| TagDto::new(t, n))
.collect();
Ok(tags)
}
}

@ -1,7 +1,10 @@
use crate::dto::FileMetadataDto;
use chrono::NaiveDateTime;
use mediarepo_core::content_descriptor::encode_content_descriptor;
use mediarepo_core::mediarepo_api::types::files::FileStatus as ApiFileStatus;
use mediarepo_database::entities::content_descriptor;
use mediarepo_database::entities::file;
use mediarepo_database::entities::file_metadata;
use mediarepo_database::entities::content_descriptor;
use crate::dto::FileMetadataDto;
#[derive(Clone, Debug)]
pub struct FileDto {
@ -11,11 +14,15 @@ pub struct FileDto {
}
impl FileDto {
pub(crate) fn new(model: file::Model, content_descriptor: content_descriptor::Model, metadata: Option<file_metadata::Model>) -> Self {
pub(crate) fn new(
model: file::Model,
content_descriptor: content_descriptor::Model,
metadata: Option<file_metadata::Model>,
) -> Self {
Self {
model,
content_descriptor,
metadata: metadata.map(FileMetadataDto::new)
metadata: metadata.map(FileMetadataDto::new),
}
}
@ -31,7 +38,73 @@ impl FileDto {
&self.content_descriptor.descriptor
}
pub fn encoded_cd(&self) -> String {
encode_content_descriptor(&self.content_descriptor.descriptor)
}
pub fn status(&self) -> FileStatus {
match self.model.status {
10 => FileStatus::Imported,
20 => FileStatus::Archived,
30 => FileStatus::Deleted,
_ => FileStatus::Imported,
}
}
pub fn mime_type(&self) -> &String {
&self.model.mime_type
}
pub fn metadata(&self) -> Option<&FileMetadataDto> {
self.metadata.as_ref()
}
}
pub fn into_metadata(self) -> Option<FileMetadataDto> {
self.metadata
}
}
#[derive(Clone, Debug)]
pub struct AddFileDto {
pub content: Vec<u8>,
pub mime_type: String,
pub creation_time: NaiveDateTime,
pub change_time: NaiveDateTime,
pub name: Option<String>,
}
#[derive(Clone, Debug)]
pub struct UpdateFileDto {
pub id: i64,
pub cd_id: Option<i64>,
pub mime_type: Option<String>,
pub status: Option<FileStatus>,
}
impl Default for UpdateFileDto {
fn default() -> Self {
Self {
id: 0,
cd_id: None,
mime_type: None,
status: None,
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum FileStatus {
Imported = 10,
Archived = 20,
Deleted = 30,
}
impl From<ApiFileStatus> for FileStatus {
fn from(s: ApiFileStatus) -> Self {
match s {
ApiFileStatus::Imported => Self::Imported,
ApiFileStatus::Archived => Self::Archived,
ApiFileStatus::Deleted => Self::Deleted,
}
}
}

@ -8,7 +8,7 @@ pub struct FileMetadataDto {
impl FileMetadataDto {
pub(crate) fn new(model: file_metadata::Model) -> Self {
Self {model}
Self { model }
}
pub fn file_id(&self) -> i64 {
@ -38,4 +38,13 @@ impl FileMetadataDto {
pub fn change_time(&self) -> NaiveDateTime {
self.model.change_time
}
}
}
#[derive(Clone, Debug, Default)]
pub struct UpdateFileMetadataDto {
pub file_id: i64,
pub name: Option<Option<String>>,
pub comment: Option<Option<String>>,
pub size: Option<i64>,
pub change_time: Option<NaiveDateTime>,
}

@ -1,15 +1,25 @@
use mediarepo_core::error::RepoResult;
use mediarepo_core::fs::thumbnail_store::Dimensions;
use std::path::PathBuf;
use tokio::fs::{File, OpenOptions};
use tokio::io::BufReader;
#[derive(Clone, Debug)]
pub struct ThumbnailDto {
path: PathBuf,
parent_cd: String,
size: Dimensions,
mime_type: String,
}
impl ThumbnailDto {
pub fn new(parent_cd: String, size: Dimensions, mime_type: String) -> Self {
Self {parent_cd, size, mime_type}
pub fn new(path: PathBuf, parent_cd: String, size: Dimensions, mime_type: String) -> Self {
Self {
path,
parent_cd,
size,
mime_type,
}
}
pub fn parent_cd(&self) -> &String {
@ -23,4 +33,10 @@ impl ThumbnailDto {
pub fn mime_type(&self) -> &String {
&self.mime_type
}
#[tracing::instrument(level = "debug")]
pub async fn get_reader(&self) -> RepoResult<BufReader<File>> {
let file = OpenOptions::new().read(true).open(&self.path).await?;
Ok(BufReader::new(file))
}
}

@ -1,5 +1,3 @@
pub mod filter;
use std::fmt::Debug;
use std::io::Cursor;
use std::str::FromStr;
@ -10,11 +8,12 @@ use sea_orm::{ConnectionTrait, DatabaseConnection, Set};
use sea_orm::{JoinType, QuerySelect};
use tokio::io::{AsyncReadExt, BufReader};
use crate::file::filter::FilterProperty;
use crate::dao::file::find;
use crate::dao::file::find::FilterProperty;
use crate::dto::FileStatus;
use crate::file_metadata::FileMetadata;
use mediarepo_core::error::{RepoError, RepoResult};
use mediarepo_core::fs::file_hash_store::FileHashStore;
use mediarepo_core::mediarepo_api::types::files::FileStatus as ApiFileStatus;
use mediarepo_core::thumbnailer::{self, Thumbnail as ThumbnailerThumb, ThumbnailSize};
use mediarepo_database::entities::content_descriptor;
use mediarepo_database::entities::content_descriptor_tag;
@ -25,22 +24,6 @@ use mediarepo_database::entities::tag;
use crate::tag::Tag;
pub enum FileStatus {
Imported = 10,
Archived = 20,
Deleted = 30,
}
impl From<ApiFileStatus> for FileStatus {
fn from(s: ApiFileStatus) -> Self {
match s {
ApiFileStatus::Imported => Self::Imported,
ApiFileStatus::Archived => Self::Archived,
ApiFileStatus::Deleted => Self::Deleted,
}
}
}
#[derive(Clone)]
pub struct File {
db: DatabaseConnection,
@ -111,29 +94,6 @@ impl File {
}
}
/// Finds the file by tags
#[tracing::instrument(level = "debug", skip(db))]
pub(crate) async fn find_by_filters(
db: DatabaseConnection,
filters: Vec<Vec<FilterProperty>>,
) -> RepoResult<Vec<Self>> {
let main_condition = filter::build_find_filter_conditions(filters);
let results: Vec<(content_descriptor::Model, Option<file::Model>)> =
content_descriptor::Entity::find()
.find_also_related(file::Entity)
.filter(main_condition)
.group_by(file::Column::Id)
.all(&db)
.await?;
let files: Vec<Self> = results
.into_iter()
.filter_map(|(hash, tag)| Some(Self::new(db.clone(), tag?, hash)))
.collect();
Ok(files)
}
/// Adds a file with its hash to the database
#[tracing::instrument(level = "debug", skip(db))]
pub(crate) async fn add(
@ -247,11 +207,16 @@ impl File {
return Ok(());
}
let cd_id = self.content_descriptor.id;
let own_tag_ids = self.tags().await?.into_iter().map(|t| t.id()).collect::<Vec<i64>>();
let own_tag_ids = self
.tags()
.await?
.into_iter()
.map(|t| t.id())
.collect::<Vec<i64>>();
let models: Vec<content_descriptor_tag::ActiveModel> = tag_ids
.into_iter()
.filter(|tag_id|!own_tag_ids.contains(tag_id))
.filter(|tag_id| !own_tag_ids.contains(tag_id))
.map(|tag_id| content_descriptor_tag::ActiveModel {
cd_id: Set(cd_id),
tag_id: Set(tag_id),

@ -2,8 +2,8 @@ pub mod content_descriptor;
pub mod file;
pub mod file_metadata;
pub mod namespace;
pub mod repo;
pub mod tag;
pub mod thumbnail;
pub mod type_keys;
pub mod dto;
pub mod dao;

@ -1,4 +1,4 @@
use crate::repo::Repo;
use crate::dao::repo::Repo;
use std::sync::Arc;
use typemap_rev::TypeMapKey;

@ -2,7 +2,10 @@ use mediarepo_core::mediarepo_api::types::files::{
FileBasicDataResponse, FileMetadataResponse, FileStatus, ThumbnailMetadataResponse,
};
use mediarepo_core::mediarepo_api::types::tags::{NamespaceResponse, TagResponse};
use mediarepo_logic::file::{File, FileStatus as FileStatusModel};
use mediarepo_logic::dto::{
FileDto, FileMetadataDto, FileStatus as FileStatusModel, TagDto, ThumbnailDto,
};
use mediarepo_logic::file::File;
use mediarepo_logic::file_metadata::FileMetadata;
use mediarepo_logic::namespace::Namespace;
use mediarepo_logic::tag::Tag;
@ -25,6 +28,19 @@ impl FromModel<FileMetadata> for FileMetadataResponse {
}
}
impl FromModel<FileMetadataDto> for FileMetadataResponse {
fn from_model(model: FileMetadataDto) -> Self {
Self {
file_id: model.file_id(),
name: model.name().cloned(),
comment: model.comment().cloned(),
creation_time: model.creation_time().to_owned(),
change_time: model.change_time().to_owned(),
import_time: model.import_time().to_owned(),
}
}
}
impl FromModel<File> for FileBasicDataResponse {
fn from_model(file: File) -> Self {
FileBasicDataResponse {
@ -36,6 +52,17 @@ impl FromModel<File> for FileBasicDataResponse {
}
}
impl FromModel<FileDto> for FileBasicDataResponse {
fn from_model(model: FileDto) -> Self {
FileBasicDataResponse {
id: model.id(),
status: FileStatus::from_model(model.status()),
cd: model.encoded_cd(),
mime_type: model.mime_type().to_owned(),
}
}
}
impl FromModel<FileStatusModel> for FileStatus {
fn from_model(status: FileStatusModel) -> Self {
match status {
@ -56,6 +83,16 @@ impl FromModel<Tag> for TagResponse {
}
}
impl FromModel<TagDto> for TagResponse {
fn from_model(model: TagDto) -> Self {
Self {
id: model.id(),
namespace: model.namespace().map(|n| n.name().to_owned()),
name: model.name().to_owned(),
}
}
}
impl FromModel<Thumbnail> for ThumbnailMetadataResponse {
fn from_model(model: Thumbnail) -> Self {
Self {
@ -67,6 +104,17 @@ impl FromModel<Thumbnail> for ThumbnailMetadataResponse {
}
}
impl FromModel<ThumbnailDto> for ThumbnailMetadataResponse {
fn from_model(model: ThumbnailDto) -> Self {
Self {
file_hash: model.parent_cd().to_owned(),
height: model.size().height,
width: model.size().width,
mime_type: model.mime_type().to_owned(),
}
}
}
impl FromModel<Namespace> for NamespaceResponse {
fn from_model(model: Namespace) -> Self {
Self {

@ -4,7 +4,7 @@ use mediarepo_core::mediarepo_api::types::misc::InfoResponse;
use mediarepo_core::settings::{PortSetting, Settings};
use mediarepo_core::tokio_graceful_shutdown::SubsystemHandle;
use mediarepo_core::type_keys::{RepoPathKey, SettingsKey, SizeMetadataKey, SubsystemKey};
use mediarepo_logic::repo::Repo;
use mediarepo_logic::dao::repo::Repo;
use mediarepo_logic::type_keys::RepoKey;
use std::net::SocketAddr;
use std::path::PathBuf;

@ -6,6 +6,8 @@ use crate::namespaces::files::searching::find_files_for_filters;
use crate::namespaces::files::sorting::sort_files_by_properties;
use crate::utils::{cd_by_identifier, file_by_identifier, get_repo_from_context};
use mediarepo_core::bromine::prelude::*;
use mediarepo_core::content_descriptor::{create_content_descriptor, encode_content_descriptor};
use mediarepo_core::error::RepoError;
use mediarepo_core::fs::thumbnail_store::Dimensions;
use mediarepo_core::itertools::Itertools;
use mediarepo_core::mediarepo_api::types::files::{
@ -17,8 +19,9 @@ use mediarepo_core::mediarepo_api::types::filtering::FindFilesRequest;
use mediarepo_core::mediarepo_api::types::identifier::FileIdentifier;
use mediarepo_core::thumbnailer::ThumbnailSize;
use mediarepo_core::utils::parse_namespace_and_tag;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{AddFileDto, UpdateFileDto, UpdateFileMetadataDto};
use tokio::io::AsyncReadExt;
use mediarepo_core::content_descriptor::create_content_descriptor;
pub struct FilesNamespace;
@ -51,7 +54,7 @@ impl FilesNamespace {
#[tracing::instrument(skip_all)]
async fn all_files(ctx: &Context, _event: Event) -> IPCResult<()> {
let repo = get_repo_from_context(ctx).await;
let files = repo.files().await?;
let files = repo.file().all().await?;
let responses: Vec<FileBasicDataResponse> = files
.into_iter()
@ -81,7 +84,17 @@ impl FilesNamespace {
let id = event.payload::<FileIdentifier>()?;
let repo = get_repo_from_context(ctx).await;
let file = file_by_identifier(id, &repo).await?;
let metadata = file.metadata().await?;
let file_id = file.id();
let metadata = if let Some(metadata) = file.into_metadata() {
metadata
} else {
repo.file()
.metadata(file_id)
.await?
.ok_or_else(|| RepoError::from("file metadata not found"))?
};
ctx.emit_to(
Self::name(),
"get_file_metadata",
@ -139,26 +152,29 @@ impl FilesNamespace {
let bytes = bytes.into_inner();
let cd = create_content_descriptor(&bytes);
let file = if let Some(file) = repo.file_by_cd(&cd).await? {
let file = if let Some(file) = repo.file().by_cd(cd).await? {
tracing::debug!("Inserted file already exists");
file
} else {
repo
.add_file(
metadata.mime_type,
bytes,
metadata.creation_time,
metadata.change_time,
)
.await?
let add_dto = AddFileDto {
content: bytes,
mime_type: metadata
.mime_type
.unwrap_or(String::from("application/octet-stream")),
creation_time: metadata.creation_time,
change_time: metadata.change_time,
name: Some(metadata.name),
};
repo.file().add(add_dto).await?
};
file.metadata().await?.set_name(metadata.name).await?;
let tags = repo
.add_all_tags(tags.into_iter().map(parse_namespace_and_tag).collect())
.await?;
let tag_ids: Vec<i64> = tags.into_iter().map(|t| t.id()).unique().collect();
file.add_tags(tag_ids).await?;
repo.tag()
.upsert_mappings(vec![file.cd_id()], tag_ids)
.await?;
ctx.emit_to(
Self::name(),
@ -175,7 +191,14 @@ impl FilesNamespace {
let request = event.payload::<UpdateFileStatusRequest>()?;
let repo = get_repo_from_context(ctx).await;
let mut file = file_by_identifier(request.file_id, &repo).await?;
file.set_status(request.status.into()).await?;
file = repo
.file()
.update(UpdateFileDto {
id: file.id(),
status: Some(request.status.into()),
..Default::default()
})
.await?;
ctx.emit_to(
Self::name(),
"update_file_status",
@ -192,7 +215,7 @@ impl FilesNamespace {
let request = event.payload::<ReadFileRequest>()?;
let repo = get_repo_from_context(ctx).await;
let file = file_by_identifier(request.id, &repo).await?;
let bytes = repo.get_file_bytes(&file).await?;
let bytes = repo.file().get_bytes(file.cd()).await?;
ctx.emit_to(Self::name(), "read_file", BytePayload::new(bytes))
.await?;
@ -206,7 +229,7 @@ impl FilesNamespace {
let id = event.payload::<FileIdentifier>()?;
let repo = get_repo_from_context(ctx).await;
let file = file_by_identifier(id, &repo).await?;
repo.delete_file(file).await?;
repo.file().delete(file).await?;
ctx.emit_to(Self::name(), "delete_file", ()).await?;
@ -219,12 +242,18 @@ impl FilesNamespace {
let request = event.payload::<GetFileThumbnailsRequest>()?;
let repo = get_repo_from_context(ctx).await;
let file_cd = cd_by_identifier(request.id.clone(), &repo).await?;
let mut thumbnails = repo.get_file_thumbnails(&file_cd).await?;
let mut thumbnails = repo
.file()
.thumbnails(encode_content_descriptor(&file_cd))
.await?;
if thumbnails.is_empty() {
tracing::debug!("No thumbnails for file found. Creating thumbnails...");
let file = file_by_identifier(request.id, &repo).await?;
thumbnails = repo.create_thumbnails_for_file(&file).await?;
thumbnails = repo
.file()
.create_thumbnails(file, vec![ThumbnailSize::Medium])
.await?;
tracing::debug!("Thumbnails for file created.");
}
@ -244,17 +273,20 @@ impl FilesNamespace {
let request = event.payload::<GetFileThumbnailOfSizeRequest>()?;
let repo = get_repo_from_context(ctx).await;
let file_cd = cd_by_identifier(request.id.clone(), &repo).await?;
let thumbnails = repo.get_file_thumbnails(&file_cd).await?;
let min_size = request.min_size;
let max_size = request.max_size;
let thumbnails = repo
.file()
.thumbnails(encode_content_descriptor(&file_cd))
.await?;
let found_thumbnail = thumbnails.into_iter().find(|thumb| {
let Dimensions { height, width } = thumb.size;
let Dimensions { height, width } = thumb.size();
height >= min_size.0
&& height <= max_size.0
&& width >= min_size.1
&& width <= max_size.1
*height >= min_size.0
&& *height <= max_size.0
&& *width >= min_size.1
&& *width <= max_size.1
});
let thumbnail = if let Some(thumbnail) = found_thumbnail {
@ -263,10 +295,14 @@ impl FilesNamespace {
let file = file_by_identifier(request.id, &repo).await?;
let middle_size = ((max_size.0 + min_size.0) / 2, (max_size.1 + min_size.1) / 2);
let thumbnail = repo
.create_file_thumbnail(&file, ThumbnailSize::Custom(middle_size))
.file()
.create_thumbnails(file, vec![ThumbnailSize::Custom(middle_size)])
.await?;
thumbnail
.into_iter()
.next()
.ok_or_else(|| RepoError::from("thumbnail could not be created"))?
};
let mut buf = Vec::new();
thumbnail.get_reader().await?.read_to_end(&mut buf).await?;
@ -288,8 +324,15 @@ impl FilesNamespace {
let repo = get_repo_from_context(ctx).await;
let request = event.payload::<UpdateFileNameRequest>()?;
let file = file_by_identifier(request.file_id, &repo).await?;
let mut metadata = file.metadata().await?;
metadata.set_name(request.name).await?;
let metadata = repo
.file()
.update_metadata(UpdateFileMetadataDto {
file_id: file.id(),
name: Some(Some(request.name)),
..Default::default()
})
.await?;
ctx.emit_to(
Self::name(),

@ -4,22 +4,23 @@ use mediarepo_core::mediarepo_api::types::files::FileStatus as ApiFileStatus;
use mediarepo_core::mediarepo_api::types::filtering::{
FilterExpression, FilterQuery, PropertyQuery, TagQuery, ValueComparator,
};
use mediarepo_logic::file::filter::NegatableComparator::{Is, IsNot};
use mediarepo_logic::file::filter::{FilterFileProperty, FilterProperty, OrderingComparator};
use mediarepo_logic::file::{File, FileStatus};
use mediarepo_logic::repo::Repo;
use mediarepo_logic::dao::file::find::NegatableComparator::{Is, IsNot};
use mediarepo_logic::dao::file::find::{FilterFileProperty, FilterProperty, OrderingComparator};
use mediarepo_logic::dao::repo::Repo;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{FileDto, FileStatus};
use std::collections::HashMap;
#[tracing::instrument(level = "debug", skip(repo))]
pub async fn find_files_for_filters(
repo: &Repo,
expressions: Vec<FilterExpression>,
) -> RepoResult<Vec<File>> {
) -> RepoResult<Vec<FileDto>> {
let tag_names = get_tag_names_from_expressions(&expressions);
let tag_id_map = repo.tag_names_to_ids(tag_names).await?;
let filters = build_filters_from_expressions(expressions, &tag_id_map);
repo.find_files_by_filters(filters).await
repo.file().find(filters).await
}
#[tracing::instrument(level = "debug")]

@ -5,9 +5,11 @@ use mediarepo_core::mediarepo_api::types::filtering::{SortDirection, SortKey};
use mediarepo_database::queries::tags::{
get_cids_with_namespaced_tags, get_content_descriptors_with_tag_count,
};
use mediarepo_logic::dao::repo::Repo;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{FileDto, FileMetadataDto};
use mediarepo_logic::file::File;
use mediarepo_logic::file_metadata::FileMetadata;
use mediarepo_logic::repo::Repo;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use std::cmp::Ordering;
use std::collections::HashMap;
@ -28,7 +30,7 @@ pub struct FileSortContext {
pub async fn sort_files_by_properties(
repo: &Repo,
sort_expression: Vec<SortKey>,
files: &mut Vec<File>,
files: &mut Vec<FileDto>,
) -> RepoResult<()> {
let contexts = build_sort_context(repo, files).await?;
@ -45,7 +47,7 @@ pub async fn sort_files_by_properties(
async fn build_sort_context(
repo: &Repo,
files: &Vec<File>,
files: &Vec<FileDto>,
) -> RepoResult<HashMap<i64, FileSortContext>> {
let hash_ids: Vec<i64> = files.par_iter().map(|f| f.cd_id()).collect();
let file_ids: Vec<i64> = files.par_iter().map(|f| f.id()).collect();
@ -54,9 +56,9 @@ async fn build_sort_context(
get_cids_with_namespaced_tags(repo.db(), hash_ids.clone()).await?;
let mut cid_tag_counts = get_content_descriptors_with_tag_count(repo.db(), hash_ids).await?;
let files_metadata = repo.get_file_metadata_for_ids(file_ids).await?;
let files_metadata = repo.file().all_metadata(file_ids).await?;
let mut file_metadata_map: HashMap<i64, FileMetadata> =
let mut file_metadata_map: HashMap<i64, FileMetadataDto> =
HashMap::from_iter(files_metadata.into_iter().map(|m| (m.file_id(), m)));
let mut contexts = HashMap::new();
@ -64,7 +66,7 @@ async fn build_sort_context(
for file in files {
if let Some(metadata) = file_metadata_map.remove(&file.id()) {
let context = FileSortContext {
name: metadata.name().to_owned(),
name: metadata.name().cloned(),
size: metadata.size() as u64,
mime_type: file.mime_type().to_owned(),
namespaces: cid_nsp

@ -6,6 +6,7 @@ use mediarepo_core::mediarepo_api::types::files::{GetFileTagsRequest, GetFilesTa
use mediarepo_core::mediarepo_api::types::tags::{
ChangeFileTagsRequest, NamespaceResponse, TagResponse,
};
use mediarepo_logic::dao::DaoProvider;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
pub struct TagsNamespace;
@ -65,7 +66,7 @@ impl TagsNamespace {
let repo = get_repo_from_context(ctx).await;
let request = event.payload::<GetFileTagsRequest>()?;
let file = file_by_identifier(request.id, &repo).await?;
let tags = file.tags().await?;
let tags = repo.tag().tags_for_cd(file.cd_id()).await?;
let responses: Vec<TagResponse> = tags.into_iter().map(TagResponse::from_model).collect();
ctx.emit_to(Self::name(), "tags_for_file", responses)
@ -126,14 +127,19 @@ impl TagsNamespace {
let file = file_by_identifier(request.file_id, &repo).await?;
if !request.added_tags.is_empty() {
file.add_tags(request.added_tags).await?;
repo.tag()
.upsert_mappings(vec![file.cd_id()], request.added_tags)
.await?;
}
if !request.removed_tags.is_empty() {
file.remove_tags(request.removed_tags).await?;
repo.tag()
.remove_mappings(vec![file.cd_id()], request.removed_tags)
.await?;
}
let responses: Vec<TagResponse> = file
.tags()
let responses: Vec<TagResponse> = repo
.tag()
.tags_for_cd(file.cd_id())
.await?
.into_iter()
.map(TagResponse::from_model)

@ -5,8 +5,9 @@ use mediarepo_core::mediarepo_api::types::identifier::FileIdentifier;
use mediarepo_core::mediarepo_api::types::repo::SizeType;
use mediarepo_core::type_keys::{RepoPathKey, SettingsKey};
use mediarepo_core::utils::get_folder_size;
use mediarepo_logic::file::File;
use mediarepo_logic::repo::Repo;
use mediarepo_logic::dao::repo::Repo;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::FileDto;
use mediarepo_logic::type_keys::RepoKey;
use std::sync::Arc;
use tokio::fs;
@ -17,10 +18,10 @@ pub async fn get_repo_from_context(ctx: &Context) -> Arc<Repo> {
Arc::clone(repo)
}
pub async fn file_by_identifier(identifier: FileIdentifier, repo: &Repo) -> RepoResult<File> {
pub async fn file_by_identifier(identifier: FileIdentifier, repo: &Repo) -> RepoResult<FileDto> {
let file = match identifier {
FileIdentifier::ID(id) => repo.file_by_id(id).await,
FileIdentifier::CD(cd) => repo.file_by_cd(&decode_content_descriptor(cd)?).await,
FileIdentifier::ID(id) => repo.file().by_id(id).await,
FileIdentifier::CD(cd) => repo.file().by_cd(decode_content_descriptor(cd)?).await,
}?;
file.ok_or_else(|| RepoError::from("File not found"))
}
@ -29,7 +30,8 @@ pub async fn cd_by_identifier(identifier: FileIdentifier, repo: &Repo) -> RepoRe
match identifier {
FileIdentifier::ID(id) => {
let file = repo
.file_by_id(id)
.file()
.by_id(id)
.await?
.ok_or_else(|| "Thumbnail not found")?;
Ok(file.cd().to_owned())

@ -9,7 +9,7 @@ use mediarepo_core::error::RepoResult;
use mediarepo_core::fs::drop_file::DropFile;
use mediarepo_core::settings::{PathSettings, Settings};
use mediarepo_core::tokio_graceful_shutdown::{SubsystemHandle, Toplevel};
use mediarepo_logic::repo::Repo;
use mediarepo_logic::dao::repo::Repo;
use mediarepo_socket::start_tcp_server;
use std::env;
use std::time::Duration;

@ -1,7 +1,7 @@
use mediarepo_core::error::RepoResult;
use mediarepo_core::settings::v1::SettingsV1;
use mediarepo_core::settings::{PathSettings, Settings};
use mediarepo_logic::repo::Repo;
use mediarepo_logic::dao::repo::Repo;
use std::path::PathBuf;
use tokio::fs;

Loading…
Cancel
Save