Skip to content

Commit

Permalink
feat(suite): add option to export labeling files
Browse files Browse the repository at this point in the history
  • Loading branch information
mroz22 committed Nov 18, 2024
1 parent 182d6b4 commit 3fae451
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 0 deletions.
44 changes: 44 additions & 0 deletions packages/suite/src/actions/suite/metadataActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createAction } from '@reduxjs/toolkit';
import { selectDevices } from '@suite-common/wallet-core';
import { Account } from '@suite-common/wallet-types';
import { StaticSessionId } from '@trezor/connect';
import { createZip } from '@trezor/utils';

import { METADATA, METADATA_LABELING } from 'src/actions/suite/constants';
import { Dispatch, GetState } from 'src/types/suite';
Expand All @@ -17,6 +18,7 @@ import {
import * as metadataUtils from 'src/utils/suite/metadata';
import { selectSelectedProviderForLabels } from 'src/reducers/suite/metadataReducer';
import type { AbstractMetadataProvider, PasswordManagerState } from 'src/types/suite/metadata';

Check warning on line 20 in packages/suite/src/actions/suite/metadataActions.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups
import { getProviderInstance } from './metadataProviderActions';

export type MetadataAction =
| { type: typeof METADATA.ENABLE }
Expand Down Expand Up @@ -165,3 +167,45 @@ export const encryptAndSaveMetadata = async ({

return providerInstance.setFileContent(fileName, encrypted);
};

export const exportMetadataToLocalFile = () => async (dispatch: Dispatch, getState: GetState) => {
const providerInstance = dispatch(
getProviderInstance({
clientId: selectSelectedProviderForLabels(getState())!.clientId,
dataType: 'labels',
}),
);

if (!providerInstance) return;

const filesListResult = await providerInstance.getFilesList();

// todo: handle error, toast "no files to export"?
if (!filesListResult.success || !filesListResult.payload?.length) return;

const files = filesListResult.payload;

return Promise.all(
files.map(file => {
return providerInstance.getFileContent(file).then(result => {
if (!result.success) throw new Error(result.error);
return { name: file, content: result.payload };

Check failure on line 192 in packages/suite/src/actions/suite/metadataActions.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

Expected blank line before this statement
});
}),
)
.then(filesContent => {
const zipBlob = createZip(filesContent);
// Trigger download
const a = document.createElement('a');
a.href = URL.createObjectURL(zipBlob);
a.download = 'archive.zip';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
})
.catch(_err => {
// todo: handle error, failed to get file content
});
};
32 changes: 32 additions & 0 deletions packages/suite/src/views/settings/SettingsDebug/Metadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react';

Check warning on line 1 in packages/suite/src/views/settings/SettingsDebug/Metadata.tsx

View workflow job for this annotation

GitHub Actions / Linting and formatting

There should be at least one empty line between import groups
import { Button } from '@trezor/components';

import { ActionColumn, SectionItem, TextColumn } from 'src/components/suite';
import { useDispatch } from 'src/hooks/suite';
import { exportMetadataToLocalFile } from 'src/actions/suite/metadataActions';

export const Metadata = () => {
const dispatch = useDispatch();
const [exporting, setExporting] = useState(false);

const onClick = () => {
setExporting(true);
dispatch(exportMetadataToLocalFile()).finally(() => {
setExporting(false);
});
};

return (
<SectionItem data-testid="@settings/debug/metadata">
<TextColumn
title="Export"
description="Export labeling files to your computer. You may use this to transfer your labeling files from your Google drive account to your Dropbox account."
/>
<ActionColumn>
<Button onClick={onClick} isDisabled={exporting} isLoading={exporting}>
Export
</Button>
</ActionColumn>
</SectionItem>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { TriggerHighlight } from './TriggerHighlight';
import { Backends } from './Backends';
import { PreField } from './PreField';
import { Tor } from './Tor';
import { Metadata } from './Metadata';

export const SettingsDebug = () => {
const flags = useSelector(selectSuiteFlags);
Expand Down Expand Up @@ -76,6 +77,9 @@ export const SettingsDebug = () => {
<SettingsSection title="Flags JSON">
<PreField>{JSON.stringify(flags)}</PreField>
</SettingsSection>
<SettingsSection title="Metadata">
<Metadata />
</SettingsSection>
</SettingsLayout>
);
};
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ export * from './throttler';
export * from './extractUrlsFromText';
export * from './isFullPath';
export * from './asciiUtils';
export * from './zip';
68 changes: 68 additions & 0 deletions packages/utils/src/zip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export const createZip = (buffers: { name: string; content: ArrayBuffer }[]) => {
const fileEntries: ArrayBuffer[] = [];
let centralDirectory: ArrayBuffer[] = [];

Check failure on line 3 in packages/utils/src/zip.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

'centralDirectory' is never reassigned. Use 'const' instead
let offset = 0;

buffers.forEach(({ name, content }) => {
const fileData = content;
const fileHeader = new Uint8Array(30 + name.length);
const localFileHeader = new DataView(fileHeader.buffer);

// Local file header signature
localFileHeader.setUint32(0, 0x04034b50, true); // "PK\3\4"
localFileHeader.setUint16(4, 0x0, true); // Version needed to extract
localFileHeader.setUint16(6, 0x0, true); // General purpose bit flag
localFileHeader.setUint16(8, 0x0, true); // Compression method (none)
localFileHeader.setUint16(10, 0x0, true); // File last mod time
localFileHeader.setUint16(12, 0x0, true); // File last mod date
localFileHeader.setUint32(14, 0, true); // CRC-32 (skipped for simplicity)
localFileHeader.setUint32(18, fileData.byteLength, true); // Compressed size
localFileHeader.setUint32(22, fileData.byteLength, true); // Uncompressed size
localFileHeader.setUint16(26, name.length, true); // Filename length

// Filename
fileHeader.set(new TextEncoder().encode(name), 30);

fileEntries.push(fileHeader, fileData);

// Central directory
const centralHeader = new Uint8Array(46 + name.length);
const centralView = new DataView(centralHeader.buffer);

centralView.setUint32(0, 0x02014b50, true); // "PK\1\2"
centralView.setUint16(4, 0x0, true); // Version made by
centralView.setUint16(6, 0x0, true); // Version needed to extract
centralView.setUint16(8, 0x0, true); // General purpose bit flag
centralView.setUint16(10, 0x0, true); // Compression method (none)
centralView.setUint16(12, 0x0, true); // File last mod time
centralView.setUint16(14, 0x0, true); // File last mod date
centralView.setUint32(16, 0, true); // CRC-32
centralView.setUint32(20, fileData.byteLength, true); // Compressed size
centralView.setUint32(24, fileData.byteLength, true); // Uncompressed size
centralView.setUint16(28, name.length, true); // Filename length
centralView.setUint32(42, offset, true); // Offset of local header

centralHeader.set(new TextEncoder().encode(name), 46);

centralDirectory.push(centralHeader);
offset += fileHeader.length + fileData.byteLength;
});

// End of central directory record
const eocd = new Uint8Array(22);
const eocdView = new DataView(eocd.buffer);

eocdView.setUint32(0, 0x06054b50, true); // "PK\5\6"
eocdView.setUint16(8, centralDirectory.length, true); // Total number of entries
eocdView.setUint16(10, centralDirectory.length, true); // Total number of entries
eocdView.setUint32(
12,
centralDirectory.reduce((sum, cd) => sum + cd.byteLength, 0),
true,
); // Size of central directory
eocdView.setUint32(16, offset, true); // Offset of start of central directory

return new Blob([...fileEntries, ...centralDirectory, eocd], {
type: 'application/zip',
});
};

0 comments on commit 3fae451

Please sign in to comment.