Compare commits
9 Commits
main
...
41b1930c70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41b1930c70 | ||
|
ed64cd886b
|
|||
|
711c27967b
|
|||
|
240240ba3c
|
|||
|
|
c1c0a0601c | ||
|
c24a20ad3f
|
|||
| b08cb54acd | |||
|
|
999d8b92aa | ||
|
ef09a717dd
|
1
.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL=postgres://postgres:.admin1@localhost/no_man_sky2
|
||||||
11
.gitignore
vendored
@@ -20,11 +20,8 @@ Cargo.lock
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
!.env.*.example
|
!.env.*.example
|
||||||
# Ignore the .env file that contains sensitive information
|
# Ignore the .env file that contains sensitive information
|
||||||
|
|
||||||
log/*
|
|
||||||
!log/.keep
|
|
||||||
|
|||||||
38
.vscode/launch.json
vendored
@@ -7,43 +7,19 @@
|
|||||||
{
|
{
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Server NoManSky",
|
"name": "Debug executable 'no-man-sky'",
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": ["build", "--bin=no-man-sky", "--package=no-man-sky"],
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=no-man-sky",
|
||||||
|
"--package=no-man-sky"
|
||||||
|
],
|
||||||
"filter": {
|
"filter": {
|
||||||
"name": "no-man-sky",
|
"name": "no-man-sky",
|
||||||
"kind": "bin"
|
"kind": "bin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"args": ["-s"],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Client No-man-sky",
|
|
||||||
"cargo": {
|
|
||||||
"args": ["build", "--bin=no-man-sky", "--package=no-man-sky"],
|
|
||||||
"filter": {
|
|
||||||
"name": "no-man-sky",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": ["add", "Platin"],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Help No-man-sky ",
|
|
||||||
"cargo": {
|
|
||||||
"args": ["build", "--bin=no-man-sky", "--package=no-man-sky"],
|
|
||||||
"filter": {
|
|
||||||
"name": "no-man-sky",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": ["--help"],
|
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
@@ -1,18 +1,11 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"DATABASENAME",
|
|
||||||
"dotenv",
|
|
||||||
"Herstellung",
|
"Herstellung",
|
||||||
"Insertable",
|
|
||||||
"Kochen",
|
"Kochen",
|
||||||
"mordit",
|
|
||||||
"MSVC",
|
|
||||||
"Quelle",
|
"Quelle",
|
||||||
"Raffination",
|
"Raffination",
|
||||||
"rustc",
|
|
||||||
"serde",
|
"serde",
|
||||||
"Stück",
|
"Stück",
|
||||||
"VARCHAR",
|
|
||||||
"Verwendung"
|
"Verwendung"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
24
Cargo.toml
@@ -3,28 +3,16 @@ name = "no-man-sky"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["merlin"]
|
publish = ["merlin"]
|
||||||
description = "NoManSky Server/Client utility"
|
description = "Utility functions to read environment with fallback and values from a file"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "ssh://git@gitea.merlinserver.de:2222/Stefan/no-man-sky-wiki.git"
|
repository = "ssh://git@gitea.merlinserver.de:2222/Stefan/merlin_env_helper.git"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
diesel = { version= "2.2.10", features = ["serde_json", "postgres", "uuid"]}
|
||||||
axum = "0.8.4"
|
env_logger = "0.11.8"
|
||||||
clap = { version = "4.5.43", features = ["derive"] }
|
|
||||||
diesel = { version= "2.2.10", features = ["serde_json", "postgres", "uuid", "r2d2"]}
|
|
||||||
diesel_migrations = "2.2.0"
|
|
||||||
dotenv = "0.15.0"
|
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
log4rs = "1.3.0"
|
merlin_env_helper = { version = "0.2.0", registry = "merlin" }
|
||||||
merlin_env_helper = { version = "0.4.0", registry = "merlin" }
|
|
||||||
once_cell = "1.21.3"
|
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = {version="0.12.15" }
|
reqwest = {version="0.12.15", features=["blocking"]}
|
||||||
#scraper = "0.23.1"
|
#scraper = "0.23.1"
|
||||||
select = "0.6.1"
|
select = "0.6.1"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
serde_json = "1.0.140"
|
|
||||||
tokio = { version = "1.46.1", features = ["full"] }
|
|
||||||
toml = "0.8.23"
|
|
||||||
url = "2.5.4"
|
|
||||||
uuid = "1.17.0"
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
44
log4jrs.yaml
@@ -1,44 +0,0 @@
|
|||||||
refresh_rate: 120 seconds
|
|
||||||
appenders:
|
|
||||||
stdout:
|
|
||||||
kind: console
|
|
||||||
target: stdout
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%Y-%m-%d %H:%M:%S)(local)} {l} {t} {h({m})}{n}"
|
|
||||||
filters:
|
|
||||||
- kind: target_filter
|
|
||||||
level: [error, warn]
|
|
||||||
negation: true
|
|
||||||
|
|
||||||
stderr:
|
|
||||||
kind: console
|
|
||||||
target: stderr
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%Y-%m-%d %H:%M:%S)(local)} {l} {t} {h({m})}{n}"
|
|
||||||
filters:
|
|
||||||
- kind: target_filter
|
|
||||||
level: [error, warn]
|
|
||||||
negation: false
|
|
||||||
no-man-sky-file:
|
|
||||||
kind: rolling_file
|
|
||||||
path: "logs/no-man-sky.log"
|
|
||||||
encoder:
|
|
||||||
pattern: "{d(%Y-%m-%d %H:%M:%S)(local)} {l} {t} {m}{n}"
|
|
||||||
policy:
|
|
||||||
kind: compound
|
|
||||||
trigger:
|
|
||||||
kind: time
|
|
||||||
interval: 1 day
|
|
||||||
modulate: false
|
|
||||||
max_random_delay: 0
|
|
||||||
roller:
|
|
||||||
kind: fixed_window
|
|
||||||
base: 1
|
|
||||||
count: 5
|
|
||||||
pattern: "logs/no-man-sky.{}.log"
|
|
||||||
root:
|
|
||||||
level: info
|
|
||||||
appenders:
|
|
||||||
- stdout
|
|
||||||
- stderr
|
|
||||||
- no-man-sky-file
|
|
||||||
352447
logs/no-man-sky.log
@@ -1,40 +1,37 @@
|
|||||||
CREATE TABLE "icon"(
|
CREATE TABLE "icon"(
|
||||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
"name" VARCHAR(255) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"content_type" VARCHAR(255),
|
"content_type" VARCHAR(255),
|
||||||
"path" VARCHAR(512) NOT NULL,
|
|
||||||
"url" VARCHAR(512),
|
"url" VARCHAR(512),
|
||||||
"width" INT4,
|
"width" INT4,
|
||||||
"height" INT4,
|
"height" INT4,
|
||||||
"state" JSONB
|
"state" JSON
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE "resource"(
|
CREATE TABLE "resource"(
|
||||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
"name" VARCHAR(255) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"title" VARCHAR(255) NOT NULL,
|
"title" VARCHAR(255) NOT NULL,
|
||||||
"url" VARCHAR(512),
|
"url" VARCHAR(512),
|
||||||
"icon" UUID,
|
"icon" UUID,
|
||||||
"state" JSONB,
|
"state" JSON,
|
||||||
FOREIGN KEY ("icon") REFERENCES "icon"("id")
|
FOREIGN KEY ("icon") REFERENCES "icon"("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE "recipe"(
|
CREATE TABLE "recipe"(
|
||||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
"resource" UUID NOT NULL,
|
"resource" UUID NOT NULL,
|
||||||
"quantity" INT4 NOT NULL,
|
|
||||||
"recipe_type" VARCHAR(50) NOT NULL,
|
"recipe_type" VARCHAR(50) NOT NULL,
|
||||||
"duration" INT4 NOT NULL,
|
"duration" INT4 NOT NULL,
|
||||||
"unit" VARCHAR(50) NOT NULL,
|
"state" JSON,
|
||||||
"state" JSONB,
|
|
||||||
FOREIGN KEY ("resource") REFERENCES "resource"("id")
|
FOREIGN KEY ("resource") REFERENCES "resource"("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE "ingredient"(
|
CREATE TABLE "ingredient"(
|
||||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
"resource" UUID NOT NULL,
|
"resource" UUID NOT NULL,
|
||||||
"quantity" INT4 NOT NULL,
|
"quantity" INT4 NOT NULL,
|
||||||
"state" JSONB,
|
"state" JSON,
|
||||||
"recipe" UUID NOT NULL,
|
"recipe" UUID NOT NULL,
|
||||||
FOREIGN KEY ("resource") REFERENCES "resource"("id"),
|
FOREIGN KEY ("resource") REFERENCES "resource"("id"),
|
||||||
FOREIGN KEY ("recipe") REFERENCES "recipe"("id")
|
FOREIGN KEY ("recipe") REFERENCES "recipe"("id")
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[main]
|
|
||||||
image_store_directory = "./images"
|
|
||||||
image_resource_store_directory = "./images/resources"
|
|
||||||
base_url = "https://nomanssky.fandom.com/de/wiki/"
|
|
||||||
|
|
||||||
[server]
|
|
||||||
port = 8080
|
|
||||||
host = "localhost"
|
|
||||||
|
|
||||||
[db]
|
|
||||||
url = "localhost"
|
|
||||||
port = 5432
|
|
||||||
database = "no_man_sky"
|
|
||||||
max_connections = 5
|
|
||||||
user = "set by env"
|
|
||||||
password = "set by env"
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
use reqwest::Client;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
configuration::CONFIGURATION,
|
|
||||||
types::{CallArguments, ClientCommands},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn run_client(
|
|
||||||
arguments: &CallArguments,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
|
||||||
if arguments.server {
|
|
||||||
eprintln!("Server mode is enabled, but this is a client function.");
|
|
||||||
return Err("Server mode cannot be used in client context".into());
|
|
||||||
}
|
|
||||||
if arguments.client_commands.is_none() {
|
|
||||||
eprintln!("No client command provided.");
|
|
||||||
return Err("No client command provided".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
match arguments.client_commands.as_ref().unwrap() {
|
|
||||||
ClientCommands::Add { name } => _add(name).await,
|
|
||||||
ClientCommands::Get { id_or_name } => _get(id_or_name).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _get_url(sub_path: &str, id: &str) -> String {
|
|
||||||
let config = CONFIGURATION.get().unwrap();
|
|
||||||
|
|
||||||
let mut url = format!("http://{}:{}", config.server.host, config.server.port);
|
|
||||||
|
|
||||||
if !sub_path.is_empty() {
|
|
||||||
if sub_path.starts_with('/') {
|
|
||||||
url.push_str(sub_path);
|
|
||||||
} else {
|
|
||||||
url.push('/');
|
|
||||||
url.push_str(sub_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if url.ends_with('/') && !id.is_empty() {
|
|
||||||
url = url[..url.len() - 1].to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !id.is_empty() {
|
|
||||||
if !id.starts_with("/") {
|
|
||||||
url.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
url.push_str(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _get(_id_or_name: &str) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
|
||||||
let url = _get_url("resource", _id_or_name);
|
|
||||||
|
|
||||||
let parsed_url = url::Url::parse(&url);
|
|
||||||
|
|
||||||
if parsed_url.is_err() {
|
|
||||||
eprintln!("Invalid URL: {}", url);
|
|
||||||
return Err(format!("Invalid URL: {}", url).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed_url = parsed_url.unwrap();
|
|
||||||
|
|
||||||
let client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let response = client.get(parsed_url.as_str()).send().await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(resp) => {
|
|
||||||
println!(
|
|
||||||
"StatusCode: {} {}",
|
|
||||||
resp.status(),
|
|
||||||
resp.status().is_success()
|
|
||||||
);
|
|
||||||
resp.status().is_success();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("code:{},error:{}", err.status().unwrap_or_default(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _add(name: &str) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
|
||||||
let url = _get_url("resource", name);
|
|
||||||
|
|
||||||
let parsed_url = url::Url::parse(&url);
|
|
||||||
|
|
||||||
if parsed_url.is_err() {
|
|
||||||
eprintln!("Invalid URL: {}", url);
|
|
||||||
return Err(format!("Invalid URL: {}", url).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed_url = parsed_url.unwrap();
|
|
||||||
|
|
||||||
let client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let response = client.post(parsed_url.as_str()).send().await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(resp) => {
|
|
||||||
println!(
|
|
||||||
"StatusCode: {} {}\n=>{}",
|
|
||||||
resp.status(),
|
|
||||||
resp.status().is_success(),
|
|
||||||
resp.text().await.unwrap_or_default()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("code:{},error:{}", err.status().unwrap_or_default(), err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
use merlin_env_helper::{EnvKey, get_env_value};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::util::validate_url;
|
|
||||||
|
|
||||||
pub static CONFIGURATION: OnceCell<Configuration> = OnceCell::new();
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct BaseConfiguration {
|
|
||||||
pub image_store_directory: String,
|
|
||||||
pub image_resource_store_directory: String,
|
|
||||||
pub base_url: String,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct DatabaseConfiguration {
|
|
||||||
pub url: String,
|
|
||||||
pub port: u16,
|
|
||||||
pub database: String,
|
|
||||||
pub user: Option<String>,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub max_connections: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DatabaseConfiguration {
|
|
||||||
pub fn connection_string(&self) -> String {
|
|
||||||
format!(
|
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
|
||||||
self.user.as_ref().unwrap(),
|
|
||||||
self.password.as_ref().unwrap(),
|
|
||||||
self.url,
|
|
||||||
self.port,
|
|
||||||
self.database
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct ServerConfiguration {
|
|
||||||
pub host: String,
|
|
||||||
pub port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Configuration {
|
|
||||||
pub main: BaseConfiguration,
|
|
||||||
pub server: ServerConfiguration,
|
|
||||||
pub db: DatabaseConfiguration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BaseConfiguration {
|
|
||||||
fn default() -> Self {
|
|
||||||
BaseConfiguration {
|
|
||||||
image_store_directory: "./images".into(),
|
|
||||||
image_resource_store_directory: "./images/resources".into(),
|
|
||||||
base_url: "http://localhost".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ServerConfiguration {
|
|
||||||
fn default() -> Self {
|
|
||||||
ServerConfiguration {
|
|
||||||
host: "localhost".into(),
|
|
||||||
port: 8080,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DatabaseConfiguration {
|
|
||||||
fn default() -> Self {
|
|
||||||
DatabaseConfiguration {
|
|
||||||
url: "localhost".into(),
|
|
||||||
port: 5432,
|
|
||||||
user: None,
|
|
||||||
password: None,
|
|
||||||
max_connections: 5,
|
|
||||||
database: "no_man_sky".into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Configuration {
|
|
||||||
fn default() -> Self {
|
|
||||||
Configuration {
|
|
||||||
main: BaseConfiguration::default(),
|
|
||||||
server: ServerConfiguration::default(),
|
|
||||||
db: DatabaseConfiguration::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Configuration {
|
|
||||||
pub async fn new() -> Self {
|
|
||||||
let mut config = &mut Configuration::default();
|
|
||||||
|
|
||||||
if Path::new("no_man_sky.toml").exists() {
|
|
||||||
// If the config file exists, load it.
|
|
||||||
let config_content = fs::read_to_string("no_man_sky.toml")
|
|
||||||
.unwrap_or_else(|e| panic!("Failed to read config file: {}", e));
|
|
||||||
|
|
||||||
let config_to_merge: Configuration =
|
|
||||||
toml::from_str(&config_content).unwrap_or_else(|e| {
|
|
||||||
panic!("Failed to parse config file: {}", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
merge(&mut config, &config_to_merge);
|
|
||||||
}
|
|
||||||
|
|
||||||
let image_store_directory = get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"IMAGE_STORE_DIRECTORY",
|
|
||||||
&config.main.image_store_directory,
|
|
||||||
))
|
|
||||||
.expect("Failed to get IMAGE_STORE_DIRECTORY from environment");
|
|
||||||
|
|
||||||
let image_resource_store_directory = get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"IMAGE_RESOURCE_STORE_DIRECTORY",
|
|
||||||
&config.main.image_resource_store_directory,
|
|
||||||
))
|
|
||||||
.expect("Failed to get IMAGE_STORE_DIRECTORY from environment");
|
|
||||||
|
|
||||||
validate_configuration(Configuration {
|
|
||||||
main: {
|
|
||||||
BaseConfiguration {
|
|
||||||
image_store_directory: image_store_directory,
|
|
||||||
image_resource_store_directory: image_resource_store_directory,
|
|
||||||
base_url: get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"MAIN_BASE_URL",
|
|
||||||
&config.main.base_url,
|
|
||||||
))
|
|
||||||
.expect("Failed to get MAIN_BASE_URL from environment"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
ServerConfiguration {
|
|
||||||
host: get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"SERVER_HOST",
|
|
||||||
&config.server.host,
|
|
||||||
))
|
|
||||||
.expect("Failed to get SERVER_HOST from environment"),
|
|
||||||
port: get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"SERVER_PORT",
|
|
||||||
&config.server.port.to_string(),
|
|
||||||
))
|
|
||||||
.expect("Failed to get SERVER_PORT from environment")
|
|
||||||
.parse::<u16>()
|
|
||||||
.expect("Failed to parse SERVER_PORT"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
db: {
|
|
||||||
DatabaseConfiguration {
|
|
||||||
url: get_env_value(&EnvKey::key_with_fallback("DB_URL", &config.db.url))
|
|
||||||
.expect("Failed to ger DB_URL from environment."),
|
|
||||||
port: get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"DB_PORT",
|
|
||||||
&config.db.port.to_string(),
|
|
||||||
))
|
|
||||||
.expect("Failed to get DB_URL from environment")
|
|
||||||
.parse::<u16>()
|
|
||||||
.expect("Failed to parse DB_PORT"),
|
|
||||||
database: get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"DB_DATABASE",
|
|
||||||
&config.db.database,
|
|
||||||
))
|
|
||||||
.expect("Failed to ger DB_DATABASE from environment."),
|
|
||||||
user: Some(
|
|
||||||
get_env_value(&EnvKey::secure_with_fallback(
|
|
||||||
"DB_USER",
|
|
||||||
"DB_USER_FILE",
|
|
||||||
&config.db.user.as_ref().unwrap(),
|
|
||||||
))
|
|
||||||
.expect("Failed to ger DB_URL from environment."),
|
|
||||||
),
|
|
||||||
password: Some(
|
|
||||||
get_env_value(&EnvKey::secure_with_fallback(
|
|
||||||
"DB_PASSWORD",
|
|
||||||
"DB_PASSWORD_FILE",
|
|
||||||
&config.db.password.as_ref().unwrap(),
|
|
||||||
))
|
|
||||||
.expect("Failed to ger DB_URL from environment."),
|
|
||||||
),
|
|
||||||
max_connections: get_env_value(&EnvKey::key_with_fallback(
|
|
||||||
"DB_MAX_CONNECTIONS",
|
|
||||||
&config.db.max_connections.to_string(),
|
|
||||||
))
|
|
||||||
.expect("Failed to get DB_MAX_CONNECTIONS from environment")
|
|
||||||
.parse::<u32>()
|
|
||||||
.expect("Failed to parse DB_MAX_CONNECTIONS"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge(config: &mut Configuration, config_content: &Configuration) {
|
|
||||||
config.main.image_store_directory = config_content.main.image_store_directory.clone();
|
|
||||||
config.main.base_url = config_content.main.base_url.clone();
|
|
||||||
config.server.host = config_content.server.host.clone();
|
|
||||||
config.server.port = config_content.server.port;
|
|
||||||
config.db.url = config_content.db.url.clone();
|
|
||||||
config.db.port = config_content.db.port;
|
|
||||||
config.db.max_connections = config_content.db.max_connections;
|
|
||||||
config.db.database = config_content.db.database.clone();
|
|
||||||
|
|
||||||
if config_content.db.user.is_some() {
|
|
||||||
config.db.user = config_content.db.user.clone();
|
|
||||||
}
|
|
||||||
if config_content.db.password.is_some() {
|
|
||||||
config.db.password = config_content.db.password.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn validate_configuration(configuration: Configuration) -> Configuration {
|
|
||||||
if !validate_url(&configuration.main.base_url).await {
|
|
||||||
panic!("Invalid base URL: {}", configuration.main.base_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validate_directory(&configuration.main.image_store_directory) {
|
|
||||||
panic!(
|
|
||||||
"Image store directory does not exist or is not a directory: {}",
|
|
||||||
configuration.main.image_store_directory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if !validate_directory(&configuration.main.image_resource_store_directory) {
|
|
||||||
panic!(
|
|
||||||
"Image resource store directory does not exist or is not a directory: {}",
|
|
||||||
configuration.main.image_resource_store_directory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validate_directory(&configuration.main.image_store_directory) {
|
|
||||||
panic!(
|
|
||||||
"Image store directory does not exist or is not a directory: {}",
|
|
||||||
configuration.main.image_store_directory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if configuration.db.url.is_empty() {
|
|
||||||
panic!("Database URL cannot be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if configuration.db.port == 0 {
|
|
||||||
panic!("Database port cannot be zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if configuration.db.user.is_none()
|
|
||||||
|| configuration.db.user.as_ref().unwrap().trim().is_empty()
|
|
||||||
|| configuration.db.password.as_ref().unwrap().trim() == "set by env"
|
|
||||||
{
|
|
||||||
panic!("Database user cannot be empty or must be set by environment.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if configuration.db.password.is_none()
|
|
||||||
|| configuration
|
|
||||||
.db
|
|
||||||
.password
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.trim()
|
|
||||||
.is_empty()
|
|
||||||
|| configuration.db.password.as_ref().unwrap().trim() == "set by env"
|
|
||||||
{
|
|
||||||
panic!("Database password cannot be empty or must be set by environment.");
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_directory(dir: &str) -> bool {
|
|
||||||
let dir = Path::new(dir);
|
|
||||||
dir.exists() && dir.is_dir()
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,2 @@
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod schema;
|
pub use models::*;
|
||||||
pub mod util;
|
|
||||||
|
|
||||||
use diesel::{
|
|
||||||
PgConnection,
|
|
||||||
r2d2::{ConnectionManager, Pool, PooledConnection},
|
|
||||||
};
|
|
||||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::error::Error;
|
|
||||||
pub use util::*;
|
|
||||||
|
|
||||||
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
|
|
||||||
pub type PgPooledConnection = PooledConnection<ConnectionManager<PgConnection>>;
|
|
||||||
|
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
|
||||||
|
|
||||||
pub static DB_POOL: OnceCell<DbConnectionPool> = OnceCell::new();
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DbConnectionPool {
|
|
||||||
pool: PgPool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbConnectionPool {
|
|
||||||
pub fn new(database_url: &str, max_connections: u32) -> Self {
|
|
||||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
|
||||||
let pool = Pool::builder()
|
|
||||||
.max_size(max_connections)
|
|
||||||
.build(manager)
|
|
||||||
.expect("Failed to create connection pool");
|
|
||||||
|
|
||||||
DbConnectionPool { pool }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_migrations(&self) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
|
|
||||||
// This will run the necessary migrations.
|
|
||||||
//
|
|
||||||
// See the documentation for `MigrationHarness` for
|
|
||||||
// all available methods.
|
|
||||||
self.get_connection()?.run_pending_migrations(MIGRATIONS)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_connection(&self) -> Result<PgPooledConnection, diesel::r2d2::PoolError> {
|
|
||||||
self.pool.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,101 +2,46 @@
|
|||||||
|
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
|
use diesel::{
|
||||||
|
prelude::Queryable,
|
||||||
|
sql_types::{Json, Uuid},
|
||||||
|
};
|
||||||
|
|
||||||
use diesel::{pg::Pg, prelude::*};
|
#[derive(Queryable, Debug)]
|
||||||
use uuid::Uuid;
|
pub struct RowIcon {
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Selectable, AsChangeset, Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
#[diesel(table_name = crate::db::schema::icon)]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct DbIcon {
|
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub content_type: String,
|
pub content_type: Option<String>,
|
||||||
pub url: String,
|
pub url: Option<String>,
|
||||||
pub path: String,
|
pub width: Option<i32>,
|
||||||
pub width: i32,
|
pub height: Option<i32>,
|
||||||
pub height: i32,
|
pub state: Option<Json>,
|
||||||
pub state: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, Debug)]
|
#[derive(Queryable, Debug)]
|
||||||
#[diesel(table_name = crate::db::schema::icon)]
|
pub struct RowIngredient {
|
||||||
pub struct NewDbIcon {
|
|
||||||
pub name: String,
|
|
||||||
pub content_type: String,
|
|
||||||
pub path: String,
|
|
||||||
pub url: String,
|
|
||||||
pub width: i32,
|
|
||||||
pub height: i32,
|
|
||||||
pub state: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, AsChangeset, Selectable, Associations, Debug, Clone)]
|
|
||||||
#[diesel(table_name = crate::db::schema::ingredient)]
|
|
||||||
#[diesel(belongs_to(DbResource, foreign_key = resource))]
|
|
||||||
#[diesel(belongs_to(DbRecipe, foreign_key = recipe))]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct DbIngredient {
|
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub resource: Uuid,
|
pub resource: Uuid,
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
pub state: Option<serde_json::Value>,
|
pub state: Option<Json>,
|
||||||
pub recipe: Uuid,
|
|
||||||
}
|
|
||||||
#[derive(Insertable, Clone)]
|
|
||||||
#[diesel(table_name = crate::db::schema::ingredient)]
|
|
||||||
pub struct NewDbIngredient {
|
|
||||||
pub resource: Uuid,
|
|
||||||
pub quantity: i32,
|
|
||||||
pub state: Option<serde_json::Value>,
|
|
||||||
pub recipe: Uuid,
|
pub recipe: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, AsChangeset, Associations, Selectable, Debug)]
|
#[derive(Queryable, Debug)]
|
||||||
#[diesel(table_name = crate::db::schema::recipe)]
|
pub struct RowRecipe {
|
||||||
#[diesel(belongs_to(DbResource, foreign_key = resource))]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct DbRecipe {
|
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub resource: Uuid,
|
pub resource: Uuid,
|
||||||
pub quantity: i32,
|
|
||||||
pub recipe_type: String,
|
pub recipe_type: String,
|
||||||
pub duration: i32,
|
pub duration: i32,
|
||||||
pub unit: String,
|
pub state: Option<Json>,
|
||||||
pub state: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, Debug, Clone)]
|
#[derive(Queryable, Debug)]
|
||||||
#[diesel(table_name = crate::db::schema::recipe)]
|
pub struct RowResource {
|
||||||
pub struct NewDbRecipe {
|
|
||||||
pub resource: Uuid,
|
|
||||||
pub quantity: i32,
|
|
||||||
pub recipe_type: String,
|
|
||||||
pub duration: i32,
|
|
||||||
pub unit: String,
|
|
||||||
pub state: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, AsChangeset, Associations, Selectable, Debug, Clone)]
|
|
||||||
#[diesel(table_name = crate::db::schema::resource)]
|
|
||||||
#[diesel(belongs_to(DbIcon, foreign_key = icon))]
|
|
||||||
#[diesel(check_for_backend(Pg))]
|
|
||||||
pub struct DbResource {
|
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub icon: Option<Uuid>,
|
pub icon: Option<Uuid>,
|
||||||
pub state: Option<serde_json::Value>,
|
pub state: Option<Json>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Insertable, Clone, PartialEq)]
|
|
||||||
#[diesel(table_name = crate::db::schema::resource)]
|
|
||||||
pub struct NewDbResource {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
pub url: Option<String>,
|
|
||||||
pub icon: Option<Uuid>,
|
|
||||||
pub state: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ diesel::table! {
|
|||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
content_type -> Varchar,
|
content_type -> Nullable<Varchar>,
|
||||||
#[max_length = 1024]
|
#[max_length = 512]
|
||||||
path -> Varchar,
|
url -> Nullable<Varchar>,
|
||||||
#[max_length = 2048]
|
width -> Nullable<Int4>,
|
||||||
url -> Varchar,
|
height -> Nullable<Int4>,
|
||||||
width -> Int4,
|
state -> Nullable<Json>,
|
||||||
height -> Int4,
|
|
||||||
state -> Nullable<Jsonb>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +20,7 @@ diesel::table! {
|
|||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
resource -> Uuid,
|
resource -> Uuid,
|
||||||
quantity -> Int4,
|
quantity -> Int4,
|
||||||
state -> Nullable<Jsonb>,
|
state -> Nullable<Json>,
|
||||||
recipe -> Uuid,
|
recipe -> Uuid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,13 +29,10 @@ diesel::table! {
|
|||||||
recipe (id) {
|
recipe (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
resource -> Uuid,
|
resource -> Uuid,
|
||||||
quantity -> Int4,
|
|
||||||
#[max_length = 50]
|
#[max_length = 50]
|
||||||
recipe_type -> Varchar,
|
recipe_type -> Varchar,
|
||||||
duration -> Int4,
|
duration -> Int4,
|
||||||
#[max_length = 50]
|
state -> Nullable<Json>,
|
||||||
unit -> Varchar,
|
|
||||||
state -> Nullable<Jsonb>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +46,7 @@ diesel::table! {
|
|||||||
#[max_length = 512]
|
#[max_length = 512]
|
||||||
url -> Nullable<Varchar>,
|
url -> Nullable<Varchar>,
|
||||||
icon -> Nullable<Uuid>,
|
icon -> Nullable<Uuid>,
|
||||||
state -> Nullable<Jsonb>,
|
state -> Nullable<Json>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,4 +55,9 @@ diesel::joinable!(ingredient -> resource (resource));
|
|||||||
diesel::joinable!(recipe -> resource (resource));
|
diesel::joinable!(recipe -> resource (resource));
|
||||||
diesel::joinable!(resource -> icon (icon));
|
diesel::joinable!(resource -> icon (icon));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(icon, ingredient, recipe, resource,);
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
icon,
|
||||||
|
ingredient,
|
||||||
|
recipe,
|
||||||
|
resource,
|
||||||
|
);
|
||||||
|
|||||||
350
src/db/util.rs
@@ -1,350 +0,0 @@
|
|||||||
use diesel::prelude::*;
|
|
||||||
use log::error;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db::{
|
|
||||||
PgPooledConnection,
|
|
||||||
models::{
|
|
||||||
DbIcon, DbIngredient, DbRecipe, DbResource, NewDbIcon, NewDbIngredient, NewDbRecipe,
|
|
||||||
NewDbResource,
|
|
||||||
},
|
|
||||||
schema::ingredient::resource,
|
|
||||||
},
|
|
||||||
types::{ID, IdOrNone},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn get_connection() -> PgPooledConnection {
|
|
||||||
crate::db::DB_POOL.get().unwrap().get_connection().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_icon_by_id(icon_id: Uuid) -> Result<Option<DbIcon>, Box<dyn std::error::Error>> {
|
|
||||||
// Implement the logic to find and return the icon based on the name
|
|
||||||
use crate::db::schema::icon::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match dsl::icon.find(icon_id).first::<DbIcon>(conn) {
|
|
||||||
Ok(icon_by_id) => Ok(Some(icon_by_id)),
|
|
||||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error fetching icon by ID: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_icon_by_name(icon_name: &str) -> Result<Option<DbIcon>, Box<dyn std::error::Error>> {
|
|
||||||
// Implement the logic to find and return the icon based on the name
|
|
||||||
use crate::db::schema::icon::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match dsl::icon
|
|
||||||
.filter(dsl::name.eq(icon_name))
|
|
||||||
.limit(1)
|
|
||||||
.load::<DbIcon>(conn)
|
|
||||||
{
|
|
||||||
Ok(it) => {
|
|
||||||
if it.len() > 0 {
|
|
||||||
Ok(Some(it[0].clone()))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error fetching icon by name: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_icon(new_icon: &NewDbIcon) -> Result<IdOrNone, Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::icon::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
if let Some(existing_icon) = get_icon_by_name(&new_icon.name)? {
|
|
||||||
Ok(IdOrNone::EXISTING(existing_icon.id))
|
|
||||||
} else {
|
|
||||||
match diesel::insert_into(dsl::icon)
|
|
||||||
.values(new_icon)
|
|
||||||
.returning(dsl::id)
|
|
||||||
.get_result::<Uuid>(conn)
|
|
||||||
{
|
|
||||||
Ok(new_icon_id) => Ok(IdOrNone::NEW(new_icon_id)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error adding new icon: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_icon(update_icon: &DbIcon) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::icon::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
if let Some(existing_icon) = get_icon_by_name(&update_icon.name)? {
|
|
||||||
match diesel::update(dsl::icon.find(existing_icon.id))
|
|
||||||
.set(update_icon)
|
|
||||||
.execute(conn)
|
|
||||||
{
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error updating icon: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Box::new(diesel::result::Error::NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_resource_by_id(
|
|
||||||
resource_id: Uuid,
|
|
||||||
) -> Result<Option<DbResource>, Box<dyn std::error::Error>> {
|
|
||||||
// Implement the logic to find and return the icon based on the name
|
|
||||||
use crate::db::schema::resource::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match dsl::resource.find(resource_id).first::<DbResource>(conn) {
|
|
||||||
Ok(it) => Ok(Some(it)),
|
|
||||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error fetching resource by ID: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_resource_by_name(
|
|
||||||
resource_name: &str,
|
|
||||||
) -> Result<Option<DbResource>, Box<dyn std::error::Error>> {
|
|
||||||
// Implement the logic to find and return the icon based on the name
|
|
||||||
use crate::db::schema::resource::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match dsl::resource
|
|
||||||
.filter(name.eq(resource_name))
|
|
||||||
.limit(1)
|
|
||||||
.load::<DbResource>(conn)
|
|
||||||
{
|
|
||||||
Ok(it) => {
|
|
||||||
if it.len() > 0 {
|
|
||||||
Ok(Some(it[0].clone()))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error fetching resource by name: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_resource(new_resource: &NewDbResource) -> Result<IdOrNone, Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::resource::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
if let Some(existing_resource) = get_resource_by_name(&new_resource.name)? {
|
|
||||||
Ok(IdOrNone::EXISTING(existing_resource.id))
|
|
||||||
} else {
|
|
||||||
match diesel::insert_into(dsl::resource)
|
|
||||||
.values(new_resource)
|
|
||||||
.returning(dsl::id)
|
|
||||||
.get_result::<Uuid>(conn)
|
|
||||||
{
|
|
||||||
Ok(new_resource_id) => Ok(IdOrNone::NEW(new_resource_id)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error adding new resource: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_resource(update_resource: &DbResource) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::resource::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
if let Some(existing_resource) = get_resource_by_name(&update_resource.name)? {
|
|
||||||
diesel::update(dsl::resource.find(existing_resource.id))
|
|
||||||
.set(update_resource)
|
|
||||||
.execute(conn)?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Box::new(diesel::result::Error::NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ingredient_by_id(
|
|
||||||
ingredient_id: &Uuid,
|
|
||||||
) -> Result<Option<DbIngredient>, Box<dyn std::error::Error>> {
|
|
||||||
// Implement the logic to find and return the icon based on the name
|
|
||||||
use crate::db::schema::ingredient::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match dsl::ingredient
|
|
||||||
.find(ingredient_id)
|
|
||||||
.first::<DbIngredient>(conn)
|
|
||||||
{
|
|
||||||
Ok(it) => Ok(Some(it)),
|
|
||||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error fetching ingredient by ID: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_ingredient(
|
|
||||||
new_ingredient: &NewDbIngredient,
|
|
||||||
) -> Result<IdOrNone, Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::ingredient::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match diesel::insert_into(dsl::ingredient)
|
|
||||||
.values(new_ingredient)
|
|
||||||
.returning(dsl::id)
|
|
||||||
.get_result::<Uuid>(conn)
|
|
||||||
{
|
|
||||||
Ok(it) => Ok(IdOrNone::NEW(it)),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error adding new ingredient: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_ingredient(
|
|
||||||
update_ingredient: &DbIngredient,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::ingredient::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
if let Some(existing_ingredient) = get_ingredient_by_id(&update_ingredient.id)? {
|
|
||||||
match diesel::update(dsl::ingredient)
|
|
||||||
.filter(dsl::id.eq(existing_ingredient.id))
|
|
||||||
.set(update_ingredient)
|
|
||||||
.execute(conn)
|
|
||||||
{
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error updating ingredient: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Box::new(diesel::result::Error::NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_recipe_by_id(by_id: &Uuid) -> Result<Option<DbRecipe>, Box<dyn std::error::Error>> {
|
|
||||||
// Implement the logic to find and return the icon based on the name
|
|
||||||
use crate::db::schema::recipe::dsl::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match recipe.find(by_id).first::<DbRecipe>(conn) {
|
|
||||||
Ok(it) => Ok(Some(it)),
|
|
||||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error fetching recipe by ID: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_recipe(new_recipe: &NewDbRecipe) -> Result<Uuid, Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::recipe::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
match diesel::insert_into(dsl::recipe)
|
|
||||||
.values(new_recipe)
|
|
||||||
.returning(dsl::id)
|
|
||||||
.get_result::<Uuid>(conn)
|
|
||||||
{
|
|
||||||
Ok(new_id) => Ok(new_id),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error adding new recipe: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_recipe(update_recipe: &DbRecipe) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::recipe::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
if let Some(existing_recipe) = get_recipe_by_id(&update_recipe.id)? {
|
|
||||||
match diesel::update(dsl::recipe.find(existing_recipe.id))
|
|
||||||
.set(update_recipe)
|
|
||||||
.execute(conn)
|
|
||||||
{
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error updating recipe: {}", e);
|
|
||||||
Err(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Box::new(diesel::result::Error::NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_recipes(
|
|
||||||
by_recipe_type: &str,
|
|
||||||
resource_id: &Uuid,
|
|
||||||
) -> Result<Vec<DbRecipe>, Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::recipe::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
let results = dsl::recipe
|
|
||||||
.filter(dsl::recipe_type.eq(by_recipe_type))
|
|
||||||
.filter(dsl::resource.eq(resource_id))
|
|
||||||
.load::<DbRecipe>(conn)?;
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_ingredients_for_recipe(
|
|
||||||
recipe: &DbRecipe,
|
|
||||||
other: &Vec<String>,
|
|
||||||
) -> Result<Option<usize>, Box<dyn std::error::Error>> {
|
|
||||||
use crate::db::schema::*;
|
|
||||||
|
|
||||||
let conn: &mut PgConnection = &mut get_connection();
|
|
||||||
|
|
||||||
let result_filtered = ingredient::table
|
|
||||||
.inner_join(resource::table)
|
|
||||||
.filter(ingredient::recipe.eq(recipe.id))
|
|
||||||
.filter(resource::name.eq_any(other))
|
|
||||||
.count()
|
|
||||||
.get_result::<i64>(conn)?;
|
|
||||||
|
|
||||||
let results_all = ingredient::table
|
|
||||||
.inner_join(resource::table)
|
|
||||||
.count()
|
|
||||||
.get_result::<i64>(conn)?;
|
|
||||||
|
|
||||||
if result_filtered == results_all {
|
|
||||||
Ok(Some(results_all as usize))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
57
src/main.rs
@@ -1,51 +1,30 @@
|
|||||||
mod client;
|
|
||||||
mod configuration;
|
|
||||||
mod db;
|
mod db;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod server;
|
|
||||||
mod types;
|
mod types;
|
||||||
mod util;
|
|
||||||
|
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
use clap::Parser;
|
use parse::parse;
|
||||||
use dotenv::dotenv;
|
|
||||||
|
|
||||||
use crate::{
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
client::run_client,
|
env_logger::init();
|
||||||
configuration::{CONFIGURATION, Configuration},
|
let html = read("test_mordit.html")?;
|
||||||
server::run_server,
|
parse(&html);
|
||||||
types::CallArguments,
|
Ok(())
|
||||||
util::initialize_logging,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
dotenv().ok();
|
|
||||||
initialize_logging("log4jrs.yaml").expect("Failed to initialize logging with log4rs");
|
|
||||||
let cli = CallArguments::parse();
|
|
||||||
let _config = Configuration::new().await;
|
|
||||||
|
|
||||||
if let Err(conf) = CONFIGURATION.set(_config) {
|
|
||||||
eprintln!("Failed to set configuration: {:?}", conf);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _config = CONFIGURATION.get().unwrap();
|
|
||||||
|
|
||||||
if cli.server {
|
|
||||||
_run_server().await.expect("Failed to run server");
|
|
||||||
} else {
|
|
||||||
_run_client(&cli).await.expect("Failed to run client");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _run_client(cli: &CallArguments) -> Result<(), Box<dyn std::error::Error>> {
|
fn _download_file(url: &str, _path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
run_client(cli).await
|
// Some simple CLI args requirements...
|
||||||
}
|
|
||||||
|
|
||||||
async fn _run_server() -> Result<(), Box<dyn std::error::Error>> {
|
eprintln!("Fetching {url:?}...");
|
||||||
run_server().await
|
|
||||||
|
// reqwest::blocking::get() is a convenience function.
|
||||||
|
//
|
||||||
|
// In most cases, you should create/build a reqwest::Client and reuse
|
||||||
|
// it for all requests.
|
||||||
|
let res = reqwest::blocking::get(url)?;
|
||||||
|
|
||||||
|
let body = res.text()?;
|
||||||
|
Ok(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(path: &str) -> Result<String, std::io::Error> {
|
fn read(path: &str) -> Result<String, std::io::Error> {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use crate::types::{
|
use crate::types::{Duration, Icon, Ingredient, Recipe, RecipeType, Resource, ResourceState};
|
||||||
Duration, Icon, Ingredient, Recipe, RecipeType, Resource, ResourceState, ResourceWithQuantity,
|
|
||||||
};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use select::{
|
use select::{
|
||||||
document::Document,
|
document::Document,
|
||||||
@@ -29,7 +27,7 @@ type ResourceTmp = (
|
|||||||
Option<Duration>, // duration
|
Option<Duration>, // duration
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn parse(html: &str) -> Vec<Recipe> {
|
pub fn parse(html: &str) {
|
||||||
let document = Document::from(html);
|
let document = Document::from(html);
|
||||||
|
|
||||||
let mut map_resource: HashMap<String, Resource> = HashMap::new();
|
let mut map_resource: HashMap<String, Resource> = HashMap::new();
|
||||||
@@ -69,7 +67,9 @@ pub fn parse(html: &str) -> Vec<Recipe> {
|
|||||||
&mut recipes,
|
&mut recipes,
|
||||||
);
|
);
|
||||||
|
|
||||||
recipes
|
print_recipe(&recipes, RecipeType::Refining);
|
||||||
|
print_recipe(&recipes, RecipeType::Production);
|
||||||
|
print_recipe(&recipes, RecipeType::Cooking);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first<T>(iter: &mut impl Iterator<Item = T>) -> Option<T> {
|
fn first<T>(iter: &mut impl Iterator<Item = T>) -> Option<T> {
|
||||||
@@ -173,7 +173,7 @@ fn parse_resource_items(
|
|||||||
if !ingredient_to_add.is_empty() {
|
if !ingredient_to_add.is_empty() {
|
||||||
let recipe = crate::types::Recipe {
|
let recipe = crate::types::Recipe {
|
||||||
recipe_type: recipe_type,
|
recipe_type: recipe_type,
|
||||||
resource: ResourceWithQuantity::from_ingredient(ingredient),
|
resource: ingredient,
|
||||||
duration: tmp_resource.4.unwrap_or(Duration {
|
duration: tmp_resource.4.unwrap_or(Duration {
|
||||||
millis: 0,
|
millis: 0,
|
||||||
unit: "Stück".to_string(),
|
unit: "Stück".to_string(),
|
||||||
@@ -193,16 +193,10 @@ fn print_recipe(recipes: &Vec<Recipe>, recipe_type: RecipeType) {
|
|||||||
.filter(|recipe| recipe.recipe_type == recipe_type)
|
.filter(|recipe| recipe.recipe_type == recipe_type)
|
||||||
{
|
{
|
||||||
println!("Recipe Type: {:?}", recipe.recipe_type);
|
println!("Recipe Type: {:?}", recipe.recipe_type);
|
||||||
let icon = match recipe.resource.icon {
|
|
||||||
Some(ref icon) => format!("{}", icon),
|
|
||||||
None => "(none)".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Resource: {} ({}) => {}",
|
"Resource: {} ({})",
|
||||||
recipe.resource.name, recipe.resource.quantity, icon
|
recipe.resource.resource.name, recipe.resource.quantity
|
||||||
);
|
);
|
||||||
|
|
||||||
println!("Duration: {} ms", recipe.duration.millis);
|
println!("Duration: {} ms", recipe.duration.millis);
|
||||||
println!("Ingredients:");
|
println!("Ingredients:");
|
||||||
for ingredient in &recipe.ingredients {
|
for ingredient in &recipe.ingredients {
|
||||||
@@ -226,6 +220,7 @@ fn parse_dst(
|
|||||||
let dest = first(&mut dest);
|
let dest = first(&mut dest);
|
||||||
|
|
||||||
if dest.is_none() {
|
if dest.is_none() {
|
||||||
|
eprintln!("No element found with the selector '#{}'", id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +368,6 @@ fn parse_li_to_resource(
|
|||||||
resource_items.push(ParseType::Img(Icon {
|
resource_items.push(ParseType::Img(Icon {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
url: url.to_string(),
|
url: url.to_string(),
|
||||||
path: None, // Path will be set later
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
content_type: "image/png".to_string(), // Assuming PNG, adjust as needed
|
content_type: "image/png".to_string(), // Assuming PNG, adjust as needed
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
pub mod router;
|
|
||||||
pub mod util;
|
|
||||||
use crate::{
|
|
||||||
configuration::CONFIGURATION,
|
|
||||||
db::{self, DB_POOL},
|
|
||||||
};
|
|
||||||
use log::info;
|
|
||||||
use router::*;
|
|
||||||
use util::*;
|
|
||||||
|
|
||||||
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// initialize database
|
|
||||||
let app = create_router().unwrap();
|
|
||||||
let config = CONFIGURATION.get().unwrap();
|
|
||||||
let db_pool =
|
|
||||||
db::DbConnectionPool::new(&config.db.connection_string(), config.db.max_connections);
|
|
||||||
let addr = format!("{}:{}", config.server.host, config.server.port);
|
|
||||||
|
|
||||||
db_pool
|
|
||||||
.run_migrations()
|
|
||||||
.expect("Failed to run database migrations");
|
|
||||||
|
|
||||||
DB_POOL
|
|
||||||
.set(db_pool)
|
|
||||||
.expect("Failed to set database connection pool");
|
|
||||||
|
|
||||||
info!("Database connection pool initialized");
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
|
||||||
info!("NoManSky server listening on {}", &addr);
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
info!("NoManSky server listening stopped");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
use std::{collections::HashMap, os::unix::process};
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
Router,
|
|
||||||
extract::{Path, Query},
|
|
||||||
response::Result,
|
|
||||||
routing::get,
|
|
||||||
};
|
|
||||||
use log::info;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
configuration::CONFIGURATION,
|
|
||||||
parse::parse,
|
|
||||||
server::{download_content, util::process_recipe},
|
|
||||||
types::ServerError,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn create_router() -> Result<Router, Box<dyn std::error::Error>> {
|
|
||||||
let router = Router::new()
|
|
||||||
.route("/", get(root))
|
|
||||||
.route("/resource/{name}", get(get_resource).post(add_resource));
|
|
||||||
Ok(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn root() -> &'static str {
|
|
||||||
"NoManSky Server!"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_resource(
|
|
||||||
Path(name): Path<String>,
|
|
||||||
Query(query): Query<HashMap<String, String>>,
|
|
||||||
) -> Result<String> {
|
|
||||||
let result = format!("Resource name: {:?}, Query: {:?}", name, query);
|
|
||||||
println!("get_resource: {}", result);
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_resource(
|
|
||||||
Path(name): Path<String>,
|
|
||||||
Query(_query): Query<HashMap<String, String>>,
|
|
||||||
) -> Result<String, ServerError> {
|
|
||||||
let url;
|
|
||||||
let config = CONFIGURATION.get().unwrap();
|
|
||||||
|
|
||||||
if name.starts_with("http") {
|
|
||||||
// If the name starts with "http", treat it as a URL
|
|
||||||
url = name.clone();
|
|
||||||
} else {
|
|
||||||
// Otherwise, construct a URL from the name
|
|
||||||
url = format!(
|
|
||||||
"{}{}{}",
|
|
||||||
config.main.base_url,
|
|
||||||
if config.main.base_url.ends_with("/") {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
"/"
|
|
||||||
},
|
|
||||||
name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Try to download resource {}", url);
|
|
||||||
|
|
||||||
let response = download_content(&url).await?;
|
|
||||||
|
|
||||||
let recipes = parse(&response);
|
|
||||||
|
|
||||||
for recipe in recipes {
|
|
||||||
process_recipe(&recipe).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(format!("Resource {} added.", name))
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
use std::{ffi::OsStr, io::Cursor, path::PathBuf};
|
|
||||||
|
|
||||||
use log::info;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
configuration::CONFIGURATION,
|
|
||||||
db::{
|
|
||||||
self, add_icon, add_ingredient, add_recipe, add_resource, find_ingredients_for_recipe,
|
|
||||||
find_recipes, get_icon_by_name, get_resource_by_name,
|
|
||||||
models::{NewDbIcon, NewDbRecipe, NewDbResource},
|
|
||||||
},
|
|
||||||
types::{ID, Icon, IdOrNone, Ingredient, Recipe, Resource},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn download_content(url: &str) -> Result<String, anyhow::Error> {
|
|
||||||
// Some simple CLI args requirements...
|
|
||||||
|
|
||||||
let res = reqwest::get(url).await;
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
info!("Failed to download file from {}: {}", url, e);
|
|
||||||
anyhow::bail!("Failed to download file from {}: {}", url, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = res.unwrap();
|
|
||||||
|
|
||||||
let body = res.text().await;
|
|
||||||
|
|
||||||
match body {
|
|
||||||
Ok(body) => {
|
|
||||||
info!("Downloaded file from {} successfully", url);
|
|
||||||
Ok(body)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
info!("Failed to download file from {}: {}", url, e);
|
|
||||||
anyhow::bail!("Failed to download file from {}: {}", url, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_icon_file(icon: &Icon) -> PathBuf {
|
|
||||||
let mut file = PathBuf::from(
|
|
||||||
CONFIGURATION
|
|
||||||
.get()
|
|
||||||
.unwrap()
|
|
||||||
.main
|
|
||||||
.image_resource_store_directory
|
|
||||||
.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
file.push(icon.name.clone());
|
|
||||||
|
|
||||||
let mut counter = 1;
|
|
||||||
let file_name = file.as_path().file_stem().unwrap_or(OsStr::new("icon"));
|
|
||||||
let extension = file.as_path().extension().unwrap_or(OsStr::new(""));
|
|
||||||
|
|
||||||
let mut file = file.clone();
|
|
||||||
|
|
||||||
while file.as_path().exists() {
|
|
||||||
let new_file_name = format!(
|
|
||||||
"{}-{}.{}",
|
|
||||||
file_name.to_string_lossy(),
|
|
||||||
counter,
|
|
||||||
extension.to_string_lossy()
|
|
||||||
);
|
|
||||||
counter += 1;
|
|
||||||
|
|
||||||
file.set_file_name(new_file_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
file.to_path_buf()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn download_file(url: &str, file_name: &PathBuf) -> Result<(), anyhow::Error> {
|
|
||||||
let response = match reqwest::get(url).await {
|
|
||||||
Ok(res) => res,
|
|
||||||
Err(e) => {
|
|
||||||
info!("Failed to download file from {}: {}", url, e);
|
|
||||||
anyhow::bail!("Failed to download file from {}: {}", url, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut file = std::fs::File::create(file_name.as_path())?;
|
|
||||||
let mut content = Cursor::new(response.bytes().await?);
|
|
||||||
std::io::copy(&mut content, &mut file)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn process_recipe(recipe: &Recipe) -> Result<ID, anyhow::Error> {
|
|
||||||
let resource_uuid = get_or_add_resource(&recipe.resource.to_resource()).await?;
|
|
||||||
match resource_uuid {
|
|
||||||
IdOrNone::NONE => {
|
|
||||||
anyhow::bail!("Error adding resource: {}", recipe.resource.name);
|
|
||||||
}
|
|
||||||
_ => get_or_add_recipe(&recipe, &resource_uuid.to_id()).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_or_add_recipe(recipe: &Recipe, resource_id: &ID) -> Result<ID, anyhow::Error> {
|
|
||||||
let recipes = match find_recipes(recipe.recipe_type.as_str(), &resource_id.as_uuid()) {
|
|
||||||
Ok(recipes) => recipes,
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!("Error finding recipes: {}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ids: Vec<String> = recipe
|
|
||||||
.ingredients
|
|
||||||
.iter()
|
|
||||||
.map(|i| i.resource.name.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for recipe_to_check in recipes {
|
|
||||||
if recipe_to_check.quantity == recipe.resource.quantity as i32
|
|
||||||
&& recipe_to_check.duration == recipe.duration.millis as i32
|
|
||||||
&& recipe_to_check.unit == recipe.duration.unit
|
|
||||||
{
|
|
||||||
match find_ingredients_for_recipe(&recipe_to_check, &ids) {
|
|
||||||
Ok(ingredients) => {
|
|
||||||
// Wenn alle Zutaten vorhanden sind existiert das Rezept
|
|
||||||
if ingredients.is_some() && ingredients.unwrap() == recipe.ingredients.len() {
|
|
||||||
return Ok(ID::EXISTING(recipe_to_check.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!("Error finding ingredients for recipe: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_recipe = NewDbRecipe {
|
|
||||||
resource: resource_id.as_uuid(),
|
|
||||||
quantity: recipe.resource.quantity as i32,
|
|
||||||
duration: recipe.duration.millis as i32,
|
|
||||||
unit: recipe.duration.unit.clone(),
|
|
||||||
state: None,
|
|
||||||
recipe_type: recipe.recipe_type.as_string().clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let recipe_id = match add_recipe(&new_recipe) {
|
|
||||||
Ok(recipe_id) => {
|
|
||||||
info!(
|
|
||||||
"Added recipe {} to database with ID {}",
|
|
||||||
recipe.resource.name, recipe_id
|
|
||||||
);
|
|
||||||
recipe_id
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Failed to add recipe for resource {} to database: {}",
|
|
||||||
recipe.resource.name,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for ingredient in &recipe.ingredients {
|
|
||||||
add_ingredient_for_recipe(ingredient, &recipe_id).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ID::NEW(recipe_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_ingredient_for_recipe(
|
|
||||||
ingredient: &Ingredient,
|
|
||||||
recipe_id: &Uuid,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
let resource_id = match get_or_add_resource(&ingredient.resource).await {
|
|
||||||
Ok(uuid) => match uuid {
|
|
||||||
IdOrNone::EXISTING(id) => id,
|
|
||||||
IdOrNone::NEW(id) => id,
|
|
||||||
IdOrNone::NONE => {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Error adding resource for ingredient {}: None",
|
|
||||||
ingredient.resource.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Error adding resource for ingredient {}: {}",
|
|
||||||
ingredient.resource.name,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_ingredient = db::models::NewDbIngredient {
|
|
||||||
recipe: *recipe_id,
|
|
||||||
resource: resource_id,
|
|
||||||
quantity: ingredient.quantity as i32,
|
|
||||||
state: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match add_ingredient(&new_ingredient) {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!("Error adding ingredient for recipe {}: {}", recipe_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_or_add_resource(resource: &Resource) -> Result<IdOrNone, anyhow::Error> {
|
|
||||||
let resource_id = match get_resource_by_name(&resource.name) {
|
|
||||||
Ok(res) => res,
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!("Error fetching resource by name: {}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(resource) = resource_id {
|
|
||||||
return Ok(IdOrNone::EXISTING(resource.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon_id = match get_or_add_icon(&resource.icon).await? {
|
|
||||||
IdOrNone::EXISTING(icon_id) => Some(icon_id),
|
|
||||||
IdOrNone::NEW(icon_id) => Some(icon_id),
|
|
||||||
IdOrNone::NONE => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_resource = NewDbResource {
|
|
||||||
name: resource.name.clone(),
|
|
||||||
title: resource.title.clone(),
|
|
||||||
url: resource.url.clone(),
|
|
||||||
icon: icon_id,
|
|
||||||
state: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match add_resource(&new_resource) {
|
|
||||||
Ok(resource_id) => {
|
|
||||||
info!(
|
|
||||||
"Added resource {} to database with ID {}",
|
|
||||||
new_resource.name, resource_id
|
|
||||||
);
|
|
||||||
Ok(resource_id)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Failed to add resource {} to database: {}",
|
|
||||||
new_resource.name,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_or_add_icon(icon: &Option<Icon>) -> Result<IdOrNone, anyhow::Error> {
|
|
||||||
if icon.is_none() {
|
|
||||||
return Ok(IdOrNone::NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon = icon.as_ref().unwrap();
|
|
||||||
|
|
||||||
let db_icon = match get_icon_by_name(&icon.name) {
|
|
||||||
Ok(db_icon) => db_icon,
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!("Error fetching icon by name: {}", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(icon) = db_icon {
|
|
||||||
return Ok(IdOrNone::EXISTING(icon.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = get_icon_file(icon);
|
|
||||||
println!("Downloading icon {} to {}", icon.name, file.display());
|
|
||||||
download_file(&icon.url, &file).await?;
|
|
||||||
|
|
||||||
let new_icon: NewDbIcon = NewDbIcon {
|
|
||||||
name: icon.name.clone(),
|
|
||||||
url: icon.url.clone(),
|
|
||||||
path: file.to_string_lossy().to_string(),
|
|
||||||
content_type: icon.content_type.clone(),
|
|
||||||
width: icon.width as i32,
|
|
||||||
height: icon.height as i32,
|
|
||||||
state: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match add_icon(&new_icon) {
|
|
||||||
Ok(icon) => {
|
|
||||||
info!("Added icon {} to database with ID {}", new_icon.name, icon);
|
|
||||||
Ok(icon)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
anyhow::bail!("Failed to add icon {} to database: {}", new_icon.name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
use std::fmt::{self, Display};
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
http::StatusCode,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum ResourceState {
|
pub enum ResourceState {
|
||||||
Parsed(bool),
|
Parsed(bool),
|
||||||
@@ -15,30 +6,16 @@ pub enum ResourceState {
|
|||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: Option<String>,
|
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub content_type: String,
|
pub content_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::default::Default for Icon {
|
|
||||||
fn default() -> Self {
|
|
||||||
Icon {
|
|
||||||
name: String::new(),
|
|
||||||
path: None,
|
|
||||||
url: String::new(),
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
content_type: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Clone for Icon {
|
impl Clone for Icon {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Icon {
|
Icon {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
path: self.path.clone(),
|
|
||||||
url: self.url.clone(),
|
url: self.url.clone(),
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
@@ -47,17 +24,7 @@ impl Clone for Icon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Icon {
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Icon {{ name: {}, url: {}, width: {}, height: {}, content_type: {} }}",
|
|
||||||
self.name, self.url, self.width, self.height, self.content_type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, Clone)]
|
|
||||||
pub struct Resource {
|
pub struct Resource {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -66,127 +33,6 @@ pub struct Resource {
|
|||||||
pub state: ResourceState,
|
pub state: ResourceState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource {
|
|
||||||
pub fn new(name: String, title: String, url: Option<String>, icon: Option<Icon>) -> Self {
|
|
||||||
Resource {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
icon,
|
|
||||||
state: ResourceState::Unparsed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_resource_with_quantity(resource: ResourceWithQuantity) -> Self {
|
|
||||||
Resource {
|
|
||||||
name: resource.name,
|
|
||||||
title: resource.title,
|
|
||||||
url: resource.url,
|
|
||||||
icon: resource.icon,
|
|
||||||
state: resource.state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialEq for Resource {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.name == other.name
|
|
||||||
&& self.title == other.title
|
|
||||||
&& self.url == other.url
|
|
||||||
&& self.icon == other.icon
|
|
||||||
&& self.state == other.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq<&ResourceWithQuantity> for Resource {
|
|
||||||
fn eq(&self, other: &&ResourceWithQuantity) -> bool {
|
|
||||||
self.name == other.name
|
|
||||||
&& self.title == other.title
|
|
||||||
&& self.url == other.url
|
|
||||||
&& self.icon == other.icon
|
|
||||||
&& self.state == other.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, Clone)]
|
|
||||||
pub struct ResourceWithQuantity {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
pub url: Option<String>,
|
|
||||||
pub icon: Option<Icon>,
|
|
||||||
pub state: ResourceState,
|
|
||||||
pub quantity: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResourceWithQuantity {
|
|
||||||
pub fn from_resource(resource: Resource, quantity: u32) -> Self {
|
|
||||||
ResourceWithQuantity {
|
|
||||||
name: resource.name,
|
|
||||||
title: resource.title,
|
|
||||||
url: resource.url,
|
|
||||||
icon: resource.icon,
|
|
||||||
state: resource.state,
|
|
||||||
quantity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_ingredient(ingredient: Ingredient) -> Self {
|
|
||||||
ResourceWithQuantity {
|
|
||||||
name: ingredient.resource.name,
|
|
||||||
title: ingredient.resource.title,
|
|
||||||
url: ingredient.resource.url,
|
|
||||||
icon: ingredient.resource.icon,
|
|
||||||
state: ingredient.resource.state,
|
|
||||||
quantity: ingredient.quantity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
name: String,
|
|
||||||
title: String,
|
|
||||||
url: Option<String>,
|
|
||||||
icon: Option<Icon>,
|
|
||||||
quantity: u32,
|
|
||||||
) -> Self {
|
|
||||||
ResourceWithQuantity {
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
icon,
|
|
||||||
state: ResourceState::Unparsed,
|
|
||||||
quantity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_resource(&self) -> Resource {
|
|
||||||
Resource {
|
|
||||||
name: self.name.clone(),
|
|
||||||
title: self.title.clone(),
|
|
||||||
url: self.url.clone(),
|
|
||||||
icon: self.icon.clone(),
|
|
||||||
state: self.state.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialEq for ResourceWithQuantity {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.name == other.name
|
|
||||||
&& self.title == other.title
|
|
||||||
&& self.url == other.url
|
|
||||||
&& self.icon == other.icon
|
|
||||||
&& self.state == other.state
|
|
||||||
&& self.quantity == other.quantity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq<&Resource> for ResourceWithQuantity {
|
|
||||||
fn eq(&self, other: &&Resource) -> bool {
|
|
||||||
self.name == other.name
|
|
||||||
&& self.title == other.title
|
|
||||||
&& self.url == other.url
|
|
||||||
&& self.icon == other.icon
|
|
||||||
&& self.state == other.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Ingredient {
|
pub struct Ingredient {
|
||||||
pub resource: Resource,
|
pub resource: Resource,
|
||||||
@@ -200,32 +46,6 @@ pub enum RecipeType {
|
|||||||
Cooking,
|
Cooking,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecipeType {
|
|
||||||
pub fn from_str(s: &str) -> Option<RecipeType> {
|
|
||||||
match s {
|
|
||||||
"production" => Some(RecipeType::Production),
|
|
||||||
"refining" => Some(RecipeType::Refining),
|
|
||||||
"cooking" => Some(RecipeType::Cooking),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
RecipeType::Production => String::from("production"),
|
|
||||||
RecipeType::Refining => String::from("refining"),
|
|
||||||
RecipeType::Cooking => String::from("cooking"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
RecipeType::Production => "production",
|
|
||||||
RecipeType::Refining => "refining",
|
|
||||||
RecipeType::Cooking => "cooking",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Duration {
|
pub struct Duration {
|
||||||
pub millis: u64,
|
pub millis: u64,
|
||||||
@@ -235,122 +55,7 @@ pub struct Duration {
|
|||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Recipe {
|
pub struct Recipe {
|
||||||
pub recipe_type: RecipeType,
|
pub recipe_type: RecipeType,
|
||||||
pub resource: ResourceWithQuantity,
|
pub resource: Ingredient,
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
pub ingredients: Vec<Ingredient>,
|
pub ingredients: Vec<Ingredient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct CallArguments {
|
|
||||||
#[arg(short, long, default_value_t = false)]
|
|
||||||
pub server: bool,
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub client_commands: Option<ClientCommands>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
pub enum ClientCommands {
|
|
||||||
/// Adds files to myapp
|
|
||||||
Add {
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
Get {
|
|
||||||
id_or_name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ServerError(anyhow::Error);
|
|
||||||
|
|
||||||
// Tell axum how to convert `AppError` into a response.
|
|
||||||
impl IntoResponse for ServerError {
|
|
||||||
fn into_response(self) -> Response {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Error: {}", self.0),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> From<E> for ServerError
|
|
||||||
where
|
|
||||||
E: Into<anyhow::Error>,
|
|
||||||
{
|
|
||||||
fn from(err: E) -> Self {
|
|
||||||
Self(err.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum IdOrNone {
|
|
||||||
NEW(Uuid),
|
|
||||||
EXISTING(Uuid),
|
|
||||||
NONE,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for IdOrNone {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
IdOrNone::NEW(id) => write!(f, "+{}", id.urn().to_string()),
|
|
||||||
IdOrNone::EXISTING(id) => write!(f, "{}", id.urn().to_string()),
|
|
||||||
IdOrNone::NONE => write!(f, "None"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IdOrNone {
|
|
||||||
pub fn to_id(&self) -> ID {
|
|
||||||
match self {
|
|
||||||
IdOrNone::NEW(id) => ID::NEW(*id),
|
|
||||||
IdOrNone::EXISTING(id) => ID::EXISTING(*id),
|
|
||||||
_ => panic!("Cannot convert ID to IdOrNone"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_id(id: ID) -> IdOrNone {
|
|
||||||
match id {
|
|
||||||
ID::NEW(id) => IdOrNone::NEW(id),
|
|
||||||
ID::EXISTING(id) => IdOrNone::EXISTING(id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum ID {
|
|
||||||
NEW(Uuid),
|
|
||||||
EXISTING(Uuid),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ID {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
ID::NEW(id) => write!(f, "+{}", id.urn().to_string()),
|
|
||||||
ID::EXISTING(id) => write!(f, "{}", id.urn().to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ID {
|
|
||||||
pub fn to_id_none(&self) -> IdOrNone {
|
|
||||||
match self {
|
|
||||||
ID::NEW(id) => IdOrNone::NEW(*id),
|
|
||||||
ID::EXISTING(id) => IdOrNone::EXISTING(*id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_id(id: IdOrNone) -> ID {
|
|
||||||
match id {
|
|
||||||
IdOrNone::NEW(id) => ID::NEW(id),
|
|
||||||
IdOrNone::EXISTING(id) => ID::EXISTING(id),
|
|
||||||
IdOrNone::NONE => panic!("Cannot convert ID::NONE to EXISTING_ID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_uuid(&self) -> Uuid {
|
|
||||||
match self {
|
|
||||||
ID::NEW(id) => *id,
|
|
||||||
ID::EXISTING(id) => *id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
3
src/util/cmd.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub fn parse_command_line_args() -> Vec<String> {
|
||||||
|
std::env::args().skip(1).collect()
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
use log::{Level, Record};
|
|
||||||
use log4rs::{
|
|
||||||
config::{Deserialize, Deserializers},
|
|
||||||
filter::{Filter, Response},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct TargetFilterConfig {
|
|
||||||
negation: bool,
|
|
||||||
level: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TargetFilter {
|
|
||||||
level: Vec<Level>,
|
|
||||||
negation: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TargetFilter {
|
|
||||||
pub fn new(level: Vec<Level>, negation: bool) -> TargetFilter {
|
|
||||||
TargetFilter { level, negation }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Filter for TargetFilter {
|
|
||||||
fn filter(&self, record: &Record) -> Response {
|
|
||||||
if self.level.contains(&record.level()) {
|
|
||||||
// Always allow error messages
|
|
||||||
if !self.negation {
|
|
||||||
Response::Accept
|
|
||||||
} else {
|
|
||||||
Response::Reject
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if self.negation {
|
|
||||||
Response::Accept
|
|
||||||
} else {
|
|
||||||
Response::Reject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TargetFilterDeserializer;
|
|
||||||
|
|
||||||
impl Deserialize for TargetFilterDeserializer {
|
|
||||||
type Trait = dyn Filter;
|
|
||||||
|
|
||||||
type Config = TargetFilterConfig;
|
|
||||||
|
|
||||||
fn deserialize(
|
|
||||||
&self,
|
|
||||||
config: TargetFilterConfig,
|
|
||||||
_: &Deserializers,
|
|
||||||
) -> anyhow::Result<Box<Self::Trait>> {
|
|
||||||
Ok(Box::new(TargetFilter::new(
|
|
||||||
config
|
|
||||||
.level
|
|
||||||
.iter()
|
|
||||||
.map(|lvl| match lvl.to_lowercase().as_str() {
|
|
||||||
"error" => Level::Error,
|
|
||||||
"warn" => Level::Warn,
|
|
||||||
"info" => Level::Info,
|
|
||||||
"debug" => Level::Debug,
|
|
||||||
"trace" => Level::Trace,
|
|
||||||
_ => panic!("Unknown log level {}", lvl), // Default to Off if level is not recognized
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
config.negation,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initialize_logging(log_file: &str) -> Result<(), anyhow::Error> {
|
|
||||||
let mut target_filter = Deserializers::new();
|
|
||||||
target_filter.insert("target_filter", TargetFilterDeserializer);
|
|
||||||
|
|
||||||
Ok(log4rs::init_file(log_file, target_filter)?)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
pub mod log_filter;
|
pub mod cmd;
|
||||||
pub mod net;
|
pub use cmd::*;
|
||||||
pub use log_filter::*;
|
|
||||||
pub use net::*;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
use reqwest::{
|
|
||||||
Client,
|
|
||||||
header::{HeaderMap, HeaderValue},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn download_file(url: &str, _path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
// Some simple CLI args requirements...
|
|
||||||
|
|
||||||
// reqwest::blocking::get() is a convenience function.
|
|
||||||
//
|
|
||||||
// In most cases, you should create/build a reqwest::Client and reuse
|
|
||||||
// it for all requests.
|
|
||||||
let res = reqwest::get(url).await?;
|
|
||||||
|
|
||||||
let body = res.text().await?;
|
|
||||||
Ok(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn validate_url(url: &str) -> bool {
|
|
||||||
// Check if the URL is valid
|
|
||||||
let parsed_url = url::Url::parse(url);
|
|
||||||
|
|
||||||
if parsed_url.is_err() {
|
|
||||||
eprintln!("Invalid URL: {}", url);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed_url = parsed_url.unwrap();
|
|
||||||
|
|
||||||
// Check if the URL has a valid scheme
|
|
||||||
if !["http", "https"].contains(&parsed_url.scheme()) {
|
|
||||||
eprintln!("Unsupported URL scheme: {}", parsed_url.scheme());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(true)
|
|
||||||
.build()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.get(parsed_url.as_str())
|
|
||||||
.headers(construct_headers())
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(resp) => {
|
|
||||||
println!(
|
|
||||||
"StatusCode: {} {}",
|
|
||||||
resp.status(),
|
|
||||||
resp.status().is_success()
|
|
||||||
);
|
|
||||||
resp.status().is_success()
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("code:{},error:{}", err.status().unwrap_or_default(), err);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn construct_headers() -> HeaderMap {
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"));
|
|
||||||
headers.insert("Accept", HeaderValue::from_static("text/html"));
|
|
||||||
headers.insert(
|
|
||||||
"Referer",
|
|
||||||
HeaderValue::from_static("https://www.google.com"),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
}
|
|
||||||