import { CellValue, ColumnType, IAttachment, ITypeOptions, Language, } from '../../types'; import { exhaustiveSwitch, isNotNullable } from '../../typeUtils'; import { transformCellValueToStringArray } from './transformCellValueToString'; import { ICollaboratorsById } from './types'; export const transformCellValueToAttachmentArray = < OT extends ColumnType = ColumnType >({ cellValue, typeOptions, opts, }: { cellValue: CellValue; typeOptions: ITypeOptions; opts?: { locale?: Language; collaboratorsById?: ICollaboratorsById; }; }) => { if (typeOptions.type === ColumnType.MULTI_ATTACHMENT) return cellValue; const attachments = ( transformCellValueToStringArray({ cellValue, typeOptions, opts, }) as CombinedAttachValue[] ) .map((attachValue) => getAttachValueParts(attachValue)) .filter(isNotNullable) .map(makeFileObjFromAttachValueParts); if (attachments.length === 0) return null; return attachments; }; /** * see: https://basarat.gitbook.io/typescript/main-1/nominaltyping#using-enums * * tl;dr think of it like a sub-class of a string */ enum CombinedAttachValueBrand { _ = '', } type CombinedAttachValue = CombinedAttachValueBrand & string; /** * Parses out the relevant components of a stringified attachment. * * @param attachValue a stringified version of an attachment */ const getAttachValueParts = (attachValue: CombinedAttachValue) => { /** * That regex is a bit of an eyeful. Here's an example parse: * * Input: "yo (https://host/0x123/shortid_filename.png)" * * Output: * * Group #1: yo * Group #2: https://host/0x123/shortid_filename.png * Group #3: 0x123/shortid_filename.png * Group #4: shortid * Group #5: filename.png * Group #6: filename * Group #7: png * * Hope that helps! */ const match = attachValue.match( /* Group indices: * 1 2 3 4 56 7 */ /([^(]+) \((https?:\/\/.*?\/((?:[^/]*\/)+(?:([^/_]*)_)?(([^/]*)\.([^./]*))))\)/ ); if (!match) { return null; } const [ , displayFilename, url, urlEncodedFileKey, urlEncodedFileId, urlEncodedFilename, urlEncodedFileNameWithoutExtension, extension, ] = match; return { displayFilename: displayFilename as string, url: url as string, urlEncodedFileKey: urlEncodedFileKey as string, urlEncodedFileId: urlEncodedFileId as string | undefined, urlEncodedFilename: urlEncodedFilename as string, urlEncodedFileNameWithoutExtension: urlEncodedFileNameWithoutExtension as string, extension: extension as string, }; }; const setImageThumbUrl = (fileObj: IAttachment) => { const url = fileObj.url; fileObj.smallThumbUrl = `${url}?x-oss-process=style/low`; fileObj.mediumThumbUrl = `${url}?x-oss-process=style/middle`; fileObj.largeThumbUrl = `${url}?x-oss-process=style/high`; return fileObj; }; const setVideoThumbUrl = (fileObj: IAttachment) => { const url = fileObj.url; fileObj.smallThumbUrl = `${url}?x-oss-process=video/snapshot,t_500,f_jpg,h_120`; fileObj.mediumThumbUrl = `${url}?x-oss-process=video/snapshot,t_500,f_jpg,h_150`; fileObj.largeThumbUrl = `${url}?x-oss-process=video/snapshot,t_500,f_jpg,h_300`; return fileObj; }; enum FileType { AUDIO, VIDEO, WORD, TEXT, ZIP, EXCEL, PPT, PDF, CODE, IMAGE, SVG, DEFAULT, } const makeFileObjFromAttachValueParts = ({ displayFilename, url, urlEncodedFileKey, urlEncodedFileId, urlEncodedFilename, urlEncodedFileNameWithoutExtension, extension, }: { displayFilename: string, url: string; urlEncodedFileKey: string; urlEncodedFileId: string | undefined; urlEncodedFilename: string; urlEncodedFileNameWithoutExtension: string; extension: string; }): IAttachment => { const decodedUrlFilename = decodeURI(urlEncodedFilename); const fileName = constructDisplayName(displayFilename, getExtension(decodedUrlFilename)); const fileObj = { // I believe that there are some older files that don't have a shortid // prepended to the filename, so in that case we fallback to using the // entire extension-less file name. fileId: (urlEncodedFileId && decodeURI(urlEncodedFileId)) ?? decodeURI(urlEncodedFileNameWithoutExtension), fileKey: decodeURI(urlEncodedFileKey), fileName, // Don't think there's anything that can be done about this fileSize: 0, fileType: REVERSE_ACCEPT_MAP[`.${extension}`][0] ?? '*/*', // these are populated below by `setImageThumbUrl` and `setVideoThumbUrl` largeThumbUrl: '', mediumThumbUrl: '', smallThumbUrl: '', // tslint:disable-next-line:object-shorthand-properties-first url, }; const fileType = getFileTypeByName(decodedUrlFilename, fileObj.fileType); switch (fileType) { case FileType.IMAGE: setImageThumbUrl(fileObj); break; case FileType.VIDEO: setVideoThumbUrl(fileObj); break; case FileType.AUDIO: case FileType.CODE: case FileType.DEFAULT: case FileType.EXCEL: case FileType.PDF: case FileType.PPT: case FileType.SVG: case FileType.TEXT: case FileType.WORD: case FileType.ZIP: // do nothing break; default: exhaustiveSwitch({ switchValue: fileType, }) } return fileObj; }; /** Helpers to get the display name with extension */ const constructDisplayName = (displayNameMayWithoutExtension: string, fileExtension: string) => { const parts = displayNameMayWithoutExtension.split('.'); if (parts.length > 1) { const currentExtension = parts[parts.length - 1].toLocaleLowerCase(); if (currentExtension !== fileExtension) { throw new Error(`display file name extension: ${currentExtension} does not match actual file extension ${fileExtension}`); } return displayNameMayWithoutExtension; } return `${displayNameMayWithoutExtension}.${fileExtension}`; } /** Helpers for getting file type and icon */ const getExtension = (filename: string) => { const parts = filename.split('.'); return parts[parts.length - 1].toLowerCase(); }; const getFileTypeByName = (name: string, type: string) => { if (type) { return ( (MIME_TYPE_MAP[type] as FileType) ?? FILE_TYPE_MAP[getExtension(name)] ?? FileType.DEFAULT ); } return FILE_TYPE_MAP[getExtension(name)] ?? FileType.DEFAULT; }; const ACCEPT_MAP: { [key: string]: string[] } = { '*/*': [], 'application/*': [], 'application/x-troff-msvideo': ['.avi'], 'video/avi': ['.avi', '.m4v', '.mp4', '.flv', '.wmv'], 'video/msvideo': ['.avi'], 'video/x-msvideo': ['.avi'], 'video/avs-video': ['.avi'], 'image/bmp': ['.bmp'], 'image/x-windows-bmp': ['.bmp'], 'application/x-pointplus': ['.css'], 'text/css': ['.css'], 'text/csv': ['.csv'], 'application/msword': ['.doc', '.dot'], 'text/html': ['.fdml', '.flx', '.htm', '.html', '.htmls', '.htx', '.json'], 'image/jpeg': ['.jpg', '.jpeg', '.jpe', '.jfif', '.gif', '.ico'], 'image/pjpeg': ['.jpe', '.jpeg', '.jpg', '.jfif'], 'image/x-jps': ['.jps'], 'application/x-javascript': ['.js', '.jsx'], 'application/javascript': ['.js'], 'application/ecmascript': ['.js'], 'text/javascript': ['.js'], 'application/x-midi': ['.mid', '.midi'], 'audio/midi': ['.mid', '.midi'], 'audio/x-mid': ['.mid', '.midi'], 'audio/x-midi': ['.mid', '.midi'], 'music/crescendo': ['.mid', '.midi'], 'x-music/x-midi': ['.mid', '.midi'], 'video/quicktime': ['.moov', '.mov'], 'video/x-sgi-movie': ['.movie', '.mv'], 'audio/mpeg': ['.m2a', '.m3a', '.mp2', '.mp3', '.mpa', '.mpg', '.mpga'], 'audio/x-mpeg': ['.mp2', '.mp3'], 'video/mpeg': ['.mpa', '.mpe', '.mpeg', '.mpg'], 'video/x-mpeg': ['.mpa', '.mpe', '.mpeg', '.mpg'], 'video/x-mpeq2a': ['.mp2'], 'audio/mpeg3': ['.mp3'], 'audio/x-mpeg-3': ['.mp3'], 'application/pdf': ['.pdf'], 'image/png': ['.png'], 'application/mspowerpoint': ['.pot', '.pps', '.ppt', '.ppz'], 'application/vnd.ms-powerpoint': ['.pot', '.pps', '.ppt', '.ppz'], 'model/x-pov': ['.pov'], 'application/powerpoint': ['.ppt'], 'application/x-mspowerpoint': ['.ppt'], 'application/plain': ['.text', '.txt'], 'text/plain': ['.txt', '.wtx'], 'image/tiff': ['.tif', '.tiff'], 'image/svg+xml': ['.svg'], 'image/x-tiff': ['.tif', '.tiff'], 'application/excel': [ '.xl', '.xla', '.xlb', '.xlc', '.xld', '.xlk', '.xll', '.xlm', '.xls', '.xlt', '.xlv', '.xlw', ], 'application/x-excel': [ '.xl', '.xla', '.xlb', '.xlc', '.xld', '.xlk', '.xll', '.xlm', '.xls', '.xlt', '.xlv', '.xlw', ], 'application/x-msexcel': ['.xla', '.xls', '.xlw'], 'application/vnd.ms-excel': [ '.xla', '.xlb', '.xlc', '.xll', '.xlm', '.xls', '.xlt', '.xlw', ], 'application/x-compressed': ['.zip', '.rar', '.pkg'], 'application/x-zip-compressed': ['.zip'], 'application/zip': ['.zip'], 'multipart/x-zip': ['.zip'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [ '.docx', ], 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': [ '.dotx', ], 'application/vnd.ms-word.document.macroEnabled.12': ['.docm'], 'application/vnd.ms-word.template.macroEnabled.12': ['.dotm'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ '.xlsx', ], 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': [ '.xltx', ], 'application/vnd.ms-excel.sheet.macroEnabled.12': ['.xlsm'], 'application/vnd.ms-excel.template.macroEnabled.12': ['.xltm'], 'application/vnd.ms-excel.addin.macroEnabled.12': ['.xlam'], 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': ['.xlsb'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': [ '.pptx', ], 'application/vnd.openxmlformats-officedocument.presentationml.template': [ '.potx', ], 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': [ '.ppsx', ], 'application/vnd.ms-powerpoint.addin.macroEnabled.12': ['.ppam'], 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': ['.pptm'], 'application/vnd.ms-powerpoint.template.macroEnabled.12': ['.potm'], 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': ['.ppsm'], }; type MimeType = string; // mapping from extension to mime types. const REVERSE_ACCEPT_MAP = Object.entries(ACCEPT_MAP).reduce<{ [extension: string]: MimeType[]; }>((acc, [mimeType, extensions]) => { extensions.forEach((extension) => { acc[extension] = acc[extension] ?? []; acc[extension].push(mimeType); }); return acc; }, {}); const FILE_TYPE_MAP: { [key: string]: FileType } = { m4v: FileType.VIDEO, avi: FileType.VIDEO, mpg: FileType.VIDEO, mp4: FileType.VIDEO, flv: FileType.VIDEO, mov: FileType.VIDEO, wmv: FileType.VIDEO, doc: FileType.WORD, docx: FileType.WORD, docm: FileType.WORD, dotm: FileType.WORD, dotx: FileType.WORD, txt: FileType.TEXT, rar: FileType.ZIP, zip: FileType.ZIP, pkg: FileType.ZIP, xlm: FileType.EXCEL, csv: FileType.EXCEL, xlsx: FileType.EXCEL, xls: FileType.EXCEL, xlsm: FileType.EXCEL, xlsb: FileType.EXCEL, ppt: FileType.PPT, pptx: FileType.PPT, pps: FileType.PPT, ppsx: FileType.PPT, pptm: FileType.PPT, potm: FileType.PPT, ppam: FileType.PPT, potx: FileType.PPT, ppsm: FileType.PPT, pdf: FileType.PDF, css: FileType.CODE, js: FileType.CODE, jsx: FileType.CODE, json: FileType.CODE, md: FileType.CODE, sql: FileType.CODE, bmp: FileType.IMAGE, gif: FileType.IMAGE, ico: FileType.IMAGE, jpeg: FileType.IMAGE, jpg: FileType.IMAGE, png: FileType.IMAGE, tif: FileType.IMAGE, tiff: FileType.IMAGE, svg: FileType.SVG, }; const MIME_TYPE_MAP: { [key: string]: FileType } = { 'application/x-troff-msvideo': FileType.VIDEO, 'video/avi': FileType.VIDEO, 'video/msvideo': FileType.VIDEO, 'video/x-msvideo': FileType.VIDEO, 'video/avs-video': FileType.VIDEO, 'image/bmp': FileType.IMAGE, 'image/x-windows-bmp': FileType.IMAGE, 'application/x-pointplus': FileType.CODE, 'text/css': FileType.CODE, 'application/msword': FileType.WORD, 'text/html': FileType.CODE, 'image/jpeg': FileType.IMAGE, 'image/pjpeg': FileType.IMAGE, 'image/x-jps': FileType.IMAGE, 'image/svg+xml': FileType.SVG, 'application/x-javascript': FileType.CODE, 'application/javascript': FileType.CODE, 'application/ecmascript': FileType.CODE, 'text/javascript': FileType.CODE, 'application/x-midi': FileType.AUDIO, 'audio/midi': FileType.AUDIO, 'audio/x-mid': FileType.AUDIO, 'audio/x-midi': FileType.AUDIO, 'music/crescendo': FileType.AUDIO, 'x-music/x-midi': FileType.AUDIO, 'video/quicktime': FileType.VIDEO, 'video/x-sgi-movie': FileType.VIDEO, 'audio/mpeg': FileType.AUDIO, 'audio/x-mpeg': FileType.AUDIO, 'video/mpeg': FileType.VIDEO, 'video/x-mpeg': FileType.VIDEO, 'video/x-mpeq2a': FileType.VIDEO, 'audio/mpeg3': FileType.AUDIO, 'audio/x-mpeg-3': FileType.AUDIO, 'application/pdf': FileType.PDF, 'image/png': FileType.IMAGE, 'application/mspowerpoint': FileType.PPT, 'application/vnd.ms-powerpoint': FileType.PPT, 'model/x-pov': FileType.PPT, 'application/powerpoint': FileType.PPT, 'application/x-mspowerpoint': FileType.PPT, 'application/plain': FileType.TEXT, 'text/plain': FileType.TEXT, 'image/tiff': FileType.IMAGE, 'image/x-tiff': FileType.IMAGE, 'application/excel': FileType.EXCEL, 'application/x-excel': FileType.EXCEL, 'application/x-msexcel': FileType.EXCEL, 'application/vnd.ms-excel': FileType.EXCEL, 'application/x-compressed': FileType.ZIP, 'application/x-zip-compressed': FileType.ZIP, 'application/zip': FileType.ZIP, 'multipart/x-zip': FileType.ZIP, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': FileType.WORD, 'application/vnd.openxmlformats-officedocument.wordprocessingml.template': FileType.WORD, 'application/vnd.ms-word.document.macroEnabled.12': FileType.WORD, 'application/vnd.ms-word.template.macroEnabled.12': FileType.WORD, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': FileType.EXCEL, 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': FileType.EXCEL, 'application/vnd.ms-excel.sheet.macroEnabled.12': FileType.EXCEL, 'application/vnd.ms-excel.template.macroEnabled.12': FileType.EXCEL, 'application/vnd.ms-excel.addin.macroEnabled.12': FileType.EXCEL, 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': FileType.EXCEL, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': FileType.PPT, 'application/vnd.openxmlformats-officedocument.presentationml.template': FileType.PPT, 'application/vnd.openxmlformats-officedocument.presentationml.slideshow': FileType.PPT, 'application/vnd.ms-powerpoint.addin.macroEnabled.12': FileType.PPT, 'application/vnd.ms-powerpoint.presentation.macroEnabled.12': FileType.PPT, 'application/vnd.ms-powerpoint.template.macroEnabled.12': FileType.PPT, 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12': FileType.PPT, };