@isrd-isi-edu/ermrestjs 2.2.0 → 2.4.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 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 = _getFilenameExtension(this.file.name, this.column.filenameExtFilter, this.column.filenameExtRegexp);
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"
@@ -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 {Object} annotation the annotation object.
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
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@isrd-isi-edu/ermrestjs",
3
3
  "description": "ERMrest client library in JavaScript",
4
- "version": "2.2.0",
4
+ "version": "2.4.0",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
7
7
  "node": ">= 20.0.0",
8
- "npm": ">=6.0.0"
8
+ "npm": ">= 7.0.0"
9
9
  },
10
10
  "main": "dist/ermrest.js",
11
11
  "module": "dist/ermrest.min.js",
@@ -42,11 +42,16 @@
42
42
  "postgresql",
43
43
  "library"
44
44
  ],
45
+ "overrides": {
46
+ "@microsoft/api-extractor": {
47
+ "typescript": "$typescript"
48
+ }
49
+ },
45
50
  "dependencies": {
46
51
  "@types/lodash-es": "^4.17.12",
47
52
  "@types/markdown-it": "^14.1.2",
48
53
  "@types/q": "^1.5.8",
49
- "axios": "1.13.2",
54
+ "axios": "1.13.5",
50
55
  "handlebars": "4.7.8",
51
56
  "lodash-es": "^4.17.23",
52
57
  "lz-string": "^1.5.0",
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?: null | { showCsvHeader: boolean };
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
- // if caption matches the expected format, just show the file name
172
- // eslint-disable-next-line no-useless-escape
173
- const parts = caption.match(/^\/hatrac\/([^\/]+\/)*([^\/:]+)(:[^:]+)?$/);
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,20 +453,20 @@ export class AssetPseudoColumn extends ReferenceColumn {
457
453
  /**
458
454
  * whether we should show the file preview or not
459
455
  */
460
- get filePreview(): null | { showCsvHeader: boolean } {
456
+ get filePreview(): FilePreviewConfig | null {
461
457
  if (this._filePreview === undefined) {
462
- const disp = this._annotation.display;
463
- const currDisplay = isObjectAndNotNull(disp) ? _getAnnotationValueByContext(this._context, disp) : null;
464
- const settings = isObjectAndKeyExists(currDisplay, 'file_preview') ? currDisplay.file_preview : {};
465
- if (settings === false) {
458
+ // if the colum has markdown-pattern, don't show the file preview
459
+ if (this.display.sourceMarkdownPattern || this._baseCol.getDisplay(this._context).isMarkdownPattern) {
466
460
  this._filePreview = null;
467
461
  } else {
468
- // by default we're hiding the CSV header.
469
- let showCsvHeader = false;
470
- if (isObjectAndKeyExists(settings, 'show_csv_header') && typeof settings.show_csv_header === 'boolean') {
471
- showCsvHeader = settings.show_csv_header;
462
+ const disp = this._annotation.display;
463
+ const currDisplay = isObjectAndNotNull(disp) ? _getAnnotationValueByContext(this._context, disp) : null;
464
+ const settings = isObjectAndKeyExists(currDisplay, 'file_preview') ? currDisplay.file_preview : {};
465
+ if (settings === false) {
466
+ this._filePreview = null;
467
+ } else {
468
+ this._filePreview = new FilePreviewConfig(settings);
472
469
  }
473
- this._filePreview = { showCsvHeader };
474
470
  }
475
471
  }
476
472
  return this._filePreview;
@@ -496,3 +492,234 @@ export class AssetPseudoColumn extends ReferenceColumn {
496
492
  return this._waitFor;
497
493
  }
498
494
  }
495
+
496
+ class FilePreviewConfig {
497
+ private static previewTypes = Object.values(FilePreviewTypes);
498
+
499
+ /**
500
+ * whether we should show the CSV header or not
501
+ * (default: false)
502
+ */
503
+ showCsvHeader: boolean = false;
504
+
505
+ /**
506
+ * the height of the preview container
507
+ */
508
+ defaultHeight: number | null = null;
509
+
510
+ private _prefetchBytes: { [key: string]: number | null } = {
511
+ image: null,
512
+ markdown: null,
513
+ csv: null,
514
+ tsv: null,
515
+ json: null,
516
+ text: null,
517
+ };
518
+
519
+ private _prefetchMaxFileSize: { [key: string]: number | null } = {
520
+ image: null,
521
+ markdown: null,
522
+ csv: null,
523
+ tsv: null,
524
+ json: null,
525
+ text: null,
526
+ };
527
+
528
+ filenameExtMapping: { [key: string]: FilePreviewTypes | false } | null = null;
529
+
530
+ contentTypeMapping: {
531
+ exactMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } | null;
532
+ prefixMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } | null;
533
+ default: FilePreviewTypes | typeof USE_EXT_MAPPING | false | null;
534
+ } | null = null;
535
+
536
+ disabledTypes: FilePreviewTypes[] = [];
537
+
538
+ /**
539
+ * populate the props based on the given annotation object.
540
+ * The supported annotation properties are:
541
+ * - show_csv_header
542
+ * - default_height
543
+ * - prefetch_bytes
544
+ * - prefetch_max_file_size
545
+ * - filename_ext_mapping
546
+ * - content_type_mapping
547
+ * - disabled
548
+ */
549
+ constructor(settings: any) {
550
+ if (isObjectAndKeyExists(settings, 'show_csv_header') && typeof settings.show_csv_header === 'boolean') {
551
+ this.showCsvHeader = settings.show_csv_header;
552
+ }
553
+
554
+ if (isObjectAndKeyExists(settings, 'default_height') && typeof settings.default_height === 'number' && settings.default_height >= 0) {
555
+ this.defaultHeight = settings.default_height;
556
+ }
557
+
558
+ this._prefetchBytes = this._populateProps<number>(settings, 'prefetch_bytes', (value: unknown) => {
559
+ return typeof value === 'number' && value >= 0;
560
+ });
561
+
562
+ this._prefetchMaxFileSize = this._populateProps<number>(settings, 'prefetch_max_file_size', (value: unknown) => {
563
+ return typeof value === 'number' && value >= 0;
564
+ });
565
+
566
+ if (isObjectAndKeyExists(settings, 'filename_ext_mapping')) {
567
+ this.filenameExtMapping = {};
568
+ for (const [key, val] of Object.entries(settings.filename_ext_mapping)) {
569
+ if (val === false || (typeof val === 'string' && isFilePreviewType(val))) {
570
+ this.filenameExtMapping[key] = val;
571
+ }
572
+ }
573
+ }
574
+
575
+ if (isObjectAndKeyExists(settings, 'content_type_mapping')) {
576
+ const exactMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {};
577
+ const prefixMatch: { [key: string]: FilePreviewTypes | typeof USE_EXT_MAPPING | false } = {};
578
+ let defaultMapping: FilePreviewTypes | typeof USE_EXT_MAPPING | false | null = null;
579
+ let hasExactMatch = false;
580
+ let hasPrefixMatch = false;
581
+ Object.keys(settings.content_type_mapping).forEach((key) => {
582
+ const val = settings.content_type_mapping[key];
583
+ const validValue = val === false || (typeof val === 'string' && (isFilePreviewType(val) || val === USE_EXT_MAPPING));
584
+ if (!validValue) return;
585
+ // * could be used for default mapping
586
+ if (key === '*') {
587
+ defaultMapping = val;
588
+ return;
589
+ }
590
+
591
+ // only type/ or type/subtype are valid
592
+ const parts = key.split('/');
593
+ if (parts.length !== 2 || parts[0].length === 0) return;
594
+
595
+ if (parts[1].length > 0) {
596
+ exactMatch[key] = val;
597
+ hasExactMatch = true;
598
+ } else {
599
+ prefixMatch[parts[0] + '/'] = val;
600
+ hasPrefixMatch = true;
601
+ }
602
+ });
603
+
604
+ if (hasPrefixMatch || hasExactMatch || defaultMapping !== null) {
605
+ this.contentTypeMapping = {
606
+ exactMatch: hasExactMatch ? exactMatch : null,
607
+ prefixMatch: hasPrefixMatch ? prefixMatch : null,
608
+ default: defaultMapping,
609
+ };
610
+ }
611
+ }
612
+
613
+ if (isObjectAndKeyExists(settings, 'disabled') && Array.isArray(settings.disabled)) {
614
+ this.disabledTypes = settings.disabled.filter(
615
+ (t: unknown) => typeof t === 'string' && FilePreviewConfig.previewTypes.includes(t as FilePreviewTypes),
616
+ );
617
+ }
618
+ }
619
+
620
+ /**
621
+ * return the number of bytes to prefetch for previewing the file
622
+ */
623
+ getPrefetchBytes(filePreviewType: FilePreviewTypes | null): number | null {
624
+ switch (filePreviewType) {
625
+ case FilePreviewTypes.IMAGE:
626
+ return this._prefetchBytes.image;
627
+ case FilePreviewTypes.MARKDOWN:
628
+ return this._prefetchBytes.markdown;
629
+ case FilePreviewTypes.CSV:
630
+ return this._prefetchBytes.csv;
631
+ case FilePreviewTypes.TSV:
632
+ return this._prefetchBytes.tsv;
633
+ case FilePreviewTypes.JSON:
634
+ return this._prefetchBytes.json;
635
+ case FilePreviewTypes.TEXT:
636
+ return this._prefetchBytes.text;
637
+ default:
638
+ return null;
639
+ }
640
+ }
641
+
642
+ /**
643
+ * return the max file size for previewing the file
644
+ */
645
+ getPrefetchMaxFileSize(filePreviewType: FilePreviewTypes | null): number | null {
646
+ switch (filePreviewType) {
647
+ case FilePreviewTypes.IMAGE:
648
+ return this._prefetchMaxFileSize.image;
649
+ case FilePreviewTypes.MARKDOWN:
650
+ return this._prefetchMaxFileSize.markdown;
651
+ case FilePreviewTypes.CSV:
652
+ return this._prefetchMaxFileSize.csv;
653
+ case FilePreviewTypes.TSV:
654
+ return this._prefetchMaxFileSize.tsv;
655
+ case FilePreviewTypes.JSON:
656
+ return this._prefetchMaxFileSize.json;
657
+ case FilePreviewTypes.TEXT:
658
+ return this._prefetchMaxFileSize.text;
659
+ default:
660
+ return null;
661
+ }
662
+ }
663
+
664
+ /**
665
+ * The settings could be either just one value or an object with different values for each type.
666
+ * This function will populate the result object with the appropriate values for each type.
667
+ */
668
+ private _populateProps<T>(settings: any, propName: string, validate?: (value: unknown) => boolean): { [key: string]: T | null } {
669
+ const res: { [key: string]: T | null } = {
670
+ text: null,
671
+ markdown: null,
672
+ csv: null,
673
+ tsv: null,
674
+ json: null,
675
+ image: null,
676
+ };
677
+ if (!isObjectAndKeyExists(settings, propName)) return res;
678
+
679
+ if (isObjectAndNotNull(settings[propName])) {
680
+ // for each preview type, try to get its value (which will fall back to * if not defined)
681
+ for (const key of FilePreviewConfig.previewTypes) {
682
+ const definedRes = this._getPropForType(key, settings[propName]);
683
+ if (!validate || validate(definedRes)) {
684
+ res[key] = definedRes;
685
+ }
686
+ }
687
+ } else {
688
+ const definedRes = settings[propName];
689
+ if (!validate || validate(definedRes)) {
690
+ for (const key of FilePreviewConfig.previewTypes) {
691
+ res[key] = definedRes;
692
+ }
693
+ }
694
+ }
695
+
696
+ return res;
697
+ }
698
+
699
+ /**
700
+ * Get the property for a specific file type. If not defined, will try to get the default value (*).
701
+ * Otherwise returns null.
702
+ */
703
+ private _getPropForType(fileType: string, settings: any): any {
704
+ const DEFAULT_TYPE = '*';
705
+
706
+ let isDefined = false;
707
+ let res;
708
+ if (isObjectAndKeyExists(settings, fileType)) {
709
+ if (typeof settings[fileType] === 'string' && settings[fileType] in FilePreviewConfig.previewTypes) {
710
+ res = this._getPropForType(settings[fileType], settings);
711
+ } else {
712
+ res = settings[fileType];
713
+ }
714
+
715
+ if (res !== null && res !== undefined) isDefined = true;
716
+ }
717
+
718
+ if (!isDefined && settings[DEFAULT_TYPE]) {
719
+ res = this._getPropForType(DEFAULT_TYPE, settings);
720
+ if (res !== null && res !== undefined) isDefined = true;
721
+ }
722
+
723
+ return isDefined ? res : null;
724
+ }
725
+ }
@@ -0,0 +1,235 @@
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
+ forcedPreviewType?: FilePreviewTypes,
102
+ forcedPrefetchBytes?: number,
103
+ forcedPrefetchMaxFileSize?: number,
104
+ ): {
105
+ previewType: FilePreviewTypes | null;
106
+ prefetchBytes: number | null;
107
+ prefetchMaxFileSize: number | null;
108
+ filename: string;
109
+ } {
110
+ const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null, filename: '' };
111
+ const filename = storedFilename || getFilename(url, contentDisposition);
112
+ const previewType = forcedPreviewType ? forcedPreviewType : FilePreviewService.getFilePreviewType(url, filename, column, contentType);
113
+ let prefetchBytes: number | null = null;
114
+ let prefetchMaxFileSize: number | null = null;
115
+
116
+ if (previewType === null) {
117
+ return disabledValue;
118
+ }
119
+
120
+ if (column && column.filePreview) {
121
+ if (column.filePreview.disabledTypes.includes(previewType)) {
122
+ return disabledValue;
123
+ }
124
+ prefetchBytes = column.filePreview.getPrefetchBytes(previewType);
125
+ prefetchMaxFileSize = column.filePreview.getPrefetchMaxFileSize(previewType);
126
+ }
127
+
128
+ if (typeof prefetchBytes !== 'number' || prefetchBytes < 0) {
129
+ prefetchBytes = FILE_PREVIEW.PREFETCH_BYTES;
130
+ }
131
+ if (typeof prefetchMaxFileSize !== 'number' || prefetchMaxFileSize < 0) {
132
+ prefetchMaxFileSize = FILE_PREVIEW.MAX_FILE_SIZE;
133
+ }
134
+
135
+ // the forced (manual) setting cannot exceed the column/default settings
136
+ if (typeof forcedPrefetchBytes === 'number' && forcedPrefetchBytes >= 0 && forcedPrefetchBytes < prefetchBytes) {
137
+ prefetchBytes = forcedPrefetchBytes;
138
+ }
139
+ if (typeof forcedPrefetchMaxFileSize === 'number' && forcedPrefetchMaxFileSize >= 0 && forcedPrefetchMaxFileSize < prefetchMaxFileSize) {
140
+ prefetchMaxFileSize = forcedPrefetchMaxFileSize;
141
+ }
142
+
143
+ // if prefetchMaxFileSize is 0, we should not show the preview
144
+ if (prefetchMaxFileSize === 0) {
145
+ return disabledValue;
146
+ }
147
+
148
+ return { previewType, prefetchBytes, prefetchMaxFileSize, filename };
149
+ }
150
+
151
+ /**
152
+ * Returns the preview type based on the given file properties and the column's file preview settings.
153
+ * @param url the file url
154
+ * @param column the asset column
155
+ * @param filename the filename (either stored filename or from content-disposition or url)
156
+ * @param contentDisposition content-disposition header value
157
+ * @param contentType content-type header value
158
+ */
159
+ private static getFilePreviewType(url: string, filename: string, column?: AssetPseudoColumn, contentType?: string): FilePreviewTypes | null {
160
+ const extension = getFilenameExtension(filename, column?.filenameExtFilter, column?.filenameExtRegexp);
161
+ let mappedFilePreviewType: FilePreviewTypes | typeof USE_EXT_MAPPING | false = USE_EXT_MAPPING;
162
+
163
+ // extend the mappings based on the annotations
164
+ let annotExtensionMapping;
165
+ let annotContentTypeMapping;
166
+ if (column && column.isAsset) {
167
+ // if file_preview is false, then no preview is allowed
168
+ if (!column.filePreview) return null;
169
+ if (column.filePreview.contentTypeMapping) {
170
+ annotContentTypeMapping = column.filePreview.contentTypeMapping;
171
+ }
172
+ if (column.filePreview.filenameExtMapping) {
173
+ annotExtensionMapping = column.filePreview.filenameExtMapping;
174
+ }
175
+ }
176
+
177
+ // if content-type is available, we must get the type from it.
178
+ if (typeof contentType === 'string' && contentType.length > 0) {
179
+ // remove any extra info like charset
180
+ contentType = contentType.split(';')[0].trim().toLowerCase();
181
+ let matched = false;
182
+
183
+ // first match through annotation mapping
184
+ if (annotContentTypeMapping) {
185
+ // if the exact match is found, use it
186
+ if (annotContentTypeMapping.exactMatch && contentType in annotContentTypeMapping.exactMatch) {
187
+ mappedFilePreviewType = annotContentTypeMapping.exactMatch[contentType];
188
+ matched = true;
189
+ }
190
+ // if exact match not found, try prefix matching
191
+ else if (annotContentTypeMapping.prefixMatch) {
192
+ const match = Object.keys(annotContentTypeMapping.prefixMatch).find((prefix) => contentType!.startsWith(prefix));
193
+ if (match) {
194
+ mappedFilePreviewType = annotContentTypeMapping.prefixMatch[match];
195
+ matched = true;
196
+ }
197
+ }
198
+
199
+ // if still not matched, check for default mapping (`*` in annotation)
200
+ if (!matched && annotContentTypeMapping.default !== null) {
201
+ mappedFilePreviewType = annotContentTypeMapping.default;
202
+ matched = true;
203
+ }
204
+ }
205
+
206
+ // if no match found through annotation, try the default mapping
207
+ if (!matched && contentType in DEFAULT_CONTENT_TYPE_MAPPING) {
208
+ mappedFilePreviewType = DEFAULT_CONTENT_TYPE_MAPPING[contentType];
209
+ matched = true;
210
+ }
211
+
212
+ // if no match found, disable the preview
213
+ if (!matched) {
214
+ mappedFilePreviewType = false;
215
+ }
216
+
217
+ $log.debug(`FilePreviewService: Mapped content-type '${contentType}' to preview type '${mappedFilePreviewType}'`);
218
+ }
219
+
220
+ // use extenstion mapping, if the content-type matching dictates so
221
+ if (mappedFilePreviewType === USE_EXT_MAPPING && typeof extension === 'string' && extension.length > 0) {
222
+ if (annotExtensionMapping && extension in annotExtensionMapping) {
223
+ mappedFilePreviewType = annotExtensionMapping[extension];
224
+ } else if (extension in DEFAULT_EXTENSION_MAPPING) {
225
+ mappedFilePreviewType = DEFAULT_EXTENSION_MAPPING[extension];
226
+ } else {
227
+ mappedFilePreviewType = false;
228
+ }
229
+
230
+ $log.debug(`FilePreviewService: Mapped extension '${extension}' to preview type '${mappedFilePreviewType}'`);
231
+ }
232
+
233
+ return isFilePreviewType(mappedFilePreviewType) ? mappedFilePreviewType : null;
234
+ }
235
+ }