Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0234.py @ 4270:0d7bb4df2343
Reformatted code base using black.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 19 Jun 2024 18:44:57 +0200 |
parents | 79c8a70e1813 |
children |
comparison
equal
deleted
inserted
replaced
4269:64a85ce8be70 | 4270:0d7bb4df2343 |
---|---|
61 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), | 61 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), |
62 } | 62 } |
63 | 63 |
64 # TODO: use a Pydantic model for extra | 64 # TODO: use a Pydantic model for extra |
65 EXTRA_ALLOWED = { | 65 EXTRA_ALLOWED = { |
66 "path", "namespace", "file_desc", "file_hash", "hash_algo", "webrtc", "call_data", | 66 "path", |
67 "size", "media_type" | 67 "namespace", |
68 "file_desc", | |
69 "file_hash", | |
70 "hash_algo", | |
71 "webrtc", | |
72 "call_data", | |
73 "size", | |
74 "media_type", | |
68 } | 75 } |
69 Range = namedtuple("Range", ("offset", "length")) | 76 Range = namedtuple("Range", ("offset", "length")) |
70 | 77 |
71 | 78 |
72 class XEP_0234(BaseApplicationHandler): | 79 class XEP_0234(BaseApplicationHandler): |
123 return True | 130 return True |
124 | 131 |
125 # generic methods | 132 # generic methods |
126 | 133 |
127 def build_file_element( | 134 def build_file_element( |
128 self, client, name=None, file_hash=None, hash_algo=None, size=None, | 135 self, |
129 mime_type=None, desc=None, modified=None, transfer_range=None, path=None, | 136 client, |
130 namespace=None, file_elt=None, **kwargs): | 137 name=None, |
138 file_hash=None, | |
139 hash_algo=None, | |
140 size=None, | |
141 mime_type=None, | |
142 desc=None, | |
143 modified=None, | |
144 transfer_range=None, | |
145 path=None, | |
146 namespace=None, | |
147 file_elt=None, | |
148 **kwargs, | |
149 ): | |
131 """Generate a <file> element with available metadata | 150 """Generate a <file> element with available metadata |
132 | 151 |
133 @param file_hash(unicode, None): hash of the file | 152 @param file_hash(unicode, None): hash of the file |
134 empty string to set <hash-used/> element | 153 empty string to set <hash-used/> element |
135 @param hash_algo(unicode, None): hash algorithm used | 154 @param hash_algo(unicode, None): hash algorithm used |
181 else: | 200 else: |
182 file_elt.addChild(self._hash.build_hash_elt(file_hash, hash_algo)) | 201 file_elt.addChild(self._hash.build_hash_elt(file_hash, hash_algo)) |
183 elif hash_algo is not None: | 202 elif hash_algo is not None: |
184 file_elt.addChild(self._hash.build_hash_used_elt(hash_algo)) | 203 file_elt.addChild(self._hash.build_hash_used_elt(hash_algo)) |
185 self.host.trigger.point( | 204 self.host.trigger.point( |
186 "XEP-0234_buildFileElement", client, file_elt, extra_args=kwargs) | 205 "XEP-0234_buildFileElement", client, file_elt, extra_args=kwargs |
206 ) | |
187 if kwargs: | 207 if kwargs: |
188 for kw in kwargs: | 208 for kw in kwargs: |
189 log.debug("ignored keyword: {}".format(kw)) | 209 log.debug("ignored keyword: {}".format(kw)) |
190 return file_elt | 210 return file_elt |
191 | 211 |
207 return self.build_file_element(client, **file_data) | 227 return self.build_file_element(client, **file_data) |
208 | 228 |
209 async def parse_file_element( | 229 async def parse_file_element( |
210 self, | 230 self, |
211 client: SatXMPPEntity, | 231 client: SatXMPPEntity, |
212 file_elt: domish.Element|None, | 232 file_elt: domish.Element | None, |
213 file_data: dict | None = None, | 233 file_data: dict | None = None, |
214 given: bool = False, | 234 given: bool = False, |
215 parent_elt: domish.Element | None = None, | 235 parent_elt: domish.Element | None = None, |
216 keep_empty_range: bool = False | 236 keep_empty_range: bool = False, |
217 ) -> dict: | 237 ) -> dict: |
218 """Parse a <file> element and updates file dictionary accordingly. | 238 """Parse a <file> element and updates file dictionary accordingly. |
219 | 239 |
220 @param client: The SatXMPPEntity instance. | 240 @param client: The SatXMPPEntity instance. |
221 @param file_elt: The file element to parse. | 241 @param file_elt: The file element to parse. |
297 if name is None: | 317 if name is None: |
298 file_data["name"] = "unnamed" | 318 file_data["name"] = "unnamed" |
299 elif name == "..": | 319 elif name == "..": |
300 # we don't want to go to parent dir when joining to a path | 320 # we don't want to go to parent dir when joining to a path |
301 file_data["name"] = "--" | 321 file_data["name"] = "--" |
302 elif "/" in name or "\\" in name: | 322 elif "/" in name or "\\" in name: |
303 file_data["name"] = regex.path_escape(name) | 323 file_data["name"] = regex.path_escape(name) |
304 | 324 |
305 try: | 325 try: |
306 file_data["mime_type"] = str( | 326 file_data["mime_type"] = str( |
307 next(file_elt.elements(NS_JINGLE_FT, "media-type")) | 327 next(file_elt.elements(NS_JINGLE_FT, "media-type")) |
308 ) | 328 ) |
309 except StopIteration: | 329 except StopIteration: |
310 pass | 330 pass |
311 | 331 |
312 try: | 332 try: |
313 file_data["size"] = int( | 333 file_data["size"] = int(str(next(file_elt.elements(NS_JINGLE_FT, "size")))) |
314 str(next(file_elt.elements(NS_JINGLE_FT, "size"))) | |
315 ) | |
316 except StopIteration: | 334 except StopIteration: |
317 pass | 335 pass |
318 | 336 |
319 try: | 337 try: |
320 file_data["modified"] = date_utils.date_parse( | 338 file_data["modified"] = date_utils.date_parse( |
357 extra_s: str, | 375 extra_s: str, |
358 profile: str, | 376 profile: str, |
359 ) -> defer.Deferred[str]: | 377 ) -> defer.Deferred[str]: |
360 client = self.host.get_client(profile) | 378 client = self.host.get_client(profile) |
361 extra = data_format.deserialise(extra_s) | 379 extra = data_format.deserialise(extra_s) |
362 d = defer.ensureDeferred(self.file_send( | 380 d = defer.ensureDeferred( |
363 client, | 381 self.file_send( |
364 jid.JID(peer_jid_s), | 382 client, |
365 filepath, | 383 jid.JID(peer_jid_s), |
366 name or None, | 384 filepath, |
367 file_desc or None, | 385 name or None, |
368 extra, | 386 file_desc or None, |
369 )) | 387 extra, |
388 ) | |
389 ) | |
370 d.addCallback(data_format.serialise) | 390 d.addCallback(data_format.serialise) |
371 return d | 391 return d |
372 | 392 |
373 async def file_send( | 393 async def file_send( |
374 self, | 394 self, |
375 client: SatXMPPEntity, | 395 client: SatXMPPEntity, |
376 peer_jid: jid.JID, | 396 peer_jid: jid.JID, |
377 filepath: str, | 397 filepath: str, |
378 name: str|None, | 398 name: str | None, |
379 file_desc: str|None = None, | 399 file_desc: str | None = None, |
380 extra: dict|None = None | 400 extra: dict | None = None, |
381 ) -> dict: | 401 ) -> dict: |
382 """Send a file using jingle file transfer | 402 """Send a file using jingle file transfer |
383 | 403 |
384 @param peer_jid: destinee jid | 404 @param peer_jid: destinee jid |
385 @param filepath: absolute path of the file | 405 @param filepath: absolute path of the file |
404 "progress_id_d": progress_id_d, | 424 "progress_id_d": progress_id_d, |
405 }, | 425 }, |
406 } | 426 } |
407 | 427 |
408 await self.host.trigger.async_point( | 428 await self.host.trigger.async_point( |
409 "XEP-0234_file_jingle_send", | 429 "XEP-0234_file_jingle_send", client, peer_jid, content |
410 client, peer_jid, content | |
411 ) | 430 ) |
412 | 431 |
413 session_id = await self._j.initiate( | 432 session_id = await self._j.initiate( |
414 client, | 433 client, peer_jid, [content], encrypted=encrypted |
415 peer_jid, | |
416 [content], | |
417 encrypted = encrypted | |
418 ) | 434 ) |
419 progress_id = await progress_id_d | 435 progress_id = await progress_id_d |
420 return { | 436 return {"progress": progress_id, "session_id": session_id} |
421 "progress": progress_id, | |
422 "session_id": session_id | |
423 } | |
424 | 437 |
425 def _file_jingle_request( | 438 def _file_jingle_request( |
426 self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, | 439 self, |
427 profile=C.PROF_KEY_NONE): | 440 peer_jid, |
441 filepath, | |
442 name="", | |
443 file_hash="", | |
444 hash_algo="", | |
445 extra=None, | |
446 profile=C.PROF_KEY_NONE, | |
447 ): | |
428 client = self.host.get_client(profile) | 448 client = self.host.get_client(profile) |
429 return defer.ensureDeferred(self.file_jingle_request( | 449 return defer.ensureDeferred( |
430 client, | 450 self.file_jingle_request( |
431 jid.JID(peer_jid), | 451 client, |
432 filepath, | 452 jid.JID(peer_jid), |
433 name or None, | 453 filepath, |
434 file_hash or None, | 454 name or None, |
435 hash_algo or None, | 455 file_hash or None, |
436 extra or None, | 456 hash_algo or None, |
437 )) | 457 extra or None, |
458 ) | |
459 ) | |
438 | 460 |
439 async def file_jingle_request( | 461 async def file_jingle_request( |
440 self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None, | 462 self, |
441 extra=None): | 463 client, |
464 peer_jid, | |
465 filepath, | |
466 name=None, | |
467 file_hash=None, | |
468 hash_algo=None, | |
469 extra=None, | |
470 ): | |
442 """Request a file using jingle file transfer | 471 """Request a file using jingle file transfer |
443 | 472 |
444 @param peer_jid(jid.JID): destinee jid | 473 @param peer_jid(jid.JID): destinee jid |
445 @param filepath(str): absolute path where the file will be downloaded | 474 @param filepath(str): absolute path where the file will be downloaded |
446 @param name(unicode, None): name of the file | 475 @param name(unicode, None): name of the file |
478 return await progress_id_d | 507 return await progress_id_d |
479 | 508 |
480 # jingle callbacks | 509 # jingle callbacks |
481 | 510 |
482 def _get_confirm_msg( | 511 def _get_confirm_msg( |
483 self, | 512 self, client: SatXMPPEntity, peer_jid: jid.JID, file_data: dict |
484 client: SatXMPPEntity, | |
485 peer_jid: jid.JID, | |
486 file_data: dict | |
487 ) -> tuple[bool, str, str]: | 513 ) -> tuple[bool, str, str]: |
488 """Get confirmation message to display to user. | 514 """Get confirmation message to display to user. |
489 | 515 |
490 This is the message to show when a file sending request is received.""" | 516 This is the message to show when a file sending request is received.""" |
491 file_name = file_data.get('name') | 517 file_name = file_data.get("name") |
492 file_size = file_data.get('size') | 518 file_size = file_data.get("size") |
493 | 519 |
494 if file_name: | 520 if file_name: |
495 file_name_msg = D_('wants to send you the file "{file_name}"').format( | 521 file_name_msg = D_('wants to send you the file "{file_name}"').format( |
496 file_name=file_name | 522 file_name=file_name |
497 ) | 523 ) |
498 else: | 524 else: |
499 file_name_msg = D_('wants to send you an unnamed file') | 525 file_name_msg = D_("wants to send you an unnamed file") |
500 | 526 |
501 if file_size is not None: | 527 if file_size is not None: |
502 file_size_msg = D_("which has a size of {file_size_human}").format( | 528 file_size_msg = D_("which has a size of {file_size_human}").format( |
503 file_size_human=get_human_size(file_size) | 529 file_size_human=get_human_size(file_size) |
504 ) | 530 ) |
505 else: | 531 else: |
506 file_size_msg = D_("which has an unknown size") | 532 file_size_msg = D_("which has an unknown size") |
507 | 533 |
508 file_description = file_data.get('desc') | 534 file_description = file_data.get("desc") |
509 if file_description: | 535 if file_description: |
510 description_msg = " Description: {}.".format(file_description) | 536 description_msg = " Description: {}.".format(file_description) |
511 else: | 537 else: |
512 description_msg = "" | 538 description_msg = "" |
513 | 539 |
514 if client.roster and peer_jid.userhostJID() not in client.roster: | 540 if client.roster and peer_jid.userhostJID() not in client.roster: |
515 is_in_roster = False | 541 is_in_roster = False |
516 confirm_msg = D_( | 542 confirm_msg = D_( |
517 "Somebody not in your contact list ({peer_jid}) {file_name_msg} {file_size_msg}.{description_msg} " | 543 "Somebody not in your contact list ({peer_jid}) {file_name_msg} {file_size_msg}.{description_msg} " |
518 "Accepting this could leak your presence and possibly your IP address. Do you accept?" | 544 "Accepting this could leak your presence and possibly your IP address. Do you accept?" |
519 ).format(peer_jid=peer_jid, file_name_msg=file_name_msg, file_size_msg=file_size_msg, description_msg=description_msg) | 545 ).format( |
546 peer_jid=peer_jid, | |
547 file_name_msg=file_name_msg, | |
548 file_size_msg=file_size_msg, | |
549 description_msg=description_msg, | |
550 ) | |
520 confirm_title = D_("File sent from an unknown contact") | 551 confirm_title = D_("File sent from an unknown contact") |
521 else: | 552 else: |
522 is_in_roster = True | 553 is_in_roster = True |
523 confirm_msg = D_( | 554 confirm_msg = D_( |
524 "{peer_jid} {file_name_msg} {file_size_msg}.{description_msg} Do you " | 555 "{peer_jid} {file_name_msg} {file_size_msg}.{description_msg} Do you " |
525 "accept?" | 556 "accept?" |
526 ).format(peer_jid=peer_jid, file_name_msg=file_name_msg, file_size_msg=file_size_msg, description_msg=description_msg) | 557 ).format( |
558 peer_jid=peer_jid, | |
559 file_name_msg=file_name_msg, | |
560 file_size_msg=file_size_msg, | |
561 description_msg=description_msg, | |
562 ) | |
527 confirm_title = D_("File Proposed") | 563 confirm_title = D_("File Proposed") |
528 | 564 |
529 return (is_in_roster, confirm_msg, confirm_title) | 565 return (is_in_roster, confirm_msg, confirm_title) |
530 | 566 |
531 async def jingle_preflight( | 567 async def jingle_preflight( |
532 self, | 568 self, client: SatXMPPEntity, session: dict, description_elt: domish.Element |
533 client: SatXMPPEntity, | |
534 session: dict, | |
535 description_elt: domish.Element | |
536 ) -> None: | 569 ) -> None: |
537 """Perform preflight checks for an incoming call session. | 570 """Perform preflight checks for an incoming call session. |
538 | 571 |
539 Check if the calls is audio only or audio/video, then, prompts the user for | 572 Check if the calls is audio only or audio/video, then, prompts the user for |
540 confirmation. | 573 confirmation. |
566 | 599 |
567 action_extra = { | 600 action_extra = { |
568 "type": action_type, | 601 "type": action_type, |
569 "session_id": session_id, | 602 "session_id": session_id, |
570 "from_jid": peer_jid.full(), | 603 "from_jid": peer_jid.full(), |
571 "file_data": file_data | 604 "file_data": file_data, |
572 } | 605 } |
573 action_extra["subtype"] = C.META_TYPE_FILE | 606 action_extra["subtype"] = C.META_TYPE_FILE |
574 accepted = await xml_tools.defer_confirm( | 607 accepted = await xml_tools.defer_confirm( |
575 self.host, | 608 self.host, |
576 confirm_msg, | 609 confirm_msg, |
577 confirm_title, | 610 confirm_title, |
578 profile=client.profile, | 611 profile=client.profile, |
579 action_extra=action_extra | 612 action_extra=action_extra, |
580 ) | 613 ) |
581 if accepted: | 614 if accepted: |
582 session["pre_accepted"] = True | 615 session["pre_accepted"] = True |
583 return accepted | 616 return accepted |
584 | 617 |
585 async def jingle_preflight_info( | 618 async def jingle_preflight_info( |
586 self, | 619 self, |
587 client: SatXMPPEntity, | 620 client: SatXMPPEntity, |
588 session: dict, | 621 session: dict, |
589 info_type: str, | 622 info_type: str, |
590 info_data: dict|None = None | 623 info_data: dict | None = None, |
591 ) -> None: | 624 ) -> None: |
592 pass | 625 pass |
593 | 626 |
594 async def jingle_preflight_cancel( | 627 async def jingle_preflight_cancel( |
595 self, | 628 self, client: SatXMPPEntity, session: dict, cancel_error: exceptions.CancelError |
596 client: SatXMPPEntity, | |
597 session: dict, | |
598 cancel_error: exceptions.CancelError | |
599 ) -> None: | 629 ) -> None: |
600 pass | 630 pass |
601 | 631 |
602 def jingle_description_elt( | 632 def jingle_description_elt( |
603 self, client, session, content_name, filepath, name, extra, progress_id_d | 633 self, client, session, content_name, filepath, name, extra, progress_id_d |
624 transport_data["webrtc"] = True | 654 transport_data["webrtc"] = True |
625 assert "file_path" not in application_data | 655 assert "file_path" not in application_data |
626 application_data["file_path"] = filepath | 656 application_data["file_path"] = filepath |
627 file_data = application_data["file_data"] = {} | 657 file_data = application_data["file_data"] = {} |
628 desc_elt = self.jingle_description_elt( | 658 desc_elt = self.jingle_description_elt( |
629 client, session, content_name, filepath, name, extra, progress_id_d) | 659 client, session, content_name, filepath, name, extra, progress_id_d |
660 ) | |
630 file_elt = desc_elt.addElement("file") | 661 file_elt = desc_elt.addElement("file") |
631 | 662 |
632 if content_data["senders"] == self._j.ROLE_INITIATOR: | 663 if content_data["senders"] == self._j.ROLE_INITIATOR: |
633 # we send a file | 664 # we send a file |
634 if name is None: | 665 if name is None: |
648 if "namespace" in extra: | 679 if "namespace" in extra: |
649 file_data["namespace"] = extra["namespace"] | 680 file_data["namespace"] = extra["namespace"] |
650 if "path" in extra: | 681 if "path" in extra: |
651 file_data["path"] = extra["path"] | 682 file_data["path"] = extra["path"] |
652 self.build_file_element_from_dict( | 683 self.build_file_element_from_dict( |
653 client, file_data, file_elt=file_elt, file_hash="") | 684 client, file_data, file_elt=file_elt, file_hash="" |
685 ) | |
654 else: | 686 else: |
655 # we request a file | 687 # we request a file |
656 file_hash = extra.pop("file_hash", "") | 688 file_hash = extra.pop("file_hash", "") |
657 if not name and not file_hash: | 689 if not name and not file_hash: |
658 raise ValueError(_("you need to provide at least name or file hash")) | 690 raise ValueError(_("you need to provide at least name or file hash")) |
738 client: SatXMPPEntity, | 770 client: SatXMPPEntity, |
739 session: dict, | 771 session: dict, |
740 content_data: dict, | 772 content_data: dict, |
741 content_name: str, | 773 content_name: str, |
742 file_data: dict, | 774 file_data: dict, |
743 file_elt: domish.Element | 775 file_elt: domish.Element, |
744 ) -> bool: | 776 ) -> bool: |
745 """parse file_elt, and handle user permission/file opening""" | 777 """parse file_elt, and handle user permission/file opening""" |
746 transport_data = content_data["transport_data"] | 778 transport_data = content_data["transport_data"] |
747 webrtc = transport_data.get("webrtc", False) | 779 webrtc = transport_data.get("webrtc", False) |
748 # file may have been already accepted in preflight | 780 # file may have been already accepted in preflight |
779 confirmed = await xml_tools.defer_confirm( | 811 confirmed = await xml_tools.defer_confirm( |
780 self.host, | 812 self.host, |
781 confirm_msg, | 813 confirm_msg, |
782 confirm_title, | 814 confirm_title, |
783 profile=client.profile, | 815 profile=client.profile, |
784 action_extra=action_extra | 816 action_extra=action_extra, |
785 ) | 817 ) |
786 else: | 818 else: |
787 | 819 |
788 if hash_algo is not None: | 820 if hash_algo is not None: |
789 file_data["hash_algo"] = hash_algo | 821 file_data["hash_algo"] = hash_algo |
804 ) | 836 ) |
805 | 837 |
806 if confirmed: | 838 if confirmed: |
807 await self.host.trigger.async_point( | 839 await self.host.trigger.async_point( |
808 "XEP-0234_file_receiving_request_conf", | 840 "XEP-0234_file_receiving_request_conf", |
809 client, session, content_data, file_elt | 841 client, |
842 session, | |
843 content_data, | |
844 file_elt, | |
810 ) | 845 ) |
811 args = [client, session, content_name, content_data] | 846 args = [client, session, content_name, content_data] |
812 finished_d.addCallbacks( | 847 finished_d.addCallbacks( |
813 self._finished_cb, self._finished_eb, args, None, args | 848 self._finished_cb, self._finished_eb, args, None, args |
814 ) | 849 ) |
868 except Exception as e: | 903 except Exception as e: |
869 self.host.bridge.progress_error( | 904 self.host.bridge.progress_error( |
870 progress_id, C.PROGRESS_ERROR_FAILED, client.profile | 905 progress_id, C.PROGRESS_ERROR_FAILED, client.profile |
871 ) | 906 ) |
872 await self._j.terminate( | 907 await self._j.terminate( |
873 client, self._j.REASON_FAILED_APPLICATION, session) | 908 client, self._j.REASON_FAILED_APPLICATION, session |
909 ) | |
874 raise e | 910 raise e |
875 else: | 911 else: |
876 # we are sending the file | 912 # we are sending the file |
877 size = file_data["size"] | 913 size = file_data["size"] |
878 # XXX: hash security is not critical here, so we just take the higher | 914 # XXX: hash security is not critical here, so we just take the higher |
887 data_cb=lambda data: hasher.update(data), | 923 data_cb=lambda data: hasher.update(data), |
888 ) | 924 ) |
889 | 925 |
890 finished_d = content_data["finished_d"] = defer.Deferred() | 926 finished_d = content_data["finished_d"] = defer.Deferred() |
891 args = [client, session, content_name, content_data] | 927 args = [client, session, content_name, content_data] |
892 finished_d.addCallbacks(self._finished_cb, self._finished_eb, args, None, args) | 928 finished_d.addCallbacks( |
929 self._finished_cb, self._finished_eb, args, None, args | |
930 ) | |
893 await self.host.trigger.async_point( | 931 await self.host.trigger.async_point( |
894 "XEP-0234_jingle_handler", | 932 "XEP-0234_jingle_handler", client, session, content_data, desc_elt |
895 client, session, content_data, desc_elt | |
896 ) | 933 ) |
897 else: | 934 else: |
898 log.warning("FIXME: unmanaged action {}".format(action)) | 935 log.warning("FIXME: unmanaged action {}".format(action)) |
899 return desc_elt | 936 return desc_elt |
900 | 937 |
957 reason = first_child.name | 994 reason = first_child.name |
958 if reason_elt.text is not None: | 995 if reason_elt.text is not None: |
959 reason = f"{reason} - {reason_elt.text}" | 996 reason = f"{reason} - {reason_elt.text}" |
960 else: | 997 else: |
961 reason = C.PROGRESS_ERROR_FAILED | 998 reason = C.PROGRESS_ERROR_FAILED |
962 self.host.bridge.progress_error( | 999 self.host.bridge.progress_error(progress_id, reason, client.profile) |
963 progress_id, reason, client.profile | |
964 ) | |
965 | 1000 |
966 def _send_check_sum(self, client, session, content_name, content_data): | 1001 def _send_check_sum(self, client, session, content_name, content_data): |
967 """Send the session-info with the hash checksum""" | 1002 """Send the session-info with the hash checksum""" |
968 file_data = content_data["application_data"]["file_data"] | 1003 file_data = content_data["application_data"]["file_data"] |
969 hasher = file_data["hash_hasher"] | 1004 hasher = file_data["hash_hasher"] |