fix: allow network paths
This commit is contained in:
+51
-69
@@ -1,12 +1,11 @@
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Response, jsonify, request, send_from_directory
|
||||
from flask_cors import CORS
|
||||
|
||||
from db_py import count_tests, del_config, get_all_tests, get_config, set_config
|
||||
from scanner import full_scan
|
||||
from scanner import full_scan, scan_results_only
|
||||
from sse_py import broadcast, stream_events
|
||||
from watcher import start_watching
|
||||
|
||||
@@ -14,9 +13,13 @@ PORT = int(os.getenv("PORT", "3001"))
|
||||
ALLOWED_KEYS = {
|
||||
"target_dir",
|
||||
"results_dir",
|
||||
"results_dir_ref",
|
||||
"avg_time_coe",
|
||||
"avg_time_p2p",
|
||||
"avg_time_p3p",
|
||||
"smb_username",
|
||||
"smb_password",
|
||||
"smb_domain",
|
||||
}
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
@@ -26,6 +29,20 @@ app = Flask(__name__, static_folder=str(DIST_DIR), static_url_path="")
|
||||
CORS(app)
|
||||
|
||||
|
||||
def _apply_smb_env_from_config():
|
||||
mapping = {
|
||||
"SMB_USERNAME": get_config("smb_username"),
|
||||
"SMB_PASSWORD": get_config("smb_password"),
|
||||
"SMB_DOMAIN": get_config("smb_domain"),
|
||||
}
|
||||
|
||||
for env_key, value in mapping.items():
|
||||
if value in (None, ""):
|
||||
os.environ.pop(env_key, None)
|
||||
else:
|
||||
os.environ[env_key] = str(value)
|
||||
|
||||
|
||||
@app.get("/api/tests")
|
||||
def get_tests_route():
|
||||
completed = request.args.get("completed")
|
||||
@@ -205,14 +222,16 @@ def set_config_route():
|
||||
else:
|
||||
set_config(key, str(value))
|
||||
|
||||
if key in {"target_dir", "results_dir"}:
|
||||
if key in {"target_dir", "results_dir", "results_dir_ref"}:
|
||||
dirs_changed = True
|
||||
|
||||
if dirs_changed:
|
||||
_apply_smb_env_from_config()
|
||||
target_dir = get_config("target_dir")
|
||||
results_dir = get_config("results_dir")
|
||||
results_dir_ref = get_config("results_dir_ref")
|
||||
try:
|
||||
full_scan(target_dir, results_dir)
|
||||
full_scan(target_dir, results_dir, results_dir_ref)
|
||||
start_watching(target_dir, results_dir)
|
||||
except Exception as exc:
|
||||
print(f"[config] fullScan error: {exc}")
|
||||
@@ -224,18 +243,21 @@ def set_config_route():
|
||||
broadcast({"type": "update"})
|
||||
return jsonify({"ok": True, "testCount": len(tests), "completedCount": completed})
|
||||
|
||||
broadcast({"type": "update"})
|
||||
return jsonify({"ok": True, "testCount": None, "completedCount": None})
|
||||
|
||||
|
||||
@app.post("/api/config/rescan")
|
||||
def rescan_route():
|
||||
_apply_smb_env_from_config()
|
||||
target_dir = get_config("target_dir")
|
||||
results_dir = get_config("results_dir")
|
||||
results_dir_ref = get_config("results_dir_ref")
|
||||
if not target_dir or not results_dir:
|
||||
return jsonify({"error": "Directories not configured"}), 400
|
||||
|
||||
try:
|
||||
full_scan(target_dir, results_dir)
|
||||
full_scan(target_dir, results_dir, results_dir_ref)
|
||||
start_watching(target_dir, results_dir)
|
||||
except Exception as exc:
|
||||
print(f"[config] rescan error: {exc}")
|
||||
@@ -248,6 +270,27 @@ def rescan_route():
|
||||
return jsonify({"ok": True, "testCount": len(tests), "completedCount": completed})
|
||||
|
||||
|
||||
@app.post("/api/config/rescan-results")
|
||||
def rescan_results_route():
|
||||
_apply_smb_env_from_config()
|
||||
results_dir = get_config("results_dir")
|
||||
results_dir_ref = get_config("results_dir_ref")
|
||||
if not results_dir:
|
||||
return jsonify({"error": "Results directory not configured"}), 400
|
||||
|
||||
try:
|
||||
scan_results_only(results_dir, results_dir_ref)
|
||||
except Exception as exc:
|
||||
print(f"[config] rescan-results error: {exc}")
|
||||
return jsonify({"error": f"Scan failed: {exc}"}), 500
|
||||
|
||||
tests = get_all_tests()
|
||||
completed = len([t for t in tests if t.get("completed")])
|
||||
print(f"[config] Results rescan complete -> {len(tests)} tests, {completed} completed")
|
||||
broadcast({"type": "update"})
|
||||
return jsonify({"ok": True, "testCount": len(tests), "completedCount": completed})
|
||||
|
||||
|
||||
@app.get("/api/browse")
|
||||
def browse_route():
|
||||
def _normalize_request_path(path):
|
||||
@@ -258,48 +301,6 @@ def browse_route():
|
||||
p = p.replace("/", "\\")
|
||||
return p
|
||||
|
||||
def _is_unc_host_only(path):
|
||||
if os.name != "nt" or not path:
|
||||
return False
|
||||
p = path.rstrip("\\")
|
||||
if not p.startswith("\\\\"):
|
||||
return False
|
||||
remainder = p[2:]
|
||||
return bool(remainder) and "\\" not in remainder
|
||||
|
||||
def _shares_for_unc_host(host):
|
||||
# Enumerate SMB shares with native Windows tooling for host-only UNC paths.
|
||||
proc = subprocess.run(
|
||||
["net", "view", f"\\\\{host}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "Cannot query network host")
|
||||
|
||||
shares = []
|
||||
in_table = False
|
||||
for raw_line in proc.stdout.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("---"):
|
||||
in_table = True
|
||||
continue
|
||||
if not in_table:
|
||||
continue
|
||||
if line.lower().startswith("the command completed successfully"):
|
||||
break
|
||||
|
||||
first = line.split()[0]
|
||||
if first and first.lower() not in {"share", "name"}:
|
||||
shares.append(first)
|
||||
|
||||
unique = sorted(set(shares), key=str.lower)
|
||||
return [{"name": share, "path": f"\\\\{host}\\{share}"} for share in unique]
|
||||
|
||||
def _configured_roots():
|
||||
raw = os.getenv("BROWSE_ROOTS", "").strip()
|
||||
if not raw:
|
||||
@@ -326,17 +327,6 @@ def browse_route():
|
||||
continue
|
||||
return False
|
||||
|
||||
def _is_unc_host_allowed(host, roots):
|
||||
if not roots:
|
||||
return True
|
||||
host_prefix = os.path.normcase(f"\\\\{host}\\")
|
||||
host_exact = host_prefix.rstrip("\\")
|
||||
for root in roots:
|
||||
normalized_root = os.path.normcase(root)
|
||||
if normalized_root == host_exact or normalized_root.startswith(host_prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
roots = _configured_roots()
|
||||
req_path = _normalize_request_path(request.args.get("path"))
|
||||
|
||||
@@ -359,16 +349,6 @@ def browse_route():
|
||||
|
||||
return jsonify({"path": None, "parent": None, "dirs": dirs})
|
||||
|
||||
if _is_unc_host_only(req_path):
|
||||
host = req_path.rstrip("\\")[2:]
|
||||
if not _is_unc_host_allowed(host, roots):
|
||||
return jsonify({"error": "Path is outside allowed browse roots"}), 403
|
||||
try:
|
||||
dirs = _shares_for_unc_host(host)
|
||||
except Exception as exc:
|
||||
return jsonify({"error": f"Cannot list shares for host: {exc}"}), 403
|
||||
return jsonify({"path": f"\\\\{host}", "parent": None, "dirs": dirs})
|
||||
|
||||
if not os.path.exists(req_path):
|
||||
return jsonify({"error": "Path does not exist"}), 400
|
||||
if not os.path.isdir(req_path):
|
||||
@@ -416,8 +396,10 @@ def static_or_spa(path=""):
|
||||
|
||||
|
||||
def bootstrap():
|
||||
_apply_smb_env_from_config()
|
||||
target_dir = get_config("target_dir")
|
||||
results_dir = get_config("results_dir")
|
||||
results_dir_ref = get_config("results_dir_ref")
|
||||
|
||||
if target_dir and results_dir:
|
||||
existing = count_tests()
|
||||
@@ -425,7 +407,7 @@ def bootstrap():
|
||||
print(f"[server] Resuming from DB -> {existing} tests already loaded.")
|
||||
else:
|
||||
print("[server] No cached data, scanning directories...")
|
||||
full_scan(target_dir, results_dir)
|
||||
full_scan(target_dir, results_dir, results_dir_ref)
|
||||
tests = get_all_tests()
|
||||
completed = len([t for t in tests if t.get("completed")])
|
||||
print(f"[server] Scan complete -> {len(tests)} tests found, {completed} completed")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
Flask>=3.0.0,<4.0.0
|
||||
Flask-Cors>=4.0.1,<5.0.0
|
||||
watchdog>=4.0.1,<5.0.0
|
||||
smbprotocol>=1.13.0,<2.0.0
|
||||
|
||||
+147
-32
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import smbclient
|
||||
|
||||
from db_py import clear_tests, get_all_tests, mark_completed, set_coe_pair, upsert_test
|
||||
from parser import (
|
||||
@@ -11,16 +12,125 @@ from parser import (
|
||||
)
|
||||
|
||||
|
||||
def full_scan(target_dir, results_dir):
|
||||
_SMB_SESSIONS = set()
|
||||
|
||||
|
||||
def _normalize_input_path(path_value):
|
||||
if not path_value:
|
||||
return path_value
|
||||
|
||||
path = str(path_value).strip()
|
||||
|
||||
# Accept //server/share style and normalize to UNC for smbclient.
|
||||
if path.startswith("//"):
|
||||
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||
|
||||
# Accept /<ipv4>/<share>/... and normalize to UNC for Linux-hosted inputs.
|
||||
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
|
||||
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _is_unc_path(path):
|
||||
return isinstance(path, str) and path.startswith("\\\\")
|
||||
|
||||
|
||||
def _extract_unc_server(path):
|
||||
if not _is_unc_path(path):
|
||||
return None
|
||||
rest = path[2:]
|
||||
return rest.split("\\", 1)[0] if rest else None
|
||||
|
||||
|
||||
def _register_smb_session_if_needed(path):
|
||||
if not _is_unc_path(path):
|
||||
return
|
||||
|
||||
server = _extract_unc_server(path)
|
||||
if not server or server in _SMB_SESSIONS:
|
||||
return
|
||||
|
||||
username = os.getenv("SMB_USERNAME", "").strip()
|
||||
password = os.getenv("SMB_PASSWORD", "")
|
||||
domain = os.getenv("SMB_DOMAIN", "").strip()
|
||||
|
||||
if username and domain and "\\" not in username and "@" not in username:
|
||||
username = f"{domain}\\{username}"
|
||||
|
||||
if username:
|
||||
smbclient.register_session(server, username=username, password=password)
|
||||
else:
|
||||
smbclient.register_session(server)
|
||||
|
||||
_SMB_SESSIONS.add(server)
|
||||
|
||||
|
||||
def _iter_dir_entries(path):
|
||||
path = _normalize_input_path(path)
|
||||
if _is_unc_path(path):
|
||||
_register_smb_session_if_needed(path)
|
||||
return list(smbclient.scandir(path))
|
||||
return list(os.scandir(path))
|
||||
|
||||
|
||||
def _join_path(path, name):
|
||||
if _is_unc_path(path):
|
||||
base = path.rstrip("\\")
|
||||
return f"{base}\\{name}"
|
||||
return os.path.join(path, name)
|
||||
|
||||
|
||||
def _read_text_lines(path):
|
||||
path = _normalize_input_path(path)
|
||||
if _is_unc_path(path):
|
||||
_register_smb_session_if_needed(path)
|
||||
with smbclient.open_file(path, mode="r", encoding="utf-8", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
yield line
|
||||
return
|
||||
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
yield line
|
||||
|
||||
|
||||
def scan_results_only(results_dir, results_dir_ref=None):
|
||||
"""Re-scan only the results directories without clearing or re-scanning targets."""
|
||||
results_dir = _normalize_input_path(results_dir)
|
||||
results_dir_ref = _normalize_input_path(results_dir_ref)
|
||||
|
||||
if not results_dir:
|
||||
return
|
||||
|
||||
print(f"[scanner] results-only scan dir : {results_dir}")
|
||||
if results_dir_ref:
|
||||
print(f"[scanner] results-only scan dir ref : {results_dir_ref}")
|
||||
|
||||
scan_results(results_dir)
|
||||
if results_dir_ref:
|
||||
scan_results(results_dir_ref)
|
||||
update_p2p_coe_pairs()
|
||||
|
||||
|
||||
def full_scan(target_dir, results_dir, results_dir_ref=None):
|
||||
target_dir = _normalize_input_path(target_dir)
|
||||
results_dir = _normalize_input_path(results_dir)
|
||||
results_dir_ref = _normalize_input_path(results_dir_ref)
|
||||
|
||||
clear_tests()
|
||||
if not target_dir or not results_dir:
|
||||
return
|
||||
|
||||
print(f"[scanner] target dir : {target_dir}")
|
||||
print(f"[scanner] results dir: {results_dir}")
|
||||
print(f"[scanner] target dir : {target_dir}")
|
||||
print(f"[scanner] results dir : {results_dir}")
|
||||
if results_dir_ref:
|
||||
print(f"[scanner] results dir ref : {results_dir_ref}")
|
||||
|
||||
scan_targets(target_dir)
|
||||
scan_results(results_dir)
|
||||
if results_dir_ref:
|
||||
scan_results(results_dir_ref)
|
||||
update_p2p_coe_pairs()
|
||||
|
||||
|
||||
@@ -66,11 +176,13 @@ def update_p2p_coe_pairs():
|
||||
|
||||
|
||||
def scan_targets(target_dir):
|
||||
target_dir = _normalize_input_path(target_dir)
|
||||
|
||||
try:
|
||||
parent_entries = [
|
||||
name
|
||||
for name in os.listdir(target_dir)
|
||||
if os.path.isdir(os.path.join(target_dir, name))
|
||||
entry.name
|
||||
for entry in _iter_dir_entries(target_dir)
|
||||
if entry.is_dir()
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read target dir: {exc}")
|
||||
@@ -79,14 +191,14 @@ def scan_targets(target_dir):
|
||||
print(f"[scanner] subdirectories found: {len(parent_entries)}")
|
||||
|
||||
for parent_name in parent_entries:
|
||||
parent_path = os.path.join(target_dir, parent_name)
|
||||
parent_path = _join_path(target_dir, parent_name)
|
||||
try:
|
||||
files = [
|
||||
name
|
||||
for name in os.listdir(parent_path)
|
||||
if os.path.isfile(os.path.join(parent_path, name))
|
||||
and not name.startswith("GLOBAL")
|
||||
and name.endswith(".ini")
|
||||
entry.name
|
||||
for entry in _iter_dir_entries(parent_path)
|
||||
if entry.is_file()
|
||||
and not entry.name.startswith("GLOBAL")
|
||||
and entry.name.endswith(".ini")
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read parent dir {parent_name}: {exc}")
|
||||
@@ -125,11 +237,13 @@ def scan_targets(target_dir):
|
||||
|
||||
|
||||
def scan_results(results_dir):
|
||||
results_dir = _normalize_input_path(results_dir)
|
||||
|
||||
try:
|
||||
entries = [
|
||||
name
|
||||
for name in os.listdir(results_dir)
|
||||
if os.path.isdir(os.path.join(results_dir, name))
|
||||
entry.name
|
||||
for entry in _iter_dir_entries(results_dir)
|
||||
if entry.is_dir()
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read results dir: {exc}")
|
||||
@@ -141,21 +255,23 @@ def scan_results(results_dir):
|
||||
|
||||
|
||||
def process_result_dir(results_dir, result_dir_name):
|
||||
results_dir = _normalize_input_path(results_dir)
|
||||
|
||||
parsed = parse_result_filename(result_dir_name)
|
||||
test_id = parsed["test_id"]
|
||||
device = parsed["device"]
|
||||
if not test_id or not device:
|
||||
return
|
||||
|
||||
result_dir_path = os.path.join(results_dir, result_dir_name)
|
||||
result_dir_path = _join_path(results_dir, result_dir_name)
|
||||
|
||||
try:
|
||||
log_files = [
|
||||
name
|
||||
for name in os.listdir(result_dir_path)
|
||||
if os.path.isfile(os.path.join(result_dir_path, name))
|
||||
and name.endswith(".txt")
|
||||
and test_id in name
|
||||
entry.name
|
||||
for entry in _iter_dir_entries(result_dir_path)
|
||||
if entry.is_file()
|
||||
and entry.name.endswith(".txt")
|
||||
and test_id in entry.name
|
||||
]
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read result dir {result_dir_name}: {exc}")
|
||||
@@ -167,7 +283,7 @@ def process_result_dir(results_dir, result_dir_name):
|
||||
return
|
||||
|
||||
latest_log = sorted(log_files)[-1]
|
||||
log_path = os.path.join(result_dir_path, latest_log)
|
||||
log_path = _join_path(result_dir_path, latest_log)
|
||||
completed_at = parse_timestamp(latest_log)
|
||||
data = extract_log_data(log_path)
|
||||
|
||||
@@ -199,17 +315,16 @@ def extract_log_data(log_file_path):
|
||||
|
||||
result = {"duration_seconds": None, "tputResults": []}
|
||||
try:
|
||||
with open(log_file_path, "r", encoding="utf-8", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
time_match = elapsed_regex.search(line)
|
||||
if time_match:
|
||||
result["duration_seconds"] = parse_elapsed_time(time_match.group(1))
|
||||
continue
|
||||
for line in _read_text_lines(log_file_path):
|
||||
time_match = elapsed_regex.search(line)
|
||||
if time_match:
|
||||
result["duration_seconds"] = parse_elapsed_time(time_match.group(1))
|
||||
continue
|
||||
|
||||
if tput_regex.search(line):
|
||||
parsed = parse_tput_rssi(line)
|
||||
if parsed:
|
||||
result["tputResults"].append(parsed)
|
||||
if tput_regex.search(line):
|
||||
parsed = parse_tput_rssi(line)
|
||||
if parsed:
|
||||
result["tputResults"].append(parsed)
|
||||
except OSError as exc:
|
||||
print(f"[scanner] Cannot read log file {log_file_path}: {exc}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user