Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / pass / store / sagas / import / import.saga.ts
bloba143fc0138346f98f39448f765c0510cecc45008
1 import { call, put, takeLeading } from 'redux-saga/effects';
2 import { c } from 'ttag';
4 import { MAX_BATCH_PER_REQUEST } from '@proton/pass/constants';
5 import { type ImportVault } from '@proton/pass/lib/import/types';
6 import { parseItemRevision } from '@proton/pass/lib/items/item.parser';
7 import { importItemsBatch } from '@proton/pass/lib/items/item.requests';
8 import { createTelemetryEvent } from '@proton/pass/lib/telemetry/event';
9 import {
10     importItemsFailure,
11     importItemsIntent,
12     importItemsProgress,
13     importItemsSuccess,
14     notification,
15     startEventPolling,
16     stopEventPolling,
17     vaultCreationIntent,
18     vaultCreationSuccess,
19 } from '@proton/pass/store/actions';
20 import type { WithSenderAction } from '@proton/pass/store/actions/enhancers/endpoint';
21 import type { RootSagaOptions } from '@proton/pass/store/types';
22 import type { ItemRevision, ItemRevisionContentsResponse, Maybe } from '@proton/pass/types';
23 import { TelemetryEventName } from '@proton/pass/types/data/telemetry';
24 import { groupByKey } from '@proton/pass/utils/array/group-by-key';
25 import { prop } from '@proton/pass/utils/fp/lens';
26 import { logger } from '@proton/pass/utils/logger';
27 import { getEpoch } from '@proton/pass/utils/time/epoch';
28 import { getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
29 import capitalize from '@proton/utils/capitalize';
30 import chunk from '@proton/utils/chunk';
32 /**
33  * When creating vaults from the import saga
34  * we want to internally trigger any saga that
35  * may result from vaultCreationSuccess (notably
36  * the event-loop channel updates) : leverage
37  * the withCallback vaultCreationIntent to await
38  * the vault creation result
39  */
40 function* createVaultForImport(vaultName: string) {
41     const date = new Date().toLocaleDateString();
42     let resolver: (shareId: Maybe<string>) => void;
43     const creationResult = new Promise<Maybe<string>>((res) => (resolver = res));
45     yield put(
46         vaultCreationIntent(
47             { content: { name: vaultName, description: c('Info').t`Imported on ${date}`, display: {} } },
48             (action) => resolver(vaultCreationSuccess.match(action) ? action.payload.share.shareId : undefined)
49         )
50     );
52     const shareId: Maybe<string> = yield creationResult;
53     if (shareId === undefined) throw new Error(c('Warning').t`Could not create vault "${vaultName}"`);
55     return shareId;
58 function* importWorker(
59     { onItemsUpdated, getTelemetry }: RootSagaOptions,
60     { payload: { data, provider }, meta }: WithSenderAction<ReturnType<typeof importItemsIntent>>
61 ) {
62     const telemetry = getTelemetry();
63     yield put(stopEventPolling());
65     let totalItems: number = 0;
66     const ignored: string[] = data.ignored;
68     const importVaults = groupByKey(data.vaults, 'shareId', { splitEmpty: true }).map(
69         ([vault, ...vaults]): ImportVault => ({
70             ...vault,
71             items: vault.items.concat(...vaults.map(prop('items'))),
72         })
73     );
75     try {
76         /* we want to apply these request sequentially to avoid
77          * swarming the network with too many parallel requests */
78         for (const vaultData of importVaults) {
79             try {
80                 const shareId: string = vaultData.shareId ?? (yield call(createVaultForImport, vaultData.name));
82                 for (const batch of chunk(vaultData.items, MAX_BATCH_PER_REQUEST)) {
83                     try {
84                         const revisions: ItemRevisionContentsResponse[] = yield importItemsBatch({
85                             shareId,
86                             importIntents: batch,
87                             onSkippedItem: ({ type, metadata }) =>
88                                 ignored.push(`[${capitalize(type)}] ${metadata.name}`),
89                         });
91                         const items: ItemRevision[] = yield Promise.all(
92                             revisions.map((revision) => parseItemRevision(shareId, revision))
93                         );
95                         totalItems += revisions.length;
96                         yield put(importItemsProgress(meta.request.id, totalItems, { shareId, items }));
97                     } catch (e) {
98                         const description = e instanceof Error ? getApiErrorMessage(e) ?? e?.message : '';
99                         ignored.push(...batch.map((item) => `[${capitalize(item.type)}] ${item.metadata.name}`));
101                         yield put(
102                             notification({
103                                 endpoint: meta.sender?.endpoint,
104                                 key: meta.request.id,
105                                 type: 'error',
106                                 text: c('Error').t`Import failed for vault "${vaultData.name}" : ${description}`,
107                             })
108                         );
109                     }
110                 }
111             } catch (e) {
112                 logger.warn('[Saga::Import]', e);
113                 yield put(
114                     notification({
115                         key: meta.request.id,
116                         endpoint: meta.sender?.endpoint,
117                         type: 'error',
118                         text: c('Error').t`Vault "${vaultData.name}" could not be created`,
119                     })
120                 );
121             }
122         }
124         void telemetry?.push(
125             createTelemetryEvent(
126                 TelemetryEventName.ImportCompletion,
127                 { item_count: totalItems, vaults: importVaults.length },
128                 { source: provider }
129             )
130         );
132         yield put(
133             importItemsSuccess(
134                 meta.request.id,
135                 {
136                     provider,
137                     ignored,
138                     total: totalItems,
139                     importedAt: getEpoch(),
140                     warnings: data.warnings,
141                 },
142                 meta.sender?.endpoint
143             )
144         );
146         onItemsUpdated?.();
147     } catch (error: any) {
148         yield put(importItemsFailure(meta.request.id, error, meta.sender?.endpoint));
149     } finally {
150         yield put(startEventPolling());
151     }
154 export default function* watcher(options: RootSagaOptions) {
155     yield takeLeading(importItemsIntent.match, importWorker, options);