From 6129cd506dbb553f7949a20a951992bcbc47da6b Mon Sep 17 00:00:00 2001 From: superlishunqin <852326703@qq.com> Date: Sun, 7 Jul 2024 01:03:28 +0800 Subject: [PATCH] 0707 --- .idea/vcs.xml | 6 + app.py | 79 ++++++++--- app.txt | 223 ++++++++++++++++++++++++++++++ index.html | 264 +++++++++++++++++++----------------- index.txt | 342 +++++++++++++++++++++++++++++++++++++++++++++++ submissions.csv | 7 + submissions.xlsx | Bin 5218 -> 6394 bytes 7 files changed, 783 insertions(+), 138 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 app.txt create mode 100644 index.txt diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app.py b/app.py index d197057..19779eb 100644 --- a/app.py +++ b/app.py @@ -45,13 +45,13 @@ submissions_file = 'submissions.csv' if not os.path.exists(submissions_file): with open(submissions_file, 'w', newline='') as file: writer = csv.writer(file) - writer.writerow(['ID', '学生姓名', '学号', '提交的文件']) + writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件']) -def add_submission(student, student_id, filename): +def add_submission(student, student_id, assignment, filename): with open(submissions_file, 'a', newline='') as file: writer = csv.writer(file) - writer.writerow([datetime.datetime.now().isoformat(), student, student_id, filename]) + writer.writerow([datetime.datetime.now().isoformat(), student, student_id, assignment, filename]) def generate_presigned_url(object_key, content_type, expiration=3600): @@ -63,7 +63,7 @@ def generate_presigned_url(object_key, content_type, expiration=3600): Params={ 'Bucket': bucket_name, 'Key': object_key, - 'ContentType': content_type # 使用实际文件的 Content-Type + 'ContentType': content_type }, ExpiresIn=expiration, HttpMethod='PUT' @@ -81,27 +81,42 @@ def get_presigned_url(): student_id = request.args.get('student_id') filename = request.args.get('filename') content_type = request.args.get('content_type', 'application/octet-stream') + assignment = request.args.get('assignment') logging.info( - f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}") + f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}, assignment: {assignment}") - if not student or not filename or not student_id: - logging.warning("Missing student, student_id or filename parameter") - return jsonify({'error': 'Student, student_id and filename parameters are required'}), 400 + if not student or not filename or not student_id or not assignment: + logging.warning("Missing student, student_id, assignment or filename parameter") + return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400 - folder_name = 'sure_homework_define_by_qin' - object_key = f'{folder_name}/{student}-{filename}' + folder_name = f'sure_homework_define_by_qin/{assignment}' + new_filename = f'{student}_{student_id}_{filename}' + object_key = f'{folder_name}/{new_filename}' - url = generate_presigned_url(object_key, content_type) # 包含 content_type + url = generate_presigned_url(object_key, content_type) if not url: logging.error("Failed to generate presigned URL") return jsonify({'error': 'Failed to generate presigned URL'}), 500 - add_submission(student, student_id, filename) - - logging.info(f"Generated URL: {url}") return jsonify({'url': url, 'content_type': content_type}) +@app.route('/record-submission', methods=['POST']) +def record_submission(): + data = request.json + student = data.get('student') + student_id = data.get('student_id') + assignment = data.get('assignment') + filename = data.get('filename') + + if not student or not filename or not student_id or not assignment: + return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400 + + add_submission(student, student_id, assignment, filename) + + return jsonify({'status': 'success'}) + + @app.route('/') def serve_index(): return send_from_directory('.', 'index.html') @@ -139,10 +154,37 @@ def health_check(): @app.route('/download-submissions') def download_submissions(): - df = pd.read_csv(submissions_file) - output_file = 'submissions.xlsx' - df.to_excel(output_file, index=False) - return send_file(output_file, as_attachment=True) + try: + # 使用 csv 模块读取文件,这样可以处理不一致的行 + with open(submissions_file, 'r', newline='') as file: + csv_reader = csv.reader(file) + headers = next(csv_reader) # 读取标题行 + data = list(csv_reader) # 读取所有数据行 + + # 确定最大列数 + max_columns = max(len(headers), max(len(row) for row in data)) + + # 创建一个新的数据列表,确保每行都有相同数量的列 + normalized_data = [] + for row in data: + normalized_data.append(row + [''] * (max_columns - len(row))) + + # 创建 DataFrame + df = pd.DataFrame(normalized_data, columns=headers + [''] * (max_columns - len(headers))) + + # 重命名列(如果需要) + expected_columns = ['ID', '学生姓名', '学号', '作业', '提交的文件'] + df.columns = expected_columns + [f'额外列{i+1}' for i in range(len(df.columns) - len(expected_columns))] + + # 删除完全为空的行和列 + df = df.dropna(how='all', axis=0).dropna(how='all', axis=1) + + output_file = 'submissions.xlsx' + df.to_excel(output_file, index=False) + return send_file(output_file, as_attachment=True) + except Exception as e: + logging.error(f"Error in download_submissions: {str(e)}", exc_info=True) + return jsonify({'error': str(e)}), 500 @app.before_request @@ -179,4 +221,3 @@ if __name__ == '__main__': # sys.exit(1) app.run(debug=True) - diff --git a/app.txt b/app.txt new file mode 100644 index 0000000..19779eb --- /dev/null +++ b/app.txt @@ -0,0 +1,223 @@ +from flask import Flask, request, jsonify, send_from_directory, make_response, send_file +from flask_cors import CORS +import boto3 +from botocore.exceptions import NoCredentialsError, ClientError, EndpointConnectionError +import os +from dotenv import load_dotenv +import logging +import datetime +import pytz +from botocore.client import Config +import csv +import pandas as pd + +# 配置日志 +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + +load_dotenv() # 从.env文件加载环境变量 + +app = Flask(__name__, static_url_path='', static_folder='.') +CORS(app, resources={r"/*": {"origins": "*", "methods": "GET,POST,PUT,DELETE,OPTIONS"}}) # 添加 CORS 支持 + +aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID') +aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY') +region_name = os.getenv('AWS_REGION') +bucket_name = os.getenv('S3_BUCKET_NAME') + +# 打印环境变量 (仅用于调试,生产环境中请移除) +print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}") +print(f"AWS_SECRET_ACCESS_KEY: {'*' * len(aws_secret_access_key) if aws_secret_access_key else 'Not set'}") +print(f"AWS_REGION: {region_name}") +print(f"S3_BUCKET_NAME: {bucket_name}") + +s3_client = boto3.client( + 's3', + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + region_name=region_name, + config=Config(signature_version='s3v4') # 使用 S3v4 签名版本 +) + +# 跟踪学生提交信息 +submissions_file = 'submissions.csv' + +# 创建或者加载提交文件 +if not os.path.exists(submissions_file): + with open(submissions_file, 'w', newline='') as file: + writer = csv.writer(file) + writer.writerow(['ID', '学生姓名', '学号', '作业', '提交的文件']) + + +def add_submission(student, student_id, assignment, filename): + with open(submissions_file, 'a', newline='') as file: + writer = csv.writer(file) + writer.writerow([datetime.datetime.now().isoformat(), student, student_id, assignment, filename]) + + +def generate_presigned_url(object_key, content_type, expiration=3600): + try: + current_time = datetime.datetime.now(pytz.UTC) + logging.info(f"Current UTC time before generating URL: {current_time}") + + response = s3_client.generate_presigned_url('put_object', + Params={ + 'Bucket': bucket_name, + 'Key': object_key, + 'ContentType': content_type + }, + ExpiresIn=expiration, + HttpMethod='PUT' + ) + logging.info(f"Generated presigned URL: {response}") + return response + except (NoCredentialsError, ClientError, EndpointConnectionError) as e: + logging.error(f"Error generating presigned URL: {str(e)}", exc_info=True) + return None + + +@app.route('/generate-url', methods=['GET']) +def get_presigned_url(): + student = request.args.get('student') + student_id = request.args.get('student_id') + filename = request.args.get('filename') + content_type = request.args.get('content_type', 'application/octet-stream') + assignment = request.args.get('assignment') + logging.info( + f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}, assignment: {assignment}") + + if not student or not filename or not student_id or not assignment: + logging.warning("Missing student, student_id, assignment or filename parameter") + return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400 + + folder_name = f'sure_homework_define_by_qin/{assignment}' + new_filename = f'{student}_{student_id}_{filename}' + object_key = f'{folder_name}/{new_filename}' + + url = generate_presigned_url(object_key, content_type) + if not url: + logging.error("Failed to generate presigned URL") + return jsonify({'error': 'Failed to generate presigned URL'}), 500 + + return jsonify({'url': url, 'content_type': content_type}) + + +@app.route('/record-submission', methods=['POST']) +def record_submission(): + data = request.json + student = data.get('student') + student_id = data.get('student_id') + assignment = data.get('assignment') + filename = data.get('filename') + + if not student or not filename or not student_id or not assignment: + return jsonify({'error': 'Student, student_id, assignment and filename parameters are required'}), 400 + + add_submission(student, student_id, assignment, filename) + + return jsonify({'status': 'success'}) + + +@app.route('/') +def serve_index(): + return send_from_directory('.', 'index.html') + + +@app.route('/health') +def health_check(): + logging.info("Health check initiated") + try: + local_time = datetime.datetime.now() + utc_time = datetime.datetime.now(pytz.UTC) + logging.info(f"Local time: {local_time}, UTC time: {utc_time}") + + logging.info("Attempting to list S3 buckets") + response = s3_client.list_buckets() + logging.info(f"Successfully listed buckets: {[bucket['Name'] for bucket in response['Buckets']]}") + return jsonify({ + 'status': 'healthy', + 'message': 'AWS credentials are valid', + 'local_time': local_time.isoformat(), + 'utc_time': utc_time.isoformat() + }), 200 + except NoCredentialsError: + logging.error("AWS credentials not found", exc_info=True) + return jsonify({'status': 'unhealthy', 'message': 'AWS credentials not found'}), 500 + except ClientError as e: + error_code = e.response['Error']['Code'] + error_message = e.response['Error']['Message'] + logging.error(f"AWS client error: {error_code} - {error_message}", exc_info=True) + return jsonify({'status': 'unhealthy', 'message': f'AWS client error: {error_code} - {error_message}'}), 500 + except Exception as e: + logging.error(f"Unexpected error during health check: {str(e)}", exc_info=True) + return jsonify({'status': 'unhealthy', 'message': f'Unexpected error: {str(e)}'}), 500 + + +@app.route('/download-submissions') +def download_submissions(): + try: + # 使用 csv 模块读取文件,这样可以处理不一致的行 + with open(submissions_file, 'r', newline='') as file: + csv_reader = csv.reader(file) + headers = next(csv_reader) # 读取标题行 + data = list(csv_reader) # 读取所有数据行 + + # 确定最大列数 + max_columns = max(len(headers), max(len(row) for row in data)) + + # 创建一个新的数据列表,确保每行都有相同数量的列 + normalized_data = [] + for row in data: + normalized_data.append(row + [''] * (max_columns - len(row))) + + # 创建 DataFrame + df = pd.DataFrame(normalized_data, columns=headers + [''] * (max_columns - len(headers))) + + # 重命名列(如果需要) + expected_columns = ['ID', '学生姓名', '学号', '作业', '提交的文件'] + df.columns = expected_columns + [f'额外列{i+1}' for i in range(len(df.columns) - len(expected_columns))] + + # 删除完全为空的行和列 + df = df.dropna(how='all', axis=0).dropna(how='all', axis=1) + + output_file = 'submissions.xlsx' + df.to_excel(output_file, index=False) + return send_file(output_file, as_attachment=True) + except Exception as e: + logging.error(f"Error in download_submissions: {str(e)}", exc_info=True) + return jsonify({'error': str(e)}), 500 + + +@app.before_request +def before_request_func(): + if request.method == 'OPTIONS': + return _build_cors_preflight_response() + + +def _build_cors_preflight_response(): + response = make_response() + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + response.headers.add("Access-Control-Allow-Headers", "Content-Type") + return response + + +if __name__ == '__main__': + local_time = datetime.datetime.now() + utc_time = datetime.datetime.now(pytz.UTC) + logging.info(f"Application starting. Local time: {local_time}, UTC time: {utc_time}") + + try: + logging.info("Validating AWS credentials on startup") + sts = boto3.client('sts', + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + region_name=region_name) + response = sts.get_caller_identity() + logging.info(f"AWS credentials validated successfully. Account ID: {response['Account']}") + except Exception as e: + logging.error(f"Failed to validate AWS credentials: {str(e)}", exc_info=True) + # 如果你想在凭证验证失败时退出程序,取消注释下面两行 + # import sys + # sys.exit(1) + + app.run(debug=True) diff --git a/index.html b/index.html index cf2e1a3..6d639cb 100644 --- a/index.html +++ b/index.html @@ -48,7 +48,7 @@ color: #555; font-weight: 500; } - input[type="text"] { + input[type="text"], select { width: 100%; padding: 10px; border: 1px solid #ddd; @@ -80,14 +80,7 @@ border-color: #ccc; } .file-input-button input[type=file] { - font-size: 100px; - position: absolute; - left: 0; - top: 0; - opacity: 0; - cursor: pointer; - width: 100%; - height: 100%; + display: none; } #file-name { margin-top: 5px; @@ -167,13 +160,20 @@ +
+ + +
-
- 选择文件 - -
+
未选择文件
@@ -195,122 +195,148 @@ + + diff --git a/index.txt b/index.txt new file mode 100644 index 0000000..6d639cb --- /dev/null +++ b/index.txt @@ -0,0 +1,342 @@ + + + + + + 秀儿文件提交系统 + + + +
+
+ 描述性文本 +
+

秀儿文件提交系统

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
未选择文件
+
+ +
+
+
+
+
+
+
0%
+
+ 速度: 0 KB/s + 0 KB / 0 KB + 剩余时间: 计算中 +
+
+ 下载统计表格 +
+ + + + + + diff --git a/submissions.csv b/submissions.csv index e8c51f5..4c6ee6f 100644 --- a/submissions.csv +++ b/submissions.csv @@ -11,3 +11,10 @@ ID,学生姓名,学号,提交的文件 2024-07-03T02:43:01.033652,534534,543534,数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf 2024-07-03T02:46:15.397526,CCC,AAA,"周易译注 (黄寿祺,张善文) (Z-Library).pdf" 2024-07-03T02:48:26.189052,2423,423423,阅读1.pdf +2024-07-03T03:06:53.527517,李顺,342,homework1,计算机网络_数据链路层.ppt +2024-07-03T03:10:31.977965,Sure,324234,homework2,Sure_324234_彩虹易支付纯净源码.zip +2024-07-03T03:15:33.951611,342,453534,homework2,342_453534_男人来自火星,女人来自金星(套装共4册) (约翰·格雷) (Z-Library).pdf +2024-07-03T03:22:12.013752,李无天,983,homework3,李无天_983_真正全集:王阳明全集(以权威的隆庆初刻本《王文成公全书》为底本,增补2卷旧本未刊内容!王阳明的“心学哲理、文治武功、传奇人生”全在这... (Z-Library).pdf +2024-07-03T03:24:24.223088,王刚,324,homework3,王刚_324_期末复习.pptx +2024-07-06T19:16:46.811930,嗷呜呜0706,321,homework1,嗷呜呜0706_321_知行合一王阳明套装 (度阴山) (Z-Library).pdf +2024-07-07T00:28:05.848994,test,432,homework1,期末复习.pptx diff --git a/submissions.xlsx b/submissions.xlsx index 1670d245a3d19b74be35dc308c145b21afb82052..e3a26596fc194dd84ca23ba9936d7dc109020d6b 100644 GIT binary patch delta 4548 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 delta 3297 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