Compare commits

...

17 Commits
v1.0.1 ... main

Author SHA1 Message Date
trivernis db01332c15
Fix build errors and update to latest tauri version 4 months ago
trivernis 7047dc7903
Update packages 4 months ago
Julius Riegel 3a25a8b812
Merge pull request #25 from Trivernis/develop
Develop
2 years ago
trivernis d83f211ceb
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 5dd8eefdcc
Add job status to maintenance menu
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 6dfefe01c2
Add maintenance menu to repository overview
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 99c224586a
Fix more clippy warnings
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 8256751a6f
Remove vacuum from automatically run commands
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 580c27bbd1
Fix cargo clippy warnings
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 37f7a2c82f
Add linting to check workflow
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 89e9e182dd
Improve ui startup performance
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel 75bc9821b1
Merge pull request #24 from Trivernis/develop
Develop
2 years ago
trivernis e7f09dd2b5
Update container builds
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 49c41ce127
Fix missing plus and edit icon
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
Julius Riegel af28527f36
Merge pull request #23 from Trivernis/develop
CSP Hotfix
2 years ago
trivernis b4e52b0db0
Increment version
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago
trivernis 146088e747
Fix csp errors
Signed-off-by: trivernis <trivernis@protonmail.com>
2 years ago

@ -36,7 +36,15 @@ jobs:
- name: Check daemon
working-directory: mediarepo-daemon
run: cargo check --no-default-features
run: cargo check
- name: Lint api
working-directory: mediarepo-api
run: cargo clippy -- -D warnings
- name: Lint daemon
working-directory: mediarepo-daemon
run: cargo clippy -- -D warnings
- name: Install UI dependencies
working-directory: mediarepo-ui
@ -47,4 +55,4 @@ jobs:
- name: Lint ui frontend
working-directory: mediarepo-ui
run: yarn lint
run: yarn lint

@ -36,6 +36,9 @@ RUN python3 scripts/check.py --install
FROM sources AS build_daemon
WORKDIR /usr/src
RUN python3 scripts/build.py daemon --verbose
RUN mkdir ./test-repo
RUN ./out/mediarepo-daemon --repo ./test-repo init
FROM sources AS build_ui
WORKDIR /usr/src

@ -0,0 +1,11 @@
[language-server.rust-analyzer]
command = "rust-analyzer"
[language-server.rust-analyzer.config]
inlayHints.bindingModeHints.enable = false
inlayHints.closingBraceHints.minLines = 10
inlayHints.closureReturnTypeHints.enable = "with_block"
inlayHints.discriminantHints.enable = "fieldless"
inlayHints.lifetimeElisionHints.enable = "skip_trivial"
inlayHints.typeHints.hideClosureInitialization = false
cargo.features = "all"

@ -1,6 +1,6 @@
[package]
name = "mediarepo-api"
version = "0.32.0"
version = "0.33.0"
edition = "2018"
license = "gpl-3"
@ -20,7 +20,7 @@ url = { version = "2.2.2", optional = true }
pathsearch = { version = "0.2.0", optional = true }
[dependencies.bromine]
version = "0.20.1"
version = "0.22.1"
optional = true
features = ["serialize_bincode", "encryption_layer"]
@ -33,7 +33,7 @@ version = "0.4.19"
features = ["serde"]
[dependencies.tauri]
version = "1.0.0-rc.4"
version = "1.5.4"
optional = true
default-features = false
features = []

@ -34,4 +34,10 @@ impl JobApi {
Ok(())
}
/// Checks if a particular job is already running
#[tracing::instrument(level = "debug", skip(self))]
pub async fn is_job_running(&self, job_type: JobType) -> ApiResult<bool> {
self.emit_and_get("is_job_running", job_type, None).await
}
}

@ -1,22 +1,22 @@
pub mod error;
pub mod file;
pub mod job;
pub mod preset;
pub mod protocol;
pub mod repo;
pub mod tag;
pub mod preset;
use crate::client_api::error::{ApiError, ApiResult};
use crate::client_api::file::FileApi;
use crate::client_api::job::JobApi;
use crate::client_api::preset::PresetApi;
use crate::client_api::repo::RepoApi;
use crate::client_api::tag::TagApi;
use crate::types::misc::{check_apis_compatible, get_api_version, InfoResponse};
use async_trait::async_trait;
use bromine::prelude::*;
use bromine::prelude::emit_metadata::EmitMetadata;
use bromine::prelude::*;
use tokio::time::Duration;
use crate::client_api::preset::PresetApi;
#[async_trait]
pub trait IPCApi {

@ -9,3 +9,11 @@ pub async fn run_job(api_state: ApiAccess<'_>, job_type: JobType, sync: bool) ->
Ok(())
}
#[tauri::command]
pub async fn is_job_running(api_state: ApiAccess<'_>, job_type: JobType) -> PluginResult<bool> {
let api = api_state.api().await?;
let running = api.job.is_job_running(job_type).await?;
Ok(running)
}

@ -57,7 +57,10 @@ fn once_scheme<R: Runtime>(app: &AppHandle<R>, request: &Request) -> Result<Resp
#[tracing::instrument(level = "debug", skip_all)]
async fn content_scheme<R: Runtime>(app: &AppHandle<R>, request: &Request) -> Result<Response> {
let buf_state = app.state::<BufferState>();
let hash = request.uri().trim_start_matches("content://");
let hash = request
.uri()
.trim_start_matches("content://")
.trim_end_matches("/");
if let Some(buffer) = buf_state.get_entry(hash) {
tracing::debug!("Fetching content from cache");

@ -75,7 +75,8 @@ impl<R: Runtime> MediarepoPlugin<R> {
get_file_tag_map,
all_sorting_presets,
add_sorting_preset,
delete_sorting_preset
delete_sorting_preset,
is_job_running
]),
}
}

@ -5,8 +5,9 @@ use std::time::{SystemTime, UNIX_EPOCH};
pub fn system_time_to_naive_date_time(system_time: SystemTime) -> NaiveDateTime {
let epoch_duration = system_time.duration_since(UNIX_EPOCH).unwrap();
NaiveDateTime::from_timestamp(
NaiveDateTime::from_timestamp_opt(
epoch_duration.as_secs() as i64,
epoch_duration.subsec_nanos(),
)
.unwrap()
}

File diff suppressed because it is too large Load Diff

@ -4,7 +4,7 @@ default-members = ["mediarepo-core", "mediarepo-database", "mediarepo-logic", "m
[package]
name = "mediarepo-daemon"
version = "1.0.1"
version = "1.0.5"
edition = "2018"
license = "gpl-3"
repository = "https://github.com/Trivernis/mediarepo-daemon"
@ -16,7 +16,7 @@ name = "mediarepo-daemon"
path = "src/main.rs"
[dependencies]
tracing = "0.1.32"
tracing = "0.1.33"
toml = "0.5.8"
structopt = "0.3.26"
glob = "0.3.0"
@ -25,11 +25,12 @@ tracing-appender = "0.2.2"
tracing-log = "0.1.2"
rolling-file = "0.1.0"
num-integer = "0.1.44"
console-subscriber = "0.1.3"
console-subscriber = "0.1.4"
log = "0.4.16"
opentelemetry = { version = "0.17.0", features = ["rt-tokio"] }
opentelemetry-jaeger = { version = "0.16.0", features = ["rt-tokio"] }
tracing-opentelemetry = "0.17.2"
human-panic = "1.0.3"
[dependencies.mediarepo-core]
path = "./mediarepo-core"
@ -44,9 +45,9 @@ path = "./mediarepo-socket"
path = "./mediarepo-worker"
[dependencies.tokio]
version = "1.17.0"
version = "1.21.2"
features = ["macros", "rt-multi-thread", "io-std", "io-util"]
[dependencies.tracing-subscriber]
version = "0.3.9"
version = "0.3.11"
features = ["env-filter", "ansi", "json"]

@ -8,7 +8,7 @@ workspace = ".."
[dependencies]
thiserror = "1.0.30"
multihash = "0.16.1"
multihash = "0.16.2"
multibase = "0.9.1"
base64 = "0.13.0"
toml = "0.5.8"
@ -16,12 +16,12 @@ serde = "1.0.136"
futures = "0.3.21"
itertools = "0.10.3"
glob = "0.3.0"
tracing = "0.1.32"
tracing = "0.1.33"
data-encoding = "2.3.2"
tokio-graceful-shutdown = "0.5.0"
thumbnailer = "0.4.0"
bincode = "1.3.3"
tracing-subscriber = "0.3.9"
tracing-subscriber = "0.3.11"
trait-bound-typemap = "0.3.3"
[dependencies.sea-orm]
@ -34,11 +34,11 @@ default-features = false
features = ["migrate"]
[dependencies.tokio]
version = "1.17.0"
version = "1.21.2"
features = ["fs", "io-util", "io-std"]
[dependencies.config]
version = "0.12.0"
version = "0.13.1"
features = ["toml"]
[dependencies.mediarepo-api]

@ -71,7 +71,7 @@ impl ThumbnailStore {
let name = file_name.to_string_lossy();
let (height, width) = name
.split_once("-")
.split_once('-')
.and_then(|(height, width)| {
Some((height.parse::<u32>().ok()?, width.parse::<u32>().ok()?))
})

@ -34,6 +34,7 @@ pub enum LogLevel {
Trace,
}
#[allow(clippy::from_over_into)]
impl Into<Option<Level>> for LogLevel {
fn into(self) -> Option<Level> {
match self {

@ -1,5 +1,5 @@
use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use config::{Config, FileFormat};
use serde::{Deserialize, Serialize};
@ -24,7 +24,7 @@ pub struct Settings {
}
impl Settings {
pub fn read(root: &PathBuf) -> RepoResult<Self> {
pub fn read(root: &Path) -> RepoResult<Self> {
let settings = Config::builder()
.add_source(config::File::from_str(
&*Settings::default().to_toml_string()?,
@ -44,7 +44,7 @@ impl Settings {
settings_main.server.tcp.enabled = true;
settings_main.server.tcp.port = PortSetting::Range(settings_v1.port_range);
settings_main.server.tcp.listen_address = settings_v1.listen_address;
settings_main.paths.thumbnail_directory = settings_v1.thumbnail_store.into();
settings_main.paths.thumbnail_directory = settings_v1.thumbnail_store;
settings_main.paths.database_directory = PathBuf::from(settings_v1.database_path)
.parent()
.map(|p| p.to_string_lossy().to_string())
@ -69,7 +69,7 @@ impl Settings {
Ok(string)
}
pub fn save(&self, root: &PathBuf) -> RepoResult<()> {
pub fn save(&self, root: &Path) -> RepoResult<()> {
let string = toml::to_string_pretty(&self)?;
fs::write(root.join("repo.toml"), string.into_bytes())?;

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
@ -21,27 +21,27 @@ impl Default for PathSettings {
impl PathSettings {
#[inline]
pub fn database_dir(&self, root: &PathBuf) -> PathBuf {
pub fn database_dir(&self, root: &Path) -> PathBuf {
root.join(&self.database_directory)
}
#[inline]
pub fn files_dir(&self, root: &PathBuf) -> PathBuf {
pub fn files_dir(&self, root: &Path) -> PathBuf {
root.join(&self.files_directory)
}
#[inline]
pub fn thumbs_dir(&self, root: &PathBuf) -> PathBuf {
pub fn thumbs_dir(&self, root: &Path) -> PathBuf {
root.join(&self.thumbnail_directory)
}
#[inline]
pub fn db_file_path(&self, root: &PathBuf) -> PathBuf {
pub fn db_file_path(&self, root: &Path) -> PathBuf {
self.database_dir(root).join("repo.db")
}
#[inline]
pub fn frontend_state_file_path(&self, root: &PathBuf) -> PathBuf {
pub fn frontend_state_file_path(&self, root: &Path) -> PathBuf {
self.database_dir(root).join("frontend-state.json")
}
}

@ -7,9 +7,15 @@ use tracing_subscriber::Layer;
pub struct DynLayerList<S>(Vec<Box<dyn Layer<S> + Send + Sync + 'static>>);
impl<S> Default for DynLayerList<S> {
fn default() -> Self {
Self(Vec::new())
}
}
impl<S> DynLayerList<S> {
pub fn new() -> Self {
Self(Vec::new())
Self::default()
}
pub fn iter(&self) -> Iter<'_, Box<dyn Layer<S> + Send + Sync>> {

@ -1,7 +0,0 @@
use async_trait::async_trait;
#[async_trait]
pub trait AsyncTryFrom<T> {
type Error;
fn async_try_from(other: T) -> Result<Self, Self::Error>;
}

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use futures::future;
use tokio::fs::{self, OpenOptions};
@ -16,7 +16,7 @@ pub fn parse_namespace_and_tag(norm_tag: String) -> (Option<String>, String) {
}
/// Parses all tags from a file
pub async fn parse_tags_file(path: PathBuf) -> RepoResult<Vec<(Option<String>, String)>> {
pub async fn parse_tags_file(path: &Path) -> 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();
@ -47,7 +47,7 @@ pub async fn get_folder_size(path: PathBuf) -> RepoResult<u64> {
}
}
}
let futures = all_files.into_iter().map(|f| read_file_size(f));
let futures = all_files.into_iter().map(read_file_size);
let results = future::join_all(futures).await;
let size = results.into_iter().filter_map(|r| r.ok()).sum();

@ -8,7 +8,7 @@ workspace = ".."
[dependencies]
chrono = "0.4.19"
tracing = "0.1.32"
tracing = "0.1.33"
[dependencies.mediarepo-core]
path = "../mediarepo-core"

@ -30,7 +30,7 @@ pub async fn get_all_counts(db: &DatabaseConnection) -> RepoResult<Counts> {
))
.one(db)
.await?
.ok_or(RepoError::from("could not retrieve metadata from database"))?;
.ok_or_else(|| RepoError::from("could not retrieve metadata from database"))?;
Ok(counts)
}

@ -55,7 +55,7 @@ fn vec_to_query_list<D: Display>(input: Vec<D>) -> String {
let mut entries = input
.into_iter()
.fold(String::new(), |acc, val| format!("{}{},", acc, val));
if entries.len() > 0 {
if !entries.is_empty() {
entries.remove(entries.len() - 1);
}

@ -11,7 +11,7 @@ chrono = "0.4.19"
serde = "1.0.136"
mime_guess = "2.0.4"
mime = "0.3.16"
tracing = "0.1.32"
tracing = "0.1.33"
async-trait = "0.1.53"
[dependencies.mediarepo-core]
@ -26,6 +26,6 @@ features = ["runtime-tokio-native-tls", "macros"]
default-features = false
[dependencies.tokio]
version = "1.17.0"
version = "1.21.2"
features = ["fs", "io-std", "io-util"]

@ -93,7 +93,7 @@ impl FileDao {
.all(&self.ctx.db)
.await?
.into_iter()
.map(|m| FileMetadataDto::new(m))
.map(FileMetadataDto::new)
.collect();
Ok(metadata)

@ -22,8 +22,8 @@ impl FileDao {
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),
cd_id: update_dto.cd_id.map(Set).unwrap_or(NotSet),
mime_type: update_dto.mime_type.map(Set).unwrap_or(NotSet),
status: update_dto.status.map(|v| Set(v as i32)).unwrap_or(NotSet),
};
let file_model = model.update(&trx).await?;
@ -62,8 +62,8 @@ impl FileDao {
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 mime_type =
mime::Mime::from_str(file.mime_type()).unwrap_or(mime::APPLICATION_OCTET_STREAM);
let thumbnails =
thumbnailer::create_thumbnails(Cursor::new(bytes), mime_type.clone(), sizes)?;
let mut dtos = Vec::new();

@ -40,7 +40,7 @@ impl JobDao {
}
}
fn build_state_filters(states: &Vec<UpsertJobStateDto>) -> Condition {
fn build_state_filters(states: &[UpsertJobStateDto]) -> Condition {
states
.iter()
.map(|s| Condition::all().add(job_state::Column::JobType.eq(s.job_type)))

@ -122,7 +122,7 @@ async fn add_keys(
async fn find_sort_keys(
trx: &DatabaseTransaction,
keys: &Vec<AddSortKeyDto>,
keys: &[AddSortKeyDto],
) -> RepoResult<Vec<SortKeyDto>> {
if keys.is_empty() {
return Ok(vec![]);

@ -77,14 +77,12 @@ fn create_cd_tag_map(
)>,
tag_id_map: HashMap<i64, TagDto>,
) -> HashMap<Vec<u8>, Vec<TagDto>> {
let cd_tag_map = tag_cd_entries
tag_cd_entries
.into_iter()
.filter_map(|(t, cd)| Some((cd?, tag_id_map.get(&t.tag_id)?.clone())))
.sorted_by_key(|(cd, _)| cd.id)
.group_by(|(cd, _)| cd.descriptor.to_owned())
.into_iter()
.map(|(key, group)| (key, group.map(|(_, t)| t).collect::<Vec<TagDto>>()))
.collect();
cd_tag_map
.collect::<HashMap<Vec<u8>, Vec<TagDto>>>()
}

@ -45,11 +45,12 @@ fn name_query_to_condition(query: TagByNameQuery) -> Option<Condition> {
let TagByNameQuery { namespace, name } = query;
let mut condition = Condition::all();
#[allow(clippy::question_mark)]
if !name.ends_with('*') {
condition = condition.add(tag::Column::Name.eq(name))
} else if name.len() > 1 {
condition =
condition.add(tag::Column::Name.like(&*format!("{}%", name.trim_end_matches("*"))))
condition.add(tag::Column::Name.like(&*format!("{}%", name.trim_end_matches('*'))))
} else if namespace.is_none() {
return None;
}

@ -58,12 +58,12 @@ impl TagDao {
async fn get_existing_mappings(
trx: &DatabaseTransaction,
cd_ids: &Vec<i64>,
tag_ids: &Vec<i64>,
cd_ids: &[i64],
tag_ids: &[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()))
.filter(content_descriptor_tag::Column::CdId.is_in(cd_ids.to_vec()))
.filter(content_descriptor_tag::Column::TagId.is_in(tag_ids.to_vec()))
.all(trx)
.await?
.into_iter()

@ -75,7 +75,7 @@ pub struct AddFileDto {
pub name: Option<String>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct UpdateFileDto {
pub id: i64,
pub cd_id: Option<i64>,
@ -83,17 +83,6 @@ pub struct UpdateFileDto {
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,

@ -84,7 +84,7 @@ impl KeyType {
}
pub fn to_number(&self) -> i32 {
self.clone() as i32
*self as i32
}
}

@ -8,10 +8,10 @@ workspace = ".."
[dependencies]
serde = "1.0.136"
tracing = "0.1.32"
tracing = "0.1.33"
compare = "0.1.0"
port_check = "0.1.5"
rayon = "1.5.1"
rayon = "1.5.2"
[dependencies.mediarepo-core]
path = "../mediarepo-core"
@ -26,8 +26,8 @@ path = "../mediarepo-logic"
path = "../mediarepo-worker"
[dependencies.tokio]
version = "1.17.0"
features = ["net"]
version = "1.21.2"
features = ["net", "rt", "tracing"]
[dependencies.chrono]
version = "0.4.19"

@ -30,24 +30,23 @@ pub fn start_tcp_server(
return Err(RepoError::PortUnavailable);
}
}
PortSetting::Range((l, r)) => port_check::free_local_port_in_range(*l, *r)
.ok_or_else(|| RepoError::PortUnavailable)?,
PortSetting::Range((l, r)) => {
port_check::free_local_port_in_range(*l, *r).ok_or(RepoError::PortUnavailable)?
}
};
let ip = settings.server.tcp.listen_address.to_owned();
let address = SocketAddr::new(ip, port);
let address_string = address.to_string();
let join_handle = tokio::task::Builder::new()
.name("mediarepo_tcp::listen")
.spawn(async move {
get_builder::<EncryptedListener<TcpListener>>(address)
.insert::<SubsystemKey>(subsystem)
.insert_all(shared_data)
.insert::<SizeMetadataKey>(Default::default())
.build_server()
.await
.expect("Failed to start tcp server")
});
let join_handle = tokio::task::spawn(async move {
get_builder::<EncryptedListener<TcpListener>>(address)
.insert::<SubsystemKey>(subsystem)
.insert_all(shared_data)
.insert::<SizeMetadataKey>(Default::default())
.build_server()
.await
.expect("Failed to start tcp server")
});
Ok((address_string, join_handle))
}
@ -65,17 +64,15 @@ pub fn create_unix_socket(
if path.exists() {
fs::remove_file(&path)?;
}
let join_handle = tokio::task::Builder::new()
.name("mediarepo_unix_socket::listen")
.spawn(async move {
get_builder::<UnixListener>(path)
.insert::<SubsystemKey>(subsystem)
.insert_all(shared_data)
.insert::<SizeMetadataKey>(Default::default())
.build_server()
.await
.expect("Failed to create unix domain socket");
});
let join_handle = tokio::task::spawn(async move {
get_builder::<UnixListener>(path)
.insert::<SubsystemKey>(subsystem)
.insert_all(shared_data)
.insert::<SizeMetadataKey>(Default::default())
.build_server()
.await
.expect("Failed to create unix domain socket");
});
Ok(join_handle)
}

@ -151,7 +151,7 @@ impl FilesNamespace {
content: bytes,
mime_type: metadata
.mime_type
.unwrap_or(String::from("application/octet-stream")),
.unwrap_or_else(|| String::from("application/octet-stream")),
creation_time: metadata.creation_time,
change_time: metadata.change_time,
name: Some(metadata.name),

@ -69,10 +69,10 @@ fn build_filters_from_expressions(
}
}
};
if filters.len() > 0 {
Some(filters)
} else {
if filters.is_empty() {
None
} else {
Some(filters)
}
})
.collect()
@ -92,7 +92,7 @@ fn map_tag_query_to_filter(
query: TagQuery,
tag_id_map: &HashMap<String, i64>,
) -> Option<FilterProperty> {
if query.tag.ends_with("*") {
if query.tag.ends_with('*') {
map_wildcard_tag_to_filter(query, tag_id_map)
} else {
map_tag_to_filter(query, tag_id_map)
@ -103,7 +103,7 @@ fn map_wildcard_tag_to_filter(
query: TagQuery,
tag_id_map: &HashMap<String, i64>,
) -> Option<FilterProperty> {
let filter_tag = query.tag.trim_end_matches("*");
let filter_tag = query.tag.trim_end_matches('*');
let relevant_ids = tag_id_map
.iter()
.filter_map(|(name, id)| {
@ -115,15 +115,15 @@ fn map_wildcard_tag_to_filter(
})
.collect::<Vec<i64>>();
if relevant_ids.len() > 0 {
if relevant_ids.is_empty() {
None
} else {
let comparator = if query.negate {
IsNot(relevant_ids)
} else {
Is(relevant_ids)
};
Some(FilterProperty::TagWildcardIds(comparator))
} else {
None
}
}

@ -71,7 +71,7 @@ async fn build_sort_context(
mime_type: file.mime_type().to_owned(),
namespaces: cid_nsp
.remove(&file.cd_id())
.unwrap_or(HashMap::with_capacity(0)),
.unwrap_or_else(|| HashMap::with_capacity(0)),
tag_count: cid_tag_counts.remove(&file.cd_id()).unwrap_or(0),
import_time: metadata.import_time().to_owned(),
create_time: metadata.import_time().to_owned(),
@ -176,11 +176,8 @@ fn adjust_for_dir(ordering: Ordering, direction: &SortDirection) -> Ordering {
}
}
fn compare_tag_lists(list_a: &Vec<String>, list_b: &Vec<String>) -> Ordering {
let first_diff = list_a
.into_iter()
.zip(list_b.into_iter())
.find(|(a, b)| *a != *b);
fn compare_tag_lists(list_a: &[String], list_b: &[String]) -> Ordering {
let first_diff = list_a.iter().zip(list_b.iter()).find(|(a, b)| *a != *b);
if let Some(diff) = first_diff {
if let (Some(num_a), Some(num_b)) = (diff.0.parse::<f32>().ok(), diff.1.parse::<f32>().ok())
{

@ -20,8 +20,9 @@ impl NamespaceProvider for JobsNamespace {
fn register(handler: &mut EventHandler) {
events!(handler,
"run_job" => Self::run_job
)
"run_job" => Self::run_job,
"is_job_running" => Self::is_job_running
);
}
}
@ -59,6 +60,26 @@ impl JobsNamespace {
Ok(Response::empty())
}
#[tracing::instrument(skip_all)]
pub async fn is_job_running(ctx: &Context, event: Event) -> IPCResult<Response> {
let job_type = event.payload::<JobType>()?;
let dispatcher = get_job_dispatcher_from_context(ctx).await;
let running = match job_type {
JobType::MigrateContentDescriptors => {
is_job_running::<MigrateCDsJob>(&dispatcher).await
}
JobType::CalculateSizes => is_job_running::<CalculateSizesJob>(&dispatcher).await,
JobType::GenerateThumbnails => {
is_job_running::<GenerateMissingThumbsJob>(&dispatcher).await
}
JobType::CheckIntegrity => is_job_running::<CheckIntegrityJob>(&dispatcher).await,
JobType::Vacuum => is_job_running::<VacuumJob>(&dispatcher).await,
};
Response::payload(ctx, running)
}
}
async fn dispatch_job<J: 'static + Job>(
@ -107,3 +128,12 @@ async fn calculate_all_sizes(ctx: &Context) -> RepoResult<()> {
Ok(())
}
async fn is_job_running<T: 'static + Job>(dispatcher: &JobDispatcher) -> bool {
if let Some(handle) = dispatcher.get_handle::<T>().await {
let state = handle.state().await;
state == JobState::Running
} else {
false
}
}

@ -95,7 +95,7 @@ async fn get_frontend_state_path(ctx: &Context) -> IPCResult<PathBuf> {
let data = ctx.data.read().await;
let settings = data.get::<SettingsKey>().unwrap();
let repo_path = data.get::<RepoPathKey>().unwrap();
let state_path = settings.paths.frontend_state_file_path(&repo_path);
let state_path = settings.paths.frontend_state_file_path(repo_path);
Ok(state_path)
}

@ -33,11 +33,7 @@ pub async fn file_by_identifier(identifier: FileIdentifier, repo: &Repo) -> Repo
pub async fn cd_by_identifier(identifier: FileIdentifier, repo: &Repo) -> RepoResult<Vec<u8>> {
match identifier {
FileIdentifier::ID(id) => {
let file = repo
.file()
.by_id(id)
.await?
.ok_or_else(|| "Thumbnail not found")?;
let file = repo.file().by_id(id).await?.ok_or("Thumbnail not found")?;
Ok(file.cd().to_owned())
}
FileIdentifier::CD(cd) => decode_content_descriptor(cd),

@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
async-trait = "0.1.53"
tracing = "0.1.32"
tracing = "0.1.33"
[dependencies.mediarepo-core]
path = "../mediarepo-core"
@ -19,7 +19,7 @@ path = "../mediarepo-logic"
path = "../mediarepo-database"
[dependencies.tokio]
version = "1.17.0"
version = "1.21.2"
features = ["macros"]
[dependencies.chrono]

@ -6,7 +6,7 @@ use mediarepo_core::mediarepo_api::types::repo::SizeType;
use mediarepo_core::settings::Settings;
use mediarepo_core::utils::get_folder_size;
use mediarepo_logic::dao::repo::Repo;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tokio::sync::broadcast::{self, Sender};
@ -72,11 +72,11 @@ impl Job for CalculateSizesJob {
async fn calculate_size(
size_type: &SizeType,
repo: &Repo,
repo_path: &PathBuf,
repo_path: &Path,
settings: &Settings,
) -> RepoResult<u64> {
let size = match &size_type {
SizeType::Total => get_folder_size(repo_path.clone()).await?,
SizeType::Total => get_folder_size(repo_path.to_path_buf()).await?,
SizeType::FileFolder => repo.get_main_store_size().await?,
SizeType::ThumbFolder => repo.get_thumb_store_size().await?,
SizeType::DatabaseFile => {

@ -1,5 +1,5 @@
use crate::job_dispatcher::JobDispatcher;
use crate::jobs::{CheckIntegrityJob, MigrateCDsJob, VacuumJob};
use crate::jobs::{CheckIntegrityJob, MigrateCDsJob};
use mediarepo_core::error::RepoError;
use mediarepo_core::tokio_graceful_shutdown::Toplevel;
use mediarepo_logic::dao::repo::Repo;
@ -19,9 +19,6 @@ pub async fn start(top_level: Toplevel, repo: Repo) -> (Toplevel, JobDispatcher)
let dispatcher = JobDispatcher::new(subsystem, repo);
tx.send(dispatcher.clone())
.map_err(|_| RepoError::from("failed to send dispatcher"))?;
dispatcher
.dispatch_periodically(VacuumJob::default(), Duration::from_secs(60 * 30))
.await;
dispatcher
.dispatch_periodically(
CheckIntegrityJob::default(),

@ -1,5 +1,5 @@
use std::fs;
use std::path::PathBuf;
use std::path::Path;
use console_subscriber::ConsoleLayer;
use opentelemetry::sdk::Resource;
@ -24,7 +24,7 @@ use mediarepo_core::tracing_layer_list::DynLayerList;
#[allow(dyn_drop)]
pub type DropGuard = Box<dyn Drop>;
pub fn init_tracing(repo_path: &PathBuf, log_cfg: &LoggingSettings) -> Vec<DropGuard> {
pub fn init_tracing(repo_path: &Path, log_cfg: &LoggingSettings) -> Vec<DropGuard> {
LogTracer::init().expect("failed to subscribe to log entries");
let log_path = repo_path.join("logs");
let mut guards = Vec::new();
@ -97,11 +97,11 @@ fn add_telemetry_layer(log_cfg: &LoggingSettings, layer_list: &mut DynLayerList<
fn add_app_log_layer(
log_cfg: &LoggingSettings,
log_path: &PathBuf,
log_path: &Path,
guards: &mut Vec<DropGuard>,
layer_list: &mut DynLayerList<Registry>,
) {
let (app_log_writer, guard) = get_application_log_writer(&log_path);
let (app_log_writer, guard) = get_application_log_writer(log_path);
guards.push(Box::new(guard) as DropGuard);
let app_log_layer = fmt::layer()
@ -115,11 +115,11 @@ fn add_app_log_layer(
fn add_bromine_layer(
log_cfg: &LoggingSettings,
log_path: &PathBuf,
log_path: &Path,
guards: &mut Vec<DropGuard>,
layer_list: &mut DynLayerList<Registry>,
) {
let (bromine_writer, guard) = get_bromine_log_writer(&log_path);
let (bromine_writer, guard) = get_bromine_log_writer(log_path);
guards.push(Box::new(guard) as DropGuard);
let bromine_layer = fmt::layer()
@ -133,11 +133,11 @@ fn add_bromine_layer(
fn add_sql_layer(
log_cfg: &LoggingSettings,
log_path: &PathBuf,
log_path: &Path,
guards: &mut Vec<DropGuard>,
layer_list: &mut DynLayerList<Registry>,
) {
let (sql_writer, guard) = get_sql_log_writer(&log_path);
let (sql_writer, guard) = get_sql_log_writer(log_path);
guards.push(Box::new(guard) as DropGuard);
let sql_layer = fmt::layer()
@ -161,18 +161,18 @@ fn add_stdout_layer(guards: &mut Vec<DropGuard>, layer_list: &mut DynLayerList<R
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_filter(
std::env::var("RUST_LOG")
.unwrap_or(String::from("info,sqlx=warn"))
.unwrap_or_else(|_| String::from("info,sqlx=warn"))
.parse::<filter::Targets>()
.unwrap_or(
.unwrap_or_else(|_| {
filter::Targets::new()
.with_default(Level::INFO)
.with_target("sqlx", Level::WARN),
),
.with_target("sqlx", Level::WARN)
}),
);
layer_list.add(stdout_layer);
}
fn get_sql_log_writer(log_path: &PathBuf) -> (NonBlocking, WorkerGuard) {
fn get_sql_log_writer(log_path: &Path) -> (NonBlocking, WorkerGuard) {
tracing_appender::non_blocking(
rolling_file::BasicRollingFileAppender::new(
log_path.join("sql.log"),
@ -183,7 +183,7 @@ fn get_sql_log_writer(log_path: &PathBuf) -> (NonBlocking, WorkerGuard) {
)
}
fn get_bromine_log_writer(log_path: &PathBuf) -> (NonBlocking, WorkerGuard) {
fn get_bromine_log_writer(log_path: &Path) -> (NonBlocking, WorkerGuard) {
tracing_appender::non_blocking(
rolling_file::BasicRollingFileAppender::new(
log_path.join("bromine.log"),
@ -194,7 +194,7 @@ fn get_bromine_log_writer(log_path: &PathBuf) -> (NonBlocking, WorkerGuard) {
)
}
fn get_application_log_writer(log_path: &PathBuf) -> (NonBlocking, WorkerGuard) {
fn get_application_log_writer(log_path: &Path) -> (NonBlocking, WorkerGuard) {
tracing_appender::non_blocking(
rolling_file::BasicRollingFileAppender::new(
log_path.join("repo.log"),

@ -1,6 +1,6 @@
use std::env;
use std::iter::FromIterator;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@ -55,6 +55,7 @@ enum SubCommand {
#[tokio::main]
async fn main() -> RepoResult<()> {
human_panic::setup_panic!();
let mut opt: Opt = Opt::from_args();
opt.repo = env::current_dir().unwrap().join(opt.repo);
@ -242,7 +243,7 @@ async fn init(opt: Opt, force: bool) -> RepoResult<()> {
Ok(())
}
async fn clean_old_connection_files(root: &PathBuf) -> RepoResult<()> {
async fn clean_old_connection_files(root: &Path) -> RepoResult<()> {
let paths = ["repo.tcp", "repo.sock"];
for path in paths {

@ -1,14 +1,14 @@
use std::path::PathBuf;
use std::path::Path;
use tokio::fs;
use mediarepo_core::error::RepoResult;
use mediarepo_core::settings::{PathSettings, Settings};
use mediarepo_core::settings::v1::SettingsV1;
use mediarepo_core::settings::{PathSettings, Settings};
use mediarepo_logic::dao::repo::Repo;
/// Loads the settings from a toml path
pub fn load_settings(root_path: &PathBuf) -> RepoResult<Settings> {
pub fn load_settings(root_path: &Path) -> RepoResult<Settings> {
let contents = std::fs::read_to_string(root_path.join("repo.toml"))?;
if let Ok(settings_v1) = SettingsV1::from_toml_string(&contents) {
@ -21,7 +21,7 @@ pub fn load_settings(root_path: &PathBuf) -> RepoResult<Settings> {
}
}
pub async fn get_repo(root_path: &PathBuf, path_settings: &PathSettings) -> RepoResult<Repo> {
pub async fn get_repo(root_path: &Path, path_settings: &PathSettings) -> RepoResult<Repo> {
Repo::connect(
format!(
"sqlite://{}",
@ -33,7 +33,7 @@ pub async fn get_repo(root_path: &PathBuf, path_settings: &PathSettings) -> Repo
.await
}
pub async fn create_paths_for_repo(root: &PathBuf, settings: &PathSettings) -> RepoResult<()> {
pub async fn create_paths_for_repo(root: &Path, settings: &PathSettings) -> RepoResult<()> {
if !root.exists() {
fs::create_dir_all(&root).await?;
}

@ -1,66 +1,57 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"quotes": [
"warn",
"double",
{
"avoidEscape": true
}
],
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"no-unused-expressions": "warn",
"semi": "error"
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": ["tsconfig.json"],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"quotes": [
"warn",
"double",
{
"avoidEscape": true
}
],
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"no-unused-expressions": "warn",
"no-extraneous-class": "off",
"semi": "error"
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {}
}
]
}

@ -0,0 +1,15 @@
[language-server.biome]
command = "biome"
args = ["lsp-proxy"]
[[language]]
name = "typescript"
language-servers = ["typescript-language-server"]
auto-format = true
formatter = { command = "biome" , args = ["format", "--stdin-file-path=file.ts"] }
[[language]]
name = "javascript"
language-servers = ["typescript-language-server", "biome"]
auto-format = true
formatter = { command = "biome" , args = ["format", "--stdin-file-path=file.js"] }

@ -3,7 +3,8 @@
"version": 1,
"cli": {
"packageManager": "yarn",
"defaultCollection": "@angular-eslint/schematics"
"defaultCollection": "@angular-eslint/schematics",
"analytics": "dc09bab7-b1ef-4661-8d46-1da5b61c8e44"
},
"newProjectRoot": "projects",
"projects": {

@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.3.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noStaticOnlyClass": "off"
}
}
}
}

@ -1,63 +1,64 @@
{
"name": "mediarepo-ui",
"version": "1.0.1",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"watch-prod": "ng build --watch --configuration production",
"test": "ng test",
"lint": "ng lint",
"tauri": "tauri"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.3.2",
"@angular/cdk": "^13.3.2",
"@angular/common": "~13.3.2",
"@angular/compiler": "~13.3.2",
"@angular/core": "~13.3.2",
"@angular/flex-layout": "^13.0.0-beta.36",
"@angular/forms": "~13.3.2",
"@angular/material": "^13.3.2",
"@angular/platform-browser": "~13.3.2",
"@angular/platform-browser-dynamic": "~13.3.2",
"@angular/router": "~13.3.2",
"@ng-icons/core": "^15.1.0",
"@ng-icons/feather-icons": "^15.1.0",
"@ng-icons/material-icons": "^15.1.0",
"@tauri-apps/api": "^1.0.0-rc.3",
"chart.js": "^3.7.1",
"primeicons": "^5.0.0",
"primeng": "^13.3.2",
"rxjs": "~7.5.5",
"tslib": "^2.3.1",
"w3c-keys": "^1.0.3",
"zone.js": "~0.11.5"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.2",
"@angular-eslint/builder": "^13.2.0",
"@angular-eslint/eslint-plugin": "^13.2.0",
"@angular-eslint/eslint-plugin-template": "^13.2.0",
"@angular-eslint/schematics": "^13.2.0",
"@angular-eslint/template-parser": "^13.2.0",
"@angular/cli": "~13.3.2",
"@angular/compiler-cli": "~13.3.2",
"@tauri-apps/cli": "^1.0.0-rc.8",
"@types/file-saver": "^2.0.4",
"@types/jasmine": "~4.0.2",
"@types/node": "^17.0.23",
"@typescript-eslint/eslint-plugin": "5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"eslint": "^8.12.0",
"jasmine-core": "~4.0.0",
"karma": "~6.3.10",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~4.0.2",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.6.3"
}
"name": "mediarepo-ui",
"version": "1.0.5",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"watch-prod": "ng build --watch --configuration production",
"test": "ng test",
"lint": "ng lint",
"tauri": "tauri"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.3.2",
"@angular/cdk": "^13.3.2",
"@angular/common": "~13.3.2",
"@angular/compiler": "~13.3.2",
"@angular/core": "~13.3.2",
"@angular/flex-layout": "^13.0.0-beta.36",
"@angular/forms": "~13.3.2",
"@angular/material": "^13.3.2",
"@angular/platform-browser": "~13.3.2",
"@angular/platform-browser-dynamic": "~13.3.2",
"@angular/router": "~13.3.2",
"@ng-icons/core": "^15.1.0",
"@ng-icons/feather-icons": "^15.1.0",
"@ng-icons/material-icons": "^15.1.0",
"@tauri-apps/api": "^1.5.3",
"chart.js": "^3.7.1",
"primeicons": "^5.0.0",
"primeng": "^13.3.2",
"rxjs": "~7.5.5",
"tslib": "^2.3.1",
"w3c-keys": "^1.0.3",
"zone.js": "~0.11.5"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.2",
"@angular-eslint/builder": "^13.2.0",
"@angular-eslint/eslint-plugin": "^13.2.0",
"@angular-eslint/eslint-plugin-template": "^13.2.0",
"@angular-eslint/schematics": "^13.2.0",
"@angular-eslint/template-parser": "^13.2.0",
"@angular/cli": "~13.3.2",
"@angular/compiler-cli": "~13.3.2",
"@angular/language-service": "^17.1.1",
"@tauri-apps/cli": "^1.5.4",
"@types/file-saver": "^2.0.4",
"@types/jasmine": "~4.0.2",
"@types/node": "^17.0.23",
"@typescript-eslint/eslint-plugin": "5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"eslint": "^8.13.0",
"jasmine-core": "~4.0.0",
"karma": "~6.3.10",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~4.0.2",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.6.3"
}
}

File diff suppressed because it is too large Load Diff

@ -1,16 +1,16 @@
[package]
name = "app"
version = "1.0.1"
version = "1.0.5"
description = "The UI for the mediarepo media management tool"
authors = ["you"]
license = ""
repository = ""
default-run = "app"
edition = "2018"
edition = "2021"
build = "src/build.rs"
[build-dependencies]
tauri-build = { version = "1.0.0-rc.5", features = [] }
tauri-build = { version = "1.5.1", features = [] }
[dependencies]
serde_json = "1.0.79"
@ -19,7 +19,7 @@ thiserror = "1.0.30"
typemap_rev = "0.1.5"
[dependencies.tauri]
version = "1.0.0-rc.4"
version = "1.5.4"
features = ["dialog-all", "path-all", "shell-all"]
[dependencies.tracing-subscriber]

@ -1,76 +1,74 @@
{
"package": {
"productName": "mediarepo-ui",
"version": "1.0.1"
},
"build": {
"distDir": "../dist/mediarepo-ui",
"devPath": "http://localhost:4200",
"beforeDevCommand": "yarn start",
"beforeBuildCommand": "yarn build"
},
"tauri": {
"bundle": {
"active": true,
"targets": "all",
"identifier": "net.trivernis.mediarepo",
"icon": [
"icons/32x32.png",
"icons/64x64.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.ico",
"icons/icon.icns"
],
"resources": [],
"externalBin": [],
"copyright": "",
"category": "Productivity",
"shortDescription": "A media management tool",
"longDescription": "",
"deb": {
"depends": [],
"useBootstrapper": false
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"updater": {
"active": false
},
"allowlist": {
"dialog": {
"all": true
},
"shell": {
"all": true
},
"path": {
"all": true
}
},
"windows": [
{
"title": "mediarepo",
"width": 1920,
"height": 1080,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self' once: thumb: content:"
}
}
"package": {
"productName": "mediarepo-ui",
"version": "1.0.4"
},
"build": {
"distDir": "../dist/mediarepo-ui",
"devPath": "http://localhost:4200",
"beforeDevCommand": "yarn start",
"beforeBuildCommand": "yarn build"
},
"tauri": {
"bundle": {
"active": true,
"targets": ["msi", "app", "dmg", "updater"],
"identifier": "net.trivernis.mediarepo",
"icon": [
"icons/32x32.png",
"icons/64x64.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.ico",
"icons/icon.icns"
],
"resources": [],
"externalBin": [],
"copyright": "",
"category": "Productivity",
"shortDescription": "A media management tool",
"longDescription": "",
"deb": {
"depends": []
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"updater": {
"active": false
},
"allowlist": {
"dialog": {
"all": true
},
"shell": {
"all": true
},
"path": {
"all": true
}
},
"windows": [
{
"title": "mediarepo",
"width": 1920,
"height": 1080,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
}
}

@ -19,6 +19,7 @@ import {
GetSizeRequest,
GetTagsForFilesRequest,
InitRepositoryRequest,
IsJobRunningRequest,
ReadFileRequest,
RemoveRepositoryRequest,
ResolvePathsToFilesRequest,
@ -187,6 +188,10 @@ export class MediarepoApi {
return this.invokePlugin(ApiFunction.RunJob, request);
}
public static async isJobRunning(request: IsJobRunningRequest): Promise<boolean> {
return this.invokePlugin(ApiFunction.IsJobRunning, request);
}
public static async getAllSortingPresets(): Promise<SortingPresetData[]> {
return ShortCache.cached("sorting-presets", () => this.invokePlugin(ApiFunction.GetAllSortingPresets), 1000);
}

@ -40,6 +40,7 @@ export enum ApiFunction {
SetFrontendState = "set_frontend_state",
// jobs
RunJob = "run_job",
IsJobRunning = "is_job_running",
// presets
GetAllSortingPresets = "all_sorting_presets",
AddSortingPreset = "add_sorting_preset",

@ -117,3 +117,7 @@ export type AddSortingPresetRequest = {
export type DeleteSortingPresetRequest = {
id: number
};
export type IsJobRunningRequest = {
jobType: JobType,
}

@ -17,7 +17,7 @@ import {MatSelectModule} from "@angular/material/select";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatDividerModule} from "@angular/material/divider";
import {NgIconsModule} from "@ng-icons/core";
import * as materialIcons from "@ng-icons/material-icons";
import {MatMoreVert, MatPlus} from "@ng-icons/material-icons/baseline";
import {MatMenuModule} from "@angular/material/menu";
import {InputModule} from "../shared/input/input.module";
import {SidebarModule} from "../shared/sidebar/sidebar.module";
@ -34,7 +34,7 @@ import {TagModule} from "../shared/tag/tag.module";
import {
DownloadDaemonDialogComponent
} from "./repositories-tab/download-daemon-dialog/download-daemon-dialog.component";
import {RepositoryModule} from "../shared/repository/repository/repository.module";
import {RepositoryModule} from "../shared/repository/repository.module";
import {MatToolbarModule} from "@angular/material/toolbar";
import {
RepositoryDetailsViewComponent
@ -72,7 +72,7 @@ import {AboutDialogComponent} from "./repositories-tab/repository-overview/about
MatProgressBarModule,
MatCheckboxModule,
ScrollingModule,
NgIconsModule.withIcons({ ...materialIcons }),
NgIconsModule.withIcons({ MatPlus, MatMoreVert }),
FlexModule,
MatButtonModule,
MatMenuModule,

@ -6,7 +6,7 @@ import {ConfirmDialogComponent} from "../../../shared/app-common/confirm-dialog/
import {BusyIndicatorComponent} from "../../../shared/app-common/busy-indicator/busy-indicator.component";
import {
EditRepositoryDialogComponent
} from "../../../shared/repository/repository/edit-repository-dialog/edit-repository-dialog.component";
} from "../../../shared/repository/edit-repository-dialog/edit-repository-dialog.component";
@Component({
selector: "app-repository-card",

@ -47,13 +47,16 @@
{{this.databaseFileSize | async}}
</app-metadata-entry>
</div>
<div class="repository-charts">
<app-chart *ngIf="this.chartData"
[datasets]="this.chartData"
[labels]="this.chartLabels"
chartType="doughnut"
class="size-chart"
title="Sizes"></app-chart>
</div>
</div>
<div class="repository-charts" fxFlex="50%">
<app-chart *ngIf="this.chartData"
[datasets]="this.chartData"
[labels]="this.chartLabels"
chartType="doughnut"
class="size-chart"
title="Sizes"></app-chart>
<div fxFlex="50%">
<app-repository-maintenance class="repo-maintenance"></app-repository-maintenance>
</div>
</div>

@ -31,14 +31,17 @@
.stats-container {
margin-left: auto;
margin-right: auto;
display: block;
width: 50%;
}
}
.repository-charts {
margin-top: 4em;
margin-right: 2em;
height: 100%;
display: block;
.repository-charts {
margin-top: 4em;
margin-right: 2em;
height: calc(100% - 4em);;
width: 50%;
display: block;
}
}
.details-content {
@ -54,3 +57,7 @@
max-width: 500px;
margin: auto;
}
.repo-maintenance {
padding: 2em;
}

@ -7,7 +7,7 @@ import {StateService} from "../../../../services/state/state.service";
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
import {
AddRepositoryDialogComponent
} from "../../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
} from "../../../shared/repository/add-repository-dialog/add-repository-dialog.component";
import {BehaviorSubject} from "rxjs";
import {BusyDialogComponent} from "../../../shared/app-common/busy-dialog/busy-dialog.component";
import {DownloadDaemonDialogComponent} from "../download-daemon-dialog/download-daemon-dialog.component";
@ -116,15 +116,10 @@ export class RepositoryOverviewComponent implements OnInit, AfterViewInit {
private async runRepositoryStartupTasks(dialogContext: BusyDialogContext): Promise<void> {
dialogContext.message.next("Checking integrity...");
await this.jobService.runJob("CheckIntegrity");
dialogContext.message.next("Running a vacuum on the database...");
await this.jobService.runJob("Vacuum");
dialogContext.message.next(
"Migrating content descriptors to new format...");
await this.jobService.runJob("MigrateContentDescriptors");
dialogContext.message.next("Calculating repository sizes...");
await this.jobService.runJob("CalculateSizes", false);
dialogContext.message.next("Generating missing thumbnails...");
await this.jobService.runJob("GenerateThumbnails");
dialogContext.message.next("Calculating repository sizes...");
await this.jobService.runJob("CalculateSizes", false);
dialogContext.message.next("Finished repository startup");
}

@ -21,7 +21,7 @@ export class ContextMenuComponent {
event.preventDefault();
this.x = event.clientX + "px";
this.y = event.clientY + "px";
this.menuTrigger.menu.focusFirstItem("mouse");
this.menuTrigger.menu?.focusFirstItem("mouse");
this.menuTrigger.openMenu();
this.changeDetector.markForCheck();
}

@ -1,8 +1,8 @@
import {Component, Inject, ViewChild} from "@angular/core";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {RepositoryFormComponent} from "../repository-form/repository-form.component";
import {RepositoryService} from "../../../../../services/repository/repository.service";
import {LoggingService} from "../../../../../services/logging/logging.service";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {LoggingService} from "../../../../services/logging/logging.service";
@Component({
selector: "app-add-repository-dialog",
@ -23,11 +23,11 @@ export class AddRepositoryDialogComponent {
public async checkLocalRepoExists() {
this.repositoryForm.localRepoExists = await this.repoService.checkLocalRepositoryExists(
this.repositoryForm.formGroup.value.path);
this.repositoryForm.formGroup.value.path as unknown as string);
}
public async initLocalRepository() {
const path = this.repositoryForm.formGroup.value.path;
const path = this.repositoryForm.formGroup.value.path as unknown as string;
try {
await this.repoService.initRepository(path);
} catch (err: any) {
@ -37,7 +37,7 @@ export class AddRepositoryDialogComponent {
}
public async addRepository() {
let { name, repositoryType, path, address } = this.repositoryForm.formGroup.value;
let { name, repositoryType, path, address } = this.repositoryForm.formGroup.value as unknown as any;
path = repositoryType === "local" ? path : undefined;
address = repositoryType === "remote" ? address : undefined;
try {

@ -1,9 +1,9 @@
import {Component, Inject, ViewChild} from "@angular/core";
import {RepositoryFormComponent} from "../repository-form/repository-form.component";
import {RepositoryService} from "../../../../../services/repository/repository.service";
import {LoggingService} from "../../../../../services/logging/logging.service";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {LoggingService} from "../../../../services/logging/logging.service";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {Repository} from "../../../../../../api/models/Repository";
import {Repository} from "../../../../../api/models/Repository";
@Component({
selector: "app-edit-repository-dialog",

@ -1,7 +1,7 @@
import {Component, Input, OnInit, Output} from "@angular/core";
import {AbstractControl, FormControl, FormGroup, ValidationErrors, Validators} from "@angular/forms";
import {Repository} from "../../../../../../api/models/Repository";
import {RepositoryService} from "../../../../../services/repository/repository.service";
import {Repository} from "../../../../../api/models/Repository";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {dialog} from "@tauri-apps/api";
import {MatDialog} from "@angular/material/dialog";

@ -0,0 +1,29 @@
<mat-card>
<mat-card-header>
<mat-card-title>
<h1>Maintenance</h1></mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="button-list">
<button (click)="this.runJob('CheckIntegrity', false)" color="primary" mat-flat-button>Check Integrity
</button>
<button (click)="this.runJob('Vacuum', false)" color="primary" mat-flat-button>Optimize Database</button>
<button (click)="this.runJob('GenerateThumbnails', true)"
[disabled]="this.jobState.GenerateThumbnails"
color="primary"
mat-flat-button>Generate missing
Thumbnails
<mat-progress-bar *ngIf="this.jobState.GenerateThumbnails" color="primary"
mode="indeterminate"></mat-progress-bar>
</button>
<button (click)="this.runJob('CalculateSizes', true)"
[disabled]="this.jobState.CalculateSizes"
color="primary"
mat-flat-button>Recalculate Repository
Size
<mat-progress-bar *ngIf="this.jobState.CalculateSizes" color="primary"
mode="indeterminate"></mat-progress-bar>
</button>
</div>
</mat-card-content>
</mat-card>

@ -0,0 +1,18 @@
mat-card-header {
display: block;
}
mat-card-title h1 {
margin: auto;
text-align: center;
}
.button-list {
display: block;
width: 100%;
button {
display: block;
margin: 2em auto;
}
}

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {RepositoryMaintenanceComponent} from "./repository-maintenance.component";
describe("RepositoryMaintenanceComponent", () => {
let component: RepositoryMaintenanceComponent;
let fixture: ComponentFixture<RepositoryMaintenanceComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RepositoryMaintenanceComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryMaintenanceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,85 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from "@angular/core";
import {JobService} from "../../../../services/job/job.service";
import {JobType} from "../../../../../api/api-types/job";
import {MatDialog} from "@angular/material/dialog";
import {BusyDialogComponent, BusyDialogData} from "../../app-common/busy-dialog/busy-dialog.component";
import {LoggingService} from "../../../../services/logging/logging.service";
import {BehaviorSubject} from "rxjs";
@Component({
selector: "app-repository-maintenance",
templateUrl: "./repository-maintenance.component.html",
styleUrls: ["./repository-maintenance.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RepositoryMaintenanceComponent implements OnInit, OnDestroy {
public jobState: { [Property in JobType]?: boolean } = {
CalculateSizes: false,
GenerateThumbnails: false,
};
private jobStatusInterval: any;
constructor(
private changeDetector: ChangeDetectorRef,
private jobService: JobService,
private dialog: MatDialog,
private logger: LoggingService
) {
}
public ngOnDestroy(): void {
clearInterval(this.jobStatusInterval);
}
public async ngOnInit() {
await this.updateJobStatus();
this.jobStatusInterval = setInterval(() => this.updateJobStatus(), 10000);
}
public async runJob(jobType: JobType, runAsync: boolean) {
if (runAsync) {
this.jobState[jobType] = true;
this.jobService.runJob(jobType).then(() => this.delay(1000)).catch(this.logger.error).finally(() => {
this.jobState[jobType] = false;
this.changeDetector.markForCheck();
});
this.changeDetector.markForCheck();
} else {
const dialog = this.dialog.open<BusyDialogComponent, BusyDialogData>(BusyDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
data: {
title: "Synchronous Job",
message: new BehaviorSubject(`Running Job ${jobType}`),
allowCancel: false,
}
});
try {
this.changeDetector.markForCheck();
await this.jobService.runJob(jobType);
} catch (err: any) {
this.logger.error(err);
} finally {
dialog.close();
this.changeDetector.markForCheck();
}
}
}
private async delay(ms: number) {
return new Promise((res, _) => setTimeout(
res,
ms
));
}
private async updateJobStatus() {
const indexedTypes: JobType[] = ["CalculateSizes", "GenerateThumbnails"];
for (const jobType of indexedTypes) {
this.jobState[jobType] = await this.jobService.isJobRunning(jobType);
}
this.changeDetector.markForCheck();
}
}

@ -11,17 +11,22 @@ import {MatInputModule} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms";
import {NgIconsModule} from "@ng-icons/core";
import {MatFolder} from "@ng-icons/material-icons/baseline";
import {RepositoryMaintenanceComponent} from "./repository-maintenance/repository-maintenance.component";
import {MatCardModule} from "@angular/material/card";
import {MatProgressBarModule} from "@angular/material/progress-bar";
@NgModule({
declarations: [
AddRepositoryDialogComponent,
EditRepositoryDialogComponent,
RepositoryFormComponent
RepositoryFormComponent,
RepositoryMaintenanceComponent
],
exports: [
AddRepositoryDialogComponent,
EditRepositoryDialogComponent,
RepositoryMaintenanceComponent,
],
imports: [
CommonModule,
@ -31,7 +36,9 @@ import {MatFolder} from "@ng-icons/material-icons/baseline";
MatSelectModule,
MatInputModule,
ReactiveFormsModule,
NgIconsModule.withIcons({ MatFolder })
NgIconsModule.withIcons({ MatFolder }),
MatCardModule,
MatProgressBarModule
]
})
export class RepositoryModule {

@ -7,6 +7,7 @@ import {
MatAddCircle,
MatChangeCircle,
MatDeleteSweep,
MatEdit,
MatExpandLess,
MatExpandMore,
MatFilterAlt,
@ -91,6 +92,7 @@ import {SortPresetItemComponent} from "./file-search/sort-preset-item/sort-prese
MatFilterAlt,
MatExpandMore,
MatExpandLess,
MatEdit
}),
MatRippleModule,
MatButtonModule,

@ -1,65 +1,71 @@
import {downloadDir} from "@tauri-apps/api/path";
import {dialog} from "@tauri-apps/api";
import {File} from "../../../api/models/File";
import { downloadDir } from "@tauri-apps/api/path";
import { dialog } from "@tauri-apps/api";
import type { File } from "../../../api/models/File";
export class FileHelper {
/**
* Opens a dialog to get a download location for the given file
* @param {File} file
*/
public static async getFileDownloadLocation(
file: File,
): Promise<string | null> {
let extension = FileHelper.getExtensionForMime(file.mimeType);
/**
* Opens a dialog to get a download location for the given file
* @param {File} file
*/
public static async getFileDownloadLocation(file: File): Promise<string | undefined> {
let extension = FileHelper.getExtensionForMime(file.mimeType);
const downloadDirectory = await downloadDir();
const suggestionPath = downloadDirectory + file.cd + "." + extension;
const downloadDirectory = await downloadDir();
const suggestionPath = downloadDirectory + file.cd + "." + extension;
return await dialog.save({
defaultPath: suggestionPath,
filters: [
{
name: file.mimeType,
extensions: [extension ?? "*"],
},
{ name: "All", extensions: ["*"] },
],
});
}
return await dialog.save({
defaultPath: suggestionPath,
filters: [{
name: file.mimeType,
extensions: [extension ?? "*"]
}, { name: "All", extensions: ["*"] }]
});
}
/**
* Parses a mime into its two components
* @param {string | undefined} mimeType
* @returns {[string, string] | undefined}
*/
public static parseMime(
mimeType: string | undefined,
): [string, string] | undefined {
if (!mimeType) {
return undefined;
}
let mimeParts = mimeType.split("/");
if (mimeParts.length < 2) {
return undefined;
}
const type = mimeParts[0];
const subtype = mimeParts[1];
/**
* Parses a mime into its two components
* @param {string | undefined} mimeType
* @returns {[string, string] | undefined}
*/
public static parseMime(mimeType: string | undefined): [string, string] | undefined {
if (!mimeType) {
return undefined;
}
let mimeParts = mimeType.split("/");
if (mimeParts.length < 2) {
return undefined;
}
const type = mimeParts[0];
const subtype = mimeParts[1];
return [type, subtype];
}
return [type, subtype];
}
/**
* Returns the extension for a mime type
* @param {string} mime
* @returns {string | undefined}
* @private
*/
public static getExtensionForMime(mime: string): string | undefined {
let parts = mime.split("/");
/**
* Returns the extension for a mime type
* @param {string} mime
* @returns {string | undefined}
* @private
*/
public static getExtensionForMime(mime: string): string | undefined {
let parts = mime.split("/");
if (parts.length === 2) {
const type = parts[0];
const subtype = parts[1];
return FileHelper.convertMimeSubtypeToExtension(subtype);
}
return undefined;
}
if (parts.length === 2) {
const type = parts[0];
const subtype = parts[1];
return FileHelper.convertMimeSubtypeToExtension(subtype);
}
return undefined;
}
private static convertMimeSubtypeToExtension(subtype: string): string {
return subtype;
}
private static convertMimeSubtypeToExtension(subtype: string): string {
return subtype;
}
}

@ -13,4 +13,8 @@ export class JobService {
public async runJob(jobType: JobType, sync: boolean = true): Promise<void> {
return MediarepoApi.runJob({ jobType, sync });
}
public async isJobRunning(jobType: JobType): Promise<boolean> {
return MediarepoApi.isJobRunning({ jobType });
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save