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",
"serde",
"serde_json",
"zeroize",
]
[[package]]

View File

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

View File

@ -5,7 +5,7 @@
![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>
@ -22,9 +22,10 @@ cargo build --release
`.env` file:
```env
ANTARES_USERCODE=your_usercode
ANTARES_PASSWORD=your_password_hash
OUT=out
URL=https://b2b.antares.hu/YOUR_BASE_URI_HERE
ANTARES_USERCODE=given_usercode
ANTARES_PASSWORD=given_password
OUT=out\\test.xlsx
```
`OUT` can be:
@ -68,4 +69,4 @@ cargo test
## 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!(
"{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam={}",
base, usercode, password, cikkszam
"{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam=",
login.url,
login.usercode,
login.password.as_str()
)
}

View File

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

View File

@ -1,6 +1,13 @@
//! Generated structs for Antares JSON response
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)]
#[serde(rename_all = "PascalCase")]

View File

@ -1,6 +1,13 @@
use std::fs;
use std::io::Write;
use std::path::PathBuf;
// Logger module
use std::{
io::Result as IO_Result,
fs::{
OpenOptions,
create_dir_all
},
io::Write,
path::PathBuf
};
use chrono::Local;
pub struct Logger {
@ -8,9 +15,9 @@ pub struct Logger {
}
impl Logger {
pub fn init() -> std::io::Result<Self> {
pub fn init() -> IO_Result<Self> {
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 log_file = log_dir.join(format!("{}.log", today));
@ -24,7 +31,7 @@ impl Logger {
println!("{}", formatted);
if let Ok(mut file) = fs::OpenOptions::new()
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)
@ -32,12 +39,4 @@ impl Logger {
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 logger;
pub mod env;
pub mod request;
pub use excel::export_to_excel;
pub use logger::Logger;
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)))
}
}