diff --git a/app/stores/DocumentsStore.ts b/app/stores/DocumentsStore.ts index 2e23f85b3..de8453f7d 100644 --- a/app/stores/DocumentsStore.ts +++ b/app/stores/DocumentsStore.ts @@ -1,4 +1,3 @@ -import path from "path"; import invariant from "invariant"; import { find, orderBy, filter, compact, omitBy } from "lodash"; import { observable, action, computed, runInAction } from "mobx"; @@ -14,6 +13,7 @@ import Team from "~/models/Team"; import env from "~/env"; import { FetchOptions, PaginationParams, SearchResult } from "~/types"; import { client } from "~/utils/ApiClient"; +import { extname } from "~/utils/files"; type FetchPageParams = PaginationParams & { template?: boolean; @@ -603,7 +603,7 @@ export default class DocumentsStore extends BaseStore { if ( file.type && !this.importFileTypes.includes(file.type) && - !this.importFileTypes.includes(path.extname(file.name)) + !this.importFileTypes.includes(extname(file.name)) ) { throw new Error(`The selected file type is not supported (${file.type})`); } diff --git a/app/utils/files.test.ts b/app/utils/files.test.ts new file mode 100644 index 000000000..d2b91b55b --- /dev/null +++ b/app/utils/files.test.ts @@ -0,0 +1,12 @@ +import { extname } from "./files"; + +describe("#extname", () => { + test("should extract file extension from string", () => { + expect(extname("one.doc")).toBe(".doc"); + expect(extname("one.test.ts")).toBe(".ts"); + expect(extname(".DS_Store")).toBe(""); + expect(extname("directory/one.pdf")).toBe(".pdf"); + expect(extname("../relative/one.doc")).toBe(".doc"); + expect(extname(".hidden/directory/one.txt")).toBe(".txt"); + }); +}); diff --git a/app/utils/files.ts b/app/utils/files.ts index 564e3393a..6607a6b12 100644 --- a/app/utils/files.ts +++ b/app/utils/files.ts @@ -87,3 +87,66 @@ export const dataUrlToBlob = (dataURL: string) => { }); return file; }; + +const CHAR_FORWARD_SLASH = 47; /* / */ +const CHAR_DOT = 46; /* . */ + +// Based on the NodeJS Library https://github.com/nodejs/node/blob/896b75a4da58a7283d551c4595e0aa454baca3e0/lib/path.js +// Copyright Joyent, Inc. and other Node contributors. +export const extname = (path: string) => { + if (typeof path !== "string") { + throw new TypeError( + `The "path" argument must be of type string. Received type ${typeof path}` + ); + } + + let startDot = -1; + let startPart = 0; + let end = -1; + let matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + let preDotState = 0; + for (let i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === CHAR_FORWARD_SLASH) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === CHAR_DOT) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) { + startDot = i; + } else if (preDotState !== 1) { + preDotState = 1; + } + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if ( + startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) + ) { + return ""; + } + return path.slice(startDot, end); +};