fix
This commit is contained in:
+125
-15
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user