media_route.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import os
  2. import time
  3. from flask import request, jsonify, current_app
  4. from flask_jwt_extended import jwt_required, get_jwt_identity
  5. from werkzeug.utils import secure_filename
  6. from app.constants import OperationType, UserRole
  7. from app.decorators import login_required
  8. from app.models import Operation, Media, db, User, Detection
  9. from app.routes import media_routes
  10. from app.utils import handle_operation_failure, allowed_image_file, handle_file_upload, handle_operation_success, \
  11. adjust_page_if_needed, get_pagination_params, allowed_video_file, get_media_info, delete_file, user_rate_limit
  12. def _current_user_context():
  13. """兼容 JWT identity 为字符串的场景,统一转换为整型主键。"""
  14. raw_identity = get_jwt_identity()
  15. try:
  16. user_id = int(raw_identity)
  17. except (TypeError, ValueError):
  18. user_id = None
  19. user = User.query.get(user_id) if user_id is not None else None
  20. return user_id, user
  21. def _resolve_static_path(raw_path):
  22. if not raw_path:
  23. return None
  24. normalized = str(raw_path).replace('\\', '/').lstrip('/')
  25. if not normalized.startswith('static/'):
  26. normalized = f'static/{normalized}'
  27. return os.path.join(current_app.root_path, normalized)
  28. @media_routes.route('/upload', methods=['POST'])
  29. @jwt_required()
  30. @login_required
  31. def upload():
  32. start_time = time.time() # 记录操作开始时间
  33. # 获取请求中的表单数据
  34. media_file = request.files.get('media_file')
  35. description = request.form.get('description', '暂无描述')
  36. # 创建一个新的操作记录
  37. new_operation = Operation(
  38. operation_type=OperationType.CREATE,
  39. description="上传媒体",
  40. ip_address=request.remote_addr,
  41. device_info=request.user_agent.string,
  42. )
  43. # 获取当前用户身份(使用 access token)
  44. current_user_id, current_user = _current_user_context()
  45. # 先获取文件名
  46. file_name = secure_filename(media_file.filename) if media_file else None
  47. # 检查文件名是否已存在
  48. existing_media = Media.query.filter_by(media_name=file_name).first() if file_name else None
  49. # 兼容“数据库有记录但物理文件丢失”的场景:自动清理殭尸记录,允许重新上传同名文件
  50. if existing_media:
  51. existing_abs_path = _resolve_static_path(existing_media.media_path)
  52. if not os.path.isfile(existing_abs_path):
  53. current_app.logger.warning(
  54. f"【上传媒体】检测到殭尸记录,自动清理:media_id={existing_media.media_id}, media_name={existing_media.media_name}"
  55. )
  56. db.session.delete(existing_media)
  57. db.session.commit()
  58. existing_media = None
  59. # 校验字段
  60. validation_checks = [
  61. (media_file and not (allowed_image_file(media_file) or allowed_video_file(media_file)),
  62. "【上传媒体失败】媒体文件不合规", 400),
  63. (media_file and existing_media, f"【上传媒体失败】媒体 {file_name} 已存在,请重新上传", 400),
  64. ]
  65. for condition, message, code in validation_checks:
  66. if condition:
  67. new_operation = handle_operation_failure(new_operation, start_time, message, current_user_id)
  68. current_app.logger.warning(message + f', operator: {current_user}')
  69. return jsonify({
  70. 'operation': new_operation.to_dict(),
  71. }), code
  72. # 保存文件到指定目录(返回相对路径)
  73. file_path = handle_file_upload(media_file, 'medias')
  74. # 获取文件类型(文件后缀)
  75. file_type = file_name.rsplit('.', 1)[1].lower()
  76. # 获取文件绝对路径
  77. abs_path = os.path.join(current_app.root_path, file_path)
  78. # 获取媒体大小、分辨率、帧数
  79. file_size, resolution_width, resolution_height, frame_count = get_media_info(abs_path)
  80. new_media = Media(
  81. media_name=file_name,
  82. media_path=file_path,
  83. description=description,
  84. file_size=file_size,
  85. file_type=file_type,
  86. resolution_width=resolution_width,
  87. resolution_height=resolution_height,
  88. frame_count=frame_count,
  89. owner_id=current_user_id,
  90. )
  91. db.session.add(new_media)
  92. db.session.commit()
  93. # 记录操作
  94. new_operation = handle_operation_success(new_operation, start_time, current_user_id)
  95. current_app.logger.info(f"【上传媒体成功】new_media: {new_media}, operator: {current_user}")
  96. return jsonify({
  97. 'operation': new_operation.to_dict(),
  98. 'new_media': new_media.to_dict(),
  99. }), 201
  100. @media_routes.route('/detail/<int:media_id>', methods=['GET'])
  101. @jwt_required()
  102. @login_required
  103. def detail(media_id):
  104. # 获取当前用户身份(使用 access token)
  105. current_user_id, current_user = _current_user_context()
  106. # 获取指定媒体
  107. media = Media.query.get(media_id)
  108. # 校验字段
  109. validation_checks = [
  110. (not media, f"【获取媒体 ID={media_id} 详情失败】该媒体不存在", 404),
  111. (media and media.owner_id != current_user_id and current_user.role != UserRole.ADMIN
  112. and current_user.role != UserRole.DEVELOPER,
  113. f"【获取媒体 ID={media_id} 详情失败】您非管理员/开发人员,无法查看他人的媒体详情", 403),
  114. ]
  115. for condition, message, code in validation_checks:
  116. if condition:
  117. current_app.logger.warning(message + f', operator: {current_user}')
  118. return jsonify({
  119. 'failure_message': message,
  120. }), code
  121. return jsonify({
  122. 'media': media.to_dict(),
  123. }), 200
  124. @media_routes.route('/update/<int:media_id>', methods=['PUT'])
  125. @jwt_required()
  126. @login_required
  127. def update(media_id):
  128. start_time = time.time() # 记录操作开始时间
  129. # 获取请求中的表单数据
  130. description = request.form.get('description')
  131. # 创建一个新的操作记录
  132. new_operation = Operation(
  133. operation_type=OperationType.UPDATE,
  134. description=f"更新媒体 ID={media_id} 信息",
  135. ip_address=request.remote_addr,
  136. device_info=request.user_agent.string,
  137. )
  138. # 获取当前用户的身份(使用 access token)
  139. current_user_id, current_user = _current_user_context()
  140. # 获取指定媒体
  141. updated_media = Media.query.get(media_id)
  142. # 校验字段
  143. validation_checks = [
  144. (not updated_media, f"【更新媒体 ID={media_id} 信息失败】该媒体不存在", 404),
  145. (updated_media and updated_media.owner_id != current_user_id and current_user.role != UserRole.ADMIN
  146. and current_user.role != UserRole.DEVELOPER,
  147. f"【更新媒体 ID={media_id} 信息失败】您非管理员/开发人员,无法更新他人的媒体信息", 403),
  148. ]
  149. for condition, message, code in validation_checks:
  150. if condition:
  151. new_operation = handle_operation_failure(new_operation, start_time, message, current_user_id)
  152. current_app.logger.warning(message + f', operator: {current_user}')
  153. return jsonify({
  154. 'operation': new_operation.to_dict(),
  155. }), code
  156. # 更新媒体信息
  157. updated_media.description = description if description else updated_media.description
  158. db.session.commit()
  159. # 记录操作
  160. new_operation = handle_operation_success(new_operation, start_time, current_user_id)
  161. current_app.logger.info(
  162. f"【更新媒体 ID={media_id} 信息成功】updated_media: {updated_media}, operator: {current_user}")
  163. return jsonify({
  164. 'operation': new_operation.to_dict(),
  165. 'updated_media': updated_media.to_dict(),
  166. }), 200
  167. @media_routes.route('/delete/<int:media_id>', methods=['DELETE'])
  168. @jwt_required()
  169. @login_required
  170. def delete_media(media_id):
  171. start_time = time.time() # 记录操作开始时间
  172. # 创建一个新的操作记录
  173. new_operation = Operation(
  174. operation_type=OperationType.DELETE,
  175. description=f"删除媒体 ID={media_id}",
  176. ip_address=request.remote_addr,
  177. device_info=request.user_agent.string,
  178. )
  179. # 获取当前用户身份(使用 access token)
  180. current_user_id, current_user = _current_user_context()
  181. # 获取指定媒体
  182. deleted_media = Media.query.get(media_id)
  183. # 校验字段
  184. validation_checks = [
  185. (not deleted_media, f"【删除媒体 ID={media_id} 失败】该媒体不存在", 404),
  186. (deleted_media and deleted_media.owner_id != current_user_id and current_user.role != UserRole.ADMIN
  187. and current_user.role != UserRole.DEVELOPER,
  188. f"【删除媒体 ID={media_id} 失败】您非管理员/开发人员,无法删除他人的媒体", 403),
  189. (deleted_media and Detection.query.filter_by(media_id=media_id).first(),
  190. f"【删除媒体 ID={media_id} 失败】该媒体存在关联的检测分割记录,无法删除", 400),
  191. ]
  192. for condition, message, code in validation_checks:
  193. if condition:
  194. new_operation = handle_operation_failure(new_operation, start_time, message, current_user_id)
  195. current_app.logger.warning(message + f', operator: {current_user}')
  196. return jsonify({
  197. 'operation': new_operation.to_dict(),
  198. }), code
  199. # 删除实际文件
  200. file_abs_path = os.path.join(current_app.root_path, deleted_media.media_path)
  201. delete_file(file_abs_path)
  202. # 删除数据库记录
  203. db.session.delete(deleted_media)
  204. db.session.commit()
  205. # 记录操作
  206. new_operation = handle_operation_success(new_operation, start_time, current_user_id)
  207. current_app.logger.info(f"【删除媒体 ID={media_id} 成功】deleted_media: {deleted_media}, operator: {current_user}")
  208. return jsonify({
  209. 'operation': new_operation.to_dict(),
  210. 'deleted_media': deleted_media.to_dict(),
  211. }), 200
  212. @media_routes.route('/medias/<int:user_id>', methods=['GET'])
  213. @jwt_required()
  214. @login_required
  215. def user_medias(user_id):
  216. # 获取分页参数(从请求中获取,默认为第 1 页,每页 5 条记录)
  217. default_page = request.args.get('page', 1, type=int)
  218. default_per_page = request.args.get('per_page', 5, type=int)
  219. page, per_page = get_pagination_params(default_page, default_per_page)
  220. # 获取当前用户身份(使用 access token)
  221. current_user_id, current_user = _current_user_context()
  222. # 获取指定用户身份
  223. user = User.query.get(user_id)
  224. # 校验字段
  225. validation_checks = [
  226. (not user, f"【获取用户 ID={user_id} 媒体失败】该用户不存在", 404),
  227. (current_user_id != user_id and current_user.role != UserRole.ADMIN and current_user.role != UserRole.DEVELOPER,
  228. f"【获取用户 ID={user_id} 媒体失败】您非管理员/开发人员,无法查看他人的媒体", 403),
  229. ]
  230. for condition, message, code in validation_checks:
  231. if condition:
  232. current_app.logger.warning(message + f', operator: {current_user}')
  233. return jsonify({
  234. 'failure_message': message,
  235. }), code
  236. # 获取指定用户媒体
  237. query = (
  238. Media.query
  239. .filter(Media.owner_id == user_id)
  240. .join(User, Media.owner_id == User.user_id)
  241. .add_columns(User.username.label('owner_username'))
  242. .order_by(Media.media_id.desc())
  243. )
  244. page, medias_total, pages = adjust_page_if_needed(query, page, per_page)
  245. paginated = query.paginate(page=page, per_page=per_page, error_out=False)
  246. medias = []
  247. stale_media_ids = []
  248. for media, owner_username in paginated.items:
  249. media_abs_path = _resolve_static_path(media.media_path)
  250. if not os.path.isfile(media_abs_path):
  251. stale_media_ids.append(media.media_id)
  252. continue
  253. media_dict = media.to_dict()
  254. media_dict.update({'owner_username': owner_username})
  255. medias.append(media_dict)
  256. if stale_media_ids:
  257. Media.query.filter(Media.media_id.in_(stale_media_ids)).delete(synchronize_session=False)
  258. db.session.commit()
  259. current_app.logger.info(
  260. f"【获取用户 ID={user_id} 媒体成功】total: {medias_total}, per_page: {per_page}, page: {page}, pages: {pages}, medias: {medias}, operator: {current_user}")
  261. return jsonify({
  262. 'medias': medias,
  263. 'total': medias_total,
  264. 'per_page': per_page,
  265. 'page': page,
  266. 'pages': pages,
  267. }), 200
  268. @media_routes.route('/medias/all', methods=['GET'])
  269. @jwt_required()
  270. @login_required
  271. def all_medias():
  272. # 获取分页参数(从请求中获取,默认为第 1 页,每页 5 条记录)
  273. default_page = request.args.get('page', 1, type=int)
  274. default_per_page = request.args.get('per_page', 5, type=int)
  275. page, per_page = get_pagination_params(default_page, default_per_page)
  276. # 获取当前用户身份(使用 access token)
  277. current_user_id, current_user = _current_user_context()
  278. if current_user.role != UserRole.ADMIN and current_user.role != UserRole.DEVELOPER:
  279. failure_message = f"【获取所有媒体失败】您非管理员/开发人员,权限不足"
  280. current_app.logger.warning(failure_message + f', operator: {current_user}')
  281. return jsonify({
  282. 'failure_message': failure_message,
  283. }), 403
  284. # 获取所有媒体
  285. query = (
  286. Media.query
  287. .join(User, Media.owner_id == User.user_id)
  288. .add_columns(User.username.label('owner_username'))
  289. .order_by(Media.media_id.desc())
  290. )
  291. page, medias_total, pages = adjust_page_if_needed(query, page, per_page)
  292. paginated = query.paginate(page=page, per_page=per_page, error_out=False)
  293. medias = []
  294. stale_media_ids = []
  295. for media, owner_username in paginated.items:
  296. media_abs_path = _resolve_static_path(media.media_path)
  297. if not os.path.isfile(media_abs_path):
  298. stale_media_ids.append(media.media_id)
  299. continue
  300. media_dict = media.to_dict()
  301. media_dict.update({'owner_username': owner_username})
  302. medias.append(media_dict)
  303. if stale_media_ids:
  304. Media.query.filter(Media.media_id.in_(stale_media_ids)).delete(synchronize_session=False)
  305. db.session.commit()
  306. current_app.logger.info(
  307. f"【获取所有媒体成功】total: {medias_total}, per_page: {per_page}, page: {page}, pages: {pages}, medias: {medias}, operator: {current_user}")
  308. return jsonify({
  309. 'medias': medias,
  310. 'total': medias_total,
  311. 'per_page': per_page,
  312. 'page': page,
  313. 'pages': pages,
  314. }), 200
  315. @media_routes.route('/statistics', methods=['GET'])
  316. @jwt_required()
  317. @login_required
  318. def statistics():
  319. # 查询媒体总数
  320. total_medias = Media.query.count()
  321. # 图片类型(png, jpg, jpeg)
  322. image_count = Media.query.filter(Media.file_type.in_(['png', 'jpg', 'jpeg'])).count()
  323. # 视频类型(mp4)
  324. video_count = Media.query.filter(Media.file_type.in_(['mp4'])).count()
  325. # 构建返回数据
  326. medias_statistics = {
  327. 'total': total_medias,
  328. 'image': image_count,
  329. 'video': video_count,
  330. }
  331. return jsonify({
  332. "medias_statistics": medias_statistics,
  333. }), 200