|
|
|
@ -4,6 +4,10 @@ from datetime import datetime, timezone |
|
|
|
import shutil |
|
|
|
import shutil |
|
|
|
import zipfile |
|
|
|
import zipfile |
|
|
|
import tarfile |
|
|
|
import tarfile |
|
|
|
|
|
|
|
from reportlab.lib.pagesizes import letter |
|
|
|
|
|
|
|
from reportlab.platypus import SimpleDocTemplate, Image as ReportLabImage, Paragraph, Spacer, PageBreak |
|
|
|
|
|
|
|
from reportlab.lib.styles import getSampleStyleSheet |
|
|
|
|
|
|
|
from PIL import Image as PILImage |
|
|
|
from PIL import Image |
|
|
|
from PIL import Image |
|
|
|
import re |
|
|
|
import re |
|
|
|
import hashlib |
|
|
|
import hashlib |
|
|
|
@ -873,6 +877,7 @@ def create_zip_from_workspace(): |
|
|
|
for p in staged: |
|
|
|
for p in staged: |
|
|
|
arcname = p.name.split("__", 1)[1] if "__" in p.name else p.name |
|
|
|
arcname = p.name.split("__", 1)[1] if "__" in p.name else p.name |
|
|
|
tf.add(p, arcname=arcname) |
|
|
|
tf.add(p, arcname=arcname) |
|
|
|
|
|
|
|
|
|
|
|
elif archive_format == "targz": |
|
|
|
elif archive_format == "targz": |
|
|
|
archive_filename = f"{archive_name}.tar.gz" |
|
|
|
archive_filename = f"{archive_name}.tar.gz" |
|
|
|
archive_path = exports_dir / archive_filename |
|
|
|
archive_path = exports_dir / archive_filename |
|
|
|
@ -880,6 +885,57 @@ def create_zip_from_workspace(): |
|
|
|
for p in staged: |
|
|
|
for p in staged: |
|
|
|
arcname = p.name.split("__", 1)[1] if "__" in p.name else p.name |
|
|
|
arcname = p.name.split("__", 1)[1] if "__" in p.name else p.name |
|
|
|
tf.add(p, arcname=arcname) |
|
|
|
tf.add(p, arcname=arcname) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif archive_format == "pdf": |
|
|
|
|
|
|
|
archive_filename = f"{archive_name}.pdf" |
|
|
|
|
|
|
|
archive_path = exports_dir / archive_filename |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
image_files = [] |
|
|
|
|
|
|
|
for p in staged: |
|
|
|
|
|
|
|
if p.suffix.lower() in [".jpg", ".jpeg", ".png", ".webp"]: |
|
|
|
|
|
|
|
image_files.append(p) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not image_files: |
|
|
|
|
|
|
|
flash("PDF Job Report needs image files only. No supported images were staged.", "warning") |
|
|
|
|
|
|
|
return redirect(url_for("main.zip_workspace")) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc = SimpleDocTemplate(str(archive_path), pagesize=letter) |
|
|
|
|
|
|
|
styles = getSampleStyleSheet() |
|
|
|
|
|
|
|
elements = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elements.append(Paragraph("OTB Cloud Job Report", styles["Title"])) |
|
|
|
|
|
|
|
elements.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
elements.append(Paragraph(f"Generated UTC: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}", styles["Normal"])) |
|
|
|
|
|
|
|
elements.append(Paragraph(f"Images included: {len(image_files)}", styles["Normal"])) |
|
|
|
|
|
|
|
elements.append(Spacer(1, 24)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
max_w = 500 |
|
|
|
|
|
|
|
max_h = 620 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for idx, img_path in enumerate(image_files, start=1): |
|
|
|
|
|
|
|
display_name = img_path.name.split("__", 1)[1] if "__" in img_path.name else img_path.name |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
|
|
with PILImage.open(img_path) as im: |
|
|
|
|
|
|
|
w, h = im.size |
|
|
|
|
|
|
|
scale = min(max_w / w, max_h / h, 1.0) |
|
|
|
|
|
|
|
draw_w = w * scale |
|
|
|
|
|
|
|
draw_h = h * scale |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elements.append(Paragraph(f"{idx}. {display_name}", styles["Heading3"])) |
|
|
|
|
|
|
|
elements.append(Spacer(1, 8)) |
|
|
|
|
|
|
|
elements.append(ReportLabImage(str(img_path), width=draw_w, height=draw_h)) |
|
|
|
|
|
|
|
elements.append(Spacer(1, 18)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if idx != len(image_files): |
|
|
|
|
|
|
|
elements.append(PageBreak()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
|
|
elements.append(Paragraph(f"Skipped image: {display_name} ({e})", styles["Normal"])) |
|
|
|
|
|
|
|
elements.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc.build(elements) |
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
|
else: |
|
|
|
archive_format = "zip" |
|
|
|
archive_format = "zip" |
|
|
|
archive_filename = f"{archive_name}.zip" |
|
|
|
archive_filename = f"{archive_name}.zip" |
|
|
|
@ -1219,12 +1275,22 @@ def browse_device_files(device_id: int): |
|
|
|
except Exception: |
|
|
|
except Exception: |
|
|
|
page = 1 |
|
|
|
page = 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
per_page_param = request.args.get("per_page", "100").strip().lower() |
|
|
|
|
|
|
|
if per_page_param == "all": |
|
|
|
|
|
|
|
per_page = None |
|
|
|
|
|
|
|
offset = 0 |
|
|
|
|
|
|
|
page = 1 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
try: |
|
|
|
|
|
|
|
per_page = int(per_page_param) |
|
|
|
|
|
|
|
except Exception: |
|
|
|
|
|
|
|
per_page = 100 |
|
|
|
|
|
|
|
if per_page not in (100, 250, 500): |
|
|
|
per_page = 100 |
|
|
|
per_page = 100 |
|
|
|
offset = (page - 1) * per_page |
|
|
|
offset = (page - 1) * per_page |
|
|
|
|
|
|
|
|
|
|
|
with db.cursor() as cur: |
|
|
|
with db.cursor() as cur: |
|
|
|
cur.execute( |
|
|
|
base_files_sql = """ |
|
|
|
""" |
|
|
|
|
|
|
|
SELECT |
|
|
|
SELECT |
|
|
|
id, |
|
|
|
id, |
|
|
|
file_kind, |
|
|
|
file_kind, |
|
|
|
@ -1245,8 +1311,16 @@ def browse_device_files(device_id: int): |
|
|
|
AND is_deleted = 0 |
|
|
|
AND is_deleted = 0 |
|
|
|
AND directory_path = %s |
|
|
|
AND directory_path = %s |
|
|
|
ORDER BY uploaded_at DESC, id DESC |
|
|
|
ORDER BY uploaded_at DESC, id DESC |
|
|
|
LIMIT %s OFFSET %s |
|
|
|
""" |
|
|
|
""", |
|
|
|
|
|
|
|
|
|
|
|
if per_page is None: |
|
|
|
|
|
|
|
cur.execute( |
|
|
|
|
|
|
|
base_files_sql, |
|
|
|
|
|
|
|
(session["otb_tenant_id"], device_id, current_directory), |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
cur.execute( |
|
|
|
|
|
|
|
base_files_sql + " LIMIT %s OFFSET %s", |
|
|
|
(session["otb_tenant_id"], device_id, current_directory, per_page, offset), |
|
|
|
(session["otb_tenant_id"], device_id, current_directory, per_page, offset), |
|
|
|
) |
|
|
|
) |
|
|
|
files = cur.fetchall() |
|
|
|
files = cur.fetchall() |
|
|
|
@ -1355,7 +1429,7 @@ def browse_device_files(device_id: int): |
|
|
|
page=page, |
|
|
|
page=page, |
|
|
|
per_page=per_page, |
|
|
|
per_page=per_page, |
|
|
|
has_prev=page > 1, |
|
|
|
has_prev=page > 1, |
|
|
|
has_next=(offset + per_page) < total_files, |
|
|
|
has_next=(per_page is not None and (offset + per_page) < total_files), |
|
|
|
view_mode=view_mode, |
|
|
|
view_mode=view_mode, |
|
|
|
current_path=current_path, |
|
|
|
current_path=current_path, |
|
|
|
parent_path=parent_path, |
|
|
|
parent_path=parent_path, |
|
|
|
@ -1765,7 +1839,6 @@ def video_enqueue(): |
|
|
|
@bp.route("/video-jobs") |
|
|
|
@bp.route("/video-jobs") |
|
|
|
def global_video_jobs(): |
|
|
|
def global_video_jobs(): |
|
|
|
from app.db import get_db |
|
|
|
from app.db import get_db |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
db = get_db() |
|
|
|
db = get_db() |
|
|
|
@ -1850,9 +1923,13 @@ def global_video_jobs(): |
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/health") |
|
|
|
@bp.route("/health") |
|
|
|
def cloud_health(): |
|
|
|
def cloud_health(): |
|
|
|
from app.db import get_db |
|
|
|
|
|
|
|
from pathlib import Path |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
version_file = Path(current_app.root_path).parent / "VERSION" |
|
|
|
|
|
|
|
app_version = version_file.read_text().strip() if version_file.exists() else "unknown" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from app.db import get_db |
|
|
|
|
|
|
|
|
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
db = get_db() |
|
|
|
db = get_db() |
|
|
|
|
|
|
|
|
|
|
|
@ -1908,6 +1985,20 @@ def cloud_health(): |
|
|
|
"gpu_seconds": 0, |
|
|
|
"gpu_seconds": 0, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with db.cursor() as cur: |
|
|
|
|
|
|
|
cur.execute( |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
SELECT COUNT(*) AS image_processed |
|
|
|
|
|
|
|
FROM files |
|
|
|
|
|
|
|
WHERE tenant_id = %s |
|
|
|
|
|
|
|
AND file_kind = 'image_processed' |
|
|
|
|
|
|
|
AND is_deleted = 0 |
|
|
|
|
|
|
|
""", |
|
|
|
|
|
|
|
(tenant_id,), |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
image_row = cur.fetchone() or {"image_processed": 0} |
|
|
|
|
|
|
|
image_processed = image_row["image_processed"] or 0 |
|
|
|
|
|
|
|
|
|
|
|
def human_bytes(n): |
|
|
|
def human_bytes(n): |
|
|
|
n = int(n or 0) |
|
|
|
n = int(n or 0) |
|
|
|
if n < 1024: |
|
|
|
if n < 1024: |
|
|
|
@ -1933,6 +2024,7 @@ def cloud_health(): |
|
|
|
|
|
|
|
|
|
|
|
return render_template( |
|
|
|
return render_template( |
|
|
|
"cloud/health.html", |
|
|
|
"cloud/health.html", |
|
|
|
|
|
|
|
app_version=app_version, |
|
|
|
uploaded_count=uploaded_count, |
|
|
|
uploaded_count=uploaded_count, |
|
|
|
uploaded_bytes=human_bytes(uploaded_bytes), |
|
|
|
uploaded_bytes=human_bytes(uploaded_bytes), |
|
|
|
lts_count=lts_count, |
|
|
|
lts_count=lts_count, |
|
|
|
@ -1940,6 +2032,7 @@ def cloud_health(): |
|
|
|
archive_count=archive_count, |
|
|
|
archive_count=archive_count, |
|
|
|
archive_bytes=human_bytes(archive_bytes), |
|
|
|
archive_bytes=human_bytes(archive_bytes), |
|
|
|
total_used=human_bytes(total_used), |
|
|
|
total_used=human_bytes(total_used), |
|
|
|
|
|
|
|
image_processed=image_processed, |
|
|
|
total_jobs=stats["total_jobs"] or 0, |
|
|
|
total_jobs=stats["total_jobs"] or 0, |
|
|
|
complete_jobs=stats["complete_jobs"] or 0, |
|
|
|
complete_jobs=stats["complete_jobs"] or 0, |
|
|
|
failed_jobs=stats["failed_jobs"] or 0, |
|
|
|
failed_jobs=stats["failed_jobs"] or 0, |
|
|
|
@ -1950,7 +2043,6 @@ def cloud_health(): |
|
|
|
@bp.route("/video-output/<int:job_id>/view") |
|
|
|
@bp.route("/video-output/<int:job_id>/view") |
|
|
|
def view_video_output(job_id): |
|
|
|
def view_video_output(job_id): |
|
|
|
from app.db import get_db |
|
|
|
from app.db import get_db |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
db = get_db() |
|
|
|
db = get_db() |
|
|
|
@ -1987,7 +2079,6 @@ def view_video_output(job_id): |
|
|
|
@bp.route("/video-output/<int:job_id>/send-to-lts", methods=["POST"]) |
|
|
|
@bp.route("/video-output/<int:job_id>/send-to-lts", methods=["POST"]) |
|
|
|
def send_video_output_to_lts(job_id): |
|
|
|
def send_video_output_to_lts(job_id): |
|
|
|
from app.db import get_db |
|
|
|
from app.db import get_db |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
import shutil |
|
|
|
import shutil |
|
|
|
|
|
|
|
|
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
tenant = session.get("tenant") or "def" |
|
|
|
@ -2093,7 +2184,6 @@ def download_video_output(job_id): |
|
|
|
if not job["output_relative_path"]: |
|
|
|
if not job["output_relative_path"]: |
|
|
|
return "No output file for this job", 404 |
|
|
|
return "No output file for this job", 404 |
|
|
|
|
|
|
|
|
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
full_path = Path(storage_root) / job["output_relative_path"] |
|
|
|
full_path = Path(storage_root) / job["output_relative_path"] |
|
|
|
|
|
|
|
|
|
|
|
if not full_path.exists(): |
|
|
|
if not full_path.exists(): |
|
|
|
@ -2166,7 +2256,6 @@ def video_queue_summary(): |
|
|
|
@portal_session_required |
|
|
|
@portal_session_required |
|
|
|
def image_process(): |
|
|
|
def image_process(): |
|
|
|
from PIL import Image, ImageOps |
|
|
|
from PIL import Image, ImageOps |
|
|
|
from pathlib import Path |
|
|
|
|
|
|
|
from datetime import datetime |
|
|
|
from datetime import datetime |
|
|
|
|
|
|
|
|
|
|
|
db = get_db() |
|
|
|
db = get_db() |
|
|
|
|