This commit is contained in:
Villers Krisztián 2026-05-05 15:16:14 +02:00
parent 84e14fedbc
commit dbbf5c5ef0
10 changed files with 130 additions and 98 deletions

1
Cargo.lock generated
View File

@ -37,6 +37,7 @@ dependencies = [
"rust_xlsxwriter", "rust_xlsxwriter",
"serde", "serde",
"serde_json", "serde_json",
"zeroize",
] ]
[[package]] [[package]]

View File

@ -11,4 +11,4 @@ rust_xlsxwriter = "0.66"
dotenv = "0.15" dotenv = "0.15"
chrono = "0.4" chrono = "0.4"
log = "0.4" log = "0.4"
zeroize = "1.5"

View File

@ -5,7 +5,7 @@
![Octopus ERP Converter](docs/images/Octopus8.ico) ![Octopus ERP Converter](docs/images/Octopus8.ico)
Rust utility to fetch product data from Antares B2B and export to Excel. Rust utility to fetch product data from Antares B2B and export to Excel which can be imported to Octopus 8.
</div> </div>
@ -22,9 +22,10 @@ cargo build --release
`.env` file: `.env` file:
```env ```env
ANTARES_USERCODE=your_usercode URL=https://b2b.antares.hu/YOUR_BASE_URI_HERE
ANTARES_PASSWORD=your_password_hash ANTARES_USERCODE=given_usercode
OUT=out ANTARES_PASSWORD=given_password
OUT=out\\test.xlsx
``` ```
`OUT` can be: `OUT` can be:
@ -68,4 +69,4 @@ cargo test
## License ## License
MIT License © 2026 Orink Hungary Kft. MIT [License](./LICENSE) © 2026 Orink Hungary Kft.

View File

@ -1,6 +1,10 @@
pub fn make_url(base: &str, usercode: &str, password: &str, cikkszam: &str) -> String { use crate::template::antares::AntaresLogin;
pub fn make_url(login: AntaresLogin) -> String {
format!( format!(
"{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam={}", "{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam=",
base, usercode, password, cikkszam login.url,
login.usercode,
login.password.as_str()
) )
} }

View File

@ -2,24 +2,22 @@ mod api;
mod template; mod template;
mod tools; mod tools;
use reqwest::blocking::Client; use zeroize::Zeroizing;
use std::{ use std::{
error::Error, error::Error,
fs, fs,
time::Duration,
io::{ io::{
Error as IO_Error, Error as IO_Error,
ErrorKind::{ ErrorKind::Other
Other, },
ConnectionRefused path::PathBuf
}
}
}; };
use api::make_url; use api::make_url;
use template::antares::Antares; use template::antares::{AntaresLogin, Antares};
use tools::{ use tools::{
Logger, Logger,
get_env, get_env,
make_request,
export_to_excel export_to_excel
}; };
@ -28,68 +26,41 @@ const USERCODE_KEY: &str = "ANTARES_USERCODE";
const PASSWORD_KEY: &str = "ANTARES_PASSWORD"; const PASSWORD_KEY: &str = "ANTARES_PASSWORD";
const OUT_KEY: &str = "OUT"; const OUT_KEY: &str = "OUT";
fn make_request(url: &str, logger: &Logger) -> Result<String, Box<dyn Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(300))
.build().map_err(|e| {
logger.log_error(&format!("Failed to create HTTP client: {}", e));
Box::new(IO_Error::new(Other, e.to_string()))
})?;
let response = client.get(url).send().map_err(|e| {
logger.log_error(&format!("HTTP request failed: {}", e));
Box::new(IO_Error::new(ConnectionRefused, e.to_string()))
})?;
if response.status().is_success() {
Ok(response.text().map_err(|e| {
logger.log_error(&format!("Failed to read response body: {}", e));
Box::new(IO_Error::new(Other, e.to_string()))
})?)
} else {
let error_msg = format!("HTTP error: {:?}", response.status());
logger.log_error(&error_msg);
Err(Box::new(IO_Error::new(ConnectionRefused, error_msg)))
}
}
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let logger = Logger::init()?; let logger = Logger::init()?;
logger.log_info("Starting Antares data export"); logger.log("Starting Antares data export");
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let usercode = get_env(USERCODE_KEY) let antares_login = AntaresLogin{
.map_err(|e| { url: get_env(URL_KEY)
logger.log_error(&e); .map_err(|e| {
e logger.log(&e);
})?; e
})?,
let password = get_env(PASSWORD_KEY) usercode: get_env(USERCODE_KEY)
.map_err(|e| { .map_err(|e| {
logger.log_error(&e); logger.log(&e);
e e
})?; })?,
password: Zeroizing::new(
get_env(PASSWORD_KEY)
.map_err(|e| {
logger.log(&e);
e
})?
)
};
let out_path = get_env(OUT_KEY) let out_path = get_env(OUT_KEY)
.map_err(|e| { .map_err(|e| {
logger.log_error(&e); logger.log(&e);
e e
})?; })?;
let url = get_env(URL_KEY) let response = match make_request(&make_url(antares_login), &logger) {
.map_err(|e| {
logger.log_error(&e);
e
})?;
let cikkszam = ""; // supply a value if needed;
let url = make_url(&url, &usercode, &password, cikkszam);
let response = match make_request(&url, &logger) {
Ok(resp) => { Ok(resp) => {
logger.log_info("API request sent successfully"); logger.log("API request sent successfully");
resp resp
} }
Err(e) => { Err(e) => {
@ -98,10 +69,11 @@ fn main() -> Result<(), Box<dyn Error>> {
}; };
// Ensure `temp/` exists and prepare path for `antares.json`. // Ensure `temp/` exists and prepare path for `antares.json`.
let temp_dir = "temp"; let temp_dir = PathBuf::from("temp");
let antares_file = format!("{}/antares.json", temp_dir); let antares_file = PathBuf::from(temp_dir.clone())
fs::create_dir_all(temp_dir).map_err(|e| { .join("antares.json");
logger.log_error(&format!("Failed to create temp directory '{}': {}", temp_dir, e)); fs::create_dir_all(&temp_dir).map_err(|e| {
logger.log(&format!("Failed to create temp directory '{:#?}': {}", temp_dir, e));
Box::new(IO_Error::new(Other, e.to_string())) Box::new(IO_Error::new(Other, e.to_string()))
})?; })?;
@ -112,12 +84,12 @@ fn main() -> Result<(), Box<dyn Error>> {
let pretty = match serde_json::to_string_pretty(&items) { let pretty = match serde_json::to_string_pretty(&items) {
Ok(pretty) => pretty, Ok(pretty) => pretty,
Err(e) => { Err(e) => {
logger.log_error(&format!("Failed to serialize items: {}", e)); logger.log(&format!("Failed to serialize items: {}", e));
return Err(Box::new(e)); return Err(Box::new(e));
} }
}; };
fs::write(&antares_file, &pretty).map_err(|e| { fs::write(&antares_file, &pretty).map_err(|e| {
logger.log_error(&format!("Failed to write {}: {}", antares_file, e)); logger.log(&format!("Failed to write {:#?}: {}", antares_file, e));
Box::new(IO_Error::new(Other, e.to_string())) Box::new(IO_Error::new(Other, e.to_string()))
})?; })?;
logger.log(&format!("API call successful - Fetched {} items", items.len())); logger.log(&format!("API call successful - Fetched {} items", items.len()));
@ -129,21 +101,21 @@ fn main() -> Result<(), Box<dyn Error>> {
let pretty = match serde_json::to_string_pretty(&json_val) { let pretty = match serde_json::to_string_pretty(&json_val) {
Ok(pretty) => pretty, Ok(pretty) => pretty,
Err(e) => { Err(e) => {
logger.log_error(&format!("Failed to serialize JSON: {}", e)); logger.log(&format!("Failed to serialize JSON: {}", e));
return Err(Box::new(e)); return Err(Box::new(e));
} }
}; };
fs::write(&antares_file, &pretty).map_err(|e| { fs::write(&antares_file, &pretty).map_err(|e| {
logger.log_error(&format!("Failed to write {}: {}", antares_file, e)); logger.log(&format!("Failed to write {:#?}: {}", antares_file, e));
Box::new(IO_Error::new(Other, e.to_string())) Box::new(IO_Error::new(Other, e.to_string()))
})?; })?;
logger.log_info("Response saved as JSON (schema mismatch)"); logger.log("Response saved as JSON (schema mismatch)");
} else { } else {
fs::write(&antares_file, &response).map_err(|e| { fs::write(&antares_file, &response).map_err(|e| {
logger.log_error(&format!("Failed to write {}: {}", antares_file, e)); logger.log(&format!("Failed to write {:#?}: {}", antares_file, e));
Box::new(IO_Error::new(Other, e.to_string())) Box::new(IO_Error::new(Other, e.to_string()))
})?; })?;
logger.log_info("Response saved as raw text (not valid JSON)"); logger.log("Response saved as raw text (not valid JSON)");
} }
Vec::new() Vec::new()
} }
@ -155,11 +127,11 @@ fn main() -> Result<(), Box<dyn Error>> {
logger.log(&format!("Export successful - {} rows exported to {}", items.len(), out_path)); logger.log(&format!("Export successful - {} rows exported to {}", items.len(), out_path));
} }
Err(e) => { Err(e) => {
logger.log_error(&format!("Export failed: {}", e)); logger.log(&format!("Export failed: {}", e));
return Err(e); return Err(e);
} }
} }
logger.log_info("Export process completed successfully"); logger.log("Export process completed successfully");
Ok(()) Ok(())
} }

View File

@ -1,6 +1,13 @@
//! Generated structs for Antares JSON response //! Generated structs for Antares JSON response
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
#[derive(Clone)]
pub struct AntaresLogin {
pub url: String,
pub usercode: String,
pub password: Zeroizing<String>
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]

View File

@ -1,6 +1,13 @@
use std::fs; // Logger module
use std::io::Write; use std::{
use std::path::PathBuf; io::Result as IO_Result,
fs::{
OpenOptions,
create_dir_all
},
io::Write,
path::PathBuf
};
use chrono::Local; use chrono::Local;
pub struct Logger { pub struct Logger {
@ -8,9 +15,9 @@ pub struct Logger {
} }
impl Logger { impl Logger {
pub fn init() -> std::io::Result<Self> { pub fn init() -> IO_Result<Self> {
let log_dir = PathBuf::from("log"); let log_dir = PathBuf::from("log");
fs::create_dir_all(&log_dir)?; create_dir_all(&log_dir)?;
let today = Local::now().format("%Y-%m-%d").to_string(); let today = Local::now().format("%Y-%m-%d").to_string();
let log_file = log_dir.join(format!("{}.log", today)); let log_file = log_dir.join(format!("{}.log", today));
@ -24,7 +31,7 @@ impl Logger {
println!("{}", formatted); println!("{}", formatted);
if let Ok(mut file) = fs::OpenOptions::new() if let Ok(mut file) = OpenOptions::new()
.create(true) .create(true)
.append(true) .append(true)
.open(&self.log_file) .open(&self.log_file)
@ -32,12 +39,4 @@ impl Logger {
let _ = writeln!(file, "{}", formatted); let _ = writeln!(file, "{}", formatted);
} }
} }
pub fn log_info(&self, message: &str) {
self.log(&format!(" {}", message));
}
pub fn log_error(&self, error: &str) {
self.log(&format!("✗ Error: {}", error));
}
} }

View File

@ -1,7 +1,9 @@
pub mod excel; pub mod excel;
pub mod logger; pub mod logger;
pub mod env; pub mod env;
pub mod request;
pub use excel::export_to_excel;
pub use logger::Logger; pub use logger::Logger;
pub use env::get_env; pub use env::get_env;
pub use request::make_request;
pub use excel::export_to_excel;

46
src/tools/request.rs Normal file
View File

@ -0,0 +1,46 @@
// Request module
use crate::tools::Logger;
use std::{
error::Error,
time::Duration,
io::{
Error as IO_Error, ErrorKind::{
ConnectionRefused, Other
}
}
};
use reqwest::blocking::Client;
pub fn get_client() -> Result<Client, reqwest::Error> {
Client::builder()
.timeout(Duration::from_secs(600))
.build()
}
pub fn make_request(url: &str, logger: &Logger) -> Result<String, Box<dyn Error>> {
let client = get_client()
.map_err(|e| {
logger.log(&format!("Failed to create HTTP client: {}", e));
Box::new(IO_Error::new(Other, e.to_string()))
})?;
let response = client.get(url)
.send()
.map_err(|e| {
logger.log(&format!("HTTP request failed: {}", e));
Box::new(IO_Error::new(ConnectionRefused, e.to_string()))
})?;
if response.status().is_success() {
Ok(response.text().map_err(|e| {
logger.log(&format!("Failed to read response body: {}", e));
Box::new(IO_Error::new(ConnectionRefused, e.to_string()))
})?)
} else {
let error_msg = format!("HTTP error: {:?}", response.status());
logger.log(&error_msg);
Err(Box::new(IO_Error::new(ConnectionRefused, error_msg)))
}
}