MinioService.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. import base64
  2. import os
  3. from datetime import timedelta, datetime
  4. from io import BytesIO
  5. import logging
  6. import chardet
  7. import fitz
  8. import unicodedata
  9. from cfgv import ValidationError
  10. from django.core.files.base import ContentFile
  11. from django.db import transaction
  12. from minio import Minio, S3Error
  13. from minio.commonconfig import CopySource
  14. from DCbackend import settings
  15. from DCbackend.utils.common import success, fail
  16. from backend.models import Admin, Knowledgebase, File, DocumentKbm, File2document
  17. from pypinyin import lazy_pinyin, Style
  18. import re
  19. # 配置 logger
  20. logger = logging.getLogger(__name__)
  21. # 初始化 MinIO 客户端
  22. minio_client = Minio(
  23. settings.MINIO_ENDPOINT,
  24. access_key=settings.MINIO_ACCESS_KEY,
  25. secret_key=settings.MINIO_SECRET_KEY,
  26. secure=settings.MINIO_SECURE
  27. )
  28. def contains_chinese(text):
  29. """检查字符串是否包含中文字符"""
  30. return any('\u4e00' <= char <= '\u9fff' for char in text)
  31. def convert_to_pinyin(text):
  32. """
  33. 将中文字符转换为不带声调的拼音,保留字母和数字,其他字符替换为连字符,
  34. 并确保生成的名称符合 bucket 命名规则
  35. """
  36. result = []
  37. for char in text:
  38. if '\u4e00' <= char <= '\u9fff':
  39. # 中文字符
  40. pinyin = lazy_pinyin(char, style=Style.NORMAL)
  41. result.extend(pinyin)
  42. elif char.isalnum():
  43. # 字母和数字
  44. result.append(char.lower())
  45. else:
  46. # 其他字符替换为连字符
  47. result.append('-')
  48. # 合并字符并去除声调
  49. bucket_name = ''.join(result)
  50. bucket_name = unicodedata.normalize('NFKD', bucket_name).encode('ASCII', 'ignore').decode('ASCII')
  51. # 合并连续的连字符
  52. bucket_name = re.sub(r'-+', '-', bucket_name)
  53. # 确保名称以字母或数字开头和结尾
  54. bucket_name = bucket_name.strip('-')
  55. # 如果名称为空,使用默认名称
  56. if not bucket_name:
  57. bucket_name = 'default-bucket'
  58. # 确保名称长度在 3-63 之间
  59. if len(bucket_name) < 3:
  60. bucket_name = bucket_name.ljust(3, 'a')
  61. elif len(bucket_name) > 63:
  62. bucket_name = bucket_name[:63]
  63. return bucket_name.lower()
  64. class MinioService:
  65. @staticmethod
  66. def listBuckets(request):
  67. #查询所有bucket信息
  68. buckets = minio_client.list_buckets()
  69. # 将 Bucket 对象转换为可序列化的字典,包含所有可用信息
  70. serializable_buckets = []
  71. for bucket in buckets:
  72. # 处理创建日期
  73. if isinstance(bucket.creation_date, datetime):
  74. # 添加8小时
  75. adjusted_date = bucket.creation_date + timedelta(hours=8)
  76. formatted_date = adjusted_date.strftime("%Y-%m-%d %H:%M:%S")
  77. else:
  78. formatted_date = str(bucket.creation_date)
  79. bucket_info = {
  80. 'bucket_name': bucket.name,
  81. 'creation_date': formatted_date,
  82. }
  83. #根据桶名查询数量
  84. try:
  85. objects = minio_client.list_objects(bucket.name, recursive=True)
  86. file_count = sum(1 for _ in objects)
  87. bucket_info['file_count'] = file_count
  88. except Exception as e:
  89. bucket_info['file_count'] = f"Error: {str(e)}"
  90. serializable_buckets.append(bucket_info)
  91. return success(serializable_buckets)
  92. @staticmethod
  93. def createBucket(request):
  94. """创建新的 bucket"""
  95. try:
  96. user_id = request.POST.get("user_id")
  97. # 从请求中获取 bucket 名称
  98. bucket_name = request.POST.get('bucket_name')
  99. description = request.POST.get('description', "")
  100. if not bucket_name:
  101. return fail('Bucket name is required')
  102. minio_name = bucket_name
  103. if contains_chinese(bucket_name):
  104. minio_name = convert_to_pinyin(bucket_name)
  105. # 检查 bucket 是否已存在
  106. if minio_client.bucket_exists(minio_name):
  107. return fail(f'Bucket "{minio_name}" 已存在')
  108. # 创建 bucket
  109. minio_client.make_bucket(minio_name)
  110. # 同步数据库
  111. return MinioService.saveBucketDb(bucket_name, user_id, minio_name, description)
  112. except S3Error as e:
  113. return fail(f'Failed to create bucket: {str(e)}')
  114. except Exception as e:
  115. return fail(f'An unexpected error occurred: {str(e)}')
  116. @staticmethod
  117. def saveBucketDb(bucket_name, user_id,minio_name,description):
  118. try:
  119. admin = Admin.objects.get(id=user_id)
  120. except Admin.DoesNotExist:
  121. return fail("用户信息不存在")
  122. try:
  123. # 假设 Knowledgebase 是您的模型类名
  124. db = Knowledgebase(
  125. role_id=admin.role_id,
  126. name=bucket_name,
  127. location=minio_name,
  128. description=description,
  129. created_by=user_id
  130. )
  131. db.save()
  132. file = File.objects.filter(
  133. role_id=admin.role_id,
  134. created_by=user_id,
  135. name='/',
  136. source_type=''
  137. ).exclude(status=4).first()
  138. if file is None:
  139. # 如果记录不存在,创建新记录
  140. file = File.objects.create(
  141. role_id=admin.role_id,
  142. name='/',
  143. source_type='',
  144. created_by= user_id,
  145. location= '',
  146. type= "folder",
  147. status= 5,
  148. )
  149. # 创建后更新parent_id
  150. file.parent_id = file.id
  151. file.save()
  152. kbm = File.objects.create(
  153. role_id=admin.role_id,
  154. name='.knowledgebase', # 根据需要修改名称
  155. source_type='knowledgebase',
  156. created_by=user_id,
  157. location='',
  158. type="folder",
  159. status=5,
  160. parent_id=file.id # 设置parent_id为主记录的id
  161. )
  162. bucket = File.objects.create(
  163. role_id=admin.role_id,
  164. name=bucket_name, # 根据需要修改名称
  165. source_type='knowledgebase',
  166. created_by=user_id,
  167. location='',
  168. type="folder",
  169. status=5,
  170. parent_id=kbm.id
  171. )
  172. else:
  173. kbm = File.objects.get(
  174. role_id=admin.role_id,
  175. created_by=user_id,
  176. name='.knowledgebase',
  177. type='folder',
  178. source_type='knowledgebase'
  179. )
  180. bucket = File.objects.create(
  181. role_id=admin.role_id,
  182. name=bucket_name, # 根据需要修改名称
  183. source_type='knowledgebase',
  184. created_by=user_id,
  185. location='',
  186. type="folder",
  187. status=5,
  188. parent_id=kbm.id
  189. )
  190. except ValidationError as e:
  191. return fail(f"验证错误: {e}")
  192. except Exception as e:
  193. return fail(f"保存失败: {str(e)}")
  194. return success(f'Bucket "{bucket_name}" 创建成功')
  195. @staticmethod
  196. def is_valid_bucket_name(bucket_name):
  197. """检查 bucket 名称是否合法"""
  198. import re
  199. # bucket 名称必须在 3-63 个字符之间,只能包含小写字母、数字和连字符
  200. pattern = r'^[a-z0-9][a-z0-9\-]{1,61}[a-z0-9]$'
  201. return re.match(pattern, bucket_name) is not None
  202. #获取指定buck内文件信息
  203. @staticmethod
  204. def getBucketContents(request):
  205. """获取指定 bucket 内的所有文件信息"""
  206. try:
  207. # 从请求中获取 bucket 名称
  208. bucket_name = request.POST.get('bucket_name')
  209. # 使用 get 方法获取 page 和 page_size,如果不存在则使用默认值
  210. page = int(request.POST.get('page', 1))
  211. page_size = int(request.POST.get('page_size', 10))
  212. print(page, page_size)
  213. if not bucket_name:
  214. return fail('请求参数为空')
  215. # 检查 bucket 是否存在
  216. if not minio_client.bucket_exists(bucket_name):
  217. return fail(f'Bucket "{bucket_name}" 不存在')
  218. # 获取 bucket 内的所有对象
  219. objects = minio_client.list_objects(bucket_name, recursive=True)
  220. # 整理文件信息
  221. file_info = list(objects) # 转换为列表以获得准确的长度
  222. # 计算总数和总页数
  223. total_count = len(file_info)
  224. total_pages = (total_count + page_size - 1) // page_size
  225. # 确保页码在有效范围内
  226. page = max(1, min(page, total_pages))
  227. # 计算切片的起始和结束索引
  228. start_index = (page - 1) * page_size
  229. end_index = min(start_index + page_size, total_count)
  230. # 获取当前页的数据
  231. paginated_files = [
  232. {
  233. 'object_name': obj.object_name,
  234. 'size': obj.size,
  235. 'last_modified': obj.last_modified,
  236. 'version_id': obj.version_id,
  237. 'etag': obj.etag
  238. }
  239. for obj in file_info[start_index:end_index]
  240. ]
  241. return success({
  242. 'bucket_name': bucket_name,
  243. 'files': paginated_files,
  244. 'page': page,
  245. 'page_size': page_size,
  246. 'total_pages': total_pages,
  247. 'total_count': total_count
  248. })
  249. except S3Error as e:
  250. return fail(f'Failed to get bucket contents: {str(e)}')
  251. except Exception as e:
  252. return fail(f'An unexpected error occurred: {str(e)}')
  253. @staticmethod
  254. @transaction.atomic()
  255. # def post(request):
  256. # """上传文件"""
  257. # uploaded_file = request.FILES['file'] # 获取上传的文件
  258. # bucket_id = request.POST.get('bucket_id') # BUCKET的名称
  259. # user_id = request.POST.get('user_id') # BUCKET的名称
  260. # file_path = request.POST.get('file_path', '')
  261. # doc_type_id = request.POST.get('doc_type_id',0)
  262. #
  263. # if not uploaded_file:
  264. # return fail('没有需要上传的文件')
  265. # if not bucket_id:
  266. # return fail('bucket_id为空')
  267. #
  268. # # 使用 file_path 和文件名构造对象名称
  269. # object_name = f"{file_path.strip('/')}/{uploaded_file.name}".lstrip('/')
  270. # count = DocumentKbm.objects.filter(name=object_name, kb_id=bucket_id).exclude(status=4).count()
  271. # if count > 0:
  272. # return fail("已有重复文件,请删除后重试")
  273. #
  274. # try:
  275. # # 读取文件内容
  276. # file_content = uploaded_file.read()
  277. #
  278. # # 使用 MinIO 客户端上传文件
  279. # knowledgebase = Knowledgebase.objects.get(id=bucket_id)
  280. # bucket_name = knowledgebase.location
  281. #
  282. # minio_client.put_object(
  283. # bucket_name,
  284. # object_name,
  285. # ContentFile(file_content),
  286. # length=len(file_content),
  287. # content_type=uploaded_file.content_type
  288. # )
  289. # MinioService.saveDocumentKbm(knowledgebase, uploaded_file, user_id, object_name,doc_type_id)
  290. #
  291. # return success("保存成功")
  292. # except Exception as e:
  293. # return fail(str(e))
  294. def post(request):
  295. """上传文件"""
  296. logger.info("Starting file upload process")
  297. try:
  298. uploaded_file = request.FILES['file']
  299. bucket_id = request.POST.get('bucket_id')
  300. user_id = request.POST.get('user_id')
  301. file_path = request.POST.get('file_path', '')
  302. doc_type_id = request.POST.get('doc_type_id', 0)
  303. if not uploaded_file:
  304. return fail('没有需要上传的文件')
  305. if not bucket_id:
  306. return fail('bucket_id为空')
  307. object_name = f"{file_path.strip('/')}/{uploaded_file.name}".lstrip('/')
  308. logger.debug(f"Constructed object name: {object_name}")
  309. count = DocumentKbm.objects.filter(name=object_name, kb_id=bucket_id).exclude(status=4).count()
  310. if count > 0:
  311. return fail("已有重复文件,请删除后重试")
  312. file_content = uploaded_file.read()
  313. knowledgebase = Knowledgebase.objects.get(id=bucket_id)
  314. bucket_name = knowledgebase.location
  315. logger.info(f"Uploading file to MinIO: {object_name} in bucket {bucket_name}")
  316. minio_client.put_object(
  317. bucket_name,
  318. object_name,
  319. ContentFile(file_content),
  320. length=len(file_content),
  321. content_type=uploaded_file.content_type
  322. )
  323. logger.info("MinIO upload successful, calling saveDocumentKbm")
  324. MinioService.saveDocumentKbm(knowledgebase, uploaded_file, user_id, object_name, doc_type_id)
  325. logger.info("File upload process completed successfully")
  326. return success("保存成功")
  327. except Exception as e:
  328. logger.error(f"Error in file upload process: {str(e)}", exc_info=True)
  329. return fail(str(e))
  330. def is_image_file(extension):
  331. image_extensions = [
  332. 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp',
  333. 'svg', 'raw', 'heif', 'heic', 'indd',
  334. 'ai', 'eps', 'psd', 'xcf', 'cr2', 'nef', 'orf', 'sr2',
  335. 'jfif', 'exif', 'ico', 'tga'
  336. ]
  337. return extension.lower().strip('.') in image_extensions
  338. def is_text_file(extension):
  339. text_extensions = [
  340. 'txt', 'pdf', 'doc', 'docx', 'rtf', 'odt', 'xls', 'xlsx',
  341. 'csv', 'tsv', 'json', 'xml', 'html', 'htm', 'md', 'markdown',
  342. 'tex', 'log', 'ini', 'cfg', 'conf', 'py', 'js', 'css', 'scss',
  343. 'less', 'sql', 'php', 'java', 'c', 'cpp', 'h', 'hpp', 'sh',
  344. 'bat', 'ps1', 'rb', 'yaml', 'yml', 'toml', 'rst', 'asciidoc',
  345. 'ppt', 'pptx', 'odp', 'key', 'pages', 'numbers'
  346. ]
  347. return extension.lower().strip('.') in text_extensions
  348. @staticmethod
  349. @transaction.atomic
  350. # def saveDocumentKbm(knowledgebase,uploaded_file,user_id,object_name,doc_type_id):
  351. # try:
  352. # size = uploaded_file.size
  353. # _, file_extension = os.path.splitext(uploaded_file.name)
  354. # # file_extension 现在包含了文件的后缀名,包括点号(例如 ".txt")
  355. # # 如果您不想要点号,可以这样做:
  356. # file_extension = file_extension[1:] if file_extension else ''
  357. # # 根据文件类型设置 parser_id
  358. # if MinioService.is_image_file(file_extension):
  359. # parser_id = 'picture'
  360. # elif MinioService.is_text_file(file_extension):
  361. # parser_id = 'naive'
  362. #
  363. # documentKbm = DocumentKbm.objects.create(
  364. # kb_id=knowledgebase.id,
  365. # parser_id=parser_id,
  366. # parser_config='{"pages": [[1, 1000000]]}',
  367. # type = file_extension,
  368. # created_by=user_id,
  369. # name=object_name,
  370. # location=object_name,
  371. # size=size,
  372. # doc_type_id=doc_type_id
  373. # )
  374. # doc_id = documentKbm.id
  375. #
  376. # admin = Admin.objects.get(id=user_id)
  377. # file = File.objects.filter(
  378. # role_id=admin.role_id,
  379. # created_by=user_id,
  380. # name=knowledgebase.name,
  381. # source_type='knowledgebase',
  382. # type='folder'
  383. # ).exclude(status=4).first()
  384. #
  385. # fileDB = File.objects.create(
  386. # role_id=admin.role_id,
  387. # name=object_name,
  388. # source_type='knowledgebase',
  389. # created_by=user_id,
  390. # location=object_name,
  391. # type=file_extension,
  392. # status=5,
  393. # size=size,
  394. # parent_id= file.id
  395. # )
  396. # file_id = fileDB.id
  397. #
  398. # file2document = File2document.objects.create(
  399. # file_id=file_id,
  400. # document_id=doc_id
  401. # )
  402. #
  403. #
  404. #
  405. # child_count = File.objects.filter(parent_id=file.id).exclude(status=4).count()
  406. #
  407. # knowledgebase.doc_num = child_count
  408. # knowledgebase.save()
  409. #
  410. #
  411. # except Exception as e:
  412. #
  413. # return fail(str(e))
  414. def saveDocumentKbm(knowledgebase, uploaded_file, user_id, object_name, doc_type_id):
  415. logger.info(f"Starting saveDocumentKbm for file: {object_name}")
  416. try:
  417. size = uploaded_file.size
  418. _, file_extension = os.path.splitext(uploaded_file.name)
  419. file_extension = file_extension[1:] if file_extension else ''
  420. logger.debug(f"File size: {size}, extension: {file_extension}")
  421. if MinioService.is_image_file(file_extension):
  422. parser_id = 'picture'
  423. elif MinioService.is_text_file(file_extension):
  424. parser_id = 'naive'
  425. else:
  426. parser_id = 'unknown'
  427. logger.debug(f"Determined parser_id: {parser_id}")
  428. documentKbm = DocumentKbm.objects.create(
  429. kb_id=knowledgebase.id,
  430. parser_id=parser_id,
  431. parser_config='{"pages": [[1, 1000000]]}',
  432. type=file_extension,
  433. created_by=user_id,
  434. name=object_name,
  435. location=object_name,
  436. size=size,
  437. doc_type_id=doc_type_id
  438. )
  439. logger.info(f"Created DocumentKbm with id: {documentKbm.id}")
  440. admin = Admin.objects.get(id=user_id)
  441. file = File.objects.filter(
  442. role_id=admin.role_id,
  443. created_by=user_id,
  444. name=knowledgebase.name,
  445. source_type='knowledgebase',
  446. type='folder'
  447. ).exclude(status=4).first()
  448. if not file:
  449. logger.warning(f"No matching File found for knowledgebase: {knowledgebase.name}")
  450. raise ValueError("No matching File found")
  451. fileDB = File.objects.create(
  452. role_id=admin.role_id,
  453. name=object_name,
  454. source_type='knowledgebase',
  455. created_by=user_id,
  456. location=object_name,
  457. type=file_extension,
  458. status=5,
  459. size=size,
  460. parent_id=file.id
  461. )
  462. logger.info(f"Created File with id: {fileDB.id}")
  463. File2document.objects.create(
  464. file_id=fileDB.id,
  465. document_id=documentKbm.id
  466. )
  467. logger.info(f"Created File2document relation")
  468. child_count = File.objects.filter(parent_id=file.id).exclude(status=4).count()
  469. knowledgebase.doc_num = child_count
  470. knowledgebase.save()
  471. logger.info(f"Updated knowledgebase doc_num to {child_count}")
  472. except Exception as e:
  473. logger.error(f"Error in saveDocumentKbm: {str(e)}", exc_info=True)
  474. raise
  475. # 根据名称获取地址
  476. @staticmethod
  477. def nameGetUrl(request):
  478. object_name = request.POST.get('object_name')
  479. bucket_name = request.POST.get('bucket_name')
  480. return MinioService.geturl(object_name,bucket_name)
  481. @staticmethod
  482. def geturl(name,bucket_name):
  483. """获取文件地址"""
  484. object_name = name
  485. if not object_name:
  486. return fail('Object name is required')
  487. try:
  488. # 生成一个预签名 URL,有效期为1小时
  489. url = minio_client.presigned_get_object(
  490. bucket_name,
  491. object_name,
  492. expires=timedelta(hours=1)
  493. )
  494. return success({'url': url})
  495. except Exception as e:
  496. return fail(str(e))
  497. @staticmethod
  498. def deleteFile(request):
  499. """根据名称删除文件"""
  500. bucket_name = request.POST.get('bucket_name')
  501. object_name = request.POST.get('object_name')
  502. if not object_name:
  503. return fail('需要删除的文件未找到')
  504. try:
  505. minio_client.remove_object(bucket_name, object_name)
  506. return success('删除成功')
  507. except S3Error as e:
  508. return fail(f'Failed to delete file: {str(e)}')
  509. except Exception as e:
  510. return fail(f'An unexpected error occurred: {str(e)}')
  511. @staticmethod
  512. def renameFile(request):
  513. """重命名 bucket 中的文件"""
  514. bucket_name = request.POST.get('bucket_name')
  515. old_name = request.POST.get('object_name')
  516. new_name = request.POST.get('new_name')
  517. if not old_name or not new_name:
  518. return fail('原文件名或新文件名未提供')
  519. # 检查新文件名是否包含后缀,如果没有则添加原文件的后缀
  520. if '.' not in new_name:
  521. old_extension = old_name.split('.')[-1] if '.' in old_name else ''
  522. new_name = f"{new_name}.{old_extension}" if old_extension else new_name
  523. try:
  524. # 复制对象到新名称
  525. result = minio_client.copy_object(
  526. bucket_name, new_name,
  527. CopySource(bucket_name, old_name)
  528. )
  529. # 如果复制成功,删除原对象
  530. if result:
  531. minio_client.remove_object(bucket_name, old_name)
  532. return success("修改昵称成功")
  533. else:
  534. return fail('文件重命名失败')
  535. except S3Error as e:
  536. return fail(f'重命名文件失败: {str(e)}')
  537. except Exception as e:
  538. return fail(f'发生意外错误: {str(e)}')
  539. @staticmethod
  540. def delete_bucket(request):
  541. """删除 bucket"""
  542. bucket_name = request.POST.get('bucket_name')
  543. if not bucket_name:
  544. return fail({'bucket_name 为空'})
  545. try:
  546. # 开始事务
  547. with transaction.atomic():
  548. # 1. 从 MinIO 删除 bucket
  549. try:
  550. # 首先删除 bucket 中的所有对象
  551. objects = minio_client.list_objects(bucket_name, recursive=True)
  552. for obj in objects:
  553. minio_client.remove_object(bucket_name, obj.object_name)
  554. # 然后删除 bucket
  555. minio_client.remove_bucket(bucket_name)
  556. except S3Error as e:
  557. return fail(f'MinIO 错误: {str(e)}')
  558. # 2. 从数据库中删除相应记录
  559. try:
  560. bucket = Knowledgebase.objects.get(name=bucket_name)
  561. bucket.delete()
  562. except Knowledgebase.DoesNotExist:
  563. return fail('数据库中不存在该 bucket 记录')
  564. return fail(f'Bucket "{bucket_name}" 已成功删除')
  565. except Exception as e:
  566. return fail(f'删除 bucket 时发生错误: {str(e)}')
  567. @staticmethod
  568. def deleteBucket(request):
  569. return MinioService.delete_bucket(request)
  570. #切片
  571. @staticmethod
  572. def readPdfSlice(request):
  573. """读取 PDF 切片,包括文本和图片"""
  574. bucket_name = request.POST.get('bucket_name')
  575. object_name = request.POST.get('object_name')
  576. start_page = int(request.POST.get('start_page', 1))
  577. end_page = int(request.POST.get('end_page', -1))
  578. if not bucket_name or not object_name:
  579. return fail('bucket_name 或 object_name 为空')
  580. try:
  581. # 从 MinIO 获取 PDF 文件
  582. response = minio_client.get_object(bucket_name, object_name)
  583. pdf_content = BytesIO(response.read())
  584. # 使用 PyMuPDF 读取 PDF
  585. doc = fitz.open(stream=pdf_content, filetype="pdf")
  586. total_pages = len(doc)
  587. # 调整页面范围
  588. start_page = max(1, start_page) - 1 # 转换为从 0 开始的索引
  589. end_page = min(total_pages, end_page if end_page > 0 else total_pages)
  590. # 读取指定页面范围的内容
  591. result = []
  592. for page_num in range(start_page, end_page):
  593. page = doc[page_num]
  594. text = page.get_text()
  595. # 提取图片
  596. images = []
  597. for img in page.get_images():
  598. xref = img[0]
  599. base_image = doc.extract_image(xref)
  600. image_data = base_image["image"]
  601. image_format = base_image["ext"]
  602. image_base64 = base64.b64encode(image_data).decode('utf-8')
  603. images.append({
  604. 'format': image_format,
  605. 'data': image_base64
  606. })
  607. result.append({
  608. 'page_number': page_num + 1,
  609. 'content': text,
  610. 'images': images
  611. })
  612. # 关闭连接
  613. doc.close()
  614. pdf_content.close()
  615. response.close()
  616. info = {
  617. 'total_pages': total_pages,
  618. 'sliced_content': result
  619. }
  620. return success(info)
  621. except Exception as e:
  622. return fail(str(e))