fix
This commit is contained in:
+196
-2
@@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use che_orm::__private::sqlx;
|
||||
use che_tauri::{ApiError, TauriApi};
|
||||
use serde::Deserialize;
|
||||
@@ -6,6 +8,8 @@ use serde::Deserialize;
|
||||
pub struct SyncContractsResult {
|
||||
pub synced: usize,
|
||||
pub pages: usize,
|
||||
pub applications: usize,
|
||||
pub files_downloaded: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -66,6 +70,33 @@ struct RemoteBillCost {
|
||||
paid_sum: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteContractDetail {
|
||||
#[serde(default)]
|
||||
contractapplicationfile_set: Vec<RemoteContractApplicationFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteContractApplicationFile {
|
||||
id: i64,
|
||||
name: String,
|
||||
file_type: String,
|
||||
#[serde(rename = "get_file_type_display")]
|
||||
file_type_display: String,
|
||||
status: String,
|
||||
#[serde(rename = "get_status_display")]
|
||||
status_display: String,
|
||||
#[serde(rename = "get_absolute_url")]
|
||||
absolute_url: String,
|
||||
#[serde(rename = "get_scan")]
|
||||
scan_name: String,
|
||||
scan: String,
|
||||
comment: String,
|
||||
bill_total: f64,
|
||||
bill_cost_total: f64,
|
||||
questionnair: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteCompany {
|
||||
id: i64,
|
||||
@@ -106,7 +137,10 @@ struct RemoteEmployee {
|
||||
avatar_small: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiError> {
|
||||
pub async fn sync_contracts(
|
||||
api: &TauriApi,
|
||||
app_data_dir: PathBuf,
|
||||
) -> Result<SyncContractsResult, ApiError> {
|
||||
let state = api.state();
|
||||
let token = state
|
||||
.auth_token()
|
||||
@@ -123,6 +157,8 @@ pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiEr
|
||||
));
|
||||
let mut synced = 0;
|
||||
let mut pages = 0;
|
||||
let mut applications = 0;
|
||||
let mut files_downloaded = 0;
|
||||
|
||||
while let Some(url) = next_url {
|
||||
let response = client
|
||||
@@ -146,13 +182,116 @@ pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiEr
|
||||
for contract in &page.results {
|
||||
upsert_contract_dependencies(api, contract).await?;
|
||||
upsert_contract(api, contract).await?;
|
||||
let detail = fetch_contract_detail(&client, &token, &remote.base_url, contract.id).await?;
|
||||
|
||||
for application in &detail.contractapplicationfile_set {
|
||||
let local_path =
|
||||
download_application_file(&client, &token, &app_data_dir, application).await?;
|
||||
upsert_contract_application_file(api, contract.id, application, &local_path).await?;
|
||||
applications += 1;
|
||||
if !local_path.is_empty() {
|
||||
files_downloaded += 1;
|
||||
}
|
||||
}
|
||||
|
||||
synced += 1;
|
||||
}
|
||||
|
||||
next_url = page.links.next;
|
||||
}
|
||||
|
||||
Ok(SyncContractsResult { synced, pages })
|
||||
Ok(SyncContractsResult {
|
||||
synced,
|
||||
pages,
|
||||
applications,
|
||||
files_downloaded,
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_contract_detail(
|
||||
client: &reqwest::Client,
|
||||
token: &str,
|
||||
base_url: &str,
|
||||
contract_id: i64,
|
||||
) -> Result<RemoteContractDetail, ApiError> {
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/api/contract/{contract_id}/",
|
||||
base_url.trim_end_matches('/')
|
||||
))
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let detail = response.text().await.unwrap_or_default();
|
||||
return Err(ApiError::new(
|
||||
"remote_error",
|
||||
format!("remote detail request failed with {status}: {detail}"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response.json::<RemoteContractDetail>().await?)
|
||||
}
|
||||
|
||||
async fn download_application_file(
|
||||
client: &reqwest::Client,
|
||||
token: &str,
|
||||
app_data_dir: &PathBuf,
|
||||
application: &RemoteContractApplicationFile,
|
||||
) -> Result<String, ApiError> {
|
||||
if application.scan.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let directory = application_download_dir(app_data_dir);
|
||||
tokio::fs::create_dir_all(&directory)
|
||||
.await
|
||||
.map_err(file_error)?;
|
||||
|
||||
let file_name = format!("{}_{}", application.id, sanitize_file_name(&application.scan_name));
|
||||
let path = directory.join(file_name);
|
||||
|
||||
let response = client
|
||||
.get(&application.scan)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let detail = response.text().await.unwrap_or_default();
|
||||
return Err(ApiError::new(
|
||||
"remote_error",
|
||||
format!("file download failed with {status}: {detail}"),
|
||||
));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
tokio::fs::write(&path, bytes).await.map_err(file_error)?;
|
||||
|
||||
Ok(path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
fn application_download_dir(app_data_dir: &PathBuf) -> PathBuf {
|
||||
app_data_dir.join("contract_applications")
|
||||
}
|
||||
|
||||
fn sanitize_file_name(file_name: &str) -> String {
|
||||
let sanitized = file_name
|
||||
.chars()
|
||||
.map(|character| match character {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
_ => character,
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
"application_file".to_string()
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
||||
async fn upsert_contract_dependencies(
|
||||
@@ -316,6 +455,61 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upsert_contract_application_file(
|
||||
api: &TauriApi,
|
||||
contract_id: i64,
|
||||
application: &RemoteContractApplicationFile,
|
||||
local_path: &str,
|
||||
) -> Result<(), ApiError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO contract_application_file (
|
||||
id, contract_id, name, file_type, file_type_display, status, status_display,
|
||||
absolute_url, scan_name, scan_url, local_path, comment, bill_total,
|
||||
bill_cost_total, questionnair
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
contract_id = excluded.contract_id,
|
||||
name = excluded.name,
|
||||
file_type = excluded.file_type,
|
||||
file_type_display = excluded.file_type_display,
|
||||
status = excluded.status,
|
||||
status_display = excluded.status_display,
|
||||
absolute_url = excluded.absolute_url,
|
||||
scan_name = excluded.scan_name,
|
||||
scan_url = excluded.scan_url,
|
||||
local_path = excluded.local_path,
|
||||
comment = excluded.comment,
|
||||
bill_total = excluded.bill_total,
|
||||
bill_cost_total = excluded.bill_cost_total,
|
||||
questionnair = excluded.questionnair",
|
||||
)
|
||||
.bind(application.id)
|
||||
.bind(contract_id)
|
||||
.bind(&application.name)
|
||||
.bind(&application.file_type)
|
||||
.bind(&application.file_type_display)
|
||||
.bind(&application.status)
|
||||
.bind(&application.status_display)
|
||||
.bind(&application.absolute_url)
|
||||
.bind(&application.scan_name)
|
||||
.bind(&application.scan)
|
||||
.bind(local_path)
|
||||
.bind(&application.comment)
|
||||
.bind(application.bill_total)
|
||||
.bind(application.bill_cost_total)
|
||||
.bind(application.questionnair)
|
||||
.execute(api.state().db().pool())
|
||||
.await
|
||||
.map_err(database_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn database_error(error: sqlx::Error) -> ApiError {
|
||||
ApiError::new("database_error", error.to_string())
|
||||
}
|
||||
|
||||
fn file_error(error: impl std::fmt::Display) -> ApiError {
|
||||
ApiError::new("file_error", error.to_string())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user