diff --git a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts index 7de7b2f551..edcd1c8f72 100644 --- a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts +++ b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts @@ -2,9 +2,10 @@ import { Readable } from 'stream'; import type { ILocalFileSourceProviderOptions } from '..'; import { createLocalFileSourceProvider } from '..'; +import { isFilePathInDirname, isPathEquivalent, unknownPathToPosix } from '../utils'; -describe('Stream assets', () => { - test('returns a stream', () => { +describe('File source provider', () => { + test('returns assets stream', () => { const options: ILocalFileSourceProviderOptions = { file: { path: './test-file', @@ -21,4 +22,108 @@ describe('Stream assets', () => { expect(stream instanceof Readable).toBeTruthy(); }); + + describe('utils', () => { + const unknownConversionCases = [ + ['some/path/on/posix', 'some/path/on/posix'], + ['some/path/on/posix/', 'some/path/on/posix/'], + ['some/path/on/posix.jpg', 'some/path/on/posix.jpg'], + ['file.jpg', 'file.jpg'], + ['noextension', 'noextension'], + ['some\\windows\\filename.jpg', 'some/windows/filename.jpg'], + ['some\\windows\\noendingslash', 'some/windows/noendingslash'], + ['some\\windows\\endingslash\\', 'some/windows/endingslash/'], + ]; + test.each(unknownConversionCases)('unknownPathToPosix: %p -> %p', (input, expected) => { + expect(unknownPathToPosix(input)).toEqual(expected); + }); + + const isFilePathInDirnameCases: [string, string, boolean][] = [ + // posix paths + ['some/path/on/posix', 'some/path/on/posix/file.jpg', true], + ['some/path/on/posix/', 'some/path/on/posix/file.jpg', true], + ['./some/path/on/posix', 'some/path/on/posix/file.jpg', true], + ['some/path/on/posix/', './some/path/on/posix/file.jpg', true], + ['some/path/on/posix/', 'some/path/on/posix/', false], // invalid; second method should include a filename + ['some/path/on/posix', 'some/path/on/posix', false], // 'posix' in second case should be interpreted as a filename + ['', 'file.jpg', true], + ['', './file.jpg', true], + ['./', './file.jpg', true], + ['noextension', 'noextension', false], // second case is a file + ['noextension', './noextension/file.jpg', true], + ['./noextension', './noextension/file.jpg', true], + ['./noextension', 'noextension/file.jpg', true], + ['noextension', 'noextension/noextension', true], + // win32 paths + ['some/path/on/win32', 'some\\path\\on\\win32\\file.jpg', true], + ['some/path/on/win32/', 'some\\path\\on\\win32\\file.jpg', true], + ['some/path/on/win32/', 'some\\path\\on\\win32\\', false], // invalid; second method should include a filename + ['some/path/on/win32', 'some\\path\\on\\win32', false], // 'win32' in second case should be interpreted as a filename + ['', 'file.jpg', true], + ['', '.\\file.jpg', true], + ['./', '.\\file.jpg', true], + ['noextension', 'noextension', false], // second case is a file + ['noextension', '.\\noextension\\file.jpg', true], + ['./noextension', '.\\noextension\\file.jpg', true], + ['./noextension', 'noextension\\file.jpg', true], + ['noextension', 'noextension\\noextension', true], + ]; + test.each(isFilePathInDirnameCases)( + 'isFilePathInDirname: %p : %p -> %p', + (inputA, inputB, expected) => { + expect(isFilePathInDirname(inputA, inputB)).toEqual(expected); + } + ); + + const isPathEquivalentCases: [string, string, boolean][] = [ + // POSITIVES + // root level + ['file.jpg', 'file.jpg', true], + ['file.jpg', '.\\file.jpg', true], + ['file.jpg', './file.jpg', true], + // cwd root level (posix) + ['./file.jpg', 'file.jpg', true], + ['./file.jpg', './file.jpg', true], + ['./file.jpg', '.\\file.jpg', true], + // cwd root level (win32) + ['.\\file.jpg', 'file.jpg', true], + ['.\\file.jpg', './file.jpg', true], + ['.\\file.jpg', '.\\file.jpg', true], + // directory with file (posix) + ['one/two/file.jpg', 'one/two/file.jpg', true], + ['one/two/file.jpg', './one/two/file.jpg', true], + ['one/two/file.jpg', 'one\\two\\file.jpg', true], + ['one/two/file.jpg', '.\\one\\two\\file.jpg', true], + // cwd with file (posix) + ['./one/two/file.jpg', 'one/two/file.jpg', true], + ['./one/two/file.jpg', './one/two/file.jpg', true], + ['./one/two/file.jpg', 'one\\two\\file.jpg', true], + ['./one/two/file.jpg', '.\\one\\two\\file.jpg', true], + // directory with file (win32) + ['one\\two\\file.jpg', 'one/two/file.jpg', true], + ['one\\two\\file.jpg', './one/two/file.jpg', true], + ['one\\two\\file.jpg', '.\\one\\two\\file.jpg', true], + ['one\\two\\file.jpg', 'one\\two\\file.jpg', true], + // cwd with file (win32) + ['.\\one\\two\\file.jpg', 'one/two/file.jpg', true], + ['.\\one\\two\\file.jpg', './one/two/file.jpg', true], + ['.\\one\\two\\file.jpg', '.\\one\\two\\file.jpg', true], + ['.\\one\\two\\file.jpg', 'one\\two\\file.jpg', true], + + // NEGATIVES + ['file.jpg', 'one/file.jpg', false], + ['file.jpg', 'one\\file.jpg', false], + ['file.jpg', '/file.jpg', false], + ['file.jpg', '\\file.jpg', false], + ['one/file.jpg', '\\one\\file.jpg', false], + ['one/file.jpg', '/one/file.jpg', false], + ['one/file.jpg', 'file.jpg', false], + ]; + test.each(isPathEquivalentCases)( + 'isPathEquivalent: %p : %p -> %p', + (inputA, inputB, expected) => { + expect(isPathEquivalent(inputA, inputB)).toEqual(expected); + } + ); + }); }); diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 119d527aee..53e5f746bb 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -15,7 +15,7 @@ import type { IAsset, IMetadata, ISourceProvider, ProviderType } from '../../../ import { createDecryptionCipher } from '../../../utils/encryption'; import { collect } from '../../../utils/stream'; import { ProviderInitializationError, ProviderTransferError } from '../../../errors/providers'; -import { isDirPathEquivalent, isPathEquivalent, unknownPathToPosix } from './utils'; +import { isFilePathInDirname, isPathEquivalent, unknownPathToPosix } from './utils'; type StreamItemArray = Parameters[0]; @@ -143,7 +143,7 @@ class LocalFileSourceProvider implements ISourceProvider { if (entry.type !== 'File') { return false; } - return isDirPathEquivalent('assets/uploads', filePath); + return isFilePathInDirname('assets/uploads', filePath); }, onentry(entry) { // TODO: Check if we need to handle win32 paths here for the assets @@ -203,7 +203,7 @@ class LocalFileSourceProvider implements ISourceProvider { return false; } - return isDirPathEquivalent(directory, filePath); + return isFilePathInDirname(directory, filePath); }, async onentry(entry) { diff --git a/packages/core/data-transfer/src/file/providers/source/utils.ts b/packages/core/data-transfer/src/file/providers/source/utils.ts index ced1cf0f7d..60111f5da3 100644 --- a/packages/core/data-transfer/src/file/providers/source/utils.ts +++ b/packages/core/data-transfer/src/file/providers/source/utils.ts @@ -12,23 +12,37 @@ import path from 'path'; * * */ -// Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName -// We must be able to assume the first argument is a path, otherwise path.dirname will interpret a path without any slashes as the filename -export const isDirPathEquivalent = (posixDirName: string, filePath: string) => { +/** + * Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName + * We must be able to assume the first argument is a path to a directory and the second is a path to a file, otherwise path.dirname will interpret a path without any slashes as the filename + * + * @param {string} posixDirName A posix path pointing to a directory + * @param {string} filePath an unknown filesystem path pointing to a file + * @returns {boolean} is the file located in the given directory + */ +export const isFilePathInDirname = (posixDirName: string, filePath: string) => { const normalizedDir = path.posix.dirname(unknownPathToPosix(filePath)); return isPathEquivalent(posixDirName, normalizedDir); }; -// Check if two paths that can be either in posix or win32 format resolves to the same file -export const isPathEquivalent = (fileA: string, fileB: string) => { +/** + * Check if two paths that can be either in posix or win32 format resolves to the same file + * + * @param {string} pathA a path that may be either win32 or posix + * @param {string} pathB a path that may be either win32 or posix + * + * @returns {boolean} do paths point to the same place + */ +export const isPathEquivalent = (pathA: string, pathB: string) => { // Check if paths appear to be win32 or posix, and if win32 convert to posix - const normalizedPathA = unknownPathToPosix(fileA); - const normalizedPathB = unknownPathToPosix(fileB); + const normalizedPathA = path.posix.normalize(unknownPathToPosix(pathA)); + const normalizedPathB = path.posix.normalize(unknownPathToPosix(pathB)); return !path.posix.relative(normalizedPathB, normalizedPathA).length; }; /** + * Convert an unknown format path (win32 or posix) to a posix path * * @param {string} filePath a path that may be either win32 or posix *