Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

metadata export #15421

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 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 @@ -18,6 +19,8 @@ import * as metadataUtils from 'src/utils/suite/metadata';
import { selectSelectedProviderForLabels } from 'src/reducers/suite/metadataReducer';
import type { AbstractMetadataProvider, PasswordManagerState } from 'src/types/suite/metadata';

import { getProviderInstance } from './metadataProviderActions';

export type MetadataAction =
| { type: typeof METADATA.ENABLE }
| { type: typeof METADATA.DISABLE }
Expand Down Expand Up @@ -165,3 +168,46 @@ 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 };
});
}),
)
.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
});
};
33 changes: 33 additions & 0 deletions packages/suite/src/views/settings/SettingsDebug/Metadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from 'react';

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[] = [];
const centralDirectory: ArrayBuffer[] = [];
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',
});
};
Loading