first commit
This commit is contained in:
		
						commit
						1760f36f7b
					
				
							
								
								
									
										4
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.env
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | AWS_ACCESS_KEY_ID=AKIAZQ3DT3KLI6N5LQUM | ||||||
|  | AWS_SECRET_ACCESS_KEY=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 | ||||||
|  | AWS_REGION=ap-northeast-1 | ||||||
|  | S3_BUCKET_NAME=sure-ae-upload | ||||||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | # 默认忽略的文件 | ||||||
|  | /shelf/ | ||||||
|  | /workspace.xml | ||||||
|  | # 基于编辑器的 HTTP 客户端请求 | ||||||
|  | /httpRequests/ | ||||||
|  | # Datasource local storage ignored files | ||||||
|  | /dataSources/ | ||||||
|  | /dataSources.local.xml | ||||||
							
								
								
									
										10
									
								
								.idea/AWS-sure.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.idea/AWS-sure.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <module type="PYTHON_MODULE" version="4"> | ||||||
|  |   <component name="NewModuleRootManager"> | ||||||
|  |     <content url="file://$MODULE_DIR$"> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/venv/lib/python3.10/site-packages" /> | ||||||
|  |     </content> | ||||||
|  |     <orderEntry type="inheritedJdk" /> | ||||||
|  |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|  |   </component> | ||||||
|  | </module> | ||||||
							
								
								
									
										23
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <profile version="1.0"> | ||||||
|  |     <option name="myName" value="Project Default" /> | ||||||
|  |     <inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||||||
|  |       <Languages> | ||||||
|  |         <language minSize="272" name="Python" /> | ||||||
|  |       </Languages> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||||||
|  |       <option name="ignoredErrors"> | ||||||
|  |         <list> | ||||||
|  |           <option value="E265" /> | ||||||
|  |           <option value="E231" /> | ||||||
|  |           <option value="E262" /> | ||||||
|  |           <option value="E225" /> | ||||||
|  |           <option value="E402" /> | ||||||
|  |           <option value="E271" /> | ||||||
|  |           <option value="E302" /> | ||||||
|  |         </list> | ||||||
|  |       </option> | ||||||
|  |     </inspection_tool> | ||||||
|  |   </profile> | ||||||
|  | </component> | ||||||
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <settings> | ||||||
|  |     <option name="USE_PROJECT_PROFILE" value="false" /> | ||||||
|  |     <version value="1.0" /> | ||||||
|  |   </settings> | ||||||
|  | </component> | ||||||
							
								
								
									
										4
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (AWS-sure)" project-jdk-type="Python SDK" /> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/.idea/AWS-sure.iml" filepath="$PROJECT_DIR$/.idea/AWS-sure.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										182
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,182 @@ | |||||||
|  | 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, filename): | ||||||
|  |     with open(submissions_file, 'a', newline='') as file: | ||||||
|  |         writer = csv.writer(file) | ||||||
|  |         writer.writerow([datetime.datetime.now().isoformat(), student, student_id, 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  # 使用实际文件的 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') | ||||||
|  |     logging.info( | ||||||
|  |         f"Received request for student: {student}, student_id: {student_id}, filename: {filename}, content_type: {content_type}") | ||||||
|  | 
 | ||||||
|  |     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 | ||||||
|  | 
 | ||||||
|  |     folder_name = 'sure_homework_define_by_qin' | ||||||
|  |     object_key = f'{folder_name}/{student}-{filename}' | ||||||
|  | 
 | ||||||
|  |     url = generate_presigned_url(object_key, content_type)  # 包含 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('/') | ||||||
|  | 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(): | ||||||
|  |     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) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @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) | ||||||
|  | 
 | ||||||
							
								
								
									
										31
									
								
								generate_presigned_url.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								generate_presigned_url.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | import boto3 | ||||||
|  | from botocore.exceptions import NoCredentialsError, ClientError | ||||||
|  | 
 | ||||||
|  | # 使用获取的访问密钥ID和秘密访问密钥来初始化S3客户端 | ||||||
|  | aws_access_key_id = 'AKIAZQ3DT3KLN5WGXZOR'          # 替换为你的访问密钥ID | ||||||
|  | aws_secret_access_key = '5UZb8SovTrbroT7yU1pBzaR5myLn+NMA+c87RvLH'  # 替换为你的秘密访问密钥 | ||||||
|  | region_name = 'ap-northeast-1'                       # 替换为你的S3存储桶所在区域 | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | # 创建一个模拟“文件夹”的空对象 | ||||||
|  | def create_s3_folder(bucket_name, folder_name): | ||||||
|  |     try: | ||||||
|  |         s3_client.put_object(Bucket=bucket_name, Key=(folder_name + '/')) | ||||||
|  |         print(f"Folder {folder_name} created in bucket {bucket_name}") | ||||||
|  |     except NoCredentialsError: | ||||||
|  |         print("Credentials not available") | ||||||
|  |     except ClientError as e: | ||||||
|  |         print(f"Error: {e}") | ||||||
|  | 
 | ||||||
|  | # Bucket name and folder name | ||||||
|  | bucket_name = 'sure-ae-upload' | ||||||
|  | folder_name = 'sure_homework_define_by_qin' | ||||||
|  | 
 | ||||||
|  | # Create folder | ||||||
|  | create_s3_folder(bucket_name, folder_name) | ||||||
							
								
								
									
										316
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,316 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="zh-CN"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|  |     <title>秀儿文件提交系统</title> | ||||||
|  |     <style> | ||||||
|  |         body { | ||||||
|  |             font-family: 'Helvetica Neue', Arial, sans-serif; | ||||||
|  |             background-color: #f0f2f5; | ||||||
|  |             margin: 0; | ||||||
|  |             padding: 0; | ||||||
|  |             display: flex; | ||||||
|  |             justify-content: center; | ||||||
|  |             align-items: center; | ||||||
|  |             min-height: 100vh; | ||||||
|  |         } | ||||||
|  |         .container { | ||||||
|  |             background-color: #ffffff; | ||||||
|  |             border-radius: 8px; | ||||||
|  |             box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | ||||||
|  |             padding: 30px; | ||||||
|  |             width: 100%; | ||||||
|  |             max-width: 400px; | ||||||
|  |         } | ||||||
|  |         .image-container { | ||||||
|  |             margin-bottom: 20px; | ||||||
|  |             border-radius: 8px; | ||||||
|  |             overflow: hidden; | ||||||
|  |         } | ||||||
|  |         .image-container img { | ||||||
|  |             width: 100%; | ||||||
|  |             height: auto; | ||||||
|  |             display: block; | ||||||
|  |         } | ||||||
|  |         h1 { | ||||||
|  |             color: #333; | ||||||
|  |             font-size: 24px; | ||||||
|  |             margin-bottom: 20px; | ||||||
|  |             text-align: center; | ||||||
|  |         } | ||||||
|  |         .form-field { | ||||||
|  |             margin-bottom: 20px; | ||||||
|  |         } | ||||||
|  |         label { | ||||||
|  |             display: block; | ||||||
|  |             margin-bottom: 5px; | ||||||
|  |             color: #555; | ||||||
|  |             font-weight: 500; | ||||||
|  |         } | ||||||
|  |         input[type="text"] { | ||||||
|  |             width: 100%; | ||||||
|  |             padding: 10px; | ||||||
|  |             border: 1px solid #ddd; | ||||||
|  |             border-radius: 4px; | ||||||
|  |             font-size: 16px; | ||||||
|  |         } | ||||||
|  |         .file-input-wrapper { | ||||||
|  |             position: relative; | ||||||
|  |             overflow: visible; | ||||||
|  |             display: inline-block; | ||||||
|  |             width: 100%; | ||||||
|  |         } | ||||||
|  |         .file-input-button { | ||||||
|  |             background-color: #f0f0f0; | ||||||
|  |             border: 1px solid #ddd; | ||||||
|  |             color: #333; | ||||||
|  |             padding: 10px 15px; | ||||||
|  |             border-radius: 4px; | ||||||
|  |             font-size: 16px; | ||||||
|  |             cursor: pointer; | ||||||
|  |             display: inline-block; | ||||||
|  |             width: 100%; | ||||||
|  |             text-align: center; | ||||||
|  |             box-sizing: border-box; | ||||||
|  |             transition: background-color 0.3s, border-color 0.3s; | ||||||
|  |         } | ||||||
|  |         .file-input-button:hover { | ||||||
|  |             background-color: #e0e0e0; | ||||||
|  |             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%; | ||||||
|  |         } | ||||||
|  |         #file-name { | ||||||
|  |             margin-top: 5px; | ||||||
|  |             font-size: 14px; | ||||||
|  |             color: #666; | ||||||
|  |         } | ||||||
|  |         button[type="submit"] { | ||||||
|  |             background-color: #1890ff; | ||||||
|  |             color: white; | ||||||
|  |             border: none; | ||||||
|  |             padding: 12px 20px; | ||||||
|  |             border-radius: 4px; | ||||||
|  |             font-size: 16px; | ||||||
|  |             cursor: pointer; | ||||||
|  |             width: 100%; | ||||||
|  |             transition: background-color 0.3s; | ||||||
|  |         } | ||||||
|  |         button[type="submit"]:hover { | ||||||
|  |             background-color: #40a9ff; | ||||||
|  |         } | ||||||
|  |         .progress-container { | ||||||
|  |             margin-top: 20px; | ||||||
|  |             display: none; | ||||||
|  |         } | ||||||
|  |         .progress { | ||||||
|  |             height: 10px; | ||||||
|  |             background-color: #f0f0f0; | ||||||
|  |             border-radius: 5px; | ||||||
|  |             overflow: hidden; | ||||||
|  |         } | ||||||
|  |         .progress-bar { | ||||||
|  |             height: 100%; | ||||||
|  |             background-color: #1890ff; | ||||||
|  |             width: 0; | ||||||
|  |             transition: width 0.3s ease; | ||||||
|  |         } | ||||||
|  |         #progress-percentage { | ||||||
|  |             font-size: 14px; | ||||||
|  |             color: #666; | ||||||
|  |             margin-top: 5px; | ||||||
|  |         } | ||||||
|  |         #status { | ||||||
|  |             margin-top: 15px; | ||||||
|  |             font-size: 14px; | ||||||
|  |             color: #666; | ||||||
|  |         } | ||||||
|  |         #download-link { | ||||||
|  |             display: inline-block; | ||||||
|  |             margin-top: 20px; | ||||||
|  |             text-decoration: none; | ||||||
|  |             color: #1890ff; | ||||||
|  |             font-size: 14px; | ||||||
|  |         } | ||||||
|  |         .upload-stats { | ||||||
|  |             margin-top: 10px; | ||||||
|  |             font-size: 14px; | ||||||
|  |             color: #666; | ||||||
|  |         } | ||||||
|  |         .upload-stats span { | ||||||
|  |             display: inline-block; | ||||||
|  |             margin-right: 15px; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <div class="image-container"> | ||||||
|  |             <img src="image.jpg" alt="描述性文本"> | ||||||
|  |         </div> | ||||||
|  |         <h1>秀儿文件提交系统</h1> | ||||||
|  |         <form id="upload-form"> | ||||||
|  |             <div class="form-field"> | ||||||
|  |                 <label for="student">学生姓名:</label> | ||||||
|  |                 <input type="text" id="student" name="student" required> | ||||||
|  |             </div> | ||||||
|  |             <div class="form-field"> | ||||||
|  |                 <label for="student-id">学号:</label> | ||||||
|  |                 <input type="text" id="student-id" name="student-id" required> | ||||||
|  |             </div> | ||||||
|  |             <div class="form-field"> | ||||||
|  |                 <label for="file">选择文件:</label> | ||||||
|  |                 <div class="file-input-wrapper"> | ||||||
|  |                     <div class="file-input-button"> | ||||||
|  |                         选择文件 | ||||||
|  |                         <input type="file" id="file" name="file" required> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <div id="file-name">未选择文件</div> | ||||||
|  |             </div> | ||||||
|  |             <button type="submit">提交 (截止日期: 2024.07.03)</button> | ||||||
|  |         </form> | ||||||
|  |         <div id="status"></div> | ||||||
|  |         <div class="progress-container" id="progress-container"> | ||||||
|  |             <div class="progress"> | ||||||
|  |                 <div class="progress-bar" id="progress-bar"></div> | ||||||
|  |             </div> | ||||||
|  |             <div id="progress-percentage">0%</div> | ||||||
|  |             <div class="upload-stats"> | ||||||
|  |                 <span id="upload-speed">速度: 0 KB/s</span> | ||||||
|  |                 <span id="upload-size">0 KB / 0 KB</span> | ||||||
|  |                 <span id="upload-time">剩余时间: 计算中</span> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <a href="/download-submissions" id="download-link">下载统计表格</a> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <script> | ||||||
|  |         document.getElementById('upload-form').addEventListener('submit', async (e) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  | 
 | ||||||
|  |     const student = document.getElementById('student').value; | ||||||
|  |     const studentId = document.getElementById('student-id').value; | ||||||
|  |     const file = document.getElementById('file').files[0]; | ||||||
|  |     const statusDiv = document.getElementById('status'); | ||||||
|  |     const progressContainer = document.getElementById('progress-container'); | ||||||
|  |     const progressBar = document.getElementById('progress-bar'); | ||||||
|  |     const progressPercentage = document.getElementById('progress-percentage'); | ||||||
|  |     const uploadSpeed = document.getElementById('upload-speed'); | ||||||
|  |     const uploadSize = document.getElementById('upload-size'); | ||||||
|  |     const uploadTime = document.getElementById('upload-time'); | ||||||
|  | 
 | ||||||
|  |     if (!student || !studentId || !file) { | ||||||
|  |         alert('请提供学生姓名、学号,并选择一个文件。'); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     statusDiv.textContent = '准备上传...'; | ||||||
|  |     progressContainer.style.display = 'block'; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |         const response = await fetch(`/generate-url?student=${encodeURIComponent(student)}&student_id=${encodeURIComponent(studentId)}&filename=${encodeURIComponent(file.name)}&content_type=${encodeURIComponent(file.type)}`, { | ||||||
|  |             method: 'GET', | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (!response.ok) { | ||||||
|  |             const errorData = await response.json(); | ||||||
|  |             throw new Error(`生成预签名URL失败: ${errorData.error}`); | ||||||
|  |         } | ||||||
|  |         const data = await response.json(); | ||||||
|  | 
 | ||||||
|  |         if (!data.url) { | ||||||
|  |             throw new Error('服务器未返回URL'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         statusDiv.textContent = '正在上传文件...'; | ||||||
|  | 
 | ||||||
|  |         const totalSize = file.size; | ||||||
|  |         let uploadedSize = 0; | ||||||
|  |         let startTime = Date.now(); | ||||||
|  | 
 | ||||||
|  |         const updateProgress = (additionalProgress = 0) => { | ||||||
|  |             uploadedSize += additionalProgress; | ||||||
|  |             const percentComplete = (uploadedSize / totalSize) * 100; | ||||||
|  |             const elapsedTime = (Date.now() - startTime) / 1000; // 秒 | ||||||
|  |             const speed = uploadedSize / elapsedTime; | ||||||
|  |             const remainingSize = totalSize - uploadedSize; | ||||||
|  |             const estimatedRemainingTime = speed > 0 ? remainingSize / speed : 0; | ||||||
|  | 
 | ||||||
|  |             progressBar.style.width = percentComplete + '%'; | ||||||
|  |             progressPercentage.textContent = percentComplete.toFixed(2) + '%'; | ||||||
|  |             uploadSpeed.textContent = `速度: ${formatSize(speed)}/s`; | ||||||
|  |             uploadSize.textContent = `${formatSize(uploadedSize)} / ${formatSize(totalSize)}`; | ||||||
|  |             uploadTime.textContent = `剩余时间: ${formatTime(estimatedRemainingTime)}`; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // 模拟进度更新 | ||||||
|  |         const progressInterval = setInterval(() => { | ||||||
|  |             if (uploadedSize < totalSize) { | ||||||
|  |                 updateProgress(totalSize / 100); // 每次更新1%的进度 | ||||||
|  |             } | ||||||
|  |         }, 200); | ||||||
|  | 
 | ||||||
|  |         // 实际上传 | ||||||
|  |         const uploadResponse = await fetch(data.url, { | ||||||
|  |             method: 'PUT', | ||||||
|  |             body: file, | ||||||
|  |             headers: { | ||||||
|  |                 'Content-Type': data.content_type | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         clearInterval(progressInterval); | ||||||
|  | 
 | ||||||
|  |         if (!uploadResponse.ok) { | ||||||
|  |             throw new Error(`Upload failed: ${uploadResponse.statusText}`); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 确保进度达到100% | ||||||
|  |         updateProgress(totalSize - uploadedSize); | ||||||
|  | 
 | ||||||
|  |         statusDiv.textContent = '文件上传成功'; | ||||||
|  |         alert('文件上传成功'); | ||||||
|  | 
 | ||||||
|  |     } catch (error) { | ||||||
|  |         console.error('错误:', error); | ||||||
|  |         statusDiv.textContent = `错误: ${error.message}`; | ||||||
|  |         alert('发生错误: ' + error.message); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function formatSize(bytes) { | ||||||
|  |     if (bytes === 0) return '0 Bytes'; | ||||||
|  |     const k = 1024; | ||||||
|  |     const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | ||||||
|  |     const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||||
|  |     return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function formatTime(seconds) { | ||||||
|  |     if (isNaN(seconds) || !isFinite(seconds)) { | ||||||
|  |         return '计算中'; | ||||||
|  |     } | ||||||
|  |     if (seconds < 60) return Math.round(seconds) + ' 秒'; | ||||||
|  |     const minutes = Math.floor(seconds / 60); | ||||||
|  |     const remainingSeconds = Math.round(seconds % 60); | ||||||
|  |     return `${minutes} 分 ${remainingSeconds} 秒`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 添加文件名显示功能 | ||||||
|  | document.getElementById('file').addEventListener('change', function(e) { | ||||||
|  |     const fileName = e.target.files[0] ? e.target.files[0].name : '未选择文件'; | ||||||
|  |     document.getElementById('file-name').textContent = fileName; | ||||||
|  | }); | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										16
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | # 这是一个示例 Python 脚本。 | ||||||
|  | 
 | ||||||
|  | # 按 ⌃R 执行或将其替换为您的代码。 | ||||||
|  | # 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def print_hi(name): | ||||||
|  |     # 在下面的代码行中使用断点来调试脚本。 | ||||||
|  |     print(f'Hi, {name}')  # 按 ⌘F8 切换断点。 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # 按间距中的绿色按钮以运行脚本。 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     print_hi('PyCharm') | ||||||
|  | 
 | ||||||
|  | # 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 | ||||||
							
								
								
									
										13
									
								
								submissions.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								submissions.csv
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | ID,学生姓名,学号,提交的文件 | ||||||
|  | 2024-07-02T15:38:06.844568,嗷呜呜汪汪汪钦,12345,fastgpt.txt | ||||||
|  | 2024-07-03T02:01:37.364487,王中王火腿肠,123,mao.png | ||||||
|  | 2024-07-03T02:02:02.087326,王中王火腿肠,123,《移动互联开发》软件设计考查材料.rar | ||||||
|  | 2024-07-03T02:31:23.019179,秀儿,000,SUS 304 Stainless Steel.png | ||||||
|  | 2024-07-03T02:31:55.184382,秀儿,000,阅读1.pdf | ||||||
|  | 2024-07-03T02:35:50.030507,嘿!,324,计算机网络_数据链路层.ppt | ||||||
|  | 2024-07-03T02:37:23.232007,CC,34534,"性学观止 (贺兰特·凯查杜里安, 胡颖翀) (Z-Library).pdf" | ||||||
|  | 2024-07-03T02:40:42.161975,4324,4234,数学分析 陈纪修 第三版 上 by 陈纪修,于崇华,金路 (z-lib.org).pdf | ||||||
|  | 2024-07-03T02:42:47.614487,324,4234,男人来自火星,女人来自金星(套装共4册) (约翰·格雷) (Z-Library).pdf | ||||||
|  | 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 | ||||||
| 
 | 
							
								
								
									
										
											BIN
										
									
								
								submissions.xlsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								submissions.xlsx
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										44
									
								
								upload_homework.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								upload_homework.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | import boto3 | ||||||
|  | from botocore.exceptions import NoCredentialsError | ||||||
|  | 
 | ||||||
|  | # 使用访问密钥ID和秘密访问密钥来初始化S3客户端 | ||||||
|  | aws_access_key_id = 'AKIAZQ3DT3KLN5WGXZOR'  # 替换为你的访问密钥ID | ||||||
|  | aws_secret_access_key = '5UZb8SovTrbroT7yU1pBzaR5myLn+NMA+c87RvLH'  # 替换为你的秘密访问密钥 | ||||||
|  | region_name = 'ap-northeast-1'  # 替换为你的S3存储桶所在区域 | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def generate_presigned_url(bucket_name, object_key, expiration=3600): | ||||||
|  |     try: | ||||||
|  |         response = s3_client.generate_presigned_url( | ||||||
|  |             'put_object', | ||||||
|  |             Params={ | ||||||
|  |                 'Bucket': bucket_name, | ||||||
|  |                 'Key': object_key, | ||||||
|  |                 'ContentType': 'application/pdf'  # 设置Content-Type | ||||||
|  |             }, | ||||||
|  |             ExpiresIn=expiration | ||||||
|  |         ) | ||||||
|  |     except NoCredentialsError: | ||||||
|  |         print("Credentials not available") | ||||||
|  |         return None | ||||||
|  |     return response | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # 生成每个学生的上传预签名URL | ||||||
|  | bucket_name = 'sure-ae-upload' | ||||||
|  | folder_name = 'sure_homework_define_by_qin' | ||||||
|  | students = ['alice', 'bob', 'charlie']  # 示例学生名单 | ||||||
|  | 
 | ||||||
|  | for student in students: | ||||||
|  |     object_key = f'{folder_name}/{student}-assignment.pdf'  # 动态生成路径 | ||||||
|  |     url = generate_presigned_url(bucket_name, object_key) | ||||||
|  | 
 | ||||||
|  |     if url: | ||||||
|  |         print(f"The presigned URL for {student} is: {url}") | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 superlishunqin
						superlishunqin