file.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. from django.db import models
  2. import os, errno
  3. import csv
  4. from api.utils import *
  5. import json
  6. from random import randint
  7. import logging
  8. from django.http import FileResponse
  9. from Crypto.Cipher import ARC4
  10. from Crypto.Protocol.KDF import PBKDF2
  11. from Crypto.Hash import SHA512
  12. from django.contrib.auth.hashers import make_password
  13. from io import TextIOWrapper, BytesIO
  14. from ast import literal_eval
  15. from typing import Any
  16. types = [
  17. ('csv', 'csv'),
  18. ]
  19. usages = [
  20. ('input', 'input'),
  21. ('show', 'show'),
  22. ('result', 'result'),
  23. ('output', 'output'),
  24. ]
  25. contents = [
  26. ('node', 'node'),
  27. ('edge', 'edge'),
  28. ]
  29. logger = logging.getLogger("file-model")
  30. # 盐值
  31. salt = "vrServer"
  32. # 加密
  33. def rc4_encrypt(key: bytes, data: bytes) -> bytes:
  34. cipher = ARC4.new(key)
  35. cipher.encrypt(b'\x00' * 1024) # 丢弃前1024字节密钥流
  36. return cipher.encrypt(data)
  37. # 解密
  38. def rc4_decrypt(key: bytes, data: bytes) -> bytes:
  39. return rc4_encrypt(key, data)
  40. # 由密码生成密钥
  41. def derive_key(password: str, salt: bytes, iterations: int) -> bytes:
  42. return PBKDF2(
  43. password.encode('utf-8'),
  44. salt,
  45. dkLen=32, # 生成256位密钥
  46. count=iterations,
  47. hmac_hash_module=SHA512
  48. )
  49. # 安全解析json
  50. def safe_json_parse(json_str: str, default: Any = None) -> Any:
  51. # 预处理空字符串
  52. stripped_str = json_str.strip()
  53. if not stripped_str:
  54. return default if default is not None else []
  55. try:
  56. data = json.loads(stripped_str)
  57. except json.JSONDecodeError:
  58. return default if default is not None else []
  59. # 递归检查嵌套空列表
  60. def is_empty_nested_list(obj):
  61. if isinstance(obj, list):
  62. return all(is_empty_nested_list(item) for item in obj)
  63. return False
  64. # 如果是空列表或嵌套空列表,返回默认值
  65. if data == [] or is_empty_nested_list(data):
  66. return default if default is not None else []
  67. return data
  68. class FileManager(models.Manager):
  69. def getHistory(self, user):
  70. # try:
  71. files = user.own_files.filter(usage="input").all()
  72. history = []
  73. for file in files:
  74. fileId = file.id
  75. if file.content == "node" and not file.own_missions_node.exists():
  76. # 输入的节点文件没有对应的任务,应该删除
  77. file.delete()
  78. continue
  79. if file.content == "edge" and not file.own_missions_edge.exists():
  80. # 输入的边文件没有对应的任务,应该删除
  81. continue
  82. directory = os.path.join(BASE_FILE_PATH, str(user.id))
  83. path = os.path.join(directory, str(fileId))
  84. try:
  85. size = os.path.getsize(path)
  86. except FileNotFoundError:
  87. print("未找到对应文件,现将记录删除", fileId, file.name)
  88. self.get(id=fileId).delete()
  89. continue
  90. except Exception as error:
  91. print("读取历史记录时出现未知错误")
  92. return FAILED
  93. if size >= 1024 * 1024:
  94. size = size / (1024 * 1024)
  95. size = f"{size:.2f} MB"
  96. else:
  97. size = size / 1024
  98. size = f"{size:.2f} KB"
  99. if file.content == 'node':
  100. missions = file.own_missions_node.all()
  101. fileInfo = {
  102. '节点总数': file.own_file_info.nodes,
  103. 'S节点数': file.own_file_info.sNodes,
  104. 'D节点数': file.own_file_info.dNodes,
  105. 'I节点数': file.own_file_info.iNodes,
  106. }
  107. elif file.content == 'edge':
  108. missions = file.own_missions_edge.all()
  109. fileInfo = {
  110. '边总数': file.own_file_info.edges,
  111. }
  112. else:
  113. logger.error(f"获取历史文件出错,文件格式错误 content: {file.content}")
  114. return FAILED
  115. history.append({
  116. 'id': file.id,
  117. 'name': file.name,
  118. 'uploadTime': file.update_time,
  119. 'size': size,
  120. 'encrypted': file.encrypted,
  121. 'content': file.content,
  122. 'missions': [{'id': mission.id, 'name': mission.name} for mission in missions],
  123. 'fileInfo': fileInfo,
  124. })
  125. return history
  126. # except Exception as error:
  127. # print("Failed to get upload history", error)
  128. # return FAILED
  129. # Create your models here.
  130. class File(models.Model):
  131. name = models.CharField(default="untitled", max_length=64)
  132. type = models.CharField(choices=types, max_length=5)
  133. usage = models.CharField(choices=usages, max_length=20)
  134. create_time = models.DateTimeField(auto_now_add=True)
  135. update_time = models.DateTimeField(auto_now=True)
  136. content = models.CharField(choices=contents, max_length=10)
  137. encrypted = models.BooleanField(default=False)
  138. key = models.CharField(blank=True, null=True, max_length=128)
  139. associate = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True)
  140. user = models.ForeignKey(to="api.User", on_delete=models.CASCADE, related_name='own_files')
  141. objects = FileManager()
  142. def encrypt(self, password):
  143. # 该密码仅用于验证
  144. verifyPassword = make_password(
  145. password,
  146. salt='vrviewer',
  147. hasher='pbkdf2_sha256'
  148. )
  149. self.key = verifyPassword
  150. if self.encrypted:
  151. logger.error(f"文件{self.id}已经过加密,无法再次加密")
  152. return False
  153. else:
  154. # 仍使用用户输入密码加密
  155. key = derive_key(
  156. password=password,
  157. salt=salt,
  158. iterations=4,
  159. )
  160. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  161. with open(path, 'rb') as f:
  162. original_data = f.read()
  163. with open(path, 'wb') as f:
  164. f.write(rc4_encrypt(key, original_data))
  165. self.encrypted = True
  166. self.save()
  167. return True
  168. def decrypted(self, password):
  169. # 仅用于验证
  170. verifyPassword = make_password(
  171. password,
  172. salt='vrviewer',
  173. hasher='pbkdf2_sha256'
  174. )
  175. if not verifyPassword == self.key:
  176. logger.error(f"文件{self.id}解密密钥错误")
  177. return False
  178. if not self.encrypted:
  179. logger.error(f"文件{self.id}未经过加密,无法进行解密")
  180. return False
  181. else:
  182. key = derive_key(
  183. password=password,
  184. salt=salt,
  185. iterations=4,
  186. )
  187. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  188. with open(path, 'rb') as f:
  189. original_data = f.read()
  190. with open(path, 'wb') as f:
  191. f.write(rc4_decrypt(key, original_data))
  192. self.encrypted = False
  193. self.save()
  194. return True
  195. def decryptToData(self, password):
  196. # 仅用于验证
  197. verifyPassword = make_password(
  198. password,
  199. salt='vrviewer',
  200. hasher='pbkdf2_sha256'
  201. )
  202. if not verifyPassword == self.key:
  203. logger.error(f"文件{self.id}解密密钥错误")
  204. return False
  205. if not self.encrypted:
  206. logger.error(f"文件{self.id}未经过加密,无法进行解密")
  207. return False
  208. else:
  209. key = derive_key(
  210. password=password,
  211. salt=salt,
  212. iterations=4,
  213. )
  214. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  215. with open(path, 'rb') as f:
  216. original_data = f.read()
  217. return TextIOWrapper( BytesIO(rc4_decrypt(key, original_data)), encoding='utf-8', newline='')
  218. def verify(self, password):
  219. verifyPassword = make_password(
  220. password,
  221. salt='vrviewer',
  222. hasher='pbkdf2_sha256'
  223. )
  224. if not self.encrypted:
  225. logger.error(f"文件{self.id}未经过加密,无法进行解密验证")
  226. return False
  227. if not verifyPassword == self.key:
  228. logger.error(f"文件{self.id}验证密钥错误")
  229. return False
  230. return True
  231. def download(self):
  232. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  233. if not os.path.exists(path):
  234. return False
  235. # 加密后文件也不允许下载
  236. if self.encrypted:
  237. return False
  238. else:
  239. response = FileResponse(open(path), 'rb')
  240. response['Content-Disposition'] = f'attachment; filename="{self.name}"'
  241. return FileResponse(open(path, 'rb'))
  242. def saveWithInfo(self):
  243. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  244. if self.content in ['node', 'nodes']:
  245. sCount = dCount = iCount = 0
  246. nodeFile = csv.reader(open(path, 'r'))
  247. for line in nodeFile:
  248. if line[1] == 'S':
  249. sCount += 1
  250. if line[1] == 'D':
  251. dCount += 1
  252. if line[1] == 'I':
  253. iCount += 1
  254. fileInfo = FileInfo()
  255. fileInfo.file = self
  256. fileInfo.nodes = sCount + dCount + iCount
  257. fileInfo.sNodes = sCount
  258. fileInfo.dNodes = dCount
  259. fileInfo.iNodes = iCount
  260. fileInfo.save()
  261. if self.content in ['edge', 'edges']:
  262. edges = 0
  263. edgeFile = csv.reader(open(path, 'r'))
  264. for line in edgeFile:
  265. if line:
  266. edges += 1
  267. fileInfo = FileInfo()
  268. fileInfo.file = self
  269. fileInfo.edges = edges
  270. fileInfo.save()
  271. self.save()
  272. def generate(self, data):
  273. # 从json结果生成文件
  274. path = os.path.join(BASE_FILE_PATH, str(self.user.id))
  275. if os.path.exists(os.path.join(path, str(self.id))):
  276. self.delete()
  277. return FILE_ALREADY_EXIST
  278. else:
  279. try:
  280. os.mkdir(path)
  281. except Exception as error:
  282. if not error.args[0] == 17:
  283. print(error)
  284. return FILE_FAILED_CREATE_DIR
  285. if self.content == 'node':
  286. nodes = []
  287. file = open(os.path.join(path, str(self.id)), 'w', newline='')
  288. csvFile = csv.writer(file)
  289. for line in data:
  290. if not str(line[0]).isdigit():
  291. logger.error("check file illegal failed node id wrong")
  292. return FAILED
  293. if not line[1] in ['S', 'D', 'I']:
  294. logger.error("check file illegal failed node type wrong")
  295. return FAILED
  296. if line[0] not in nodes:
  297. nodes.append(line[0])
  298. else:
  299. logger.error("check file illegal failed node dudplicate id")
  300. return FAILED
  301. # 除了节点编号和节点类型外,其余参数全部放在line的后续位置,以字符串json的格式保存
  302. csvFile.writerow(line)
  303. file.close()
  304. return OK
  305. if self.content == 'edge':
  306. edges = []
  307. file = open(os.path.join(path, str(self.id)), 'w', newline='')
  308. csvFile = csv.writer(file)
  309. for line in data:
  310. if not str(line[0]).isdigit() or not str(line[1]).isdigit():
  311. logger.error("check file illegal failed edge len =2")
  312. return FAILED
  313. # 注意默认将边视为无向边
  314. # 检查重复
  315. if [line[0], line[1]] not in edges and [line[1], line[0]] not in edges:
  316. edges.append([line[0], line[1]])
  317. # 后续参数放在line的后续位置
  318. csvFile.writerow(line)
  319. file.close()
  320. return OK
  321. return UNKNOWN_CONTENT
  322. def storage(self, file):
  323. try:
  324. path = os.path.join(BASE_FILE_PATH, str(self.user.id))
  325. if os.path.exists(os.path.join(path, str(self.id))):
  326. self.delete()
  327. return FILE_ALREADY_EXIST
  328. else:
  329. try:
  330. os.mkdir(path)
  331. except Exception as error:
  332. if not error.args[0] == 17:
  333. print(error)
  334. return FILE_FAILED_CREATE_DIR
  335. file_path = os.path.join(path, str(self.id))
  336. f = open(file_path, 'wb')
  337. for bite in file:
  338. f.write(bite)
  339. f.close()
  340. return OK
  341. except Exception as error:
  342. logger.error(error)
  343. return FAILED
  344. # 检查文件是否合法
  345. def checkIllegal(self):
  346. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  347. path2 = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.associate.id))
  348. if self.content == 'node':
  349. file = csv.reader(open(path, 'r'))
  350. # 针对csv文件的检测
  351. if self.type == 'csv':
  352. nodes = []
  353. for line in file:
  354. if not len(line) >= 2:
  355. logger.error("check file illegal failed node len >= 2")
  356. return False
  357. if not line[0].isdigit():
  358. logger.error("check file illegal failed node id wrong")
  359. return False
  360. if not line[1] in ['S', 'D', 'I']:
  361. logger.error(f"check file illegal failed node type wrong:{line}")
  362. return False
  363. if line[0] not in nodes:
  364. nodes.append(line[0])
  365. else:
  366. logger.error("check file illegal failed node dudplicate id")
  367. return False
  368. return True
  369. if self.content == 'edge':
  370. edgeFile = csv.reader(open(path, 'r'))
  371. nodeFile = csv.reader(open(path2, 'r'))
  372. # 针对csv文件的检测
  373. if self.type == 'csv':
  374. nodes = []
  375. edges = []
  376. for line in nodeFile:
  377. if not len(line) >= 2:
  378. logger.error("check file illegal failed node len >= 2")
  379. return False
  380. if not line[0].isdigit():
  381. logger.error("check file illegal failed node id wrong")
  382. return False
  383. nodes.append(line[0])
  384. for line in edgeFile:
  385. if not len(line) == 2:
  386. logger.error("check file illegal failed edge len =2")
  387. return False
  388. if line[0] not in nodes or line[1] not in nodes:
  389. logger.error("check file illegal failed edge id not exist")
  390. return False
  391. if [line[0], line[1]] not in edges and [line[1], line[0]] not in edges:
  392. edges.append([line[0], line[1]])
  393. else:
  394. # 将图视为无向图,同一条边的正反算作重复
  395. # 直接去除重复边
  396. logger.error("check file illegal failed edge duplicate edge")
  397. return False
  398. return True
  399. def toJson(self, request=None):
  400. # 检查是否为加密文件,只有当文件usage为input时才应该存在加密属性
  401. if self.usage == 'input' and self.encrypted:
  402. # 如果被加密则需要从request中获取解密密钥
  403. key = request.session.get('encrypt-keys', {}).get(str(self.id), '')
  404. if key:
  405. file = csv.reader(self.decryptToData(key))
  406. else:
  407. raise KeyError(f"解密文件{self.id}所需密钥不存在")
  408. else:
  409. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  410. file = csv.reader(open(path, 'r'))
  411. if self.content == 'node':
  412. if self.type == 'csv':
  413. nodes = []
  414. for line in file:
  415. # 如果有额外数据,则放入第三个字段中
  416. node = {'id': line[0], 'type': line[1], 'meta': []}
  417. for el in range(2, len(line)):
  418. # 对于meta字段,写入时数据为不带双引号,以冒号分割的字串
  419. # 或者是直接正常的json字段,应尝试两种方式解析
  420. try:
  421. metaJson = safe_json_parse(line[el].replace('\'', '\"'))
  422. # 检测是否嵌套过多
  423. while metaJson:
  424. if type(metaJson[0]) == list:
  425. metaJson = metaJson[0]
  426. else:
  427. break
  428. node['meta'] = metaJson
  429. except Exception as error:
  430. logger.info(f"尝试以json格式解析文件meta内容{line[el]}失败,尝试以非标准格式解析{error}")
  431. # 尝试以冒号分隔格式解析
  432. elList = el.split(':')
  433. if len(elList) != 2:
  434. logger.info(f"尝试以非标准格式解析文件meta内容{el}失败,放弃解析")
  435. continue
  436. else:
  437. node['meta'].append({
  438. elList[0]: elList[1]
  439. })
  440. # # 测试用,添加optimize
  441. # el = '{"optimize": "old"}'
  442. # node['meta'].append(json.loads(el))
  443. # # 测试用,添加group
  444. # el = '{"group": "' + str(randint(1,5)) + '"}'
  445. # node['meta'].append(json.loads(el))
  446. nodes.append(node)
  447. return nodes
  448. if self.content == 'edge':
  449. if self.type == 'csv':
  450. edges = []
  451. for line in file:
  452. # 如果有额外数据,则放入第三个字段中
  453. edge = {'from': line[0], 'to': line[1], 'meta': []}
  454. for el in range(2, len(line)):
  455. try:
  456. metaJson = safe_json_parse(line[el].replace('\'', '\"'))
  457. # 检测是否嵌套过多
  458. while metaJson:
  459. if type(metaJson[0]) == list:
  460. metaJson = metaJson[0]
  461. else:
  462. break
  463. edge['meta'] = metaJson
  464. except Exception as error:
  465. logger.info(f"尝试以json格式解析文件meta内容{line[el]}失败,尝试以非标准格式解析{error}")
  466. # 尝试以冒号分隔格式解析
  467. elList = el.split(':')
  468. if len(elList) != 2:
  469. logger.info(f"尝试以非标准格式解析文件meta内容{el}失败,放弃解析")
  470. continue
  471. else:
  472. edge['meta'].append({
  473. elList[0]: elList[1]
  474. })
  475. # # 测试用,添加optimize
  476. # el = '{"optimize": "old"}'
  477. # edge['meta'].append(json.loads(el))
  478. edges.append(edge)
  479. # logger.info(edges)
  480. return edges
  481. def deleteStorage(self):
  482. path = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.id))
  483. if self.associate:
  484. path2 = os.path.join(os.path.join(BASE_FILE_PATH, str(self.user.id)), str(self.associate.id))
  485. else:
  486. path2 = ""
  487. failedFlag = False
  488. for p in [path, path2]:
  489. if os.path.exists(p):
  490. try:
  491. os.remove(p)
  492. except Exception as error:
  493. # 可能出现失败的原因是文件被占用
  494. logger.error(f"删除文件{self.id} {self.name}失败:{error}")
  495. failedFlag = True
  496. # 无论文件删除是否成功,都要把记录删除,多余的文件可以再后续清理时删除
  497. if self.associate:
  498. self.associate.delete()
  499. if self:
  500. self.delete()
  501. if failedFlag:
  502. return FAILED
  503. return OK
  504. class Meta:
  505. app_label = 'api'
  506. class FileInfo(models.Model):
  507. file = models.OneToOneField(File, on_delete=models.CASCADE, related_name='own_file_info')
  508. nodes = models.IntegerField(default=0)
  509. sNodes = models.IntegerField(default=0)
  510. dNodes = models.IntegerField(default=0)
  511. iNodes = models.IntegerField(default=0)
  512. edges = models.IntegerField(default=0)
  513. # 待添加集中度等边的信息
  514. class Meta:
  515. app_label = 'api'