@isrd-isi-edu/ermrestjs 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/hatrac.js +2 -60
- package/js/utils/helpers.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/models/reference-column/asset-pseudo-column.ts +241 -19
- package/src/services/file-preview.ts +229 -0
- package/src/services/logger.ts +6 -7
- package/src/utils/constants.ts +5 -0
- package/src/utils/file-utils.ts +198 -0
package/js/hatrac.js
CHANGED
|
@@ -15,6 +15,7 @@ import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
|
|
|
15
15
|
import { hexToBase64 } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
16
16
|
import { isObject, isObjectAndNotNull } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
17
17
|
import { contextHeaderName, ENV_IS_NODE } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
18
|
+
import { getFilenameExtension } from '@isrd-isi-edu/ermrestjs/src/utils/file-utils';
|
|
18
19
|
|
|
19
20
|
// legacy
|
|
20
21
|
import { _validateTemplate, _renderTemplate, _getFormattedKeyValues, _parseUrl } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
@@ -138,65 +139,6 @@ const _generateContextHeader = function (contextHeaderParams) {
|
|
|
138
139
|
return headers;
|
|
139
140
|
};
|
|
140
141
|
|
|
141
|
-
/**
|
|
142
|
-
* given a filename, will return the extension
|
|
143
|
-
* By default, it will extract the last of the filename after the last `.`.
|
|
144
|
-
* The second parameter can be used for passing a regular expression
|
|
145
|
-
* if we want a different method of extracting the extension.
|
|
146
|
-
* @param {string} filename
|
|
147
|
-
* @param {string[]} allowedExtensions
|
|
148
|
-
* @param {string[]} regexArr
|
|
149
|
-
* @returns the filename extension string. if we cannot find any matches, it will return null
|
|
150
|
-
* @private
|
|
151
|
-
* @ignore
|
|
152
|
-
*/
|
|
153
|
-
const _getFilenameExtension = function (filename, allowedExtensions, regexArr) {
|
|
154
|
-
if (typeof filename !== 'string' || filename.length === 0) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// first find in the list of allowed extensions
|
|
159
|
-
var res = -1;
|
|
160
|
-
var isInAllowed =
|
|
161
|
-
Array.isArray(allowedExtensions) &&
|
|
162
|
-
allowedExtensions.some(function (ext) {
|
|
163
|
-
res = ext;
|
|
164
|
-
return typeof ext === 'string' && ext.length > 0 && filename.endsWith(ext);
|
|
165
|
-
});
|
|
166
|
-
if (isInAllowed) {
|
|
167
|
-
return res;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// we will return null if we cannot find anything
|
|
171
|
-
res = null;
|
|
172
|
-
// no matching allowed extension, try the regular expressions
|
|
173
|
-
if (Array.isArray(regexArr) && regexArr.length > 0) {
|
|
174
|
-
regexArr.some(function (regexp) {
|
|
175
|
-
// since regular expression comes from annotation, it might not be valid
|
|
176
|
-
try {
|
|
177
|
-
var matches = filename.match(new RegExp(regexp, 'g'));
|
|
178
|
-
if (matches && matches[0] && typeof matches[0] === 'string') {
|
|
179
|
-
res = matches[0];
|
|
180
|
-
} else {
|
|
181
|
-
res = null;
|
|
182
|
-
}
|
|
183
|
-
return res;
|
|
184
|
-
} catch {
|
|
185
|
-
res = null;
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
} else {
|
|
190
|
-
var dotIndex = filename.lastIndexOf('.');
|
|
191
|
-
// it's only a valid filename if there's some string after `.`
|
|
192
|
-
if (dotIndex !== -1 && dotIndex !== filename.length - 1) {
|
|
193
|
-
res = filename.slice(dotIndex);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return res;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
142
|
/**
|
|
201
143
|
* @desc upload Object
|
|
202
144
|
* Create a new instance with new upload(file, otherInfo)
|
|
@@ -801,7 +743,7 @@ Upload.prototype._generateURL = function (row, linkedData, templateVariables) {
|
|
|
801
743
|
row[this.column.name].md5_base64 = this.hash.md5_base64;
|
|
802
744
|
row[this.column.name].sha256 = this.hash.sha256;
|
|
803
745
|
row[this.column.name].filename = this.file.name;
|
|
804
|
-
var filename_ext =
|
|
746
|
+
var filename_ext = getFilenameExtension(this.file.name, this.column.filenameExtFilter, this.column.filenameExtRegexp);
|
|
805
747
|
row[this.column.name].filename_ext = filename_ext;
|
|
806
748
|
// filename_basename is everything from the file name except the last ext
|
|
807
749
|
// For example if we have a file named "file.tar.zip"
|
package/js/utils/helpers.js
CHANGED
|
@@ -404,7 +404,7 @@ import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
|
|
|
404
404
|
|
|
405
405
|
/**
|
|
406
406
|
* @param {string} context the context that we want the value of.
|
|
407
|
-
* @param {
|
|
407
|
+
* @param {any} annotation the annotation object.
|
|
408
408
|
* @param {Boolean=} dontUseDefaultContext Whether we should use the default (*) context
|
|
409
409
|
* @desc returns the annotation value based on the given context.
|
|
410
410
|
*/
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ import HandlebarsService from '@isrd-isi-edu/ermrestjs/src/services/handlebars';
|
|
|
77
77
|
import { Exporter } from '@isrd-isi-edu/ermrestjs/js/export';
|
|
78
78
|
import validateJSONLD from '@isrd-isi-edu/ermrestjs/js/json_ld_validator.js';
|
|
79
79
|
import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
|
|
80
|
+
import FilePreviewService from '@isrd-isi-edu/ermrestjs/src/services/file-preview';
|
|
80
81
|
|
|
81
82
|
const logError = ErrorService.logError;
|
|
82
83
|
const responseToError = ErrorService.responseToError;
|
|
@@ -111,6 +112,7 @@ export {
|
|
|
111
112
|
AuthnService,
|
|
112
113
|
Exporter,
|
|
113
114
|
HistoryService,
|
|
115
|
+
FilePreviewService,
|
|
114
116
|
|
|
115
117
|
// constants
|
|
116
118
|
contextHeaderName,
|
|
@@ -3,10 +3,14 @@ import { ReferenceColumn, ReferenceColumnTypes } from '@isrd-isi-edu/ermrestjs/s
|
|
|
3
3
|
import type SourceObjectWrapper from '@isrd-isi-edu/ermrestjs/src/models/source-object-wrapper';
|
|
4
4
|
import type { Reference, Tuple, VisibleColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference';
|
|
5
5
|
|
|
6
|
+
// services
|
|
7
|
+
import { FilePreviewTypes, isFilePreviewType, USE_EXT_MAPPING } from '@isrd-isi-edu/ermrestjs/src/services/file-preview';
|
|
8
|
+
|
|
6
9
|
// utils
|
|
7
10
|
import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
|
|
8
11
|
import { isDefinedAndNotNull, isObjectAndKeyExists, isObjectAndNotNull, isStringAndNotEmpty } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
9
12
|
import { _annotations, _contexts, _classNames } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
13
|
+
import { getFilename } from '@isrd-isi-edu/ermrestjs/src/utils/file-utils';
|
|
10
14
|
|
|
11
15
|
// legacy
|
|
12
16
|
import { _getAnnotationValueByContext, _isEntryContext, _renderTemplate, _isSameHost } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
@@ -70,7 +74,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
70
74
|
private _filenameExtFilter?: string[];
|
|
71
75
|
private _filenameExtRegexp?: string[];
|
|
72
76
|
private _displayImagePreview?: boolean;
|
|
73
|
-
private _filePreview?:
|
|
77
|
+
private _filePreview?: FilePreviewConfig | null;
|
|
74
78
|
|
|
75
79
|
constructor(reference: Reference, column: Column, sourceObjectWrapper?: SourceObjectWrapper, name?: string, mainTuple?: Tuple) {
|
|
76
80
|
// call the parent constructor
|
|
@@ -168,17 +172,9 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
168
172
|
|
|
169
173
|
// if we're using the url as caption
|
|
170
174
|
if (urlCaption) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (parts && parts.length === 4) {
|
|
175
|
-
caption = parts[2];
|
|
176
|
-
} else {
|
|
177
|
-
// otherwise return the last part of url
|
|
178
|
-
const newCaption = caption.split('/').pop();
|
|
179
|
-
if (newCaption && newCaption.length !== 0) {
|
|
180
|
-
caption = newCaption;
|
|
181
|
-
}
|
|
175
|
+
const newCaption = getFilename(caption);
|
|
176
|
+
if (newCaption && newCaption.length !== 0) {
|
|
177
|
+
caption = newCaption;
|
|
182
178
|
}
|
|
183
179
|
}
|
|
184
180
|
|
|
@@ -457,7 +453,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
457
453
|
/**
|
|
458
454
|
* whether we should show the file preview or not
|
|
459
455
|
*/
|
|
460
|
-
get filePreview():
|
|
456
|
+
get filePreview(): FilePreviewConfig | null {
|
|
461
457
|
if (this._filePreview === undefined) {
|
|
462
458
|
const disp = this._annotation.display;
|
|
463
459
|
const currDisplay = isObjectAndNotNull(disp) ? _getAnnotationValueByContext(this._context, disp) : null;
|
|
@@ -465,12 +461,7 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
465
461
|
if (settings === false) {
|
|
466
462
|
this._filePreview = null;
|
|
467
463
|
} else {
|
|
468
|
-
|
|
469
|
-
let showCsvHeader = false;
|
|
470
|
-
if (isObjectAndKeyExists(settings, 'show_csv_header') && typeof settings.show_csv_header === 'boolean') {
|
|
471
|
-
showCsvHeader = settings.show_csv_header;
|
|
472
|
-
}
|
|
473
|
-
this._filePreview = { showCsvHeader };
|
|
464
|
+
this._filePreview = new FilePreviewConfig(settings);
|
|
474
465
|
}
|
|
475
466
|
}
|
|
476
467
|
return this._filePreview;
|
|
@@ -496,3 +487,234 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
496
487
|
return this._waitFor;
|
|
497
488
|
}
|
|
498
489
|
}
|
|
490
|
+
|
|
491
|
+
class FilePreviewConfig {
|
|
492
|
+
private static previewTypes = Object.values(FilePreviewTypes);
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* whether we should show the CSV header or not
|
|
496
|
+
* (default: false)
|
|
497
|
+
*/
|
|
498
|
+
showCsvHeader: boolean = false;
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* the height of the preview container
|
|
502
|
+
*/
|
|
503
|
+
defaultHeight: number | null = null;
|
|
504
|
+
|
|
505
|
+
private _prefetchBytes: { [key: string]: number | null } = {
|
|
506
|
+
image: null,
|
|
507
|
+
markdown: null,
|
|
508
|
+
csv: null,
|
|
509
|
+
tsv: null,
|
|
510
|
+
json: null,
|
|
511
|
+
text: null,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
private _prefetchMaxFileSize: { [key: string]: number | null } = {
|
|
515
|
+
image: null,
|
|
516
|
+
markdown: null,
|
|
517
|
+
csv: null,
|
|
518
|
+
tsv: null,
|
|
519
|
+
json: null,
|
|
520
|
+
text: null,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
filenameExtMapping: { [key: string]: FilePreviewTypes | false } | null = null;
|
|
524
|
+
|
|
525
|
+
contentTypeMapping: {
|
|
526
|
+
exactMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } | null;
|
|
527
|
+
prefixMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } | null;
|
|
528
|
+
default: FilePreviewTypes | typeof USE_EXT_MAPPING | false | null;
|
|
529
|
+
} | null = null;
|
|
530
|
+
|
|
531
|
+
disabledTypes: FilePreviewTypes[] = [];
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* populate the props based on the given annotation object.
|
|
535
|
+
* The supported annotation properties are:
|
|
536
|
+
* - show_csv_header
|
|
537
|
+
* - default_height
|
|
538
|
+
* - prefetch_bytes
|
|
539
|
+
* - prefetch_max_file_size
|
|
540
|
+
* - filename_ext_mapping
|
|
541
|
+
* - content_type_mapping
|
|
542
|
+
* - disabled
|
|
543
|
+
*/
|
|
544
|
+
constructor(settings: any) {
|
|
545
|
+
if (isObjectAndKeyExists(settings, 'show_csv_header') && typeof settings.show_csv_header === 'boolean') {
|
|
546
|
+
this.showCsvHeader = settings.show_csv_header;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (isObjectAndKeyExists(settings, 'default_height') && typeof settings.default_height === 'number' && settings.default_height >= 0) {
|
|
550
|
+
this.defaultHeight = settings.default_height;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this._prefetchBytes = this._populateProps<number>(settings, 'prefetch_bytes', (value: unknown) => {
|
|
554
|
+
return typeof value === 'number' && value >= 0;
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this._prefetchMaxFileSize = this._populateProps<number>(settings, 'prefetch_max_file_size', (value: unknown) => {
|
|
558
|
+
return typeof value === 'number' && value >= 0;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
if (isObjectAndKeyExists(settings, 'filename_ext_mapping')) {
|
|
562
|
+
this.filenameExtMapping = {};
|
|
563
|
+
for (const [key, val] of Object.entries(settings.filename_ext_mapping)) {
|
|
564
|
+
if (val === false || (typeof val === 'string' && isFilePreviewType(val))) {
|
|
565
|
+
this.filenameExtMapping[key] = val;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (isObjectAndKeyExists(settings, 'content_type_mapping')) {
|
|
571
|
+
const exactMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {};
|
|
572
|
+
const prefixMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {};
|
|
573
|
+
let defaultMapping: FilePreviewTypes | typeof USE_EXT_MAPPING | false | null = null;
|
|
574
|
+
let hasExactMatch = false;
|
|
575
|
+
let hasPrefixMatch = false;
|
|
576
|
+
Object.keys(settings.content_type_mapping).forEach((key) => {
|
|
577
|
+
const val = settings.content_type_mapping[key];
|
|
578
|
+
const validValue = val === false || (typeof val === 'string' && (isFilePreviewType(val) || val === USE_EXT_MAPPING));
|
|
579
|
+
if (!validValue) return;
|
|
580
|
+
// * could be used for default mapping
|
|
581
|
+
if (key === '*') {
|
|
582
|
+
defaultMapping = val;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// only type/ or type/subtype are valid
|
|
587
|
+
const parts = key.split('/');
|
|
588
|
+
if (parts.length !== 2 || parts[0].length === 0) return;
|
|
589
|
+
|
|
590
|
+
if (parts[1].length > 0) {
|
|
591
|
+
exactMatch[key] = val;
|
|
592
|
+
hasExactMatch = true;
|
|
593
|
+
} else {
|
|
594
|
+
prefixMatch[parts[0] + '/'] = val;
|
|
595
|
+
hasPrefixMatch = true;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (hasPrefixMatch || hasExactMatch || defaultMapping !== null) {
|
|
600
|
+
this.contentTypeMapping = {
|
|
601
|
+
exactMatch: hasExactMatch ? exactMatch : null,
|
|
602
|
+
prefixMatch: hasPrefixMatch ? prefixMatch : null,
|
|
603
|
+
default: defaultMapping,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (isObjectAndKeyExists(settings, 'disabled') && Array.isArray(settings.disabled)) {
|
|
609
|
+
this.disabledTypes = settings.disabled.filter(
|
|
610
|
+
(t: unknown) => typeof t === 'string' && FilePreviewConfig.previewTypes.includes(t as FilePreviewTypes),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* return the number of bytes to prefetch for previewing the file
|
|
617
|
+
*/
|
|
618
|
+
getPrefetchBytes(filePreviewType: FilePreviewTypes | null): number | null {
|
|
619
|
+
switch (filePreviewType) {
|
|
620
|
+
case FilePreviewTypes.IMAGE:
|
|
621
|
+
return this._prefetchBytes.image;
|
|
622
|
+
case FilePreviewTypes.MARKDOWN:
|
|
623
|
+
return this._prefetchBytes.markdown;
|
|
624
|
+
case FilePreviewTypes.CSV:
|
|
625
|
+
return this._prefetchBytes.csv;
|
|
626
|
+
case FilePreviewTypes.TSV:
|
|
627
|
+
return this._prefetchBytes.tsv;
|
|
628
|
+
case FilePreviewTypes.JSON:
|
|
629
|
+
return this._prefetchBytes.json;
|
|
630
|
+
case FilePreviewTypes.TEXT:
|
|
631
|
+
return this._prefetchBytes.text;
|
|
632
|
+
default:
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* return the max file size for previewing the file
|
|
639
|
+
*/
|
|
640
|
+
getPrefetchMaxFileSize(filePreviewType: FilePreviewTypes | null): number | null {
|
|
641
|
+
switch (filePreviewType) {
|
|
642
|
+
case FilePreviewTypes.IMAGE:
|
|
643
|
+
return this._prefetchMaxFileSize.image;
|
|
644
|
+
case FilePreviewTypes.MARKDOWN:
|
|
645
|
+
return this._prefetchMaxFileSize.markdown;
|
|
646
|
+
case FilePreviewTypes.CSV:
|
|
647
|
+
return this._prefetchMaxFileSize.csv;
|
|
648
|
+
case FilePreviewTypes.TSV:
|
|
649
|
+
return this._prefetchMaxFileSize.tsv;
|
|
650
|
+
case FilePreviewTypes.JSON:
|
|
651
|
+
return this._prefetchMaxFileSize.json;
|
|
652
|
+
case FilePreviewTypes.TEXT:
|
|
653
|
+
return this._prefetchMaxFileSize.text;
|
|
654
|
+
default:
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The settings could be either just one value or an object with different values for each type.
|
|
661
|
+
* This function will populate the result object with the appropriate values for each type.
|
|
662
|
+
*/
|
|
663
|
+
private _populateProps<T>(settings: any, propName: string, validate?: (value: unknown) => boolean): { [key: string]: T | null } {
|
|
664
|
+
const res: { [key: string]: T | null } = {
|
|
665
|
+
text: null,
|
|
666
|
+
markdown: null,
|
|
667
|
+
csv: null,
|
|
668
|
+
tsv: null,
|
|
669
|
+
json: null,
|
|
670
|
+
image: null,
|
|
671
|
+
};
|
|
672
|
+
if (!isObjectAndKeyExists(settings, propName)) return res;
|
|
673
|
+
|
|
674
|
+
if (isObjectAndNotNull(settings[propName])) {
|
|
675
|
+
// for each preview type, try to get its value (which will fall back to * if not defined)
|
|
676
|
+
for (const key of FilePreviewConfig.previewTypes) {
|
|
677
|
+
const definedRes = this._getPropForType(key, settings[propName]);
|
|
678
|
+
if (!validate || validate(definedRes)) {
|
|
679
|
+
res[key] = definedRes;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
const definedRes = settings[propName];
|
|
684
|
+
if (!validate || validate(definedRes)) {
|
|
685
|
+
for (const key of FilePreviewConfig.previewTypes) {
|
|
686
|
+
res[key] = definedRes;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return res;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Get the property for a specific file type. If not defined, will try to get the default value (*).
|
|
696
|
+
* Otherwise returns null.
|
|
697
|
+
*/
|
|
698
|
+
private _getPropForType(fileType: string, settings: any): any {
|
|
699
|
+
const DEFAULT_TYPE = '*';
|
|
700
|
+
|
|
701
|
+
let isDefined = false;
|
|
702
|
+
let res;
|
|
703
|
+
if (isObjectAndKeyExists(settings, fileType)) {
|
|
704
|
+
if (typeof settings[fileType] === 'string' && settings[fileType] in FilePreviewConfig.previewTypes) {
|
|
705
|
+
res = this._getPropForType(settings[fileType], settings);
|
|
706
|
+
} else {
|
|
707
|
+
res = settings[fileType];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (res !== null && res !== undefined) isDefined = true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!isDefined && settings[DEFAULT_TYPE]) {
|
|
714
|
+
res = this._getPropForType(DEFAULT_TYPE, settings);
|
|
715
|
+
if (res !== null && res !== undefined) isDefined = true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return isDefined ? res : null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { AssetPseudoColumn } from '@isrd-isi-edu/ermrestjs/src/models/reference-column';
|
|
2
|
+
import { FILE_PREVIEW } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
3
|
+
|
|
4
|
+
import { getFilename, getFilenameExtension } from '@isrd-isi-edu/ermrestjs/src/utils/file-utils';
|
|
5
|
+
import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The supported file preview types
|
|
9
|
+
*/
|
|
10
|
+
export enum FilePreviewTypes {
|
|
11
|
+
IMAGE = 'image',
|
|
12
|
+
MARKDOWN = 'markdown',
|
|
13
|
+
CSV = 'csv',
|
|
14
|
+
TSV = 'tsv',
|
|
15
|
+
JSON = 'json',
|
|
16
|
+
TEXT = 'text',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type guard to check if a value is a FilePreviewTypes
|
|
21
|
+
*/
|
|
22
|
+
export const isFilePreviewType = (value: unknown): value is FilePreviewTypes => {
|
|
23
|
+
if (typeof value !== 'string') return false;
|
|
24
|
+
return Object.values(FilePreviewTypes).includes(value as FilePreviewTypes);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const USE_EXT_MAPPING = 'use_ext_mapping';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONTENT_TYPE_MAPPING: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {
|
|
30
|
+
// image:
|
|
31
|
+
'image/png': FilePreviewTypes.IMAGE,
|
|
32
|
+
'image/jpeg': FilePreviewTypes.IMAGE,
|
|
33
|
+
'image/jpg': FilePreviewTypes.IMAGE,
|
|
34
|
+
'image/gif': FilePreviewTypes.IMAGE,
|
|
35
|
+
'image/bmp': FilePreviewTypes.IMAGE,
|
|
36
|
+
'image/webp': FilePreviewTypes.IMAGE,
|
|
37
|
+
'image/svg+xml': FilePreviewTypes.IMAGE,
|
|
38
|
+
'image/x-icon': FilePreviewTypes.IMAGE,
|
|
39
|
+
'image/avif': FilePreviewTypes.IMAGE,
|
|
40
|
+
'image/apng': FilePreviewTypes.IMAGE,
|
|
41
|
+
// markdown:
|
|
42
|
+
'text/markdown': FilePreviewTypes.MARKDOWN,
|
|
43
|
+
// csv:
|
|
44
|
+
'text/csv': FilePreviewTypes.CSV,
|
|
45
|
+
// tsv:
|
|
46
|
+
'text/tab-separated-values': FilePreviewTypes.TSV,
|
|
47
|
+
// json:
|
|
48
|
+
'application/json': FilePreviewTypes.JSON,
|
|
49
|
+
// text:
|
|
50
|
+
'chemical/x-mmcif': FilePreviewTypes.TEXT,
|
|
51
|
+
'chemical/x-cif': FilePreviewTypes.TEXT,
|
|
52
|
+
// generic:
|
|
53
|
+
'text/plain': USE_EXT_MAPPING,
|
|
54
|
+
'application/octet-stream': USE_EXT_MAPPING,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const DEFAULT_EXTENSION_MAPPING: { [key: string]: FilePreviewTypes | false } = {
|
|
58
|
+
// image:
|
|
59
|
+
'.png': FilePreviewTypes.IMAGE,
|
|
60
|
+
'.jpeg': FilePreviewTypes.IMAGE,
|
|
61
|
+
'.jpg': FilePreviewTypes.IMAGE,
|
|
62
|
+
'.gif': FilePreviewTypes.IMAGE,
|
|
63
|
+
'.bmp': FilePreviewTypes.IMAGE,
|
|
64
|
+
'.webp': FilePreviewTypes.IMAGE,
|
|
65
|
+
'.svg': FilePreviewTypes.IMAGE,
|
|
66
|
+
'.ico': FilePreviewTypes.IMAGE,
|
|
67
|
+
'.avif': FilePreviewTypes.IMAGE,
|
|
68
|
+
'.apng': FilePreviewTypes.IMAGE,
|
|
69
|
+
// markdown:
|
|
70
|
+
'.md': FilePreviewTypes.MARKDOWN,
|
|
71
|
+
'.markdown': FilePreviewTypes.MARKDOWN,
|
|
72
|
+
// csv:
|
|
73
|
+
'.csv': FilePreviewTypes.CSV,
|
|
74
|
+
// tsv:
|
|
75
|
+
'.tsv': FilePreviewTypes.TSV,
|
|
76
|
+
// json:
|
|
77
|
+
'.json': FilePreviewTypes.JSON,
|
|
78
|
+
'.mvsj': FilePreviewTypes.JSON, // MolViewSpec JSON (mol* viewer)
|
|
79
|
+
// text:
|
|
80
|
+
'.txt': FilePreviewTypes.TEXT,
|
|
81
|
+
'.log': FilePreviewTypes.TEXT,
|
|
82
|
+
'.cif': FilePreviewTypes.TEXT,
|
|
83
|
+
'.pdb': FilePreviewTypes.TEXT,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default class FilePreviewService {
|
|
87
|
+
/**
|
|
88
|
+
* Returns the preview info based on the given file properties and the column's file preview settings.
|
|
89
|
+
* @param url the file url
|
|
90
|
+
* @param column the asset column
|
|
91
|
+
* @param storedFilename the stored filename
|
|
92
|
+
* @param contentDisposition content-disposition header value
|
|
93
|
+
* @param contentType content-type header value
|
|
94
|
+
*/
|
|
95
|
+
static getFilePreviewInfo(
|
|
96
|
+
url: string,
|
|
97
|
+
column?: AssetPseudoColumn,
|
|
98
|
+
storedFilename?: string,
|
|
99
|
+
contentDisposition?: string,
|
|
100
|
+
contentType?: string,
|
|
101
|
+
): {
|
|
102
|
+
previewType: FilePreviewTypes | null;
|
|
103
|
+
prefetchBytes: number | null;
|
|
104
|
+
prefetchMaxFileSize: number | null;
|
|
105
|
+
} {
|
|
106
|
+
const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null };
|
|
107
|
+
const previewType = FilePreviewService.getFilePreviewType(url, column, storedFilename, contentDisposition, contentType);
|
|
108
|
+
let prefetchBytes: number | null = null;
|
|
109
|
+
let prefetchMaxFileSize: number | null = null;
|
|
110
|
+
|
|
111
|
+
if (previewType === null) {
|
|
112
|
+
return disabledValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (column && column.filePreview) {
|
|
116
|
+
if (column.filePreview.disabledTypes.includes(previewType)) {
|
|
117
|
+
return disabledValue;
|
|
118
|
+
}
|
|
119
|
+
prefetchBytes = column.filePreview.getPrefetchBytes(previewType);
|
|
120
|
+
prefetchMaxFileSize = column.filePreview.getPrefetchMaxFileSize(previewType);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof prefetchBytes !== 'number' || prefetchBytes < 0) {
|
|
124
|
+
prefetchBytes = FILE_PREVIEW.PREFETCH_BYTES;
|
|
125
|
+
}
|
|
126
|
+
if (typeof prefetchMaxFileSize !== 'number' || prefetchMaxFileSize < 0) {
|
|
127
|
+
prefetchMaxFileSize = FILE_PREVIEW.MAX_FILE_SIZE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// if prefetchMaxFileSize is 0, we should not show the preview
|
|
131
|
+
if (prefetchMaxFileSize === 0) {
|
|
132
|
+
return disabledValue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { previewType, prefetchBytes, prefetchMaxFileSize };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns the preview type based on the given file properties and the column's file preview settings.
|
|
140
|
+
* @param url the file url
|
|
141
|
+
* @param column the asset column
|
|
142
|
+
* @param storedFilename the stored filename
|
|
143
|
+
* @param contentDisposition content-disposition header value
|
|
144
|
+
* @param contentType content-type header value
|
|
145
|
+
*/
|
|
146
|
+
private static getFilePreviewType(
|
|
147
|
+
url: string,
|
|
148
|
+
column?: AssetPseudoColumn,
|
|
149
|
+
storedFilename?: string,
|
|
150
|
+
contentDisposition?: string,
|
|
151
|
+
contentType?: string,
|
|
152
|
+
): FilePreviewTypes | null {
|
|
153
|
+
const filename = storedFilename || getFilename(url, contentDisposition);
|
|
154
|
+
const extension = getFilenameExtension(filename, column?.filenameExtFilter, column?.filenameExtRegexp);
|
|
155
|
+
let mappedFilePreviewType: FilePreviewTypes | typeof USE_EXT_MAPPING | false = USE_EXT_MAPPING;
|
|
156
|
+
|
|
157
|
+
// extend the mappings based on the annotations
|
|
158
|
+
let annotExtensionMapping;
|
|
159
|
+
let annotContentTypeMapping;
|
|
160
|
+
if (column && column.isAsset) {
|
|
161
|
+
// if file_preview is false, then no preview is allowed
|
|
162
|
+
if (!column.filePreview) return null;
|
|
163
|
+
if (column.filePreview.contentTypeMapping) {
|
|
164
|
+
annotContentTypeMapping = column.filePreview.contentTypeMapping;
|
|
165
|
+
}
|
|
166
|
+
if (column.filePreview.filenameExtMapping) {
|
|
167
|
+
annotExtensionMapping = column.filePreview.filenameExtMapping;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// if content-type is available, we must get the type from it.
|
|
172
|
+
if (typeof contentType === 'string' && contentType.length > 0) {
|
|
173
|
+
// remove any extra info like charset
|
|
174
|
+
contentType = contentType.split(';')[0].trim().toLowerCase();
|
|
175
|
+
let matched = false;
|
|
176
|
+
|
|
177
|
+
// first match through annotation mapping
|
|
178
|
+
if (annotContentTypeMapping) {
|
|
179
|
+
// if the exact match is found, use it
|
|
180
|
+
if (annotContentTypeMapping.exactMatch && contentType in annotContentTypeMapping.exactMatch) {
|
|
181
|
+
mappedFilePreviewType = annotContentTypeMapping.exactMatch[contentType];
|
|
182
|
+
matched = true;
|
|
183
|
+
}
|
|
184
|
+
// if exact match not found, try prefix matching
|
|
185
|
+
else if (annotContentTypeMapping.prefixMatch) {
|
|
186
|
+
const match = Object.keys(annotContentTypeMapping.prefixMatch).find((prefix) => contentType!.startsWith(prefix));
|
|
187
|
+
if (match) {
|
|
188
|
+
mappedFilePreviewType = annotContentTypeMapping.prefixMatch[match];
|
|
189
|
+
matched = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// if still not matched, check for default mapping (`*` in annotation)
|
|
194
|
+
if (!matched && annotContentTypeMapping.default !== null) {
|
|
195
|
+
mappedFilePreviewType = annotContentTypeMapping.default;
|
|
196
|
+
matched = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// if no match found through annotation, try the default mapping
|
|
201
|
+
if (!matched && contentType in DEFAULT_CONTENT_TYPE_MAPPING) {
|
|
202
|
+
mappedFilePreviewType = DEFAULT_CONTENT_TYPE_MAPPING[contentType];
|
|
203
|
+
matched = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// if no match found, disable the preview
|
|
207
|
+
if (!matched) {
|
|
208
|
+
mappedFilePreviewType = false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$log.debug(`FilePreviewService: Mapped content-type '${contentType}' to preview type '${mappedFilePreviewType}'`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// use extenstion mapping, if the content-type matching dictates so
|
|
215
|
+
if (mappedFilePreviewType === USE_EXT_MAPPING && typeof extension === 'string' && extension.length > 0) {
|
|
216
|
+
if (annotExtensionMapping && extension in annotExtensionMapping) {
|
|
217
|
+
mappedFilePreviewType = annotExtensionMapping[extension];
|
|
218
|
+
} else if (extension in DEFAULT_EXTENSION_MAPPING) {
|
|
219
|
+
mappedFilePreviewType = DEFAULT_EXTENSION_MAPPING[extension];
|
|
220
|
+
} else {
|
|
221
|
+
mappedFilePreviewType = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
$log.debug(`FilePreviewService: Mapped extension '${extension}' to preview type '${mappedFilePreviewType}'`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return isFilePreviewType(mappedFilePreviewType) ? mappedFilePreviewType : null;
|
|
228
|
+
}
|
|
229
|
+
}
|
package/src/services/logger.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-duplicate-enum-values */
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
2
|
|
|
4
3
|
import { ENV_IS_DEV_MODE } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
5
4
|
|
|
@@ -32,32 +31,32 @@ class Logger {
|
|
|
32
31
|
this._level = level;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
public trace(...args:
|
|
34
|
+
public trace(...args: unknown[]): void {
|
|
36
35
|
if (!this.isAllowed(LoggerLevels.TRACE)) return;
|
|
37
36
|
console.trace(...args);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
public debug(...args:
|
|
39
|
+
public debug(...args: unknown[]): void {
|
|
41
40
|
if (!this.isAllowed(LoggerLevels.DEBUG)) return;
|
|
42
41
|
console.debug(...args);
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
public info(...args:
|
|
44
|
+
public info(...args: unknown[]): void {
|
|
46
45
|
if (!this.isAllowed(LoggerLevels.INFO)) return;
|
|
47
46
|
console.info(...args);
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
public log(...args:
|
|
49
|
+
public log(...args: unknown[]): void {
|
|
51
50
|
if (!this.isAllowed(LoggerLevels.LOG)) return;
|
|
52
51
|
console.log(...args);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
public warn(...args:
|
|
54
|
+
public warn(...args: unknown[]): void {
|
|
56
55
|
if (!this.isAllowed(LoggerLevels.WARN)) return;
|
|
57
56
|
console.warn(...args);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
public error(...args:
|
|
59
|
+
public error(...args: unknown[]): void {
|
|
61
60
|
if (!this.isAllowed(LoggerLevels.ERROR)) return;
|
|
62
61
|
console.error(...args);
|
|
63
62
|
}
|
package/src/utils/constants.ts
CHANGED
|
@@ -27,6 +27,11 @@ export const URL_PATH_LENGTH_LIMIT = 4000;
|
|
|
27
27
|
*/
|
|
28
28
|
export const CONTEXT_HEADER_LENGTH_LIMIT = 6500;
|
|
29
29
|
|
|
30
|
+
export const FILE_PREVIEW = {
|
|
31
|
+
PREFETCH_BYTES: 0.5 * 1024 * 1024,
|
|
32
|
+
MAX_FILE_SIZE: 1 * 1024 * 1024,
|
|
33
|
+
};
|
|
34
|
+
|
|
30
35
|
export enum _constraintTypes {
|
|
31
36
|
KEY = 'k',
|
|
32
37
|
FOREIGN_KEY = 'fk',
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* given a url and optional content-disposition header, will return the filename.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: might return an empty string if no filename is found.
|
|
5
|
+
*/
|
|
6
|
+
export const getFilename = (url: string, contentDisposition?: string): string => {
|
|
7
|
+
if (contentDisposition) {
|
|
8
|
+
// try UTF-8 encoded filename first
|
|
9
|
+
const prefixUTF8 = "filename*=UTF-8''";
|
|
10
|
+
let filenameIndex = contentDisposition.indexOf(prefixUTF8);
|
|
11
|
+
if (filenameIndex !== -1) {
|
|
12
|
+
const filename = contentDisposition.substring(filenameIndex + prefixUTF8.length);
|
|
13
|
+
if (filename) return filename.replace(/"/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// try standard filename=
|
|
17
|
+
const prefixStandard = 'filename=';
|
|
18
|
+
filenameIndex = contentDisposition.indexOf(prefixStandard);
|
|
19
|
+
if (filenameIndex !== -1) {
|
|
20
|
+
let filename = contentDisposition.substring(filenameIndex + prefixStandard.length);
|
|
21
|
+
// remove quotes and any trailing content after semicolon
|
|
22
|
+
filename = filename.split(';')[0].replace(/"/g, '').trim();
|
|
23
|
+
if (filename) return filename;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// hatrac files have a different format
|
|
28
|
+
// eslint-disable-next-line no-useless-escape
|
|
29
|
+
const parts = url.match(/^\/hatrac\/([^\/]+\/)*([^\/:]+)(:[^:]+)?$/);
|
|
30
|
+
if (parts && parts.length === 4) {
|
|
31
|
+
return parts[2];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// strip query parameters and fragments from URL
|
|
35
|
+
const cleanUrl = url.split('?')[0].split('#')[0];
|
|
36
|
+
return cleanUrl.split('/').pop() || '';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* given a filename, will return the extension
|
|
41
|
+
* By default, it will extract the last of the filename after the last `.` (including the dot).
|
|
42
|
+
* The second parameter can be used for passing a regular expression
|
|
43
|
+
* if we want a different method of extracting the extension.
|
|
44
|
+
* @param {string} filename
|
|
45
|
+
* @param {string[]} allowedExtensions
|
|
46
|
+
* @param {string[]} regexArr
|
|
47
|
+
* @returns the filename extension string. if we cannot find any matches, it will return null
|
|
48
|
+
* @private
|
|
49
|
+
* @ignore
|
|
50
|
+
*/
|
|
51
|
+
export const getFilenameExtension = function (filename: string, allowedExtensions?: string[], regexArr?: string[]): string | null {
|
|
52
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// first find in the list of allowed extensions (case-insensitive)
|
|
57
|
+
let res: string | null = null;
|
|
58
|
+
const filenameLower = filename.toLowerCase();
|
|
59
|
+
const isInAllowed =
|
|
60
|
+
Array.isArray(allowedExtensions) &&
|
|
61
|
+
allowedExtensions.some((ext) => {
|
|
62
|
+
res = ext;
|
|
63
|
+
return typeof ext === 'string' && ext.length > 0 && filenameLower.endsWith(ext.toLowerCase());
|
|
64
|
+
});
|
|
65
|
+
if (isInAllowed) {
|
|
66
|
+
return res;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// we will return null if we cannot find anything
|
|
70
|
+
res = null;
|
|
71
|
+
// no matching allowed extension, try the regular expressions
|
|
72
|
+
if (Array.isArray(regexArr) && regexArr.length > 0) {
|
|
73
|
+
regexArr.some((regexp) => {
|
|
74
|
+
// since regular expression comes from annotation, it might not be valid
|
|
75
|
+
try {
|
|
76
|
+
const matches = filename.match(new RegExp(regexp, 'g'));
|
|
77
|
+
if (matches && matches[0] && typeof matches[0] === 'string') {
|
|
78
|
+
res = matches[0];
|
|
79
|
+
} else {
|
|
80
|
+
res = null;
|
|
81
|
+
}
|
|
82
|
+
return res;
|
|
83
|
+
} catch {
|
|
84
|
+
res = null;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
const dotIndex = filename.lastIndexOf('.');
|
|
90
|
+
// it's only a valid filename if there's some string after `.`
|
|
91
|
+
if (dotIndex !== -1 && dotIndex !== filename.length - 1) {
|
|
92
|
+
res = filename.slice(dotIndex).toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return res;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if file is a text-like file based on content type or extension
|
|
101
|
+
*/
|
|
102
|
+
export const checkIsTextFile = (contentType?: string, extension?: string | null): boolean => {
|
|
103
|
+
// text-like files using content-type
|
|
104
|
+
if (
|
|
105
|
+
contentType &&
|
|
106
|
+
(contentType.startsWith('text/') ||
|
|
107
|
+
// cif files
|
|
108
|
+
contentType === 'chemical/x-mmcif' ||
|
|
109
|
+
contentType === 'chemical/x-cif')
|
|
110
|
+
) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// text-like files using extension
|
|
115
|
+
if (extension && ['.txt', '.js', '.log', '.cif', '.pdb'].includes(extension)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if file is markdown based on content type or extension
|
|
124
|
+
*/
|
|
125
|
+
export const checkIsMarkdownFile = (contentType?: string, extension?: string | null): boolean => {
|
|
126
|
+
if (contentType && (contentType.includes('markdown') || contentType.includes('md'))) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (extension && ['.md', '.markdown'].includes(extension)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if file is CSV based on content type or extension
|
|
139
|
+
*/
|
|
140
|
+
export const checkIsCsvFile = (contentType?: string, extension?: string | null): boolean => {
|
|
141
|
+
if (contentType && (contentType.includes('csv') || contentType.includes('comma-separated-values'))) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (extension && extension === '.csv') {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if file is TSV based on content type or extension
|
|
154
|
+
*/
|
|
155
|
+
export const checkIsTsvFile = (contentType?: string, extension?: string | null): boolean => {
|
|
156
|
+
if (contentType && contentType.includes('tab-separated-values')) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (extension && extension === '.tsv') {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if file is JSON based on content type or extension
|
|
169
|
+
*/
|
|
170
|
+
export const checkIsJSONFile = (contentType?: string, extension?: string | null): boolean => {
|
|
171
|
+
if (contentType && contentType.includes('application/json')) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// mvsj: MolViewSpec JSON (mol* viewer)
|
|
176
|
+
if (extension && ['.json', '.mvsj'].includes(extension)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if file is an image based on content type or extension
|
|
185
|
+
* Checks for common image formats supported by HTML img tag
|
|
186
|
+
*/
|
|
187
|
+
export const checkIsImageFile = (contentType?: string, extension?: string | null): boolean => {
|
|
188
|
+
// TODO this should be more specific based on supported image formats
|
|
189
|
+
if (contentType && contentType.startsWith('image/')) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (extension && ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.avif', '.apng'].includes(extension)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
};
|