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';
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';
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
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));
47 { content: { name: vaultName, description: c('Info').t`Imported on ${date}`, display: {} } },
48 (action) => resolver(vaultCreationSuccess.match(action) ? action.payload.share.shareId : undefined)
52 const shareId: Maybe<string> = yield creationResult;
53 if (shareId === undefined) throw new Error(c('Warning').t`Could not create vault "${vaultName}"`);
58 function* importWorker(
59 { onItemsUpdated, getTelemetry }: RootSagaOptions,
60 { payload: { data, provider }, meta }: WithSenderAction<ReturnType<typeof importItemsIntent>>
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 => ({
71 items: vault.items.concat(...vaults.map(prop('items'))),
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) {
80 const shareId: string = vaultData.shareId ?? (yield call(createVaultForImport, vaultData.name));
82 for (const batch of chunk(vaultData.items, MAX_BATCH_PER_REQUEST)) {
84 const revisions: ItemRevisionContentsResponse[] = yield importItemsBatch({
87 onSkippedItem: ({ type, metadata }) =>
88 ignored.push(`[${capitalize(type)}] ${metadata.name}`),
91 const items: ItemRevision[] = yield Promise.all(
92 revisions.map((revision) => parseItemRevision(shareId, revision))
95 totalItems += revisions.length;
96 yield put(importItemsProgress(meta.request.id, totalItems, { shareId, items }));
98 const description = e instanceof Error ? getApiErrorMessage(e) ?? e?.message : '';
99 ignored.push(...batch.map((item) => `[${capitalize(item.type)}] ${item.metadata.name}`));
103 endpoint: meta.sender?.endpoint,
104 key: meta.request.id,
106 text: c('Error').t`Import failed for vault "${vaultData.name}" : ${description}`,
112 logger.warn('[Saga::Import]', e);
115 key: meta.request.id,
116 endpoint: meta.sender?.endpoint,
118 text: c('Error').t`Vault "${vaultData.name}" could not be created`,
124 void telemetry?.push(
125 createTelemetryEvent(
126 TelemetryEventName.ImportCompletion,
127 { item_count: totalItems, vaults: importVaults.length },
139 importedAt: getEpoch(),
140 warnings: data.warnings,
142 meta.sender?.endpoint
147 } catch (error: any) {
148 yield put(importItemsFailure(meta.request.id, error, meta.sender?.endpoint));
150 yield put(startEventPolling());
154 export default function* watcher(options: RootSagaOptions) {
155 yield takeLeading(importItemsIntent.match, importWorker, options);