Addresses review requests
- Brings 1pux-to-csv important types and functions inline - Doesn't try to parse 1pux file unless it matches the extension - Move reading of file to a bit later - Improves "add dependency" command - Adds "remove dependency" command
This commit is contained in:
parent
feb7a3ce53
commit
db18adf7fd
|
@ -61,6 +61,8 @@ For more configuration options, see [Configuration](#configuration)
|
||||||
| `npm run dev` | Starts backend server and client app in dev mode, which watches for changes in the source files and automatically rebuilds/restarts the corresponding components. |
|
| `npm run dev` | Starts backend server and client app in dev mode, which watches for changes in the source files and automatically rebuilds/restarts the corresponding components. |
|
||||||
| `npm test` | Run tests. |
|
| `npm test` | Run tests. |
|
||||||
|
|
||||||
|
To add dependencies, you can use `scope=[scope-without-@padloc/] npm run add [package]` and to remove them, run `scope=[scope-without-@padloc/] npm run remove [package]`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
| Environment Variable | Default | Description |
|
| Environment Variable | Default | Description |
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"repl": "cd packages/server && npm run repl && cd ../..",
|
"repl": "cd packages/server && npm run repl && cd ../..",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"locale:extract": "lerna run extract --scope '@padloc/locale'",
|
"locale:extract": "lerna run extract --scope '@padloc/locale'",
|
||||||
"add": "lerna add"
|
"add": "lerna add $1 --scope=@padloc/$scope",
|
||||||
|
"remove": "rm packages/$scope/package-lock.json && lerna exec \"npm uninstall $1\" --scope=@padloc/$scope"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,7 +20,6 @@
|
||||||
"npm": "8.2.0"
|
"npm": "8.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"1pux-to-csv": "1.1.0",
|
|
||||||
"@padloc/core": "4.0.0",
|
"@padloc/core": "4.0.0",
|
||||||
"@padloc/locale": "4.0.0",
|
"@padloc/locale": "4.0.0",
|
||||||
"@simplewebauthn/browser": "4.0.0",
|
"@simplewebauthn/browser": "4.0.0",
|
||||||
|
@ -41,6 +40,7 @@
|
||||||
"event-target-shim": "6.0.2",
|
"event-target-shim": "6.0.2",
|
||||||
"http-server": "0.12.3",
|
"http-server": "0.12.3",
|
||||||
"jsqr": "1.4.0",
|
"jsqr": "1.4.0",
|
||||||
|
"jszip": "3.7.1",
|
||||||
"lit": "2.0.0-rc.2",
|
"lit": "2.0.0-rc.2",
|
||||||
"localforage": "1.9.0",
|
"localforage": "1.9.0",
|
||||||
"marked": "3.0.4",
|
"marked": "3.0.4",
|
||||||
|
|
|
@ -13,9 +13,9 @@ import { saveFile } from "@padloc/core/src/platform";
|
||||||
import { stringToBytes } from "@padloc/core/src/encoding";
|
import { stringToBytes } from "@padloc/core/src/encoding";
|
||||||
|
|
||||||
@customElement("pl-import-dialog")
|
@customElement("pl-import-dialog")
|
||||||
export class ImportDialog extends Dialog<string | Uint8Array, void> {
|
export class ImportDialog extends Dialog<File, void> {
|
||||||
@state()
|
@state()
|
||||||
private _rawData: string | Uint8Array = "";
|
private _rawData: null | string | ArrayBuffer = "";
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _items: VaultItem[] = [];
|
private _items: VaultItem[] = [];
|
||||||
|
@ -35,7 +35,7 @@ export class ImportDialog extends Dialog<string | Uint8Array, void> {
|
||||||
id="formatSelect"
|
id="formatSelect"
|
||||||
.options=${imp.supportedFormats}
|
.options=${imp.supportedFormats}
|
||||||
.label=${$l("Format")}
|
.label=${$l("Format")}
|
||||||
@change=${this._parseString}
|
@change=${this._parseData}
|
||||||
disabled
|
disabled
|
||||||
></pl-select>
|
></pl-select>
|
||||||
|
|
||||||
|
@ -67,13 +67,24 @@ export class ImportDialog extends Dialog<string | Uint8Array, void> {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async show(input: string | Uint8Array) {
|
async show(file: File) {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
const result = super.show();
|
const result = super.show();
|
||||||
this._rawData = input;
|
|
||||||
this._formatSelect.value = ((await imp.guessFormat(input)) || imp.CSV).value;
|
const reader = new FileReader();
|
||||||
this._parseString();
|
reader.onload = async () => {
|
||||||
this._vaultSelect.value = app.mainVault!;
|
this._rawData = reader.result;
|
||||||
|
this._formatSelect.value = (imp.guessFormat(file, this._rawData) || imp.CSV).value;
|
||||||
|
this._parseData();
|
||||||
|
this._vaultSelect.value = app.mainVault!;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (imp.doesFileRequireReadingAsBinary(file)) {
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
} else {
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +99,7 @@ Github,"work,coding",https://github.com,john.doe@gmail.com,129lskdf93`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _parseString(): Promise<void> {
|
private async _parseData(): Promise<void> {
|
||||||
const rawStr = this._rawData;
|
const rawStr = this._rawData;
|
||||||
|
|
||||||
switch (this._formatSelect.value) {
|
switch (this._formatSelect.value) {
|
||||||
|
|
|
@ -27,17 +27,8 @@ export class SettingsTools extends StateMixin(LitElement) {
|
||||||
|
|
||||||
private async _importFile() {
|
private async _importFile() {
|
||||||
const file = this._fileInput.files![0];
|
const file = this._fileInput.files![0];
|
||||||
const reader = new FileReader();
|
await this._importDialog.show(file);
|
||||||
reader.onload = async () => {
|
this._fileInput.value = "";
|
||||||
await this._importDialog.show(reader.result as string | Uint8Array);
|
|
||||||
this._fileInput.value = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
if (file.name.endsWith('.1pux')) {
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
} else {
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _export() {
|
private _export() {
|
||||||
|
|
|
@ -0,0 +1,308 @@
|
||||||
|
import { loadAsync } from 'jszip';
|
||||||
|
|
||||||
|
export type OnePuxItemDetailsLoginField = {
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
fieldType: 'A' | 'B' | 'C' | 'E' | 'I' | 'N' | 'P' | 'R' | 'S' | 'T' | 'U';
|
||||||
|
designation?: 'username' | 'password';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxItemDetailsSection = {
|
||||||
|
title: string;
|
||||||
|
name: string;
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
value: {
|
||||||
|
concealed?: string;
|
||||||
|
reference?: string;
|
||||||
|
string?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
url?: string;
|
||||||
|
totp?: string;
|
||||||
|
gender?: string;
|
||||||
|
creditCardType?: string;
|
||||||
|
creditCardNumber?: string;
|
||||||
|
monthYear?: number;
|
||||||
|
date?: number;
|
||||||
|
};
|
||||||
|
indexAtSource: number;
|
||||||
|
guarded: boolean;
|
||||||
|
multiline: boolean;
|
||||||
|
dontGenerate: boolean;
|
||||||
|
inputTraits: {
|
||||||
|
keyboard: string;
|
||||||
|
correction: string;
|
||||||
|
capitalization: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxItemDetailsPasswordHistory = {
|
||||||
|
value: string;
|
||||||
|
time: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxItemOverviewUrl = {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxItem = {
|
||||||
|
item?: {
|
||||||
|
uuid: string;
|
||||||
|
favIndex: number;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
trashed: boolean;
|
||||||
|
categoryUuid: string;
|
||||||
|
details: {
|
||||||
|
loginFields: OnePuxItemDetailsLoginField[];
|
||||||
|
notesPlain?: string;
|
||||||
|
sections: OnePuxItemDetailsSection[];
|
||||||
|
passwordHistory: OnePuxItemDetailsPasswordHistory[];
|
||||||
|
documentAttributes?: {
|
||||||
|
fileName: string;
|
||||||
|
documentId: string;
|
||||||
|
decryptedSize: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
overview: {
|
||||||
|
subtitle: string;
|
||||||
|
urls?: OnePuxItemOverviewUrl[];
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
ps?: number;
|
||||||
|
pbe?: number;
|
||||||
|
pgrng?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
file?: {
|
||||||
|
attrs: {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxVault = {
|
||||||
|
attrs: {
|
||||||
|
uuid: string;
|
||||||
|
desc: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
type: 'P' | 'E' | 'U';
|
||||||
|
};
|
||||||
|
items: OnePuxItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxAccount = {
|
||||||
|
attrs: {
|
||||||
|
accountName: string;
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
email: string;
|
||||||
|
uuid: string;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
vaults: OnePuxVault[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxData = {
|
||||||
|
accounts: OnePuxAccount[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxAttributes = {
|
||||||
|
version: number;
|
||||||
|
description: string;
|
||||||
|
createdAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnePuxExport = {
|
||||||
|
attributes: OnePuxAttributes;
|
||||||
|
data: OnePuxData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parse1PuxFile = async (
|
||||||
|
fileContents: string | ArrayBuffer,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const zip = await loadAsync(fileContents);
|
||||||
|
|
||||||
|
const attributesContent = await zip
|
||||||
|
.file('export.attributes')!
|
||||||
|
.async('string');
|
||||||
|
const attributes = JSON.parse(attributesContent);
|
||||||
|
const dataContent = await zip.file('export.data')!.async('string');
|
||||||
|
const data = JSON.parse(dataContent);
|
||||||
|
|
||||||
|
return {
|
||||||
|
attributes,
|
||||||
|
data,
|
||||||
|
} as OnePuxExport;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse .1pux file');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowData = {
|
||||||
|
name: string;
|
||||||
|
tags: string;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
notes: string;
|
||||||
|
extraFields: ExtraField[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExtraFieldType =
|
||||||
|
| 'username'
|
||||||
|
| 'password'
|
||||||
|
| 'url'
|
||||||
|
| 'email'
|
||||||
|
| 'date'
|
||||||
|
| 'month'
|
||||||
|
| 'credit'
|
||||||
|
| 'phone'
|
||||||
|
| 'totp'
|
||||||
|
| 'text';
|
||||||
|
|
||||||
|
type ExtraField = { name: string; value: string; type: ExtraFieldType };
|
||||||
|
|
||||||
|
type ParseFieldTypeToExtraFieldType = (
|
||||||
|
field: OnePuxItemDetailsLoginField,
|
||||||
|
) => ExtraFieldType;
|
||||||
|
|
||||||
|
const parseFieldTypeToExtraFieldType: ParseFieldTypeToExtraFieldType = (
|
||||||
|
field,
|
||||||
|
) => {
|
||||||
|
if (field.designation === 'username') {
|
||||||
|
return 'username';
|
||||||
|
} else if (field.designation === 'password') {
|
||||||
|
return 'password';
|
||||||
|
} else if (field.fieldType === 'E') {
|
||||||
|
return 'email';
|
||||||
|
} else if (field.fieldType === 'U') {
|
||||||
|
return 'url';
|
||||||
|
}
|
||||||
|
return 'text';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseToRowData = (
|
||||||
|
item: OnePuxItem['item'],
|
||||||
|
defaultTags?: string[],
|
||||||
|
) => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowData: RowData = {
|
||||||
|
name: item.overview.title,
|
||||||
|
tags: [...(defaultTags || []), ...(item.overview.tags || [])].join(','),
|
||||||
|
url: item.overview.url || '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
notes: item.details.notesPlain || '',
|
||||||
|
extraFields: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip documents
|
||||||
|
if (
|
||||||
|
item.details.documentAttributes &&
|
||||||
|
item.details.loginFields.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract username, password, and some extraFields
|
||||||
|
item.details.loginFields.forEach((field) => {
|
||||||
|
if (field.designation === 'username') {
|
||||||
|
rowData.username = field.value;
|
||||||
|
} else if (field.designation === 'password') {
|
||||||
|
rowData.password = field.value;
|
||||||
|
} else if (
|
||||||
|
field.fieldType === 'I' ||
|
||||||
|
field.fieldType === 'C' ||
|
||||||
|
field.id.includes(';opid=__') ||
|
||||||
|
field.value === ''
|
||||||
|
) {
|
||||||
|
// Skip these noisy form-fields
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
rowData.extraFields.push({
|
||||||
|
name: field.name || field.id,
|
||||||
|
value: field.value,
|
||||||
|
type: parseFieldTypeToExtraFieldType(field),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract some more extraFields
|
||||||
|
item.details.sections.forEach((section) => {
|
||||||
|
section.fields.forEach((field) => {
|
||||||
|
let value = '';
|
||||||
|
let type: ExtraFieldType = 'text';
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(field.value, 'concealed')) {
|
||||||
|
value = field.value.concealed || '';
|
||||||
|
} else if (
|
||||||
|
Object.prototype.hasOwnProperty.call(field.value, 'reference')
|
||||||
|
) {
|
||||||
|
value = field.value.reference || '';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'string')) {
|
||||||
|
value = field.value.string || '';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'email')) {
|
||||||
|
value = field.value.email || '';
|
||||||
|
type = 'email';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'phone')) {
|
||||||
|
value = field.value.phone || '';
|
||||||
|
type = 'phone';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'url')) {
|
||||||
|
value = field.value.url || '';
|
||||||
|
type = 'url';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'totp')) {
|
||||||
|
value = field.value.totp || '';
|
||||||
|
type = 'totp';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'gender')) {
|
||||||
|
value = field.value.gender || '';
|
||||||
|
} else if (
|
||||||
|
Object.prototype.hasOwnProperty.call(field.value, 'creditCardType')
|
||||||
|
) {
|
||||||
|
value = field.value.creditCardType || '';
|
||||||
|
} else if (
|
||||||
|
Object.prototype.hasOwnProperty.call(field.value, 'creditCardNumber')
|
||||||
|
) {
|
||||||
|
value = field.value.creditCardNumber || '';
|
||||||
|
type = 'credit';
|
||||||
|
} else if (
|
||||||
|
Object.prototype.hasOwnProperty.call(field.value, 'monthYear')
|
||||||
|
) {
|
||||||
|
value =
|
||||||
|
(field.value.monthYear && field.value.monthYear.toString()) || '';
|
||||||
|
type = 'month';
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(field.value, 'date')) {
|
||||||
|
value = (field.value.date && field.value.date.toString()) || '';
|
||||||
|
type = 'date';
|
||||||
|
} else {
|
||||||
|
// Default, so no data is lost when something new comes up
|
||||||
|
value = JSON.stringify(field.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData.extraFields.push({
|
||||||
|
name: field.title || field.id,
|
||||||
|
value,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return rowData;
|
||||||
|
};
|
|
@ -1,5 +1,3 @@
|
||||||
import { parse1PuxFile, parseToRowData } from "1pux-to-csv";
|
|
||||||
import { OnePuxItem } from "1pux-to-csv/types";
|
|
||||||
import { unmarshal, bytesToString } from "@padloc/core/src/encoding";
|
import { unmarshal, bytesToString } from "@padloc/core/src/encoding";
|
||||||
import { PBES2Container } from "@padloc/core/src/container";
|
import { PBES2Container } from "@padloc/core/src/container";
|
||||||
import { validateLegacyContainer, parseLegacyContainer } from "@padloc/core/src/legacy";
|
import { validateLegacyContainer, parseLegacyContainer } from "@padloc/core/src/legacy";
|
||||||
|
@ -8,6 +6,8 @@ import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||||
import { uuid } from "@padloc/core/src/util";
|
import { uuid } from "@padloc/core/src/util";
|
||||||
import { translate as $l } from "@padloc/locale/src/translate";
|
import { translate as $l } from "@padloc/locale/src/translate";
|
||||||
|
|
||||||
|
import { parse1PuxFile, parseToRowData, OnePuxItem } from "./1pux-parser";
|
||||||
|
|
||||||
export interface ImportFormat {
|
export interface ImportFormat {
|
||||||
value: "csv" | "padlock-legacy" | "lastpass" | "padloc" | "1pux";
|
value: "csv" | "padlock-legacy" | "lastpass" | "padloc" | "1pux";
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -298,8 +298,7 @@ async function parse1PuxItem(accountName: string, vaultName: string, item: OnePu
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore All of extraField.type possibilities match FieldType.*
|
fields.push(new Field({ name: extraField.name, value: extraField.value, type: extraField.type as FieldType }));
|
||||||
fields.push(new Field({ name: extraField.name, value: extraField.value, type: extraField.type }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -307,9 +306,13 @@ async function parse1PuxItem(accountName: string, vaultName: string, item: OnePu
|
||||||
return createVaultItem(itemName, fields, tags);
|
return createVaultItem(itemName, fields, tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function as1Pux(file: string | Uint8Array): Promise<VaultItem[]> {
|
export async function as1Pux(data: null | string | ArrayBuffer): Promise<VaultItem[]> {
|
||||||
|
if (!data) {
|
||||||
|
throw new Err(ErrorCode.INVALID_1PUX);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataExport = await parse1PuxFile(file);
|
const dataExport = await parse1PuxFile(data);
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
|
@ -333,19 +336,13 @@ export async function as1Pux(file: string | Uint8Array): Promise<VaultItem[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given string/Uint8Array represents a 1Password 1pux file
|
* Checks if a given file name ends with .1pux to avoid trying to parse unnecessarily
|
||||||
*/
|
*/
|
||||||
export async function is1Pux(file: string | Uint8Array): Promise<boolean> {
|
export function is1Pux(file: File): boolean {
|
||||||
try {
|
return file.name.endsWith('.1pux');
|
||||||
const dataExport = await parse1PuxFile(file);
|
|
||||||
return Boolean(dataExport.attributes && dataExport.data);
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function guessFormat(data: string | Uint8Array): Promise<ImportFormat | null> {
|
export function guessFormat(file: File, data: string | null | ArrayBuffer): ImportFormat {
|
||||||
if (isPBES2Container(data as string)) {
|
if (isPBES2Container(data as string)) {
|
||||||
return PBES2;
|
return PBES2;
|
||||||
}
|
}
|
||||||
|
@ -355,9 +352,17 @@ export async function guessFormat(data: string | Uint8Array): Promise<ImportForm
|
||||||
if (isLastPass(data as string)) {
|
if (isLastPass(data as string)) {
|
||||||
return LASTPASS;
|
return LASTPASS;
|
||||||
}
|
}
|
||||||
if (await is1Pux(data)) {
|
if (is1Pux(file)) {
|
||||||
return ONEPUX;
|
return ONEPUX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CSV;
|
return CSV;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doesFileRequireReadingAsBinary(file: File): boolean {
|
||||||
|
if (is1Pux(file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue