Добавлен фитбек переноса файлов
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
logs/
|
logs/
|
||||||
*.log
|
*.log
|
||||||
out.txt
|
out.txt
|
||||||
|
.build_timing
|
||||||
|
|
||||||
# Выходные файлы
|
# Выходные файлы
|
||||||
output/
|
output/
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ impl SmbFileAccess {
|
|||||||
|
|
||||||
/// Построить полный UNC путь к файлу, гарантируя один слеш между базой и именем
|
/// Построить полный UNC путь к файлу, гарантируя один слеш между базой и именем
|
||||||
fn make_file_unc(base: &UncPath, filename: &str) -> Result<UncPath> {
|
fn make_file_unc(base: &UncPath, filename: &str) -> Result<UncPath> {
|
||||||
let base_str = base.to_string(); // \\server\share\path
|
let base_str = base.to_string();
|
||||||
// Убираем завершающий слеш, если есть
|
|
||||||
let clean_base = base_str.trim_end_matches('\\');
|
let clean_base = base_str.trim_end_matches('\\');
|
||||||
// Убираем возможный ведущий слеш у filename, чтобы избежать двойного
|
// Заменяем все прямые слеши на обратные для UNC
|
||||||
let clean_filename = filename.trim_start_matches('\\');
|
let sanitized = filename.replace('/', "\\");
|
||||||
|
let clean_filename = sanitized.trim_start_matches('\\');
|
||||||
let full = format!("{}\\{}", clean_base, clean_filename);
|
let full = format!("{}\\{}", clean_base, clean_filename);
|
||||||
UncPath::from_str(&full).map_err(|e| anyhow!("Invalid file UNC: {}", e))
|
UncPath::from_str(&full).map_err(|e| anyhow!("Invalid file UNC: {}", e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,8 +222,9 @@
|
|||||||
const previewBtn = job.state === 'finished'
|
const previewBtn = job.state === 'finished'
|
||||||
? `<button class="btn btn-outline btn-sm" onclick="playVideo('${escapeHtml(job.outfile_name)}')"><i class="fas fa-play"></i></button>`
|
? `<button class="btn btn-outline btn-sm" onclick="playVideo('${escapeHtml(job.outfile_name)}')"><i class="fas fa-play"></i></button>`
|
||||||
: '';
|
: '';
|
||||||
const moveBtn = job.state === 'finished'
|
// Заглушка‑иконка, обновится асинхронно
|
||||||
? `<button class="btn btn-primary btn-sm" onclick="moveVideo('${escapeHtml(job.outfile_name)}')"><i class="fas fa-share"></i></button>`
|
const moveIcon = job.state === 'finished'
|
||||||
|
? `<span class="move-icon" data-file="${escapeHtml(job.outfile_name)}"><i class="fas fa-spinner fa-spin"></i></span>`
|
||||||
: '';
|
: '';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><div class="job-filename" title="${escapeHtml(job.outfile_name)}">${escapeHtml(job.outfile_name)}</div></td>
|
<td><div class="job-filename" title="${escapeHtml(job.outfile_name)}">${escapeHtml(job.outfile_name)}</div></td>
|
||||||
@@ -232,11 +233,12 @@
|
|||||||
<td class="datetime">${updated}</td>
|
<td class="datetime">${updated}</td>
|
||||||
<td class="uid" title="${escapeHtml(job.uid)}">${job.uid.substring(0, 10)}...</td>
|
<td class="uid" title="${escapeHtml(job.uid)}">${job.uid.substring(0, 10)}...</td>
|
||||||
<td>${previewBtn}</td>
|
<td>${previewBtn}</td>
|
||||||
<td>${moveBtn}</td>
|
<td>${moveIcon}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
// Запускаем проверку статусов после рендера
|
||||||
|
updateMoveStatuses();
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }
|
function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }
|
||||||
function getStateClass(state) {
|
function getStateClass(state) {
|
||||||
const classes = { 'finished': 'badge-finished', 'started': 'badge-started', 'processing': 'badge-processing', 'queued': 'badge-queued', 'error': 'badge-error', 'pending': 'badge-pending' };
|
const classes = { 'finished': 'badge-finished', 'started': 'badge-started', 'processing': 'badge-processing', 'queued': 'badge-queued', 'error': 'badge-error', 'pending': 'badge-pending' };
|
||||||
@@ -375,6 +377,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateMoveStatuses() {
|
||||||
|
const icons = document.querySelectorAll('.move-icon');
|
||||||
|
for (const icon of icons) {
|
||||||
|
const filename = icon.dataset.file;
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/videos/' + encodeURIComponent(filename) + '/status');
|
||||||
|
const status = await resp.json();
|
||||||
|
const exists = status.exists_in_dest;
|
||||||
|
const subfolder = status.subfolder;
|
||||||
|
if (exists) {
|
||||||
|
icon.innerHTML = `<i class="fas fa-check-circle" style="color: var(--accent-success);" title="Already in ${subfolder}"></i>`;
|
||||||
|
} else {
|
||||||
|
icon.innerHTML = `<button class="btn btn-primary btn-sm" onclick="moveVideo('${escapeHtml(filename)}')"><i class="fas fa-share"></i></button>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
icon.innerHTML = `<i class="fas fa-question-circle" title="Status unknown"></i>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== ИНИЦИАЛИЗАЦИЯ ==========
|
// ========== ИНИЦИАЛИЗАЦИЯ ==========
|
||||||
initTheme();
|
initTheme();
|
||||||
refreshJobs();
|
refreshJobs();
|
||||||
|
|||||||
48
src/web.rs
48
src/web.rs
@@ -121,6 +121,7 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
|
|||||||
.route("/api/videos", get(list_videos))
|
.route("/api/videos", get(list_videos))
|
||||||
.route("/api/videos/{filename}", get(stream_video))
|
.route("/api/videos/{filename}", get(stream_video))
|
||||||
.route("/api/videos/{filename}/move", post(move_video))
|
.route("/api/videos/{filename}/move", post(move_video))
|
||||||
|
.route("/api/videos/{filename}/status", get(get_video_status))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
@@ -361,15 +362,37 @@ async fn move_video(
|
|||||||
.source_access
|
.source_access
|
||||||
.read_file(&filename)
|
.read_file(&filename)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError(StatusCode::NOT_FOUND, e.to_string()))?;
|
.map_err(|e| AppError(StatusCode::NOT_FOUND, format!("File not found: {}", e)))?;
|
||||||
|
|
||||||
|
// Определяем целевую подпапку по имени файла
|
||||||
|
let subfolder = if filename.contains("_Today") {
|
||||||
|
"Today"
|
||||||
|
} else if filename.contains("_Tomorrow") {
|
||||||
|
"Tomorrow"
|
||||||
|
} else {
|
||||||
|
"Data"
|
||||||
|
};
|
||||||
|
let dest_filename = format!("{}/{}", subfolder, filename);
|
||||||
|
|
||||||
|
// Проверяем, существует ли уже файл в целевой папке
|
||||||
|
if state.dest_access.read_file(&dest_filename).await.is_ok() {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
format!("File {} already exists in {} folder.", filename, subfolder),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись
|
||||||
state
|
state
|
||||||
.dest_access
|
.dest_access
|
||||||
.write_file(&filename, &data)
|
.write_file(&dest_filename, &data)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
let mut message = format!("File {} moved successfully.", filename);
|
let mut message = format!(
|
||||||
|
"File {} moved successfully to {} folder.",
|
||||||
|
filename, subfolder
|
||||||
|
);
|
||||||
|
|
||||||
if remove {
|
if remove {
|
||||||
state
|
state
|
||||||
@@ -405,6 +428,25 @@ async fn fa_brands_woff2() -> impl IntoResponse {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_video_status(
|
||||||
|
Path(filename): Path<String>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<serde_json::Value>, AppError> {
|
||||||
|
let subfolder = if filename.contains("_Today") {
|
||||||
|
"Today"
|
||||||
|
} else if filename.contains("_Tomorrow") {
|
||||||
|
"Tomorrow"
|
||||||
|
} else {
|
||||||
|
"Data"
|
||||||
|
};
|
||||||
|
let dest_filename = format!("{}/{}", subfolder, filename);
|
||||||
|
let exists = state.dest_access.read_file(&dest_filename).await.is_ok();
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"exists_in_dest": exists,
|
||||||
|
"subfolder": subfolder
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
struct AppError(StatusCode, String);
|
struct AppError(StatusCode, String);
|
||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
|
|||||||
Reference in New Issue
Block a user