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 02c8dee
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 0 deletions.
112 changes: 112 additions & 0 deletions packages/suite/src/actions/suite/metadataActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 19 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 +166,114 @@ export const encryptAndSaveMetadata = async ({

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

type Input = { name: string; content: ArrayBuffer };

const createZip = (buffers: Input[]) => {
const fileEntries: any[] = [];
let centralDirectory: any[] = [];

Check failure on line 174 in packages/suite/src/actions/suite/metadataActions.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.length, 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',
});
};

export const exportMetadataToLocalFile = () => async (dispatch: Dispatch, getState: GetState) => {

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

View workflow job for this annotation

GitHub Actions / Linting and formatting

Async arrow function has no 'await' expression
const providerInstance = dispatch(
getProviderInstance({
clientId: selectSelectedProviderForLabels(getState())!.clientId,
dataType: 'labels',
}),
);

if (!providerInstance) return;

providerInstance.getFilesList().then(result => {
if (!result.success || !result.payload?.length) return;
const files = result.payload;

Promise.all(
files.map(file => {
return providerInstance.getFileContent(file).then(result2 => {
if (!result2.success || !result2.payload) return;
return { name: file, content: result2.payload };

Check failure on line 259 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(buffers => {
const meow = buffers.filter(Boolean) as Input[];
console.log('buffers', meow);

const zipBlob = createZip(meow);

// 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);
});
});
};
22 changes: 22 additions & 0 deletions packages/suite/src/views/settings/SettingsDebug/Metadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 onClick = () => {
dispatch(exportMetadataToLocalFile());
};

return (
<SectionItem data-testid="@settings/debug/metadata">
<TextColumn title="Export" description="Export metadata to local files" />
<ActionColumn>
<Button onClick={onClick}>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>
);
};

0 comments on commit 02c8dee

Please sign in to comment.