schema.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from datetime import datetime
  2. from urllib.parse import urlparse
  3. from fastapi import Query
  4. from pydantic import (
  5. BaseModel,
  6. ConfigDict,
  7. Field,
  8. field_validator,
  9. model_validator,
  10. )
  11. from app.common.enums import QueueEnum
  12. class ResourceItemSchema(BaseModel):
  13. """资源项目模型"""
  14. model_config = ConfigDict(from_attributes=True)
  15. name: str = Field(..., description="文件名")
  16. file_url: str = Field(..., description="文件URL路径")
  17. relative_path: str = Field(..., description="相对路径")
  18. is_file: bool = Field(..., description="是否为文件")
  19. is_dir: bool = Field(..., description="是否为目录")
  20. size: int | None = Field(None, description="文件大小(字节)")
  21. created_time: datetime | None = Field(None, description="创建时间")
  22. modified_time: datetime | None = Field(None, description="修改时间")
  23. is_hidden: bool = Field(False, description="是否为隐藏文件")
  24. @field_validator("file_url")
  25. @classmethod
  26. def _validate_file_url(cls, v: str) -> str:
  27. v = v.strip()
  28. parsed = urlparse(v)
  29. # 允许相对路径(以 / 开头)和完整的 http/https URL
  30. if parsed.scheme and parsed.scheme not in ("http", "https"):
  31. raise ValueError("文件URL必须为 http/https 或相对路径")
  32. return v
  33. @field_validator("relative_path")
  34. @classmethod
  35. def _validate_relative_path(cls, v: str) -> str:
  36. v = v.strip()
  37. if ".." in v or v.startswith("\\"):
  38. raise ValueError("相对路径包含不安全字符")
  39. return v
  40. @model_validator(mode="after")
  41. def _validate_flags(self):
  42. if self.is_file and self.is_dir:
  43. raise ValueError("不能同时为文件和目录")
  44. if not self.is_file and not self.is_dir:
  45. raise ValueError("必须是文件或目录之一")
  46. # 根据名称自动修正隐藏标记
  47. self.is_hidden = self.name.startswith(".")
  48. return self
  49. class ResourceDirectorySchema(BaseModel):
  50. """资源目录模型"""
  51. model_config = ConfigDict(from_attributes=True)
  52. path: str = Field(..., description="目录路径")
  53. name: str = Field(..., description="目录名称")
  54. items: list[ResourceItemSchema] = Field(default_factory=list, description="目录项")
  55. total_files: int = Field(0, description="文件总数")
  56. total_dirs: int = Field(0, description="目录总数")
  57. total_size: int = Field(0, description="总大小")
  58. class ResourceUploadSchema(BaseModel):
  59. """资源上传响应模型"""
  60. model_config = ConfigDict(from_attributes=True)
  61. filename: str = Field(..., description="文件名")
  62. file_url: str = Field(..., description="访问URL")
  63. file_size: int = Field(..., description="文件大小")
  64. upload_time: datetime = Field(..., description="上传时间")
  65. class ResourceMoveSchema(BaseModel):
  66. """资源移动模型"""
  67. model_config = ConfigDict(from_attributes=True)
  68. source_path: str = Field(..., description="源路径")
  69. target_path: str = Field(..., description="目标路径")
  70. overwrite: bool = Field(False, description="是否覆盖")
  71. @field_validator("source_path", "target_path")
  72. @classmethod
  73. def validate_paths(cls, value: str):
  74. """
  75. 校验移动/复制涉及的源路径与目标路径非空并去首尾空格。
  76. 参数:
  77. - value (str): 路径字段当前值。
  78. 返回:
  79. - str: 去空格后的路径。
  80. 异常:
  81. - ValueError: 路径为空时抛出。
  82. """
  83. if not value or len(value.strip()) == 0:
  84. raise ValueError("路径不能为空")
  85. return value.strip()
  86. class ResourceCopySchema(ResourceMoveSchema):
  87. """资源复制模型"""
  88. class ResourceRenameSchema(BaseModel):
  89. """资源重命名模型"""
  90. model_config = ConfigDict(from_attributes=True)
  91. old_path: str = Field(..., description="原路径")
  92. new_name: str = Field(..., description="新名称")
  93. @field_validator("old_path", "new_name")
  94. @classmethod
  95. def validate_inputs(cls, value: str):
  96. """
  97. 校验重命名所需的原路径与新名称非空并去首尾空格。
  98. 参数:
  99. - value (str): 字段当前值。
  100. 返回:
  101. - str: 去空格后的值。
  102. 异常:
  103. - ValueError: 值为空时抛出。
  104. """
  105. if not value or len(value.strip()) == 0:
  106. raise ValueError("参数不能为空")
  107. return value.strip()
  108. @field_validator("new_name")
  109. @classmethod
  110. def _validate_new_name(cls, v: str) -> str:
  111. v = v.strip()
  112. if ".." in v or "/" in v or "\\" in v:
  113. raise ValueError("新名称包含不安全字符")
  114. return v
  115. class ResourceCreateDirSchema(BaseModel):
  116. """创建目录模型"""
  117. model_config = ConfigDict(from_attributes=True)
  118. parent_path: str = Field(..., description="父目录路径")
  119. dir_name: str = Field(..., description="目录名称", max_length=255)
  120. @field_validator("parent_path", "dir_name")
  121. @classmethod
  122. def validate_inputs(cls, value: str, info):
  123. """
  124. 校验创建目录的父路径与目录名,防止路径遍历等不安全输入。
  125. 参数:
  126. - value (str): 当前字段值。
  127. - info: Pydantic 校验上下文(含 `field_name`)。
  128. 返回:
  129. - str: 规范化后的字段值。
  130. 异常:
  131. - ValueError: 含不安全字符或目录名为空时抛出。
  132. """
  133. # 对于parent_path允许为空字符串(表示根目录)或 '/',其他情况必须非空
  134. if info.field_name == "parent_path":
  135. # 对于parent_path仍然严格检查路径遍历
  136. if ".." in value or value.startswith("\\"):
  137. raise ValueError("参数包含不安全字符")
  138. else: # 对于dir_name仍然严格检查
  139. if not value or len(value.strip()) == 0:
  140. raise ValueError("参数不能为空")
  141. if ".." in value or value.startswith(("/", "\\")):
  142. raise ValueError("参数包含不安全字符")
  143. return value.strip()
  144. class ResourceSearchQueryParam:
  145. """资源搜索查询参数"""
  146. def __init__(
  147. self,
  148. name: str | None = Query(None, description="搜索关键词"),
  149. path: str | None = Query(None, description="目录路径"),
  150. ) -> None:
  151. # 模糊查询字段
  152. self.name = (QueueEnum.like.value, name) if name else None
  153. # 精确查询字段
  154. self.path = path