import os import re import uuid import shutil from datetime import datetime from flask import Flask, request, jsonify, render_template from config import ( NAS_DATA_PATH, PORT, ALLOWED_EXTENSIONS, MAX_FILES_COUNT, MAX_FILE_SIZE_MB, ALLOWED_MIME_TYPES ) app = Flask(__name__) # ========== 工具函数 ========== def get_camp_folders(): """获取所有训练营文件夹列表,按期数排序""" if not os.path.exists(NAS_DATA_PATH): os.makedirs(NAS_DATA_PATH, exist_ok=True) return [] folders = [] for name in os.listdir(NAS_DATA_PATH): full_path = os.path.join(NAS_DATA_PATH, name) if os.path.isdir(full_path) and re.match(r'^训练营\d+期$', name): folders.append(name) # 按期数数字排序 folders.sort(key=lambda x: int(re.search(r'\d+', x).group()) if re.search(r'\d+', x) else 0) return folders def get_class_folders(camp_name): """获取某个训练营下的所有班级文件夹,按班号排序""" camp_path = os.path.join(NAS_DATA_PATH, camp_name) if not os.path.exists(camp_path): return [] classes = [] for name in os.listdir(camp_path): full_path = os.path.join(camp_path, name) if os.path.isdir(full_path) and re.match(r'^\d+班$', name): classes.append(name) # 按班号数字排序 classes.sort(key=lambda x: int(re.search(r'\d+', x).group()) if re.search(r'\d+', x) else 0) return classes def validate_file(file): """验证上传文件是否合法,返回 (是否合法, 错误原因)""" if not file or not file.filename: return False, "空文件" # 检查文件大小 file.seek(0, os.SEEK_END) size = file.tell() file.seek(0) if size > MAX_FILE_SIZE_MB * 1024 * 1024: return False, f"文件超过 {MAX_FILE_SIZE_MB}MB 限制" if size == 0: return False, "空文件" # 检查扩展名 ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else '' if ext not in ALLOWED_EXTENSIONS: return False, f"不支持的格式: {ext}" # 检查MIME类型 mime = file.mimetype expected_mime = ALLOWED_MIME_TYPES.get(ext) if expected_mime and mime and not mime.startswith(expected_mime.split('/')[0]): return False, f"MIME类型不匹配: {mime}" return True, None def generate_filename(original_name, camp_name, class_name): """生成新文件名:训练营{n}期-{x}班-{时间戳}-{随机数}.{扩展名}""" camp_match = re.search(r'(\d+)', camp_name) class_match = re.search(r'(\d+)', class_name) camp_num = camp_match.group(1) if camp_match else '?' class_num = class_match.group(1) if class_match else '?' now = datetime.now() timestamp = now.strftime('%Y%m%d%H%M%S') random_str = uuid.uuid4().hex[:6] ext = original_name.rsplit('.', 1)[-1].lower() if '.' in original_name else 'jpg' return f"训练营{camp_num}期-{class_num}班-{timestamp}-{random_str}.{ext}" def get_unique_filepath(dir_path, filename): """获取不冲突的文件路径,若存在则追加序号""" filepath = os.path.join(dir_path, filename) if not os.path.exists(filepath): return filepath # 分离文件名和扩展名 name, ext = os.path.splitext(filename) counter = 1 while True: new_name = f"{name}-{counter}{ext}" new_path = os.path.join(dir_path, new_name) if not os.path.exists(new_path): return new_path counter += 1 def ensure_folders(camp_name, class_name): """确保训练营和班级文件夹存在,不存在则创建""" camp_path = os.path.join(NAS_DATA_PATH, camp_name) class_path = os.path.join(camp_path, class_name) stat_file = os.path.join(camp_path, '统计.md') created_camp = False if not os.path.exists(camp_path): os.makedirs(camp_path, exist_ok=True) created_camp = True if not os.path.exists(class_path): os.makedirs(class_path, exist_ok=True) # 如果训练营是新创建的,初始化统计文件 if created_camp and not os.path.exists(stat_file): update_statistics(camp_name) return camp_path, class_path def update_statistics(camp_name): """更新某个训练营的统计.md文件""" camp_path = os.path.join(NAS_DATA_PATH, camp_name) stat_file = os.path.join(camp_path, '统计.md') if not os.path.exists(camp_path): return # 扫描所有班级文件夹 class_stats = [] total = 0 for name in os.listdir(camp_path): full_path = os.path.join(camp_path, name) if not os.path.isdir(full_path): continue if not re.match(r'^\d+班$', name): continue # 统计该班级下的图片数量 count = 0 for fname in os.listdir(full_path): fpath = os.path.join(full_path, fname) if os.path.isfile(fpath): ext = fname.rsplit('.', 1)[-1].lower() if '.' in fname else '' if ext in ALLOWED_EXTENSIONS: count += 1 class_stats.append((name, count)) total += count # 按班号排序 class_stats.sort(key=lambda x: int(re.search(r'\d+', x[0]).group()) if re.search(r'\d+', x[0]) else 0) # 写入统计文件 now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') lines = [ f"# {camp_name} 图片统计", "", f"更新时间:{now}", f"图片总数:{total}", "", "| 班级 | 图片数量 |", "|------|----------|", ] for class_name, count in class_stats: lines.append(f"| {class_name} | {count} |") with open(stat_file, 'w', encoding='utf-8') as f: f.write('\n'.join(lines) + '\n') # ========== 页面路由 ========== @app.route('/') def index(): return render_template('index.html') # ========== API 路由 ========== @app.route('/api/folders', methods=['GET']) def api_folders(): """获取所有训练营及其下的班级文件夹""" folders = [] for camp_name in get_camp_folders(): classes = get_class_folders(camp_name) folders.append({ 'name': camp_name, 'classes': classes, }) return jsonify({'folders': folders}) @app.route('/api/upload', methods=['POST']) def api_upload(): """上传图片""" camp_name = request.form.get('folder', '').strip() class_name = request.form.get('class', '').strip() files = request.files.getlist('files') # 参数校验 if not camp_name or not re.match(r'^训练营\d+期$', camp_name): return jsonify({'success': False, 'error': '无效的训练营名称'}), 400 if not class_name or not re.match(r'^\d+班$', class_name): return jsonify({'success': False, 'error': '无效的班级名称'}), 400 if not files: return jsonify({'success': False, 'error': '未选择文件'}), 400 if len(files) > MAX_FILES_COUNT: return jsonify({'success': False, 'error': f'单次最多上传 {MAX_FILES_COUNT} 张'}), 400 # 确保文件夹存在 camp_path, class_path = ensure_folders(camp_name, class_name) # 逐个处理文件 uploaded = [] failed = [] has_new_upload = False for file in files: # 验证文件 valid, reason = validate_file(file) if not valid: failed.append({ 'filename': file.filename if file.filename else '未知文件', 'status': 'failed', 'reason': reason, }) continue # 生成文件名并保存 new_filename = generate_filename(file.filename, camp_name, class_name) filepath = get_unique_filepath(class_path, new_filename) try: file.save(filepath) uploaded.append({ 'filename': os.path.basename(filepath), 'status': 'success', }) has_new_upload = True except Exception as e: failed.append({ 'filename': file.filename, 'status': 'failed', 'reason': str(e), }) # 如果有新文件上传成功,更新统计 if has_new_upload: try: update_statistics(camp_name) except Exception: pass # 统计更新失败不影响上传结果 return jsonify({ 'success': len(uploaded) > 0, 'uploaded': uploaded, 'failed': failed, }) # ========== 启动 ========== if __name__ == '__main__': # 确保存储目录存在 if not os.path.exists(NAS_DATA_PATH): os.makedirs(NAS_DATA_PATH, exist_ok=True) print(f"已创建存储目录: {NAS_DATA_PATH}") print(f"服务启动在 http://0.0.0.0:{PORT}") app.run(host='0.0.0.0', port=PORT, debug=False)