@tutao/node-mimimi 259.250106.0 → 259.250108.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  edition = "2021"
3
3
  name = "tutao_node-mimimi"
4
- version = "259.250102.0"
4
+ version = "259.250108.1"
5
5
 
6
6
  [lib]
7
7
  # need to have lib to be able to use this from rust tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tutao/node-mimimi",
3
- "version": "259.250106.0",
3
+ "version": "259.250108.1",
4
4
  "main": "./dist/binding.cjs",
5
5
  "types": "./dist/binding.d.ts",
6
6
  "napi": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "license": "MIT",
18
18
  "devDependencies": {
19
- "@tutao/otest": "259.250106.0",
19
+ "@tutao/otest": "259.250108.1",
20
20
  "@napi-rs/cli": "^2.18.4",
21
21
  "typescript": "5.3.3",
22
22
  "zx": "8.1.5"
@@ -33,8 +33,8 @@
33
33
  "version": "napi version"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@tutao/node-mimimi-win32-x64-msvc": "259.250106.0",
37
- "@tutao/node-mimimi-linux-x64-gnu": "259.250106.0",
38
- "@tutao/node-mimimi-darwin-universal": "259.250106.0"
36
+ "@tutao/node-mimimi-win32-x64-msvc": "259.250108.1",
37
+ "@tutao/node-mimimi-linux-x64-gnu": "259.250108.1",
38
+ "@tutao/node-mimimi-darwin-universal": "259.250108.1"
39
39
  }
40
40
  }
package/src/importer.rs CHANGED
@@ -8,10 +8,10 @@ use file_reader::{FileImport, FileIterationError};
8
8
  use imap_reader::ImapImportConfig;
9
9
  use imap_reader::{ImapImport, ImapIterationError};
10
10
  use importable_mail::ImportableMail;
11
+ use napi::tokio::sync::{Mutex, MutexGuard};
11
12
  use std::fs;
12
- use std::future::Future;
13
13
  use std::path::PathBuf;
14
- use std::sync::Arc;
14
+ use std::sync::{Arc, OnceLock};
15
15
  use std::time::{SystemTime, UNIX_EPOCH};
16
16
  use tutasdk::blobs::blob_facade::FileData;
17
17
  use tutasdk::crypto::aes;
@@ -20,6 +20,7 @@ use tutasdk::crypto::key::{GenericAesKey, VersionedAesKey};
20
20
  use tutasdk::crypto::randomizer_facade::RandomizerFacade;
21
21
  use tutasdk::entities::generated::sys::{BlobReferenceTokenWrapper, StringWrapper};
22
22
 
23
+ use std::collections::HashMap;
23
24
  use tutasdk::entities::generated::tutanota::{
24
25
  ImportAttachment, ImportMailGetIn, ImportMailPostIn, ImportMailPostOut, ImportMailState,
25
26
  };
@@ -41,6 +42,12 @@ pub const MAX_REQUEST_SIZE: usize = 1024 * 1024 * 8;
41
42
  #[cfg(test)]
42
43
  pub const MAX_REQUEST_SIZE: usize = 1024 * 5;
43
44
 
45
+ type MailboxId = String;
46
+
47
+ pub static GLOBAL_IMPORTER_STATES: OnceLock<
48
+ Mutex<HashMap<MailboxId, Arc<Mutex<LocalImportState>>>>,
49
+ > = OnceLock::new();
50
+
44
51
  // We need this type because IdTupleGenerated cannot be converted to a napi value.
45
52
  #[cfg_attr(feature = "javascript", napi_derive::napi(object))]
46
53
  #[cfg_attr(test, derive(Debug))]
@@ -115,6 +122,8 @@ pub enum ImportError {
115
122
  FileDeletionError(std::io::Error, PathBuf),
116
123
  IOError(std::io::Error),
117
124
  CannotLoadMailbox,
125
+ ImporterAlreadyRunning,
126
+ NoRunningImport,
118
127
  }
119
128
 
120
129
  #[derive(Debug)]
@@ -148,6 +157,7 @@ pub enum ImportStatus {
148
157
  Paused = 1,
149
158
  Canceled = 2,
150
159
  Finished = 3,
160
+ Error = 4,
151
161
  }
152
162
 
153
163
  /// A running import can be stopped or paused
@@ -179,6 +189,7 @@ pub struct LocalImportState {
179
189
  pub total_count: i64,
180
190
  pub success_count: i64,
181
191
  pub failed_count: i64,
192
+ pub import_progress_action: ImportProgressAction,
182
193
  }
183
194
 
184
195
  pub struct ImportEssential {
@@ -189,13 +200,6 @@ pub struct ImportEssential {
189
200
  randomizer_facade: RandomizerFacade,
190
201
  }
191
202
 
192
- pub struct Importer {
193
- pub state: LocalImportState,
194
- pub essentials: ImportEssential,
195
- source: ImportSource,
196
- import_directory: PathBuf,
197
- }
198
-
199
203
  pub enum ImportSource {
200
204
  RemoteImap { imap_import_client: Box<ImapImport> },
201
205
  LocalFile { fs_email_client: FileImport },
@@ -233,13 +237,6 @@ impl Iterator for ImportSource {
233
237
 
234
238
  pub type ImportableMailsButcher<Source> =
235
239
  super::reduce_to_chunks::Butcher<{ MAX_REQUEST_SIZE }, AttachmentUploadData, Source>;
236
- impl Importer {
237
- fn make_random_aggregate_id(randomizer_facade: &RandomizerFacade) -> CustomId {
238
- let new_id_bytes = randomizer_facade.generate_random_array::<4>();
239
- let new_id_string = BASE64_URL_SAFE_NO_PAD.encode(new_id_bytes);
240
- CustomId(new_id_string)
241
- }
242
- }
243
240
 
244
241
  impl ImportEssential {
245
242
  const IMPORT_DISABLED_ERR: ApiCallError = ApiCallError::ServerResponseError {
@@ -472,12 +469,24 @@ impl ImportEssential {
472
469
  }
473
470
 
474
471
  impl LocalImportState {
475
- fn change_status(&mut self, new_status: ImportStatus) {
472
+ pub(crate) fn change_status(&mut self, new_status: ImportStatus) {
476
473
  self.current_status = new_status;
477
474
  }
478
475
  }
479
476
 
477
+ pub struct Importer {
478
+ pub(super) state: Arc<Mutex<LocalImportState>>,
479
+ pub(super) essentials: ImportEssential,
480
+ source: ImportSource,
481
+ import_directory: PathBuf,
482
+ }
480
483
  impl Importer {
484
+ fn make_random_aggregate_id(randomizer_facade: &RandomizerFacade) -> CustomId {
485
+ let new_id_bytes = randomizer_facade.generate_random_array::<4>();
486
+ let new_id_string = BASE64_URL_SAFE_NO_PAD.encode(new_id_bytes);
487
+ CustomId(new_id_string)
488
+ }
489
+
481
490
  pub async fn create_imap_importer(
482
491
  logged_in_sdk: Arc<LoggedInSdk>,
483
492
  target_owner_group: GeneratedId,
@@ -543,6 +552,7 @@ impl Importer {
543
552
  let randomizer_facade = RandomizerFacade::from_core(rand::rngs::OsRng);
544
553
 
545
554
  Self {
555
+ state: Arc::new(Mutex::new(LocalImportState::new())),
546
556
  source: import_source,
547
557
  essentials: ImportEssential {
548
558
  logged_in_sdk,
@@ -551,23 +561,24 @@ impl Importer {
551
561
  target_mailset,
552
562
  randomizer_facade,
553
563
  },
554
- state: LocalImportState::new(),
555
564
  import_directory,
556
565
  }
557
566
  }
558
567
 
559
- fn update_remote_state_id(
560
- local_state: &mut LocalImportState,
568
+ async fn update_remote_state_id(
569
+ &self,
561
570
  state_id: IdTupleGenerated,
562
571
  import_directory: PathBuf,
563
572
  ) -> Result<(), ImportError> {
564
573
  let min_id = GeneratedId::min_id();
565
574
 
566
- if local_state.remote_state_id.list_id == min_id.as_str()
567
- && local_state.remote_state_id.element_id == min_id.as_str()
575
+ let remote_state_id = self.get_state(|state| state.remote_state_id.clone()).await;
576
+ if remote_state_id.list_id == min_id.as_str()
577
+ && remote_state_id.element_id == min_id.as_str()
568
578
  {
569
579
  let generated = state_id.clone();
570
- local_state.remote_state_id = state_id.into();
580
+ self.update_state(|mut state| state.remote_state_id = state_id.clone().into())
581
+ .await;
571
582
  let mut state_id_file = import_directory.clone();
572
583
  state_id_file.push("import_mail_state");
573
584
 
@@ -575,8 +586,9 @@ impl Importer {
575
586
  return Ok(());
576
587
  }
577
588
 
589
+ let remote_state_id = self.get_state(|state| state.remote_state_id.clone()).await;
578
590
  // once id is set, it should always be same
579
- if local_state.remote_state_id != state_id.into() {
591
+ if remote_state_id != state_id.into() {
580
592
  return Err(ImportError::InconsistentStateId);
581
593
  }
582
594
 
@@ -594,31 +606,28 @@ impl Importer {
594
606
  .load::<ImportMailState, _>(&id)
595
607
  .await
596
608
  }
597
- async fn mark_remote_final_state(
598
- &mut self,
599
- final_status: ImportStatus,
609
+ pub(super) async fn mark_remote_final_state(
610
+ logged_in_sdk: &LoggedInSdk,
611
+ local_state: &LocalImportState,
600
612
  ) -> Result<(), ImportError> {
601
613
  assert!(
602
- final_status == ImportStatus::Finished
603
- || final_status == ImportStatus::Canceled
604
- || final_status == ImportStatus::Paused,
614
+ local_state.current_status == ImportStatus::Finished
615
+ || local_state.current_status == ImportStatus::Canceled
616
+ || local_state.current_status == ImportStatus::Paused,
605
617
  "only cancel and finished should be final state"
606
618
  );
607
619
 
608
620
  // we reached final state before making first call, was either empty mails or was cancelled before making first post call
609
- if self.state.remote_state_id
621
+ if local_state.remote_state_id
610
622
  != IdTupleGenerated::new(GeneratedId::min_id(), GeneratedId::min_id()).into()
611
623
  {
612
- let mut import_state = Self::load_import_state(
613
- &self.essentials.logged_in_sdk,
614
- self.state.remote_state_id.clone(),
615
- )
616
- .await
617
- .map_err(|e| ImportError::sdk("loading importState before Finished", e))?;
618
- import_state.status = final_status as i64;
624
+ let mut import_state =
625
+ Self::load_import_state(logged_in_sdk, local_state.remote_state_id.clone())
626
+ .await
627
+ .map_err(|e| ImportError::sdk("loading importState before Finished", e))?;
628
+ import_state.status = local_state.current_status as i64;
619
629
 
620
- self.essentials
621
- .logged_in_sdk
630
+ logged_in_sdk
622
631
  .mail_facade()
623
632
  .get_crypto_entity_client()
624
633
  .update_instance(import_state)
@@ -630,11 +639,10 @@ impl Importer {
630
639
  }
631
640
 
632
641
  pub async fn import_next_chunk(&mut self) -> Result<(), ImportError> {
642
+ let import_essentials = &self.essentials;
633
643
  let Self {
634
- essentials: import_essentials,
635
644
  source: import_source,
636
- state: import_state,
637
- import_directory,
645
+ ..
638
646
  } = self;
639
647
 
640
648
  let attachment_upload_data = import_source.into_iter().map(|importable_mail| {
@@ -648,17 +656,17 @@ impl Importer {
648
656
  ImportableMailsButcher::new(attachment_upload_data, |upload_data| {
649
657
  estimate_json_size(&upload_data.keyed_import_mail_data.import_mail_data)
650
658
  });
651
-
652
659
  match chunked_mails_provider.next() {
653
660
  // everything have been finished
654
661
  None => {
655
- self.state.change_status(ImportStatus::Finished);
662
+ self.update_state(|mut state| state.change_status(ImportStatus::Finished))
663
+ .await;
656
664
  Ok(())
657
665
  },
658
666
 
659
667
  // this chunk was too big to import
660
668
  Some(Err(_too_big_chunk)) => {
661
- self.state.failed_count += 1;
669
+ self.update_state(|mut state| state.failed_count += 1).await;
662
670
  Err(ImportError::TooBigChunk)?
663
671
  },
664
672
 
@@ -674,35 +682,32 @@ impl Importer {
674
682
  .map(|id| id.keyed_import_mail_data.eml_file_path.clone())
675
683
  .collect();
676
684
 
685
+ let mut failed_count: i64 = 0;
686
+ let remote_state_id = self.get_state(|state| state.remote_state_id.clone()).await;
677
687
  let unit_import_data = import_essentials
678
688
  .upload_attachments_for_chunk(chunked_import_data)
679
689
  .await
680
- .inspect_err(|_e| {
681
- import_state.failed_count += import_count_in_this_chunk;
682
- })?;
690
+ .inspect_err(|_e| failed_count += import_count_in_this_chunk)?;
683
691
  let importable_post_data = import_essentials
684
- .make_serialized_chunk(
685
- import_state.remote_state_id.clone().into(),
686
- unit_import_data,
687
- )
692
+ .make_serialized_chunk(remote_state_id.into(), unit_import_data)
688
693
  .await
689
- .inspect_err(|_e| {
690
- import_state.failed_count += import_count_in_this_chunk;
691
- })?;
694
+ .inspect_err(|_e| failed_count += import_count_in_this_chunk)?;
692
695
 
693
696
  let import_mails_post_out = import_essentials
694
697
  .make_import_service_call(importable_post_data)
695
698
  .await
696
- .inspect_err(|_e| {
697
- import_state.failed_count += import_count_in_this_chunk;
698
- })?;
699
+ .inspect_err(|_e| failed_count += import_count_in_this_chunk)?;
699
700
 
700
- Self::update_remote_state_id(
701
- import_state,
701
+ self.update_state(|mut state| {
702
+ state.failed_count += failed_count;
703
+ state.success_count += import_count_in_this_chunk;
704
+ })
705
+ .await;
706
+ self.update_remote_state_id(
702
707
  import_mails_post_out.mailState,
703
- import_directory.clone(),
704
- )?;
705
- import_state.success_count += import_count_in_this_chunk;
708
+ self.import_directory.clone(),
709
+ )
710
+ .await?;
706
711
  for eml_file_path in eml_file_paths.into_iter().flatten() {
707
712
  fs::remove_file(&eml_file_path)
708
713
  .map_err(|e| ImportError::FileDeletionError(e, eml_file_path))?;
@@ -713,49 +718,45 @@ impl Importer {
713
718
  }
714
719
  }
715
720
 
716
- pub async fn start_stateful_import<CallbackHandle, Err>(
717
- &mut self,
718
- callback_handle: impl Fn(LocalImportState) -> CallbackHandle,
719
- ) -> Result<(), Err>
720
- where
721
- CallbackHandle: Future<Output = Result<StateCallbackResponse, Err>>,
722
- Err: From<ImportError>,
723
- {
724
- self.state.change_status(ImportStatus::Running);
725
- 'import: loop {
726
- let callback_response = callback_handle(self.state.clone()).await?;
727
- match callback_response.action {
728
- ImportProgressAction::Pause => {
729
- self.state.change_status(ImportStatus::Paused);
730
- callback_handle(self.state.clone()).await?;
731
- self.mark_remote_final_state(ImportStatus::Paused).await?;
732
- break 'import Ok(());
733
- },
734
- ImportProgressAction::Stop => {
735
- self.state.change_status(ImportStatus::Canceled);
736
- self.mark_remote_final_state(ImportStatus::Canceled).await?;
737
- Self::delete_import_dir(&self.import_directory)?;
738
- callback_handle(self.state.clone()).await?;
739
- break 'import Ok(());
740
- },
741
- ImportProgressAction::Continue => {
742
- self.import_next_chunk().await?;
743
- if self.state.current_status == ImportStatus::Finished {
744
- self.mark_remote_final_state(ImportStatus::Finished).await?;
745
- Self::delete_import_dir(&self.import_directory)?;
746
- break 'import Ok(());
747
- }
748
- },
721
+ pub async fn start_stateful_import(&mut self) -> Result<(), ImportError> {
722
+ self.update_state(|mut state| state.change_status(ImportStatus::Running))
723
+ .await;
724
+ let mut import_progress_action = ImportProgressAction::Continue;
725
+
726
+ while import_progress_action == ImportProgressAction::Continue {
727
+ self.import_next_chunk().await?;
728
+
729
+ let updated_state = self.get_state(|state| state.clone()).await;
730
+ import_progress_action = updated_state.import_progress_action;
731
+ eprintln!(
732
+ "Import state Id: {}",
733
+ updated_state.remote_state_id.element_id
734
+ );
735
+
736
+ if updated_state.current_status == ImportStatus::Finished {
737
+ Importer::mark_remote_final_state(&self.essentials.logged_in_sdk, &updated_state)
738
+ .await?;
739
+ Self::delete_import_dir(&self.import_directory)?;
740
+ break;
749
741
  }
750
742
  }
743
+
744
+ Ok(())
745
+ }
746
+
747
+ pub(super) async fn update_state<F: Fn(MutexGuard<LocalImportState>)>(&self, f: F) {
748
+ f(self.state.lock().await)
749
+ }
750
+
751
+ pub(super) async fn get_state<T>(&self, f: fn(state: MutexGuard<LocalImportState>) -> T) -> T {
752
+ f(self.state.lock().await)
751
753
  }
752
754
 
753
755
  pub(super) async fn get_resumable_import(
754
756
  config_directory: String,
755
757
  mailbox_id: String,
756
758
  ) -> Result<ResumableImport, ImportError> {
757
- let import_directory_path =
758
- Importer::get_import_directory(config_directory, GeneratedId::from(mailbox_id));
759
+ let import_directory_path = Importer::get_import_directory(config_directory, &mailbox_id);
759
760
  let mut state_file_path = import_directory_path.clone();
760
761
  state_file_path.push("import_mail_state");
761
762
 
@@ -777,19 +778,19 @@ impl Importer {
777
778
  }
778
779
  }
779
780
 
780
- Self::delete_import_dir(&state_file_path.parent().unwrap().to_path_buf())?;
781
+ Self::delete_import_dir(&import_directory_path)?;
781
782
 
782
783
  Err(ImportError::NoElementIdForState)
783
784
  }
784
785
 
785
- fn delete_import_dir(import_directory_path: &PathBuf) -> Result<(), ImportError> {
786
+ pub(super) fn delete_import_dir(import_directory_path: &PathBuf) -> Result<(), ImportError> {
786
787
  if import_directory_path.exists() {
787
788
  fs::remove_dir_all(import_directory_path)
788
789
  .map_err(|e| ImportError::FileDeletionError(e, import_directory_path.clone()))?;
789
790
  }
790
791
  Ok(())
791
792
  }
792
- pub fn get_import_directory(config_directory: String, mailbox_id: GeneratedId) -> PathBuf {
793
+ pub fn get_import_directory(config_directory: String, mailbox_id: &str) -> PathBuf {
793
794
  [
794
795
  config_directory,
795
796
  "current_imports".into(),
@@ -827,6 +828,7 @@ impl LocalImportState {
827
828
  total_count: 0,
828
829
  success_count: 0,
829
830
  failed_count: 0,
831
+ import_progress_action: ImportProgressAction::Continue,
830
832
  }
831
833
  }
832
834
  }
@@ -856,18 +858,12 @@ mod tests {
856
858
  new_count
857
859
  }
858
860
  pub async fn import_all_of_source(importer: &mut Importer) -> Result<(), ImportError> {
859
- importer
860
- .start_stateful_import(|_| async {
861
- Ok(StateCallbackResponse {
862
- action: ImportProgressAction::Continue,
863
- })
864
- })
865
- .await
861
+ importer.start_stateful_import().await
866
862
  }
867
863
 
868
864
  fn assert_same_remote_and_local_state(
869
865
  remote_state: &ImportMailState,
870
- local_state: &LocalImportState,
866
+ local_state: MutexGuard<LocalImportState>,
871
867
  ) {
872
868
  // todo! sug
873
869
  // assert_eq!(remote_state.status, local_state.current_status as i64);
@@ -996,13 +992,14 @@ mod tests {
996
992
  greenmail.store_mail("sug@example.org", email_second.as_str());
997
993
 
998
994
  import_all_of_source(&mut importer).await.unwrap();
999
- let remote_state = Importer::load_import_state(
1000
- &importer.essentials.logged_in_sdk,
1001
- importer.state.remote_state_id.clone(),
1002
- )
1003
- .await
1004
- .unwrap();
1005
- assert_same_remote_and_local_state(&remote_state, &importer.state);
995
+ let remote_state_id = importer
996
+ .get_state(|state| state.remote_state_id.clone())
997
+ .await;
998
+ let remote_state =
999
+ Importer::load_import_state(&importer.essentials.logged_in_sdk, remote_state_id)
1000
+ .await
1001
+ .unwrap();
1002
+ assert_same_remote_and_local_state(&remote_state, importer.state.lock().await);
1006
1003
 
1007
1004
  assert_eq!(remote_state.status, ImportStatus::Finished as i64);
1008
1005
  assert_eq!(remote_state.failedMails, 0);
@@ -1017,14 +1014,15 @@ mod tests {
1017
1014
  greenmail.store_mail("sug@example.org", email.as_str());
1018
1015
 
1019
1016
  import_all_of_source(&mut importer).await.unwrap();
1020
- let remote_state = Importer::load_import_state(
1021
- &importer.essentials.logged_in_sdk,
1022
- importer.state.remote_state_id.clone(),
1023
- )
1024
- .await
1025
- .unwrap();
1017
+ let remote_state_id = importer
1018
+ .get_state(|state| state.remote_state_id.clone())
1019
+ .await;
1020
+ let remote_state =
1021
+ Importer::load_import_state(&importer.essentials.logged_in_sdk, remote_state_id)
1022
+ .await
1023
+ .unwrap();
1026
1024
 
1027
- assert_same_remote_and_local_state(&remote_state, &importer.state);
1025
+ assert_same_remote_and_local_state(&remote_state, importer.state.lock().await);
1028
1026
  assert_eq!(remote_state.status, ImportStatus::Finished as i64);
1029
1027
  assert_eq!(remote_state.failedMails, 0);
1030
1028
  assert_eq!(remote_state.successfulMails, 1);
@@ -1034,14 +1032,15 @@ mod tests {
1034
1032
  async fn can_import_single_eml_file_without_attachment() {
1035
1033
  let mut importer = init_file_importer(vec!["sample.eml"]).await;
1036
1034
  import_all_of_source(&mut importer).await.unwrap();
1037
- let remote_state = Importer::load_import_state(
1038
- &importer.essentials.logged_in_sdk,
1039
- importer.state.remote_state_id.clone(),
1040
- )
1041
- .await
1042
- .unwrap();
1035
+ let remote_state_id = importer
1036
+ .get_state(|state| state.remote_state_id.clone())
1037
+ .await;
1038
+ let remote_state =
1039
+ Importer::load_import_state(&importer.essentials.logged_in_sdk, remote_state_id)
1040
+ .await
1041
+ .unwrap();
1043
1042
 
1044
- assert_same_remote_and_local_state(&remote_state, &importer.state);
1043
+ assert_same_remote_and_local_state(&remote_state, importer.state.lock().await);
1045
1044
  assert_eq!(remote_state.status, ImportStatus::Finished as i64);
1046
1045
  assert_eq!(remote_state.failedMails, 0);
1047
1046
  assert_eq!(remote_state.successfulMails, 1);
@@ -1051,14 +1050,15 @@ mod tests {
1051
1050
  async fn can_import_single_eml_file_with_attachment() {
1052
1051
  let mut importer = init_file_importer(vec!["attachment_sample.eml"]).await;
1053
1052
  import_all_of_source(&mut importer).await.unwrap();
1054
- let remote_state = Importer::load_import_state(
1055
- &importer.essentials.logged_in_sdk,
1056
- importer.state.remote_state_id.clone(),
1057
- )
1058
- .await
1059
- .unwrap();
1053
+ let remote_state_id = importer
1054
+ .get_state(|state| state.remote_state_id.clone())
1055
+ .await;
1056
+ let remote_state =
1057
+ Importer::load_import_state(&importer.essentials.logged_in_sdk, remote_state_id)
1058
+ .await
1059
+ .unwrap();
1060
1060
 
1061
- assert_same_remote_and_local_state(&remote_state, &importer.state);
1061
+ assert_same_remote_and_local_state(&remote_state, importer.state.lock().await);
1062
1062
  assert_eq!(remote_state.status, ImportStatus::Finished as i64);
1063
1063
  assert_eq!(remote_state.failedMails, 0);
1064
1064
  assert_eq!(remote_state.successfulMails, 1);
@@ -1069,21 +1069,14 @@ mod tests {
1069
1069
  async fn should_stop_if_on_stop_action() {
1070
1070
  let mut importer = init_file_importer(vec!["sample.eml"; 3]).await;
1071
1071
 
1072
- let callback_resolver = |_| async {
1073
- Result::<_, ImportError>::Ok(StateCallbackResponse {
1074
- action: ImportProgressAction::Stop,
1075
- })
1076
- };
1077
- importer
1078
- .start_stateful_import(callback_resolver)
1079
- .await
1080
- .unwrap();
1081
- let remote_state = Importer::load_import_state(
1082
- &importer.essentials.logged_in_sdk,
1083
- importer.state.remote_state_id.clone(),
1084
- )
1085
- .await
1086
- .unwrap();
1072
+ importer.start_stateful_import().await.unwrap();
1073
+ let remote_state_id = importer
1074
+ .get_state(|state| state.remote_state_id.clone())
1075
+ .await;
1076
+ let remote_state =
1077
+ Importer::load_import_state(&importer.essentials.logged_in_sdk, remote_state_id)
1078
+ .await
1079
+ .unwrap();
1087
1080
 
1088
1081
  assert_eq!(remote_state.status, ImportStatus::Canceled as i64);
1089
1082
  assert_eq!(remote_state.failedMails, 0);
@@ -1,11 +1,12 @@
1
1
  use super::importer::{
2
- ImportError, ImportMailStateId, Importer, IterationError, LocalImportState, ResumableImport,
3
- StateCallbackResponse,
2
+ ImportError, ImportMailStateId, ImportProgressAction, ImportStatus, Importer, IterationError,
3
+ LocalImportState, ResumableImport, GLOBAL_IMPORTER_STATES,
4
4
  };
5
5
  use crate::importer::file_reader::FileImport;
6
- use napi::bindgen_prelude::Promise;
7
- use napi::threadsafe_function::ThreadsafeFunction;
6
+ use napi::tokio::sync::Mutex;
7
+ use napi::tokio::sync::MutexGuard;
8
8
  use napi::Env;
9
+ use std::collections::HashMap;
9
10
  use std::fs;
10
11
  use std::path::PathBuf;
11
12
  use std::sync::Arc;
@@ -13,17 +14,6 @@ use tutasdk::login::{CredentialType, Credentials};
13
14
  use tutasdk::net::native_rest_client::NativeRestClient;
14
15
  use tutasdk::{GeneratedId, IdTupleGenerated, LoggedInSdk};
15
16
 
16
- pub type NapiTokioMutex<T> = napi::tokio::sync::Mutex<T>;
17
-
18
- /// Javascript function to check for state change
19
- type StateCallback =
20
- ThreadsafeFunction<LocalImportState, napi::threadsafe_function::ErrorStrategy::Fatal>;
21
-
22
- #[napi_derive::napi]
23
- pub struct ImporterApi {
24
- pub(crate) inner: Arc<NapiTokioMutex<Importer>>,
25
- }
26
-
27
17
  #[napi_derive::napi(object)]
28
18
  #[derive(Clone)]
29
19
  /// Passed in from js-side, will be validated before being converted to proper tuta sdk credentials.
@@ -38,18 +28,29 @@ pub struct TutaCredentials {
38
28
  pub is_internal_credential: bool,
39
29
  }
40
30
 
31
+ #[napi_derive::napi]
32
+ pub struct ImporterApi {}
33
+
41
34
  impl ImporterApi {
35
+ pub async fn get_running_imports<'a>(
36
+ ) -> MutexGuard<'a, HashMap<String, Arc<Mutex<LocalImportState>>>> {
37
+ GLOBAL_IMPORTER_STATES
38
+ .get_or_init(|| Mutex::new(HashMap::new()))
39
+ .lock()
40
+ .await
41
+ }
42
+
42
43
  pub async fn create_file_importer_inner(
43
44
  logged_in_sdk: Arc<LoggedInSdk>,
44
45
  target_owner_group: String,
45
46
  target_mailset: IdTupleGenerated,
46
47
  source_paths: Vec<PathBuf>,
47
48
  import_directory: PathBuf,
48
- ) -> napi::Result<ImporterApi> {
49
+ ) -> napi::Result<Importer> {
49
50
  let target_owner_group = GeneratedId(target_owner_group);
50
51
 
51
52
  let source_count = source_paths.len() as i64;
52
- let mut importer = Importer::create_file_importer(
53
+ let importer = Importer::create_file_importer(
53
54
  logged_in_sdk,
54
55
  target_owner_group,
55
56
  target_mailset,
@@ -58,10 +59,11 @@ impl ImporterApi {
58
59
  )
59
60
  .await?;
60
61
 
61
- importer.state.total_count = source_count;
62
- Ok(ImporterApi {
63
- inner: Arc::new(NapiTokioMutex::new(importer)),
64
- })
62
+ importer
63
+ .update_state(|mut state| state.total_count = source_count)
64
+ .await;
65
+
66
+ Ok(importer)
65
67
  }
66
68
 
67
69
  async fn create_sdk(
@@ -83,64 +85,77 @@ impl ImporterApi {
83
85
  #[napi_derive::napi]
84
86
  impl ImporterApi {
85
87
  #[napi]
86
- pub async unsafe fn start_import(
87
- &mut self,
88
- callback_handle: StateCallback,
89
- ) -> napi::Result<()> {
90
- let callback_handle_provider = |local_state: LocalImportState| async {
91
- let res = callback_handle
92
- .call_async::<Promise<StateCallbackResponse>>(local_state)
93
- .await;
94
- match res {
95
- Ok(promise) => promise.await,
96
- Err(e) => Err(e),
97
- }
98
- };
99
-
100
- let mut importer = self.inner.lock().await;
101
- importer
102
- .start_stateful_import(callback_handle_provider)
103
- .await?;
104
-
105
- Ok(())
88
+ pub async fn get_import_state(mailbox_id: String) -> napi::Result<Option<LocalImportState>> {
89
+ let locked_importer_states = Self::get_running_imports().await;
90
+ match locked_importer_states.get(&mailbox_id) {
91
+ Some(locked_state) => Ok(Some({
92
+ let state = locked_state.lock().await.clone();
93
+ state
94
+ })),
95
+ None => Ok(None),
96
+ }
106
97
  }
107
98
 
108
99
  #[napi]
109
- pub async fn create_file_importer(
100
+ pub async fn start_file_import(
101
+ mailbox_id: String,
110
102
  tuta_credentials: TutaCredentials,
111
103
  target_owner_group: String,
112
104
  target_mailset_id: (String, String),
113
105
  source_paths: Vec<String>,
114
106
  config_directory: String,
115
- ) -> napi::Result<ImporterApi> {
116
- let (target_mailset_lid, target_mailset_eid) = target_mailset_id;
117
-
107
+ ) -> napi::Result<()> {
118
108
  let logged_in_sdk = ImporterApi::create_sdk(tuta_credentials).await?;
109
+
110
+ let mut running_imports = Self::get_running_imports().await;
111
+ if let Some(import) = running_imports.get_mut(&mailbox_id) {
112
+ let current_status = import.lock().await.current_status;
113
+ if current_status != ImportStatus::Running {
114
+ running_imports.remove(&mailbox_id);
115
+ } else {
116
+ Err(ImportError::ImporterAlreadyRunning)?;
117
+ }
118
+ }
119
+
120
+ let (target_mailset_lid, target_mailset_eid) = target_mailset_id;
119
121
  let target_mailset = IdTupleGenerated::new(
120
122
  GeneratedId(target_mailset_lid),
121
123
  GeneratedId(target_mailset_eid),
122
124
  );
123
- let mailbox_id = logged_in_sdk
124
- .mail_facade()
125
- .load_user_mailbox()
126
- .await
127
- .map_err(|e| ImportError::sdk("loading mailbox", e))?
128
- ._id
129
- .ok_or(ImportError::CannotLoadMailbox)?;
130
125
  let import_directory: PathBuf =
131
- Importer::get_import_directory(config_directory, mailbox_id);
126
+ Importer::get_import_directory(config_directory, &mailbox_id);
132
127
  let source_paths = source_paths.into_iter().map(PathBuf::from).collect();
133
128
  let eml_sources = FileImport::prepare_import(import_directory.clone(), source_paths)
134
129
  .map_err(|e| ImportError::IterationError(IterationError::File(e)))?;
135
130
 
136
- Self::create_file_importer_inner(
131
+ let inner = Self::create_file_importer_inner(
137
132
  logged_in_sdk,
138
133
  target_owner_group,
139
134
  target_mailset,
140
135
  eml_sources,
141
136
  import_directory,
142
137
  )
143
- .await
138
+ .await?;
139
+
140
+ running_imports.insert(mailbox_id.clone(), inner.state.clone());
141
+ drop(running_imports);
142
+
143
+ Self::spawn_importer_task(inner);
144
+ Ok(())
145
+ }
146
+
147
+ fn spawn_importer_task(mut inner: Importer) {
148
+ napi::tokio::task::spawn(async move {
149
+ match inner.start_stateful_import().await {
150
+ Ok(_) => {},
151
+ Err(e) => {
152
+ log::error!("Importer task failed: {:?}", e);
153
+ inner
154
+ .update_state(|mut state| state.change_status(ImportStatus::Error))
155
+ .await;
156
+ },
157
+ };
158
+ });
144
159
  }
145
160
 
146
161
  #[napi]
@@ -155,10 +170,11 @@ impl ImporterApi {
155
170
 
156
171
  #[napi]
157
172
  pub async fn resume_file_import(
173
+ mailbox_id: String,
158
174
  tuta_credentials: TutaCredentials,
159
175
  mail_state_id: ImportMailStateId,
160
176
  config_directory: String,
161
- ) -> napi::Result<ImporterApi> {
177
+ ) -> napi::Result<()> {
162
178
  let logged_in_sdk = ImporterApi::create_sdk(tuta_credentials).await?;
163
179
  let import_state = Importer::load_import_state(&logged_in_sdk, mail_state_id)
164
180
  .await
@@ -168,15 +184,8 @@ impl ImporterApi {
168
184
  let target_owner_group = import_state
169
185
  ._ownerGroup
170
186
  .expect("import state should have ownerGroup");
171
- let mailbox_id = logged_in_sdk
172
- .mail_facade()
173
- .load_user_mailbox()
174
- .await
175
- .map_err(|e| ImportError::sdk("loading mailbox", e))?
176
- ._id
177
- .ok_or(ImportError::CannotLoadMailbox)?;
178
- let import_directory: PathBuf =
179
- Importer::get_import_directory(config_directory, mailbox_id);
187
+
188
+ let import_directory = Importer::get_import_directory(config_directory, &mailbox_id);
180
189
 
181
190
  let dir_entries = fs::read_dir(&import_directory)?;
182
191
  let mut source_paths: Vec<PathBuf> = vec![];
@@ -191,24 +200,82 @@ impl ImporterApi {
191
200
  }
192
201
  }
193
202
 
194
- Self::create_file_importer_inner(
203
+ let inner = Self::create_file_importer_inner(
195
204
  logged_in_sdk,
196
205
  target_owner_group.as_str().to_string(),
197
206
  target_mailset,
198
207
  source_paths,
199
208
  import_directory,
200
209
  )
201
- .await
210
+ .await?;
211
+
212
+ {
213
+ let mut running_imports = Self::get_running_imports().await;
214
+ running_imports.insert(mailbox_id.clone(), inner.state.clone());
215
+ }
216
+
217
+ Self::spawn_importer_task(inner);
218
+ Ok(())
219
+ }
220
+
221
+ #[napi]
222
+ pub async fn set_progress_action(
223
+ mailbox_id: String,
224
+ tuta_credentials: TutaCredentials,
225
+ import_progress_action: ImportProgressAction,
226
+ config_directory: String,
227
+ ) -> napi::Result<()> {
228
+ let mut running_imports = Self::get_running_imports().await;
229
+ let locked_local_state = running_imports.get_mut(mailbox_id.as_str());
230
+ let import_directory_path = Importer::get_import_directory(config_directory, &mailbox_id);
231
+
232
+ match locked_local_state {
233
+ Some(local_import_state) => {
234
+ let logged_in_sdk = ImporterApi::create_sdk(tuta_credentials).await?;
235
+ let mut local_import_state = local_import_state.lock().await;
236
+ local_import_state.import_progress_action = import_progress_action;
237
+
238
+ match import_progress_action {
239
+ ImportProgressAction::Continue => Ok(()),
240
+
241
+ ImportProgressAction::Pause => {
242
+ local_import_state.current_status = ImportStatus::Paused;
243
+ Importer::mark_remote_final_state(&logged_in_sdk, &local_import_state)
244
+ .await?;
245
+
246
+ Ok(())
247
+ },
248
+
249
+ ImportProgressAction::Stop => {
250
+ let previous_status = local_import_state.current_status;
251
+ local_import_state.current_status = ImportStatus::Canceled;
252
+ Importer::mark_remote_final_state(&logged_in_sdk, &local_import_state)
253
+ .await?;
254
+
255
+ if previous_status != ImportStatus::Running {
256
+ Importer::delete_import_dir(&import_directory_path)?;
257
+ }
258
+
259
+ Ok(())
260
+ },
261
+ }
262
+ },
263
+
264
+ None => {
265
+ Importer::delete_import_dir(&import_directory_path)?;
266
+ Ok(())
267
+ },
268
+ }
202
269
  }
203
270
 
204
271
  #[napi]
205
- pub unsafe fn init_log(env: Env) {
272
+ pub fn init_log(env: Env) {
206
273
  // this is in a separate fn because Env isn't Send, so can't be used in async fn.
207
274
  crate::logging::console::Console::init(env)
208
275
  }
209
276
 
210
277
  #[napi]
211
- pub unsafe fn deinit_log() {
278
+ pub fn deinit_log() {
212
279
  crate::logging::console::Console::deinit();
213
280
  }
214
281
  }
@@ -1,18 +1,22 @@
1
1
  use crate::logging::logger::{LogLevel, LogMessage, Logger};
2
2
  use log::{Level, LevelFilter, Log, Metadata, Record};
3
- use napi::sys::{napi_async_work, napi_cancel_async_work};
4
- use napi::{AsyncWorkPromise, Env, Task};
5
- use std::sync::OnceLock;
3
+ use napi::Env;
4
+ use std::sync::mpsc::Sender;
5
+ use std::sync::Once;
6
+ use std::sync::RwLock;
6
7
 
7
8
  const TAG: &str = file!();
8
9
 
9
- pub static mut GLOBAL_CONSOLE: OnceLock<Console> = OnceLock::new();
10
+ /// Maintain one instance of the logger, as the log crate can only have the logger be set exactly
11
+ /// once.
12
+ static GLOBAL_CONSOLE: Console = Console {
13
+ sender: RwLock::new(None),
14
+ };
10
15
 
11
16
  /// A way for the rust code to log messages to the main applications log files
12
17
  /// without having to deal with obtaining a reference to console each time.
13
- #[derive(Clone)]
14
18
  pub struct Console {
15
- tx: std::sync::mpsc::Sender<LogMessage>,
19
+ sender: RwLock<Option<Sender<LogMessage>>>,
16
20
  }
17
21
 
18
22
  impl Log for Console {
@@ -28,56 +32,59 @@ impl Log for Console {
28
32
 
29
33
  let tag = record.file().unwrap_or("<unknown>").to_string();
30
34
 
31
- let _ = self.tx.send(LogMessage {
32
- level: record.metadata().level().into(),
33
- message: format!("{}", record.args()),
34
- tag,
35
- });
35
+ let lock = self.sender.read().expect("poisoned");
36
+ if let Some(sender) = lock.as_ref() {
37
+ let _ = sender.send(LogMessage {
38
+ level: record.metadata().level().into(),
39
+ message: format!("{}", record.args()),
40
+ tag,
41
+ });
42
+ }
36
43
  }
37
44
 
38
45
  fn flush(&self) {}
39
46
  }
40
47
 
41
48
  impl Console {
42
- pub unsafe fn init(env: Env) {
43
- if GLOBAL_CONSOLE.get().is_some() {
49
+ pub fn init(env: Env) {
50
+ Self::init_global_state();
51
+
52
+ let mut current_sender = GLOBAL_CONSOLE.sender.write().expect("poisoned");
53
+ if current_sender.is_some() {
44
54
  // some other thread already initialized the cell, we don't need to set up the logger.
45
55
  return;
46
56
  }
57
+
47
58
  let (tx, rx) = std::sync::mpsc::channel::<LogMessage>();
48
- let console = Console { tx };
49
59
  let logger = Logger::new(rx);
50
60
  let maybe_async_task = env.spawn(logger);
51
61
 
52
62
  match maybe_async_task {
53
63
  Ok(_logger_task) => {
54
- console.log(
64
+ *current_sender = Some(tx);
65
+ drop(current_sender);
66
+ GLOBAL_CONSOLE.log(
55
67
  &Record::builder()
56
68
  .level(Level::Info)
57
69
  .file(Some(TAG))
58
70
  .args(format_args!("{}", "spawned logger"))
59
71
  .build(),
60
72
  );
61
-
62
- GLOBAL_CONSOLE
63
- .set(console)
64
- .map_err(|_| "can not set")
65
- .unwrap();
66
-
67
- let console = GLOBAL_CONSOLE.get().unwrap();
68
- set_panic_hook(&console);
69
- log::set_logger(console).unwrap_or_else(|e| eprintln!("failed to set logger: {e}"));
70
- log::set_max_level(LevelFilter::Info);
71
73
  },
72
74
  Err(e) => {
73
75
  eprintln!("failed to spawn logger: {e}");
74
76
  },
75
77
  }
76
78
  }
77
- pub unsafe fn deinit() {
78
- let console = GLOBAL_CONSOLE.take().expect("cannot deinit logger before initializing");
79
- console
80
- .tx
79
+
80
+ pub fn deinit() {
81
+ let sender = GLOBAL_CONSOLE
82
+ .sender
83
+ .write()
84
+ .expect("poisoned")
85
+ .take()
86
+ .expect("cannot deinit logger before initializing");
87
+ sender
81
88
  .send(LogMessage {
82
89
  level: LogLevel::Finish,
83
90
  message: "called deinit".to_string(),
@@ -85,42 +92,59 @@ impl Console {
85
92
  })
86
93
  .expect("Can not send finish log message. Receiver already disconnected");
87
94
  }
88
- }
89
95
 
90
- /// set a panic hook that tries to log the panic to the JS side before continuing
91
- /// a normal unwind. should work unless the panicking thread is the main thread.
92
- fn set_panic_hook(console: &'static Console) {
93
- let logger_thread_id = std::thread::current().id();
94
- let panic_console = console.clone();
95
- let old_panic_hook = std::panic::take_hook();
96
- std::panic::set_hook(Box::new(move |panic_info| {
97
- let formatted_info = panic_info.to_string();
98
- let formatted_stack = std::backtrace::Backtrace::force_capture().to_string();
99
- if logger_thread_id == std::thread::current().id() {
100
- // logger is (probably) running on the currently panicking thread,
101
- // so we can't use it to log to JS. this at least shows up in stderr.
102
- eprintln!("PANIC MAIN {}", formatted_info);
103
- eprintln!("PANIC MAIN {}", formatted_stack);
104
- } else {
105
- panic_console.log(
106
- &Record::builder()
107
- .level(Level::Error)
108
- .file(Some("PANIC"))
109
- .args(format_args!(
110
- "thread {} {}",
111
- std::thread::current().name().unwrap_or("<unknown>"),
112
- formatted_info
113
- ))
114
- .build(),
115
- );
116
- panic_console.log(
117
- &Record::builder()
118
- .level(Level::Error)
119
- .file(Some("PANIC"))
120
- .args(format_args!("{}", formatted_stack.as_str()))
121
- .build(),
122
- );
123
- }
124
- old_panic_hook(panic_info)
125
- }));
96
+ /// Sets the panic hook and global logger.
97
+ ///
98
+ /// This function must be called once before the console can be used, but it can be safely
99
+ /// called multiple times, and it is thread-safe.
100
+ fn init_global_state() {
101
+ // Limit scope to this function only.
102
+ static GLOBAL_CONSOLE_SETUP: Once = Once::new();
103
+
104
+ GLOBAL_CONSOLE_SETUP.call_once(|| {
105
+ // Sets the logger to the static instance and sets up the panic hook.
106
+ // This can fail if the logger was set somewhere else.
107
+ if let Err(e) = log::set_logger(&GLOBAL_CONSOLE) {
108
+ eprintln!("failed to set logger: {e}");
109
+ } else {
110
+ log::set_max_level(LevelFilter::Info);
111
+ }
112
+
113
+ // set a panic hook that tries to log the panic to the JS side before continuing
114
+ // a normal unwind. should work unless the panicking thread is the main thread.
115
+ let logger_thread_id = std::thread::current().id();
116
+ let old_panic_hook = std::panic::take_hook();
117
+
118
+ std::panic::set_hook(Box::new(move |panic_info| {
119
+ let formatted_info = panic_info.to_string();
120
+ let formatted_stack = std::backtrace::Backtrace::force_capture().to_string();
121
+ if logger_thread_id == std::thread::current().id() {
122
+ // logger is (probably) running on the currently panicking thread,
123
+ // so we can't use it to log to JS. this at least shows up in stderr.
124
+ eprintln!("PANIC MAIN {}", formatted_info);
125
+ eprintln!("PANIC MAIN {}", formatted_stack);
126
+ } else {
127
+ GLOBAL_CONSOLE.log(
128
+ &Record::builder()
129
+ .level(Level::Error)
130
+ .file(Some("PANIC"))
131
+ .args(format_args!(
132
+ "thread {} {}",
133
+ std::thread::current().name().unwrap_or("<unknown>"),
134
+ formatted_info
135
+ ))
136
+ .build(),
137
+ );
138
+ GLOBAL_CONSOLE.log(
139
+ &Record::builder()
140
+ .level(Level::Error)
141
+ .file(Some("PANIC"))
142
+ .args(format_args!("{}", formatted_stack.as_str()))
143
+ .build(),
144
+ );
145
+ }
146
+ old_panic_hook(panic_info)
147
+ }));
148
+ });
149
+ }
126
150
  }