parent
cb493b4651
commit
745c6bf856
@ -1,101 +0,0 @@
|
|||||||
use crate::file::File;
|
|
||||||
use mediarepo_core::content_descriptor::convert_v1_descriptor_to_v2;
|
|
||||||
use mediarepo_core::error::RepoResult;
|
|
||||||
use mediarepo_database::entities::content_descriptor;
|
|
||||||
use mediarepo_database::entities::file;
|
|
||||||
use sea_orm::prelude::*;
|
|
||||||
use sea_orm::{DatabaseConnection, Set};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
pub struct ContentDescriptor {
|
|
||||||
db: DatabaseConnection,
|
|
||||||
model: content_descriptor::Model,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContentDescriptor {
|
|
||||||
#[tracing::instrument(level = "trace")]
|
|
||||||
pub(crate) fn new(db: DatabaseConnection, model: content_descriptor::Model) -> Self {
|
|
||||||
Self { db, model }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn all(db: DatabaseConnection) -> RepoResult<Vec<Self>> {
|
|
||||||
let descriptors = content_descriptor::Entity::find()
|
|
||||||
.all(&db)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|model| Self::new(db.clone(), model))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(descriptors)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Searches for the hash by id
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult<Option<Self>> {
|
|
||||||
let hash = content_descriptor::Entity::find_by_id(id)
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
.map(|model| Self::new(db, model));
|
|
||||||
|
|
||||||
Ok(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the hash by value
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub async fn by_value<D: AsRef<[u8]> + Debug>(
|
|
||||||
db: DatabaseConnection,
|
|
||||||
descriptor: D,
|
|
||||||
) -> RepoResult<Option<Self>> {
|
|
||||||
let cid = content_descriptor::Entity::find()
|
|
||||||
.filter(content_descriptor::Column::Descriptor.eq(descriptor.as_ref()))
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
.map(|model| Self::new(db, model));
|
|
||||||
|
|
||||||
Ok(cid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a new hash to the database
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub async fn add(db: DatabaseConnection, descriptor: Vec<u8>) -> RepoResult<Self> {
|
|
||||||
let active_model = content_descriptor::ActiveModel {
|
|
||||||
descriptor: Set(descriptor),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let model = active_model.insert(&db).await?;
|
|
||||||
|
|
||||||
Ok(Self::new(db, model))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> i64 {
|
|
||||||
self.model.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn descriptor(&self) -> &[u8] {
|
|
||||||
&self.model.descriptor[..]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the file associated with the hash
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn file(&self) -> RepoResult<Option<File>> {
|
|
||||||
let file = self
|
|
||||||
.model
|
|
||||||
.find_related(file::Entity)
|
|
||||||
.one(&self.db)
|
|
||||||
.await?
|
|
||||||
.map(|file_model| File::new(self.db.clone(), file_model, self.model.clone()));
|
|
||||||
|
|
||||||
Ok(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn convert_v1_to_v2(&mut self) -> RepoResult<()> {
|
|
||||||
let descriptor = convert_v1_descriptor_to_v2(&self.model.descriptor)?;
|
|
||||||
let active_model = content_descriptor::ActiveModel {
|
|
||||||
id: Set(self.id()),
|
|
||||||
descriptor: Set(descriptor),
|
|
||||||
};
|
|
||||||
self.model = active_model.update(&self.db).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,59 @@
|
|||||||
|
use crate::dao::{DaoContext, DaoProvider};
|
||||||
|
use mediarepo_core::content_descriptor::{
|
||||||
|
convert_v1_descriptor_to_v2, encode_content_descriptor, is_v1_content_descriptor,
|
||||||
|
};
|
||||||
|
use mediarepo_core::error::RepoResult;
|
||||||
|
use mediarepo_database::entities::content_descriptor;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::ConnectionTrait;
|
||||||
|
|
||||||
|
pub struct JobDao {
|
||||||
|
ctx: DaoContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DaoProvider for JobDao {
|
||||||
|
fn dao_ctx(&self) -> DaoContext {
|
||||||
|
self.ctx.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobDao {
|
||||||
|
pub fn new(ctx: DaoContext) -> JobDao {
|
||||||
|
Self { ctx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn migrate_content_descriptors(&self) -> RepoResult<()> {
|
||||||
|
let cds: Vec<content_descriptor::Model> =
|
||||||
|
content_descriptor::Entity::find().all(&self.ctx.db).await?;
|
||||||
|
|
||||||
|
tracing::info!("Converting content descriptors to v2 format...");
|
||||||
|
let mut converted_count = 0;
|
||||||
|
|
||||||
|
for mut cd in cds {
|
||||||
|
if is_v1_content_descriptor(&cd.descriptor) {
|
||||||
|
let trx = self.ctx.db.begin().await?;
|
||||||
|
let src_cd = cd.descriptor;
|
||||||
|
let dst_cd = convert_v1_descriptor_to_v2(&src_cd)?;
|
||||||
|
|
||||||
|
let active_model = content_descriptor::ActiveModel {
|
||||||
|
id: Set(cd.id),
|
||||||
|
descriptor: Set(dst_cd.clone()),
|
||||||
|
};
|
||||||
|
self.ctx.main_storage.rename_file(&src_cd, &dst_cd).await?;
|
||||||
|
self.ctx
|
||||||
|
.thumbnail_storage
|
||||||
|
.rename_parent(
|
||||||
|
encode_content_descriptor(&src_cd),
|
||||||
|
encode_content_descriptor(&dst_cd),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
trx.commit().await?;
|
||||||
|
converted_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!("Converted {} descriptors", converted_count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,300 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
use std::io::Cursor;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use mediarepo_core::content_descriptor::encode_content_descriptor;
|
|
||||||
use sea_orm::prelude::*;
|
|
||||||
use sea_orm::{ConnectionTrait, DatabaseConnection, Set};
|
|
||||||
use sea_orm::{JoinType, QuerySelect};
|
|
||||||
use tokio::io::{AsyncReadExt, BufReader};
|
|
||||||
|
|
||||||
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::thumbnailer::{self, Thumbnail as ThumbnailerThumb, ThumbnailSize};
|
|
||||||
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 mediarepo_database::entities::namespace;
|
|
||||||
use mediarepo_database::entities::tag;
|
|
||||||
|
|
||||||
use crate::tag::Tag;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct File {
|
|
||||||
db: DatabaseConnection,
|
|
||||||
model: file::Model,
|
|
||||||
content_descriptor: content_descriptor::Model,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl File {
|
|
||||||
#[tracing::instrument(level = "trace")]
|
|
||||||
pub(crate) fn new(
|
|
||||||
db: DatabaseConnection,
|
|
||||||
model: file::Model,
|
|
||||||
hash: content_descriptor::Model,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
db,
|
|
||||||
model,
|
|
||||||
content_descriptor: hash,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of all known stored files
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub async fn all(db: DatabaseConnection) -> RepoResult<Vec<File>> {
|
|
||||||
let files: Vec<(file::Model, Option<content_descriptor::Model>)> = file::Entity::find()
|
|
||||||
.find_also_related(content_descriptor::Entity)
|
|
||||||
.all(&db)
|
|
||||||
.await?;
|
|
||||||
let files = files
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(f, h)| {
|
|
||||||
let h = h?;
|
|
||||||
Some(Self::new(db.clone(), f, h))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetches the file by id
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult<Option<Self>> {
|
|
||||||
if let Some((model, Some(hash))) = file::Entity::find_by_id(id)
|
|
||||||
.find_also_related(content_descriptor::Entity)
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
let file = File::new(db, model, hash);
|
|
||||||
Ok(Some(file))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds the file by hash
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub async fn by_cd(db: DatabaseConnection, cd: &[u8]) -> RepoResult<Option<Self>> {
|
|
||||||
if let Some((hash, Some(model))) = content_descriptor::Entity::find()
|
|
||||||
.filter(content_descriptor::Column::Descriptor.eq(cd))
|
|
||||||
.find_also_related(file::Entity)
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
let file = File::new(db, model, hash);
|
|
||||||
Ok(Some(file))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a file with its hash to the database
|
|
||||||
#[tracing::instrument(level = "debug", skip(db))]
|
|
||||||
pub(crate) async fn add(
|
|
||||||
db: DatabaseConnection,
|
|
||||||
cd_id: i64,
|
|
||||||
mime_type: String,
|
|
||||||
) -> RepoResult<Self> {
|
|
||||||
let file = file::ActiveModel {
|
|
||||||
cd_id: Set(cd_id),
|
|
||||||
mime_type: Set(mime_type),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let file: file::ActiveModel = file.insert(&db).await?.into();
|
|
||||||
let file = Self::by_id(db, file.id.unwrap())
|
|
||||||
.await?
|
|
||||||
.expect("Inserted file does not exist");
|
|
||||||
|
|
||||||
Ok(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the unique identifier of the file
|
|
||||||
pub fn id(&self) -> i64 {
|
|
||||||
self.model.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the hash of the file (content identifier)
|
|
||||||
pub fn cd(&self) -> &[u8] {
|
|
||||||
&self.content_descriptor.descriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the encoded content descriptor
|
|
||||||
pub fn encoded_cd(&self) -> String {
|
|
||||||
encode_content_descriptor(self.cd())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the id of the civ (content identifier value) of the file
|
|
||||||
pub fn cd_id(&self) -> i64 {
|
|
||||||
self.content_descriptor.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the mime type of the file
|
|
||||||
pub fn mime_type(&self) -> &String {
|
|
||||||
&self.model.mime_type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the status of the file
|
|
||||||
pub fn status(&self) -> FileStatus {
|
|
||||||
match self.model.status {
|
|
||||||
10 => FileStatus::Imported,
|
|
||||||
20 => FileStatus::Archived,
|
|
||||||
30 => FileStatus::Deleted,
|
|
||||||
_ => FileStatus::Imported,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_status(&mut self, status: FileStatus) -> RepoResult<()> {
|
|
||||||
let active_model = file::ActiveModel {
|
|
||||||
id: Set(self.model.id),
|
|
||||||
status: Set(status as i32),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
self.model = active_model.update(&self.db).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the metadata associated with this file
|
|
||||||
/// A file MUST always have metadata associated
|
|
||||||
pub async fn metadata(&self) -> RepoResult<FileMetadata> {
|
|
||||||
FileMetadata::by_id(self.db.clone(), self.model.id)
|
|
||||||
.await
|
|
||||||
.and_then(|f| f.ok_or_else(|| RepoError::from("missing file metadata")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the list of tags of the file
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn tags(&self) -> RepoResult<Vec<Tag>> {
|
|
||||||
let tags: Vec<(tag::Model, Option<namespace::Model>)> = tag::Entity::find()
|
|
||||||
.find_also_related(namespace::Entity)
|
|
||||||
.join(
|
|
||||||
JoinType::LeftJoin,
|
|
||||||
content_descriptor_tag::Relation::Tag.def().rev(),
|
|
||||||
)
|
|
||||||
.filter(content_descriptor_tag::Column::CdId.eq(self.content_descriptor.id))
|
|
||||||
.all(&self.db)
|
|
||||||
.await?;
|
|
||||||
let tags = tags
|
|
||||||
.into_iter()
|
|
||||||
.map(|(tag, namespace)| Tag::new(self.db.clone(), tag, namespace))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a single tag to the file
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn add_tag(&mut self, tag_id: i64) -> RepoResult<()> {
|
|
||||||
let cd_id = self.content_descriptor.id;
|
|
||||||
let active_model = content_descriptor_tag::ActiveModel {
|
|
||||||
cd_id: Set(cd_id),
|
|
||||||
tag_id: Set(tag_id),
|
|
||||||
};
|
|
||||||
active_model.insert(&self.db).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds multiple tags to the file at once
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn add_tags(&self, tag_ids: Vec<i64>) -> RepoResult<()> {
|
|
||||||
if tag_ids.is_empty() {
|
|
||||||
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 models: Vec<content_descriptor_tag::ActiveModel> = tag_ids
|
|
||||||
.into_iter()
|
|
||||||
.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),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if models.len() > 0 {
|
|
||||||
content_descriptor_tag::Entity::insert_many(models)
|
|
||||||
.exec(&self.db)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes multiple tags from the file
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn remove_tags(&self, tag_ids: Vec<i64>) -> RepoResult<()> {
|
|
||||||
let hash_id = self.content_descriptor.id;
|
|
||||||
content_descriptor_tag::Entity::delete_many()
|
|
||||||
.filter(content_descriptor_tag::Column::CdId.eq(hash_id))
|
|
||||||
.filter(content_descriptor_tag::Column::TagId.is_in(tag_ids))
|
|
||||||
.exec(&self.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the reader for the file
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn get_reader(
|
|
||||||
&self,
|
|
||||||
storage: &FileHashStore,
|
|
||||||
) -> RepoResult<BufReader<tokio::fs::File>> {
|
|
||||||
storage
|
|
||||||
.get_file(&self.content_descriptor.descriptor)
|
|
||||||
.await
|
|
||||||
.map(|(_, f)| f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a thumbnail for the file
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn create_thumbnail<I: IntoIterator<Item = ThumbnailSize> + Debug>(
|
|
||||||
&self,
|
|
||||||
storage: &FileHashStore,
|
|
||||||
sizes: I,
|
|
||||||
) -> RepoResult<Vec<ThumbnailerThumb>> {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
self.get_reader(storage)
|
|
||||||
.await?
|
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.await?;
|
|
||||||
let mime_type = self.model.mime_type.clone();
|
|
||||||
let mime_type =
|
|
||||||
mime::Mime::from_str(&mime_type).unwrap_or_else(|_| mime::APPLICATION_OCTET_STREAM);
|
|
||||||
let thumbs = thumbnailer::create_thumbnails(Cursor::new(buf), mime_type, sizes)?;
|
|
||||||
|
|
||||||
Ok(thumbs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes the file as well as the content descriptor, tag mappings and metadata about the file
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
|
||||||
pub async fn delete(self) -> RepoResult<()> {
|
|
||||||
let trx = self.db.begin().await?;
|
|
||||||
file_metadata::Entity::delete_many()
|
|
||||||
.filter(file_metadata::Column::FileId.eq(self.model.id))
|
|
||||||
.exec(&trx)
|
|
||||||
.await?;
|
|
||||||
self.model.delete(&trx).await?;
|
|
||||||
content_descriptor_tag::Entity::delete_many()
|
|
||||||
.filter(content_descriptor_tag::Column::CdId.eq(self.content_descriptor.id))
|
|
||||||
.exec(&trx)
|
|
||||||
.await?;
|
|
||||||
content_descriptor::Entity::delete_many()
|
|
||||||
.filter(content_descriptor::Column::Id.eq(self.content_descriptor.id))
|
|
||||||
.exec(&trx)
|
|
||||||
.await?;
|
|
||||||
trx.commit().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
use mediarepo_core::error::RepoResult;
|
|
||||||
use mediarepo_core::fs::thumbnail_store::Dimensions;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::fs::{self, File, OpenOptions};
|
|
||||||
use tokio::io::BufReader;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Thumbnail {
|
|
||||||
pub file_hash: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub size: Dimensions,
|
|
||||||
pub mime_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Thumbnail {
|
|
||||||
/// Returns the reader of the thumbnail file
|
|
||||||
#[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))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes the thumbnail
|
|
||||||
#[tracing::instrument(level = "debug")]
|
|
||||||
pub async fn delete(self) -> RepoResult<()> {
|
|
||||||
fs::remove_file(&self.path).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue