cc/startup_app/materials.py
원본 내용을 읽기 전용으로 표시합니다.
"""허용 목록에 있는 공개용 사본만 읽는 자료실. 로컬 파일 실행/업로드 기능 없음."""import csvimport jsonimport refrom functools import lru_cachefrom itertools import islicefrom pathlib import Pathfrom flask import Blueprint, abort, current_app, render_template, request, send_filebp = Blueprint('materials', __name__, url_prefix='/materials')LABELS = {'code': '프로그램·코드', 'map': '결과 지도', 'image': '그래프', 'csv': 'CSV 데이터', 'text': '문서·자료'}PREVIEW_LIMIT = 240_000@lru_cache(maxsize=8)def load_catalog(directory):root = Path(directory).resolve()manifest = root / 'manifest.json'if not manifest.is_file():return root, {}, ''data = json.loads(manifest.read_text(encoding='utf-8'))return root, {entry['id']: entry for entry in data['files']}, data['created']def catalog():return load_catalog(str(current_app.config['MATERIALS_DIR']))def selected(file_id):root, entries, _ = catalog()if not re.fullmatch(r'[0-9a-f]{20}', file_id) or file_id not in entries:abort(404)entry = entries[file_id]base = (root / 'files').resolve()path = (base / entry['path']).resolve()if not path.is_relative_to(base) or not path.is_file():abort(404)return entry, pathdef size_label(value):return f'{value / 1024 / 1024:.1f} MB' if value >= 1024 * 1024 else f'{value / 1024:.1f} KB'@bp.get('/')def index():_, entries, created = catalog()files = list(entries.values())folders = sorted({str(Path(e['path']).parent).replace('\\', '/') for e in files})folder, query = request.args.get('folder', ''), request.args.get('q', '').strip()[:200]if folder and folder not in folders:abort(404)shown = [e for e in files if (not folder or str(Path(e['path']).parent).replace('\\', '/') == folder) and (not query or query.casefold() in e['path'].casefold())]return render_template('materials_index.html', files=shown, total=len(files), folders=folders, folder=folder, query=query, labels=LABELS, size_label=size_label, created=created)@bp.get('/file/<file_id>')def detail(file_id):entry, path = selected(file_id)source = request.args.get('view') == 'source'lines, rows, header, truncated = [], [], [], Falseif entry['kind'] == 'csv' and not source:with path.open(encoding='utf-8-sig', newline='') as stream:reader = csv.reader(stream)header = next(reader, [])rows = [[value[:500] for value in row] for row in islice(reader, 50)]elif entry['kind'] not in {'map', 'image'} or source and entry['kind'] == 'map':with path.open('rb') as stream:raw = stream.read(PREVIEW_LIMIT + 1)text = raw[:PREVIEW_LIMIT].decode('utf-8-sig', errors='replace')all_lines = text.splitlines()lines = all_lines[:3000]truncated = len(raw) > PREVIEW_LIMIT or len(all_lines) > 3000return render_template('materials_detail.html', entry=entry, labels=LABELS, size_label=size_label, source=source, lines=lines, header=header, rows=rows, truncated=truncated)@bp.get('/download/<file_id>')def download(file_id):entry, path = selected(file_id)return send_file(path, as_attachment=True, download_name=path.name, conditional=True, max_age=3600)@bp.get('/image/<file_id>')def image(file_id):entry, path = selected(file_id)if entry['kind'] != 'image':abort(404)return send_file(path, mimetype='image/png', conditional=True, max_age=3600)@bp.get('/preview/<file_id>')def preview(file_id):entry, path = selected(file_id)if entry['kind'] != 'map':abort(404)response = send_file(path, mimetype='text/html', conditional=True, max_age=3600)response.headers['Content-Security-Policy'] = ("sandbox allow-scripts; default-src 'none'; ""script-src 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://code.jquery.com https://unpkg.com; ""style-src 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://code.jquery.com https://unpkg.com https://netdna.bootstrapcdn.com; ""img-src data: https://*.tile.openstreetmap.fr https://*.tile.openstreetmap.org https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; ""font-src data: https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://netdna.bootstrapcdn.com; ""connect-src https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://code.jquery.com https://unpkg.com; ""base-uri 'none'; form-action 'none'; frame-ancestors 'self'")return response
파일 확인 정보
SHA-256: 35ffd8106963694a108baf1313aac96698de62675512760b98143f7f7edced2d