Upload validation (#364)

This commit is contained in:
Piotr Rogowski 2022-01-08 21:51:09 +01:00 committed by GitHub
parent 0c307b2e4d
commit bd56b26a5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 271 additions and 54 deletions

63
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@reduxjs/toolkit": "^1.7.1",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@speedy-tuner/ini": "^0.2.2",
"@speedy-tuner/types": "^0.2.1",
"antd": "^4.18.2",
"easymde": "^2.15.0",
@ -3907,6 +3908,33 @@
"eslint-plugin-prettier": "^4.0.0"
}
},
"node_modules/@speedy-tuner/ini": {
"version": "0.2.2",
"resolved": "https://npm.pkg.github.com/download/@speedy-tuner/ini/0.2.2/38755d6ecbf8f478233f2a40251989192659620e0195f62eb6f4a79b920b9a0e",
"integrity": "sha512-sktFLjNF7oZa9haK+a71Ebc6csJ5uC/Huba879WiANydWRF7iIPdFyL6sfp8gOfoOj+I/jhg1MB+1z972hKdWw==",
"license": "MIT",
"dependencies": {
"@speedy-tuner/types": "^0.2.1",
"js-yaml": "^4.1.0",
"parsimmon": "^1.18.1"
}
},
"node_modules/@speedy-tuner/ini/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/@speedy-tuner/ini/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@speedy-tuner/types": {
"version": "0.2.1",
"resolved": "https://npm.pkg.github.com/download/@speedy-tuner/types/0.2.1/378faaf77fc78a8b33a9f09b9a742b272cc01a9349fe6fe61081e202d953210b",
@ -15733,6 +15761,11 @@
"node": ">= 0.8"
}
},
"node_modules/parsimmon": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
},
"node_modules/pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
@ -27125,6 +27158,31 @@
"eslint-plugin-prettier": "^4.0.0"
}
},
"@speedy-tuner/ini": {
"version": "0.2.2",
"resolved": "https://npm.pkg.github.com/download/@speedy-tuner/ini/0.2.2/38755d6ecbf8f478233f2a40251989192659620e0195f62eb6f4a79b920b9a0e",
"integrity": "sha512-sktFLjNF7oZa9haK+a71Ebc6csJ5uC/Huba879WiANydWRF7iIPdFyL6sfp8gOfoOj+I/jhg1MB+1z972hKdWw==",
"requires": {
"@speedy-tuner/types": "^0.2.1",
"js-yaml": "^4.1.0",
"parsimmon": "^1.18.1"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^2.0.1"
}
}
}
},
"@speedy-tuner/types": {
"version": "0.2.1",
"resolved": "https://npm.pkg.github.com/download/@speedy-tuner/types/0.2.1/378faaf77fc78a8b33a9f09b9a742b272cc01a9349fe6fe61081e202d953210b",
@ -36304,6 +36362,11 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"parsimmon": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz",
"integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="
},
"pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",

View File

@ -34,6 +34,7 @@
"@reduxjs/toolkit": "^1.7.1",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@speedy-tuner/ini": "^0.2.2",
"@speedy-tuner/types": "^0.2.1",
"antd": "^4.18.2",
"easymde": "^2.15.0",

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react';
import Storage from '../utils/storage';
import Storage from '../utils/Storage';
const useStorage = () => {
const storage = useMemo(() => new Storage(), []);

View File

@ -108,9 +108,12 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
setFileSize(formatBytes(compositeRaw.byteLength));
setStep(1);
const parser = new TriggerLogsParser();
const resultComposite = parser.parse(pako.inflate(new Uint8Array(compositeRaw))).getCompositeLogs();
const resultTooth = parser.parse(pako.inflate(new Uint8Array(toothRaw))).getToothLogs();
const resultComposite = (new TriggerLogsParser(pako.inflate(new Uint8Array(compositeRaw))))
.parse()
.getCompositeLogs();
const resultTooth = (new TriggerLogsParser(pako.inflate(new Uint8Array(toothRaw))))
.parse()
.getToothLogs();
setLogs(resultComposite);
setToothLogs(resultTooth);

View File

@ -9,6 +9,7 @@ import {
Divider,
Input,
notification,
Row,
Skeleton,
Space,
Switch,
@ -25,6 +26,7 @@ import {
ShareAltOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import { INI } from '@speedy-tuner/ini';
import { UploadRequestOption } from 'rc-upload/lib/interface';
import { UploadFile } from 'antd/lib/upload/interface';
import { useHistory } from 'react-router-dom';
@ -55,6 +57,7 @@ import TuneParser from '../utils/tune/TuneParser';
import 'easymde/dist/easymde.min.css';
import TriggerLogsParser from '../utils/logs/TriggerLogsParser';
import LogParser from '../utils/logs/LogParser';
enum MaxFiles {
TUNE_FILES = 1,
@ -122,7 +125,7 @@ const UploadPage = () => {
const [isPublic, setIsPublic] = useState(true);
const [isListed, setIsListed] = useState(true);
const [description, setDescription] = useState('# My Tune \ndescription');
const [tuneFile, setTuneFile] = useState<UploadedFile | null>(null);
const [tuneFile, setTuneFile] = useState<UploadedFile | null | false>(null);
const [logFiles, setLogFiles] = useState<UploadedFile>({});
const [toothLogFiles, setToothLogFiles] = useState<UploadedFile>({});
const [customIniFile, setCustomIniFile] = useState<UploadedFile | null>(null);
@ -166,12 +169,10 @@ const UploadPage = () => {
}
};
const removeFile = (path: string) => {
const removeFile = async (path: string) => {
try {
return deleteObject(storageRef(storage, path));
return await deleteObject(storageRef(storage, path));
} catch (error) {
console.error(error);
genericError(error as Error);
return Promise.reject(error);
}
};
@ -278,17 +279,24 @@ const UploadPage = () => {
const { path } = (options.data as unknown as UploadFileData);
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
setTuneFile(tune);
upload(path, options, () => {
updateDbData(newTuneId!, { tuneFile: path });
}, async (file) => {
const { result, message } = await validateSize(file);
if (!result) {
setTuneFile(false);
return { result, message };
}
const valid = (new TuneParser()).parse(await file.arrayBuffer()).isValid();
if (!valid) {
setTuneFile(false);
} else {
setTuneFile(tune);
}
return {
result: (new TuneParser()).parse(await file.arrayBuffer()).isValid(),
result: valid,
message: 'Tune file is not valid!',
};
});
@ -297,14 +305,42 @@ const UploadPage = () => {
const uploadLogs = async (options: UploadRequestOption) => {
const { path } = (options.data as unknown as UploadFileData);
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
const uuid = (options.file as UploadFile).uid;
tune[uuid] = path;
const newValues = { ...logFiles, ...tune };
setLogFiles(newValues);
upload(path, options, () => {
updateDbData(newTuneId!, { logFiles: Object.values(newValues) });
}, async (file) => {
const { result, message } = await validateSize(file);
return { result, message };
if (!result) {
return { result, message };
}
let valid = true;
const extension = file.name.split('.').pop();
const parser = new LogParser(await file.arrayBuffer());
switch (extension) {
case 'mlg':
valid = parser.isMLG();
break;
case 'msl':
case 'csv':
valid = parser.isMSL();
break;
default:
valid = false;
break;
}
if (valid) {
setLogFiles(newValues);
}
return {
result: valid,
message: 'Log file is empty or not valid!',
};
});
};
@ -313,7 +349,6 @@ const UploadPage = () => {
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
const newValues = { ...toothLogFiles, ...tune };
setToothLogFiles(newValues);
upload(path, options, () => {
updateDbData(newTuneId!, { toothLogFiles: Object.values(newValues) });
}, async (file) => {
@ -322,10 +357,15 @@ const UploadPage = () => {
return { result, message };
}
const parser = (new TriggerLogsParser()).parse(await file.arrayBuffer());
const parser = new TriggerLogsParser(await file.arrayBuffer());
const valid = parser.isComposite() || parser.isTooth();
if (valid) {
setToothLogFiles(newValues);
}
return {
result: parser.isComposite() || parser.isTooth(),
result: valid,
message: 'Tooth logs file is empty or not valid!',
};
});
@ -335,21 +375,42 @@ const UploadPage = () => {
const { path } = (options.data as unknown as UploadFileData);
const tune: UploadedFile = {};
tune[(options.file as UploadFile).uid] = path;
setCustomIniFile(tune);
upload(path, options, () => {
updateDbData(newTuneId!, { customIniFile: path });
}, () => Promise.resolve({ result: true, message: '' }));
}, async (file) => {
const { result, message } = await validateSize(file);
if (!result) {
return { result, message };
}
// TODO: change to common interface, add some validation method
const parser = new INI((new TextDecoder()).decode(await file.arrayBuffer()));
const valid = parser.parse().megaTune.signature.length > 0;
if (valid) {
setCustomIniFile(tune);
}
return {
result: valid,
message: 'INI file is empty or not valid!',
};
});
};
const removeTuneFile = async (file: UploadFile) => {
if (tuneFile) {
removeFile(tuneFile[file.uid]);
}
setTuneFile(null);
removeFile(tuneFile![file.uid]);
updateDbData(newTuneId!, { tuneFile: null });
};
const removeLogFile = async (file: UploadFile) => {
const { uid } = file;
removeFile(logFiles[file.uid]);
if (logFiles[file.uid]) {
removeFile(logFiles[file.uid]);
}
const newValues = { ...logFiles };
delete newValues[uid];
setLogFiles(newValues);
@ -358,7 +419,9 @@ const UploadPage = () => {
const removeToothLogFile = async (file: UploadFile) => {
const { uid } = file;
removeFile(toothLogFiles[file.uid]);
if (toothLogFiles[file.uid]) {
removeFile(toothLogFiles[file.uid]);
}
const newValues = { ...toothLogFiles };
delete newValues[uid];
setToothLogFiles(newValues);
@ -366,7 +429,9 @@ const UploadPage = () => {
};
const removeCustomIniFile = async (file: UploadFile) => {
removeFile(customIniFile![file.uid]);
if (customIniFile) {
removeFile(customIniFile![file.uid]);
}
setCustomIniFile(null);
updateDbData(newTuneId!, { customIniFile: null });
};
@ -415,29 +480,33 @@ const UploadPage = () => {
const shareSection = (
<>
<Divider>Publish & Share</Divider>
<Input
style={{ width: `calc(100% - ${hasNavigatorShare ? 160 : 128}px)` }}
value={shareUrl!}
/>
<Tooltip title={copied ? 'Copied!' : 'Copy URL'}>
<Button icon={<CopyOutlined />} onClick={copyToClipboard} />
</Tooltip>
{hasNavigatorShare && (
<Tooltip title="Share">
<Button
icon={<ShareAltOutlined />}
onClick={() => navigator.share({ url: shareUrl! })}
/>
<Row>
<Input
style={{ width: `calc(100% - ${hasNavigatorShare ? 65 : 35}px)` }}
value={shareUrl!}
/>
<Tooltip title={copied ? 'Copied!' : 'Copy URL'}>
<Button icon={<CopyOutlined />} onClick={copyToClipboard} />
</Tooltip>
)}
<Button
type="primary"
style={{ float: 'right' }}
disabled={isPublished || isLoading}
onClick={publish}
>
{isPublished && !isLoading ? 'Published' : 'Publish'}
</Button>
{hasNavigatorShare && (
<Tooltip title="Share">
<Button
icon={<ShareAltOutlined />}
onClick={() => navigator.share({ url: shareUrl! })}
/>
</Tooltip>
)}
</Row>
<Row style={{ marginTop: 10 }}>
<Button
type="primary"
block
disabled={isPublished || isLoading}
onClick={publish}
>
{isPublished && !isLoading ? 'Published' : 'Publish'}
</Button>
</Row>
</>
);
@ -562,7 +631,7 @@ const UploadPage = () => {
disabled={isPublished}
accept=".msq"
>
{!tuneFile && uploadButton}
{tuneFile === null && uploadButton}
</Upload>
{tuneFile && optionalSection}
</div>

View File

@ -0,0 +1,7 @@
export interface ParserInterface {
parse(): this;
}
export interface ParserConstructor {
new(buffer: ArrayBuffer): ParserInterface;
}

View File

@ -1,5 +1,5 @@
import BrowserStorage from './storage/browserStorage';
import { StorageInterface } from './storageInterface';
import BrowserStorage from './storage/BrowserStorage';
import { StorageInterface } from './StorageInterface';
class Storage {
private storage: StorageInterface;

View File

@ -0,0 +1,56 @@
import { ParserInterface } from '../ParserInterface';
class LogParser implements ParserInterface {
private MLG_FORMAT_LENGTH = 6;
private isMLGLogs: boolean = false;
private isMSLLogs: boolean = false;
private buffer: ArrayBuffer = new ArrayBuffer(0);
private raw: string = '';
constructor(buffer: ArrayBuffer) {
this.buffer = buffer;
this.raw = (new TextDecoder()).decode(buffer);
this.checkMLG();
this.checkMSL();
}
parse(): this {
return this;
}
isMLG(): boolean {
return this.isMLGLogs;
}
isMSL(): boolean {
return this.isMSLLogs;
}
private checkMLG() {
const fileFormat = new TextDecoder('utf8')
.decode(this.buffer.slice(0, this.MLG_FORMAT_LENGTH))
// eslint-disable-next-line no-control-regex
.replace(/\x00/gu, '');
if (fileFormat === 'MLVLG') {
this.isMLGLogs = true;
}
}
private checkMSL() {
const lines = this.raw.split('\n');
for (let index = 0; index < lines.length; index++) {
if (lines[index].startsWith('Time')) {
this.isMSLLogs = true;
break;
}
}
}
}
export default LogParser;

View File

@ -1,3 +1,4 @@
import { ParserInterface } from '../ParserInterface';
import { isNumber } from '../tune/expression';
export enum EntryType {
@ -23,7 +24,7 @@ export interface ToothLogEntry {
time: number;
}
class TriggerLogsParser {
class TriggerLogsParser implements ParserInterface {
private COMMENT_PREFIX = '#';
private MARKER_PREFIX = 'MARK';
@ -36,10 +37,17 @@ class TriggerLogsParser {
private resultTooth: ToothLogEntry[] = [];
parse(buffer: ArrayBuffer): TriggerLogsParser {
const raw = (new TextDecoder()).decode(buffer);
this.parseCompositeLogs(raw);
this.parseToothLogs(raw);
private alreadyParsed: boolean = false;
private raw: string = '';
constructor(buffer: ArrayBuffer) {
this.raw = (new TextDecoder()).decode(buffer);
}
parse(): this {
this.parseCompositeLogs(this.raw);
this.parseToothLogs(this.raw);
if (this.resultComposite.length > 0) {
this.isCompositeLogs = true;
@ -49,6 +57,8 @@ class TriggerLogsParser {
this.isToothLogs = true;
}
this.alreadyParsed = true;
return this;
}
@ -61,10 +71,18 @@ class TriggerLogsParser {
}
isTooth(): boolean {
if (!this.alreadyParsed) {
this.parse();
}
return this.isToothLogs;
}
isComposite(): boolean {
if (!this.alreadyParsed) {
this.parse();
}
return this.isCompositeLogs;
}

View File

@ -1,4 +1,4 @@
import { StorageInterface } from '../storageInterface';
import { StorageInterface } from '../StorageInterface';
class BrowserStorage implements StorageInterface {
private storage: Storage;