Add tag import to file import

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 17edb0a72f
commit dbe0c20f63

@ -1,3 +1,8 @@
use crate::error::RepoResult;
use std::path::PathBuf;
use tokio::fs::OpenOptions;
use tokio::io::{AsyncBufReadExt, BufReader};
/// Parses a normalized tag into its two components of namespace and tag /// Parses a normalized tag into its two components of namespace and tag
pub fn parse_namespace_and_tag(norm_tag: String) -> (Option<String>, String) { pub fn parse_namespace_and_tag(norm_tag: String) -> (Option<String>, String) {
norm_tag norm_tag
@ -5,3 +10,16 @@ pub fn parse_namespace_and_tag(norm_tag: String) -> (Option<String>, String) {
.map(|(n, t)| (Some(n.trim().to_string()), t.trim().to_string())) .map(|(n, t)| (Some(n.trim().to_string()), t.trim().to_string()))
.unwrap_or((None, norm_tag.trim().to_string())) .unwrap_or((None, norm_tag.trim().to_string()))
} }
/// Parses all tags from a file
pub async fn parse_tags_file(path: PathBuf) -> RepoResult<Vec<(Option<String>, String)>> {
let file = OpenOptions::new().read(true).open(path).await?;
let mut lines = BufReader::new(file).lines();
let mut tags = Vec::new();
while let Some(line) = lines.next_line().await? {
tags.push(parse_namespace_and_tag(line));
}
Ok(tags)
}

@ -0,0 +1,8 @@
-- Add migration script here
DELETE FROM thumbnails WHERE file_id NOT IN (SELECT MIN(files.id) FROM files GROUP BY hash_id);
DELETE FROM files WHERE ROWID NOT IN (SELECT MIN(ROWID) FROM files GROUP BY hash_id);
DELETE FROM thumbnails WHERE hash_id NOT IN (SELECT MIN(hashes.id) FROM hashes GROUP BY value);
DELETE FROM files WHERE hash_id NOT IN (SELECT MIN(hashes.id) FROM hashes GROUP BY value);
DELETE FROM hashes WHERE ROWID NOT IN (SELECT MIN(ROWID) FROM hashes GROUP BY value);
CREATE UNIQUE INDEX hash_value_index ON hashes (value);
CREATE UNIQUE INDEX file_hash_id ON files (hash_id);

@ -0,0 +1,33 @@
-- Add migration script here
PRAGMA foreign_keys=off;
ALTER TABLE tags RENAME TO _tags_old;
CREATE TABLE tags
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
namespace_id INTEGER,
name VARCHAR(128),
FOREIGN KEY (namespace_id) REFERENCES namespaces (id)
);
CREATE UNIQUE INDEX tag_namespace_name_index ON tags (namespace_id, name);
INSERT INTO tags SELECT * FROM _tags_old;
DROP TABLE _tags_old;
ALTER TABLE hash_tag_mappings RENAME TO _hash_tag_mappings_old;
CREATE TABLE hash_tag_mappings
(
hash_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (hash_id, tag_id),
FOREIGN KEY (hash_id) REFERENCES hashes (id),
FOREIGN KEY (tag_id) REFERENCES tags (id)
);
CREATE UNIQUE INDEX hash_tag_mappings_hash_tag ON hash_tag_mappings (hash_id, tag_id);
INSERT INTO hash_tag_mappings SELECT * FROM _hash_tag_mappings_old;
DROP TABLE _hash_tag_mappings_old;
PRAGMA foreign_keys=on;

@ -19,7 +19,7 @@ pub enum Relation {
Namespace, Namespace,
} }
impl Related<super::namespace::Entity> for Entity { impl Related<super::hash::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
super::hash_tag::Relation::Hash.def() super::hash_tag::Relation::Hash.def()
} }
@ -29,4 +29,10 @@ impl Related<super::namespace::Entity> for Entity {
} }
} }
impl Related<super::namespace::Entity> for Entity {
fn to() -> RelationDef {
Relation::Namespace.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

@ -8,23 +8,22 @@ use mediarepo_core::image_processing::{
create_thumbnail, get_image_bytes_png, read_image, ThumbnailSize, create_thumbnail, get_image_bytes_png, read_image, ThumbnailSize,
}; };
use mediarepo_database::entities::file; use mediarepo_database::entities::file;
use mediarepo_database::entities::file::ActiveModel as ActiveFile;
use mediarepo_database::entities::file::Model as FileModel;
use mediarepo_database::entities::hash; use mediarepo_database::entities::hash;
use mediarepo_database::entities::hash::Model as HashModel; use mediarepo_database::entities::hash_tag;
use mime::Mime; use mime::Mime;
use sea_orm::prelude::*; use sea_orm::prelude::*;
use sea_orm::{DatabaseConnection, Set}; use sea_orm::{DatabaseConnection, Set};
use tokio::io::BufReader; use tokio::io::BufReader;
#[derive(Clone)]
pub struct File { pub struct File {
db: DatabaseConnection, db: DatabaseConnection,
model: FileModel, model: file::Model,
hash: HashModel, hash: hash::Model,
} }
impl File { impl File {
pub(crate) fn new(db: DatabaseConnection, model: FileModel, hash: HashModel) -> Self { pub(crate) fn new(db: DatabaseConnection, model: file::Model, hash: hash::Model) -> Self {
Self { db, model, hash } Self { db, model, hash }
} }
@ -198,6 +197,32 @@ impl File {
Ok(()) Ok(())
} }
/// Adds a single tag to the file
pub async fn add_tag(&mut self, tag_id: i64) -> RepoResult<()> {
let hash_id = self.hash.id;
let active_model = hash_tag::ActiveModel {
hash_id: Set(hash_id),
tag_id: Set(tag_id),
};
active_model.insert(&self.db).await?;
Ok(())
}
/// Adds multiple tags to the file at once
pub async fn add_tags(&self, tag_ids: Vec<i64>) -> RepoResult<()> {
let hash_id = self.hash.id;
let models: Vec<hash_tag::ActiveModel> = tag_ids
.into_iter()
.map(|tag_id| hash_tag::ActiveModel {
hash_id: Set(hash_id),
tag_id: Set(tag_id),
})
.collect();
hash_tag::Entity::insert_many(models).exec(&self.db).await?;
Ok(())
}
/// Returns the reader for the file /// Returns the reader for the file
pub async fn get_reader(&self) -> RepoResult<BufReader<tokio::fs::File>> { pub async fn get_reader(&self) -> RepoResult<BufReader<tokio::fs::File>> {
let storage = self.storage().await?; let storage = self.storage().await?;
@ -233,8 +258,8 @@ impl File {
} }
/// Returns the active model of the file with only the id set /// Returns the active model of the file with only the id set
fn get_active_model(&self) -> ActiveFile { fn get_active_model(&self) -> file::ActiveModel {
ActiveFile { file::ActiveModel {
id: Set(self.id()), id: Set(self.id()),
..Default::default() ..Default::default()
} }

@ -152,7 +152,7 @@ impl Repo {
} }
/// Adds or finds an unnamespaced tag /// Adds or finds an unnamespaced tag
async fn add_or_find_unnamespaced_tag(&self, name: String) -> RepoResult<Tag> { pub async fn add_or_find_unnamespaced_tag(&self, name: String) -> RepoResult<Tag> {
if let Some(tag) = Tag::by_name(self.db.clone(), &name).await? { if let Some(tag) = Tag::by_name(self.db.clone(), &name).await? {
Ok(tag) Ok(tag)
} else { } else {
@ -166,7 +166,11 @@ impl Repo {
} }
/// Adds or finds a namespaced tag /// Adds or finds a namespaced tag
async fn add_or_find_namespaced_tag(&self, name: String, namespace: String) -> RepoResult<Tag> { pub async fn add_or_find_namespaced_tag(
&self,
name: String,
namespace: String,
) -> RepoResult<Tag> {
if let Some(tag) = Tag::by_name_and_namespace(self.db.clone(), &name, &namespace).await? { if let Some(tag) = Tag::by_name_and_namespace(self.db.clone(), &name, &namespace).await? {
Ok(tag) Ok(tag)
} else { } else {

@ -3,8 +3,7 @@ use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::namespace; use mediarepo_database::entities::namespace;
use mediarepo_database::entities::tag; use mediarepo_database::entities::tag;
use sea_orm::prelude::*; use sea_orm::prelude::*;
use sea_orm::QuerySelect; use sea_orm::{DatabaseConnection, Set};
use sea_orm::{DatabaseConnection, JoinType, Set};
#[derive(Clone)] #[derive(Clone)]
pub struct Tag { pub struct Tag {
@ -29,7 +28,8 @@ impl Tag {
/// Returns all tags stored in the database /// Returns all tags stored in the database
pub async fn all(db: DatabaseConnection) -> RepoResult<Vec<Self>> { pub async fn all(db: DatabaseConnection) -> RepoResult<Vec<Self>> {
let tags: Vec<Self> = tag::Entity::find() let tags: Vec<Self> = tag::Entity::find()
.find_also_related(namespace::Entity) .inner_join(namespace::Entity)
.select_also(namespace::Entity)
.all(&db) .all(&db)
.await? .await?
.into_iter() .into_iter()
@ -73,7 +73,6 @@ impl Tag {
) -> RepoResult<Option<Self>> { ) -> RepoResult<Option<Self>> {
let tag = tag::Entity::find() let tag = tag::Entity::find()
.find_also_related(namespace::Entity) .find_also_related(namespace::Entity)
.join(JoinType::InnerJoin, namespace::Relation::Tag.def())
.filter(namespace::Column::Name.eq(namespace.as_ref())) .filter(namespace::Column::Name.eq(namespace.as_ref()))
.filter(tag::Column::Name.eq(name.as_ref())) .filter(tag::Column::Name.eq(name.as_ref()))
.one(&db) .one(&db)

@ -17,6 +17,7 @@ impl NamespaceProvider for TagsNamespace {
} }
impl TagsNamespace { impl TagsNamespace {
/// Returns a list of all tags in the database
async fn all_tags(ctx: &Context, event: Event) -> IPCResult<()> { async fn all_tags(ctx: &Context, event: Event) -> IPCResult<()> {
let repo = get_repo_from_context(ctx).await; let repo = get_repo_from_context(ctx).await;
let tags: Vec<TagResponse> = repo let tags: Vec<TagResponse> = repo

@ -5,9 +5,10 @@ use crate::constants::{DEFAULT_STORAGE_NAME, SETTINGS_PATH, THUMBNAIL_STORAGE_NA
use crate::utils::{create_paths_for_repo, get_repo, load_settings}; use crate::utils::{create_paths_for_repo, get_repo, load_settings};
use log::LevelFilter; use log::LevelFilter;
use mediarepo_core::error::RepoResult; use mediarepo_core::error::RepoResult;
use mediarepo_core::futures::future;
use mediarepo_core::settings::Settings; use mediarepo_core::settings::Settings;
use mediarepo_core::type_keys::SettingsKey; use mediarepo_core::type_keys::SettingsKey;
use mediarepo_core::utils::parse_tags_file;
use mediarepo_model::file::File;
use mediarepo_model::repo::Repo; use mediarepo_model::repo::Repo;
use mediarepo_model::type_keys::RepoKey; use mediarepo_model::type_keys::RepoKey;
use mediarepo_socket::get_builder; use mediarepo_socket::get_builder;
@ -163,14 +164,18 @@ async fn import(opt: Opt, path: String) -> RepoResult<()> {
let (_s, repo) = init_repo(&opt).await?; let (_s, repo) = init_repo(&opt).await?;
log::info!("Importing"); log::info!("Importing");
let promises = glob::glob(&path) let paths: Vec<PathBuf> = glob::glob(&path)
.unwrap() .unwrap()
.into_iter() .into_iter()
.filter_map(|r| r.ok()) .filter_map(|r| r.ok())
.filter(|e| e.is_file()) .filter(|e| e.is_file())
.map(|path| import_single_image(path, &repo)); .collect();
future::join_all(promises).await; for path in paths {
if let Err(e) = import_single_image(path, &repo).await {
log::error!("Import failed: {:?}", e);
}
}
Ok(()) Ok(())
} }
@ -178,9 +183,35 @@ async fn import(opt: Opt, path: String) -> RepoResult<()> {
/// Creates thumbnails of all sizes /// Creates thumbnails of all sizes
async fn import_single_image(path: PathBuf, repo: &Repo) -> RepoResult<()> { async fn import_single_image(path: PathBuf, repo: &Repo) -> RepoResult<()> {
log::info!("Importing file"); log::info!("Importing file");
let file = repo.add_file_by_path(path).await?; let file = repo.add_file_by_path(path.clone()).await?;
log::info!("Creating thumbnails"); log::info!("Creating thumbnails");
repo.create_thumbnails_for_file(file).await?; repo.create_thumbnails_for_file(file.clone()).await?;
let tags_path = PathBuf::from(format!("{}{}", path.to_str().unwrap(), ".txt"));
add_tags_from_tags_file(tags_path, repo, file).await?;
Ok(()) Ok(())
} }
async fn add_tags_from_tags_file(tags_path: PathBuf, repo: &Repo, file: File) -> RepoResult<()> {
log::info!("Adding tags");
if tags_path.exists() {
let tags = parse_tags_file(tags_path).await?;
let mut tag_ids = Vec::new();
for (namespace, name) in tags {
let tag = if let Some(namespace) = namespace {
log::info!("Adding namespaced tag '{}:{}'", namespace, name);
repo.add_or_find_namespaced_tag(name, namespace).await?
} else {
log::info!("Adding unnamespaced tag '{}'", name);
repo.add_or_find_unnamespaced_tag(name).await?
};
tag_ids.push(tag.id());
}
log::info!("Mapping {} tags to the file", tag_ids.len());
file.add_tags(tag_ids).await?;
} else {
log::info!("No tags file '{:?}' found", tags_path);
}
Ok(())
}

Loading…
Cancel
Save