Upload validation (#364)
This commit is contained in:
parent
0c307b2e4d
commit
bd56b26a5d
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(), []);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
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;
|
||||
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;
|
||||
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) => {
|
||||
if (customIniFile) {
|
||||
removeFile(customIniFile![file.uid]);
|
||||
}
|
||||
setCustomIniFile(null);
|
||||
updateDbData(newTuneId!, { customIniFile: null });
|
||||
};
|
||||
|
@ -415,8 +480,9 @@ const UploadPage = () => {
|
|||
const shareSection = (
|
||||
<>
|
||||
<Divider>Publish & Share</Divider>
|
||||
<Row>
|
||||
<Input
|
||||
style={{ width: `calc(100% - ${hasNavigatorShare ? 160 : 128}px)` }}
|
||||
style={{ width: `calc(100% - ${hasNavigatorShare ? 65 : 35}px)` }}
|
||||
value={shareUrl!}
|
||||
/>
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy URL'}>
|
||||
|
@ -430,14 +496,17 @@ const UploadPage = () => {
|
|||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Row>
|
||||
<Row style={{ marginTop: 10 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ float: 'right' }}
|
||||
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>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface ParserInterface {
|
||||
parse(): this;
|
||||
}
|
||||
|
||||
export interface ParserConstructor {
|
||||
new(buffer: ArrayBuffer): ParserInterface;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { StorageInterface } from '../storageInterface';
|
||||
import { StorageInterface } from '../StorageInterface';
|
||||
|
||||
class BrowserStorage implements StorageInterface {
|
||||
private storage: Storage;
|
Loading…
Reference in New Issue