This commit is contained in:
che
2026-07-27 10:51:18 +05:00
parent 560b3fe44e
commit c1b24206b9
13 changed files with 405 additions and 226 deletions
+125 -15
View File
@@ -9,6 +9,7 @@ use che_tauri::{
TauriApi,
};
use tauri::Manager;
use tauri_plugin_opener::OpenerExt;
use crate::sync::SyncContractsResult;
@@ -256,23 +257,76 @@ async fn sync_contracts(
}
#[tauri::command]
async fn load_application_file(local_path: String, scan_url: String) -> Result<Vec<u8>, ApiError> {
if !local_path.is_empty() {
match tokio::fs::metadata(&local_path).await {
Ok(metadata) if metadata.len() > 0 => {
return tokio::fs::read(&local_path)
.await
.map_err(|error| ApiError::new("file_error", error.to_string()));
}
Ok(_) | Err(_) => {}
}
}
async fn load_remote_file(
app: tauri::AppHandle,
api: tauri::State<'_, TauriApi>,
url: String,
) -> Result<Vec<u8>, ApiError> {
let path = cache_remote_file(&app, &api, &url).await?;
tokio::fs::read(path)
.await
.map_err(|error| ApiError::new("file_error", error.to_string()))
}
if scan_url.is_empty() {
#[tauri::command]
async fn ensure_remote_file(
app: tauri::AppHandle,
api: tauri::State<'_, TauriApi>,
url: String,
) -> Result<String, ApiError> {
let path = cache_remote_file(&app, &api, &url).await?;
Ok(path.to_string_lossy().into_owned())
}
#[tauri::command]
async fn open_remote_file(
app: tauri::AppHandle,
api: tauri::State<'_, TauriApi>,
url: String,
) -> Result<(), ApiError> {
let path = cache_remote_file(&app, &api, &url).await?;
app.opener()
.open_path(path.to_string_lossy().into_owned(), None::<&str>)
.map_err(|error| ApiError::new("file_error", error.to_string()))
}
async fn cache_remote_file(
app: &tauri::AppHandle,
api: &tauri::State<'_, TauriApi>,
url: &str,
) -> Result<PathBuf, ApiError> {
if url.trim().is_empty() {
return Err(ApiError::new("file_error", "Файл недоступен"));
}
let response = reqwest::Client::new().get(&scan_url).send().await?;
let remote = api.state().remote_config().ok_or_else(|| {
ApiError::bad_request("load_remote_file requires [remote].base_url config")
})?;
let file_url = normalize_remote_file_url(&remote.base_url, url)?;
let relative_path = remote_file_cache_path(file_url.path())?;
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
let path = app_data_dir.join("remote_files").join(relative_path);
match tokio::fs::metadata(&path).await {
Ok(metadata) if metadata.len() > 0 => {
return Ok(path);
}
Ok(_) | Err(_) => {}
}
let token = api
.state()
.auth_token()
.ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?;
let response = reqwest::Client::new()
.get(file_url)
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::new(
"remote_error",
@@ -285,7 +339,61 @@ async fn load_application_file(local_path: String, scan_url: String) -> Result<V
return Err(ApiError::new("file_error", "Файл пустой"));
}
Ok(bytes.to_vec())
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
}
tokio::fs::write(&path, &bytes)
.await
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
Ok(path)
}
fn normalize_remote_file_url(base_url: &str, url: &str) -> Result<reqwest::Url, ApiError> {
if let Ok(parsed) = reqwest::Url::parse(url.trim()) {
return Ok(parsed);
}
let base = reqwest::Url::parse(&format!("{}/", base_url.trim_end_matches('/')))
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
base.join(url.trim_start_matches('/'))
.map_err(|error| ApiError::new("file_error", error.to_string()))
}
fn remote_file_cache_path(url_path: &str) -> Result<PathBuf, ApiError> {
let mut path = PathBuf::new();
for segment in url_path.split('/') {
if segment.is_empty() || segment == "." || segment == ".." {
continue;
}
path.push(sanitize_path_segment(segment));
}
if path.as_os_str().is_empty() {
Err(ApiError::new("file_error", "URL не содержит имя файла"))
} else {
Ok(path)
}
}
fn sanitize_path_segment(segment: &str) -> String {
let sanitized = segment
.chars()
.map(|character| match character {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
_ => character,
})
.collect::<String>();
if sanitized.is_empty() {
"file".to_string()
} else {
sanitized
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -323,7 +431,9 @@ pub fn run() {
reset_app_settings,
current_employee,
sync_contracts,
load_application_file
load_remote_file,
ensure_remote_file,
open_remote_file
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+3 -73
View File
@@ -158,7 +158,7 @@ struct RemoteEmployee {
pub async fn sync_contracts(
api: &TauriApi,
app_data_dir: PathBuf,
_app_data_dir: PathBuf,
) -> Result<SyncContractsResult, ApiError> {
let state = api.state();
let token = state
@@ -205,12 +205,9 @@ pub async fn sync_contracts(
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?;
upsert_contract_application_file(api, contract.id, application, "").await?;
applications += 1;
if !local_path.is_empty() {
if !application.scan.is_empty() {
files_downloaded += 1;
}
}
@@ -324,69 +321,6 @@ async fn fetch_contract_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(
api: &TauriApi,
contract: &RemoteContract,
@@ -604,7 +538,3 @@ async fn upsert_contract_application_file(
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())
}