Compare commits
2 Commits
5a4bbc5297
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e6f2397da6
|
|||
|
1089196226
|
13
.gitignore
vendored
@@ -20,8 +20,11 @@ Cargo.lock
|
||||
# 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.
|
||||
#.idea/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.*.example
|
||||
# Ignore the .env file that contains sensitive information
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.*.example
|
||||
# Ignore the .env file that contains sensitive information
|
||||
|
||||
log/*
|
||||
!log/.keep
|
||||
|
||||
110
.vscode/launch.json
vendored
@@ -1,45 +1,69 @@
|
||||
{
|
||||
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
||||
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
||||
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'no-man-sky'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=no-man-sky",
|
||||
"--package=no-man-sky"
|
||||
],
|
||||
"filter": {
|
||||
"name": "no-man-sky",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'no-man-sky'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=no-man-sky",
|
||||
"--package=no-man-sky"
|
||||
],
|
||||
"filter": {
|
||||
"name": "no-man-sky",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
||||
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
||||
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Server NoManSky",
|
||||
"cargo": {
|
||||
"args": ["build", "--bin=no-man-sky", "--package=no-man-sky"],
|
||||
"filter": {
|
||||
"name": "no-man-sky",
|
||||
"kind": "bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"args": ["-s"],
|
||||
"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}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'no-man-sky'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=no-man-sky",
|
||||
"--package=no-man-sky"
|
||||
],
|
||||
"filter": {
|
||||
"name": "no-man-sky",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
7
.vscode/settings.json
vendored
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"DATABASENAME",
|
||||
"dotenv",
|
||||
"Herstellung",
|
||||
"Insertable",
|
||||
"Kochen",
|
||||
"mordit",
|
||||
"MSVC",
|
||||
"Quelle",
|
||||
"Raffination",
|
||||
"rustc",
|
||||
"serde",
|
||||
"Stück",
|
||||
"VARCHAR",
|
||||
"Verwendung"
|
||||
]
|
||||
}
|
||||
|
||||
24
Cargo.toml
@@ -3,16 +3,28 @@ name = "no-man-sky"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = ["merlin"]
|
||||
description = "Utility functions to read environment with fallback and values from a file"
|
||||
description = "NoManSky Server/Client utility"
|
||||
license = "MIT"
|
||||
repository = "ssh://git@gitea.merlinserver.de:2222/Stefan/merlin_env_helper.git"
|
||||
repository = "ssh://git@gitea.merlinserver.de:2222/Stefan/no-man-sky-wiki.git"
|
||||
|
||||
[dependencies]
|
||||
diesel = { version= "2.2.10", features = ["serde_json", "postgres", "uuid"]}
|
||||
env_logger = "0.11.8"
|
||||
anyhow = "1.0.98"
|
||||
axum = "0.8.4"
|
||||
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"
|
||||
merlin_env_helper = { version = "0.2.0", registry = "merlin" }
|
||||
log4rs = "1.3.0"
|
||||
merlin_env_helper = { version = "0.4.0", registry = "merlin" }
|
||||
once_cell = "1.21.3"
|
||||
regex = "1.11.1"
|
||||
reqwest = {version="0.12.15", features=["blocking"]}
|
||||
reqwest = {version="0.12.15" }
|
||||
#scraper = "0.23.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"
|
||||
|
||||
BIN
images/resources/NANITE.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
images/resources/PRODUCT.HEXCORE.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/resources/Product.atlasstone.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/resources/Product.metallic.1-1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/resources/Product.metallic.1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/resources/Product.metallic.2.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/resources/Product.metallic.3-1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/resources/Product.metallic.3.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/resources/Product.metallic.4.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/resources/SUBSTANCE.AIR.1.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.2.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.3-1.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.3-2.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.3-3.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.3-4.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/resources/SUBSTANCE.ASTEROID.3.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/resources/SUBSTANCE.LAND.1.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/resources/SUBSTANCE.LAND.3.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/resources/SUBSTANCE.STELLAR.2.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
44
log4jrs.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
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
|
||||
0
logs/.keep
Normal file
352447
logs/no-man-sky.log
Normal file
@@ -1,37 +1,40 @@
|
||||
CREATE TABLE "icon"(
|
||||
"id" UUID NOT NULL PRIMARY KEY,
|
||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"content_type" VARCHAR(255),
|
||||
"path" VARCHAR(512) NOT NULL,
|
||||
"url" VARCHAR(512),
|
||||
"width" INT4,
|
||||
"height" INT4,
|
||||
"state" JSON
|
||||
"state" JSONB
|
||||
);
|
||||
|
||||
CREATE TABLE "resource"(
|
||||
"id" UUID NOT NULL PRIMARY KEY,
|
||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"title" VARCHAR(255) NOT NULL,
|
||||
"url" VARCHAR(512),
|
||||
"icon" UUID,
|
||||
"state" JSON,
|
||||
"state" JSONB,
|
||||
FOREIGN KEY ("icon") REFERENCES "icon"("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "recipe"(
|
||||
"id" UUID NOT NULL PRIMARY KEY,
|
||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
||||
"resource" UUID NOT NULL,
|
||||
"quantity" INT4 NOT NULL,
|
||||
"recipe_type" VARCHAR(50) NOT NULL,
|
||||
"duration" INT4 NOT NULL,
|
||||
"state" JSON,
|
||||
"unit" VARCHAR(50) NOT NULL,
|
||||
"state" JSONB,
|
||||
FOREIGN KEY ("resource") REFERENCES "resource"("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "ingredient"(
|
||||
"id" UUID NOT NULL PRIMARY KEY,
|
||||
"id" UUID DEFAULT gen_random_uuid () NOT NULL PRIMARY KEY,
|
||||
"resource" UUID NOT NULL,
|
||||
"quantity" INT4 NOT NULL,
|
||||
"state" JSON,
|
||||
"state" JSONB,
|
||||
"recipe" UUID NOT NULL,
|
||||
FOREIGN KEY ("resource") REFERENCES "resource"("id"),
|
||||
FOREIGN KEY ("recipe") REFERENCES "recipe"("id")
|
||||
|
||||
16
no_man_sky.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[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"
|
||||
123
src/client/mod.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
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(())
|
||||
}
|
||||
274
src/configuration/mod.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
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,2 +1,50 @@
|
||||
pub mod models;
|
||||
pub use models::*;
|
||||
pub mod schema;
|
||||
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,46 +2,101 @@
|
||||
|
||||
#![allow(unused)]
|
||||
#![allow(clippy::all)]
|
||||
use diesel::{
|
||||
prelude::Queryable,
|
||||
sql_types::{Json, Uuid},
|
||||
};
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
pub struct RowIcon {
|
||||
use diesel::{pg::Pg, prelude::*};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[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 name: String,
|
||||
pub content_type: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub width: Option<i32>,
|
||||
pub height: Option<i32>,
|
||||
pub state: Option<Json>,
|
||||
pub content_type: String,
|
||||
pub url: String,
|
||||
pub path: String,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub state: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
pub struct RowIngredient {
|
||||
#[derive(Insertable, Debug)]
|
||||
#[diesel(table_name = crate::db::schema::icon)]
|
||||
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 resource: Uuid,
|
||||
pub quantity: i32,
|
||||
pub state: Option<Json>,
|
||||
pub state: Option<serde_json::Value>,
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
pub struct RowRecipe {
|
||||
#[derive(Identifiable, Queryable, AsChangeset, Associations, Selectable, Debug)]
|
||||
#[diesel(table_name = crate::db::schema::recipe)]
|
||||
#[diesel(belongs_to(DbResource, foreign_key = resource))]
|
||||
#[diesel(check_for_backend(Pg))]
|
||||
pub struct DbRecipe {
|
||||
pub id: Uuid,
|
||||
pub resource: Uuid,
|
||||
pub quantity: i32,
|
||||
pub recipe_type: String,
|
||||
pub duration: i32,
|
||||
pub state: Option<Json>,
|
||||
pub unit: String,
|
||||
pub state: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug)]
|
||||
pub struct RowResource {
|
||||
#[derive(Insertable, Debug, Clone)]
|
||||
#[diesel(table_name = crate::db::schema::recipe)]
|
||||
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 name: String,
|
||||
pub title: String,
|
||||
pub url: Option<String>,
|
||||
pub icon: Option<Uuid>,
|
||||
pub state: Option<Json>,
|
||||
pub state: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[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,12 +6,14 @@ diesel::table! {
|
||||
#[max_length = 255]
|
||||
name -> Varchar,
|
||||
#[max_length = 255]
|
||||
content_type -> Nullable<Varchar>,
|
||||
#[max_length = 512]
|
||||
url -> Nullable<Varchar>,
|
||||
width -> Nullable<Int4>,
|
||||
height -> Nullable<Int4>,
|
||||
state -> Nullable<Json>,
|
||||
content_type -> Varchar,
|
||||
#[max_length = 1024]
|
||||
path -> Varchar,
|
||||
#[max_length = 2048]
|
||||
url -> Varchar,
|
||||
width -> Int4,
|
||||
height -> Int4,
|
||||
state -> Nullable<Jsonb>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +22,7 @@ diesel::table! {
|
||||
id -> Uuid,
|
||||
resource -> Uuid,
|
||||
quantity -> Int4,
|
||||
state -> Nullable<Json>,
|
||||
state -> Nullable<Jsonb>,
|
||||
recipe -> Uuid,
|
||||
}
|
||||
}
|
||||
@@ -29,10 +31,13 @@ diesel::table! {
|
||||
recipe (id) {
|
||||
id -> Uuid,
|
||||
resource -> Uuid,
|
||||
quantity -> Int4,
|
||||
#[max_length = 50]
|
||||
recipe_type -> Varchar,
|
||||
duration -> Int4,
|
||||
state -> Nullable<Json>,
|
||||
#[max_length = 50]
|
||||
unit -> Varchar,
|
||||
state -> Nullable<Jsonb>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +51,7 @@ diesel::table! {
|
||||
#[max_length = 512]
|
||||
url -> Nullable<Varchar>,
|
||||
icon -> Nullable<Uuid>,
|
||||
state -> Nullable<Json>,
|
||||
state -> Nullable<Jsonb>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +60,4 @@ diesel::joinable!(ingredient -> resource (resource));
|
||||
diesel::joinable!(recipe -> resource (resource));
|
||||
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
Normal file
@@ -0,0 +1,350 @@
|
||||
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,30 +1,51 @@
|
||||
mod client;
|
||||
mod configuration;
|
||||
mod db;
|
||||
mod parse;
|
||||
mod server;
|
||||
mod types;
|
||||
mod util;
|
||||
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use parse::parse;
|
||||
use clap::Parser;
|
||||
use dotenv::dotenv;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
let html = read("test_mordit.html")?;
|
||||
parse(&html);
|
||||
Ok(())
|
||||
use crate::{
|
||||
client::run_client,
|
||||
configuration::{CONFIGURATION, Configuration},
|
||||
server::run_server,
|
||||
types::CallArguments,
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
fn _download_file(url: &str, _path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Some simple CLI args requirements...
|
||||
async fn _run_client(cli: &CallArguments) -> Result<(), Box<dyn std::error::Error>> {
|
||||
run_client(cli).await
|
||||
}
|
||||
|
||||
eprintln!("Fetching {url:?}...");
|
||||
|
||||
// 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)
|
||||
async fn _run_server() -> Result<(), Box<dyn std::error::Error>> {
|
||||
run_server().await
|
||||
}
|
||||
|
||||
fn read(path: &str) -> Result<String, std::io::Error> {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::types::{Duration, Icon, Ingredient, Recipe, RecipeType, Resource, ResourceState};
|
||||
use crate::types::{
|
||||
Duration, Icon, Ingredient, Recipe, RecipeType, Resource, ResourceState, ResourceWithQuantity,
|
||||
};
|
||||
use regex::Regex;
|
||||
use select::{
|
||||
document::Document,
|
||||
@@ -27,7 +29,7 @@ type ResourceTmp = (
|
||||
Option<Duration>, // duration
|
||||
);
|
||||
|
||||
pub fn parse(html: &str) {
|
||||
pub fn parse(html: &str) -> Vec<Recipe> {
|
||||
let document = Document::from(html);
|
||||
|
||||
let mut map_resource: HashMap<String, Resource> = HashMap::new();
|
||||
@@ -67,9 +69,7 @@ pub fn parse(html: &str) {
|
||||
&mut recipes,
|
||||
);
|
||||
|
||||
print_recipe(&recipes, RecipeType::Refining);
|
||||
print_recipe(&recipes, RecipeType::Production);
|
||||
print_recipe(&recipes, RecipeType::Cooking);
|
||||
recipes
|
||||
}
|
||||
|
||||
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() {
|
||||
let recipe = crate::types::Recipe {
|
||||
recipe_type: recipe_type,
|
||||
resource: ingredient,
|
||||
resource: ResourceWithQuantity::from_ingredient(ingredient),
|
||||
duration: tmp_resource.4.unwrap_or(Duration {
|
||||
millis: 0,
|
||||
unit: "Stück".to_string(),
|
||||
@@ -193,10 +193,16 @@ fn print_recipe(recipes: &Vec<Recipe>, recipe_type: RecipeType) {
|
||||
.filter(|recipe| recipe.recipe_type == recipe_type)
|
||||
{
|
||||
println!("Recipe Type: {:?}", recipe.recipe_type);
|
||||
let icon = match recipe.resource.icon {
|
||||
Some(ref icon) => format!("{}", icon),
|
||||
None => "(none)".to_string(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"Resource: {} ({})",
|
||||
recipe.resource.resource.name, recipe.resource.quantity
|
||||
"Resource: {} ({}) => {}",
|
||||
recipe.resource.name, recipe.resource.quantity, icon
|
||||
);
|
||||
|
||||
println!("Duration: {} ms", recipe.duration.millis);
|
||||
println!("Ingredients:");
|
||||
for ingredient in &recipe.ingredients {
|
||||
@@ -220,7 +226,6 @@ fn parse_dst(
|
||||
let dest = first(&mut dest);
|
||||
|
||||
if dest.is_none() {
|
||||
eprintln!("No element found with the selector '#{}'", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -368,6 +373,7 @@ fn parse_li_to_resource(
|
||||
resource_items.push(ParseType::Img(Icon {
|
||||
name: name.to_string(),
|
||||
url: url.to_string(),
|
||||
path: None, // Path will be set later
|
||||
width,
|
||||
height,
|
||||
content_type: "image/png".to_string(), // Assuming PNG, adjust as needed
|
||||
|
||||
34
src/server/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
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(())
|
||||
}
|
||||
73
src/server/router.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
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))
|
||||
}
|
||||
290
src/server/util.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
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,3 +1,12 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use clap::{Parser, Subcommand};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ResourceState {
|
||||
Parsed(bool),
|
||||
@@ -6,16 +15,30 @@ pub enum ResourceState {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Icon {
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
pub url: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
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 {
|
||||
fn clone(&self) -> Self {
|
||||
Icon {
|
||||
name: self.name.clone(),
|
||||
path: self.path.clone(),
|
||||
url: self.url.clone(),
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
@@ -24,7 +47,17 @@ impl Clone for Icon {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
impl Display for Icon {
|
||||
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 name: String,
|
||||
pub title: String,
|
||||
@@ -33,6 +66,127 @@ pub struct Resource {
|
||||
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)]
|
||||
pub struct Ingredient {
|
||||
pub resource: Resource,
|
||||
@@ -46,6 +200,32 @@ pub enum RecipeType {
|
||||
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)]
|
||||
pub struct Duration {
|
||||
pub millis: u64,
|
||||
@@ -55,7 +235,122 @@ pub struct Duration {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Recipe {
|
||||
pub recipe_type: RecipeType,
|
||||
pub resource: Ingredient,
|
||||
pub resource: ResourceWithQuantity,
|
||||
pub duration: Duration,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
pub fn parse_command_line_args() -> Vec<String> {
|
||||
std::env::args().skip(1).collect()
|
||||
}
|
||||
79
src/util/log_filter.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
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,2 +1,4 @@
|
||||
pub mod cmd;
|
||||
pub use cmd::*;
|
||||
pub mod log_filter;
|
||||
pub mod net;
|
||||
pub use log_filter::*;
|
||||
pub use net::*;
|
||||
|
||||
72
src/util/net.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
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
|
||||
}
|
||||