|
1 | 1 | # Test deploy via GitHub Actions |
2 | | -from flask import Flask, request, redirect, url_for, render_template, send_from_directory, flash, session |
| 2 | +from flask import Flask, request, redirect, url_for, render_template, send_from_directory, flash, session, current_app, abort, jsonify |
3 | 3 | import os |
4 | 4 | import pandas as pd |
5 | 5 | import numpy as np |
@@ -108,48 +108,63 @@ def tableize(name: str) -> str: |
108 | 108 | print(f"auto_import_uploads: done, {imported} table(s) updated.") |
109 | 109 | return imported |
110 | 110 |
|
111 | | -def auto_log_material_files(): |
112 | | - if not os.path.exists(UPLOAD_FOLDER): |
113 | | - return |
| 111 | +# the current auto_log_material_files() --- |
| 112 | +@app.before_first_request |
| 113 | +def _seed_uploads_log(): |
| 114 | + auto_log_material_files() |
114 | 115 |
|
115 | | - all_allowed_exts = ALLOWED_DATASET_EXTENSIONS | ALLOWED_RESULTS_EXTENSIONS |
116 | 116 |
|
| 117 | +def ensure_uploads_log_schema(): |
| 118 | + # Creates table if missing and enforces uniqueness on (property, tab, filename) |
117 | 119 | with sqlite3.connect(DB_NAME) as conn: |
118 | 120 | c = conn.cursor() |
119 | | - # Ensure uniqueness constraint exists |
120 | 121 | c.execute(""" |
121 | | - INSERT OR IGNORE INTO uploads_log(property, tab, filename, logged_at) |
122 | | - VALUES (?, ?, ?, CURRENT_TIMESTAMP) |
123 | | - """, (property, tab, filename)) |
| 122 | + CREATE TABLE IF NOT EXISTS uploads_log ( |
| 123 | + property TEXT NOT NULL, |
| 124 | + tab TEXT NOT NULL, |
| 125 | + filename TEXT NOT NULL |
| 126 | + ) |
| 127 | + """) |
| 128 | + c.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_uploads_unique ON uploads_log(property, tab, filename)") |
124 | 129 | conn.commit() |
125 | 130 |
|
126 | | - for root, dirs, files in os.walk(UPLOAD_FOLDER): |
127 | | - for filename in files: |
128 | | - ext = filename.rsplit('.', 1)[-1].lower() |
| 131 | +def auto_log_material_files(): |
| 132 | + ensure_uploads_log_schema() |
| 133 | + |
| 134 | + upload_root = current_app.config.get("UPLOAD_FOLDER", UPLOAD_FOLDER) |
| 135 | + if not os.path.exists(upload_root): |
| 136 | + return |
| 137 | + |
| 138 | + all_allowed_exts = ALLOWED_DATASET_EXTENSIONS | ALLOWED_RESULTS_EXTENSIONS |
| 139 | + |
| 140 | + to_insert = [] |
| 141 | + for root, dirs, files in os.walk(upload_root): |
| 142 | + for fname in files: |
| 143 | + ext = fname.rsplit(".", 1)[-1].lower() if "." in fname else "" |
129 | 144 | if ext not in all_allowed_exts: |
130 | 145 | continue |
131 | 146 |
|
132 | | - filepath = os.path.join(root, filename) |
133 | | - rel_path = os.path.relpath(filepath, UPLOAD_FOLDER) |
| 147 | + fpath = os.path.join(root, fname) |
| 148 | + rel_path = os.path.relpath(fpath, upload_root) |
134 | 149 | parts = rel_path.split(os.sep) |
135 | 150 |
|
136 | 151 | # Skip music uploads under /uploads/clips/ |
137 | | - if parts[0] == 'clips': |
| 152 | + if parts and parts[0] == "clips": |
138 | 153 | continue |
139 | 154 |
|
140 | 155 | if len(parts) >= 3: |
141 | | - property_name = parts[0] |
142 | | - tab = parts[1] |
143 | | - file_name = parts[2] |
| 156 | + property_name, tab, file_name = parts[0], parts[1], parts[2] |
| 157 | + to_insert.append((property_name, tab, file_name)) |
144 | 158 |
|
145 | | - with sqlite3.connect(DB_NAME) as conn: |
146 | | - c = conn.cursor() |
147 | | - c.execute(""" |
148 | | - INSERT OR IGNORE INTO uploads_log (property, tab, filename, uploaded_at) |
149 | | - VALUES (?, ?, ?, ?) |
150 | | - """, (property_name, tab, file_name, datetime.datetime.now().isoformat())) |
151 | | - conn.commit() |
152 | | - print(f"Auto-logged: {rel_path}") |
| 159 | + if to_insert: |
| 160 | + with sqlite3.connect(DB_NAME) as conn: |
| 161 | + c = conn.cursor() |
| 162 | + # idempotent insert: ignore duplicates quietly |
| 163 | + c.executemany( |
| 164 | + "INSERT OR IGNORE INTO uploads_log(property, tab, filename) VALUES (?, ?, ?)", |
| 165 | + to_insert |
| 166 | + ) |
| 167 | + conn.commit() |
153 | 168 |
|
154 | 169 |
|
155 | 170 | # ========== FLASK APP ========== |
@@ -423,8 +438,6 @@ def property_detail(property_name, tab): |
423 | 438 | ) |
424 | 439 |
|
425 | 440 |
|
426 | | -from flask import abort |
427 | | - |
428 | 441 | @app.route('/uploads/<path:filename>') |
429 | 442 | def uploaded_file(filename): |
430 | 443 | full_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) |
@@ -611,16 +624,32 @@ def delete_clip(clip_id): |
611 | 624 | def delete_dataset_file(property_name, tab, filename): |
612 | 625 | if not session.get('admin'): |
613 | 626 | return redirect(url_for('login')) |
614 | | - safe_filename = secure_filename(os.path.basename(filename)) |
615 | | - # Remove from disk |
616 | | - file_path = os.path.join(app.config['UPLOAD_FOLDER'], property_name, tab, safe_filename) |
617 | | - if os.path.isfile(file_path): |
618 | | - os.remove(file_path) |
619 | | - # Remove from DB |
| 627 | + |
| 628 | + # Build and validate the target path |
| 629 | + uploads_root = current_app.config.get("UPLOAD_FOLDER", UPLOAD_FOLDER) |
| 630 | + base_dir = os.path.join(uploads_root, property_name, tab) |
| 631 | + safe_name = secure_filename(os.path.basename(filename)) |
| 632 | + target_path = os.path.realpath(os.path.join(base_dir, safe_name)) |
| 633 | + base_dir_real = os.path.realpath(base_dir) |
| 634 | + if not (target_path == base_dir_real or target_path.startswith(base_dir_real + os.sep)): |
| 635 | + abort(400, description="Invalid file path") |
| 636 | + |
| 637 | + # Remove file if present |
| 638 | + try: |
| 639 | + if os.path.isfile(target_path): |
| 640 | + os.remove(target_path) |
| 641 | + except Exception as e: |
| 642 | + print(f"File delete warning: {e}") |
| 643 | + |
| 644 | + # Remove exactly one row by composite key |
620 | 645 | with sqlite3.connect(DB_NAME) as conn: |
621 | 646 | c = conn.cursor() |
622 | | - c.execute("DELETE FROM uploads_log WHERE property=? AND tab=? AND filename=?", (property_name, tab, safe_filename)) |
| 647 | + c.execute( |
| 648 | + "DELETE FROM uploads_log WHERE property=? AND tab=? AND filename=?", |
| 649 | + (property_name, tab, safe_name) |
| 650 | + ) |
623 | 651 | conn.commit() |
| 652 | + |
624 | 653 | return redirect(url_for('property_detail', property_name=property_name, tab=tab)) |
625 | 654 |
|
626 | 655 | @app.route('/add_drive_clip', methods=['GET', 'POST']) |
|
0 commit comments