diff --git a/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts index fcdfa9099c..13c96e4863 100644 --- a/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts +++ b/foundations/core/packages/storage-client/src/__tests__/datalake-storage.test.ts @@ -57,14 +57,24 @@ describe('DatalakeStorage', () => { expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}`) }) - it('should handle special characters in parameters', () => { + it('should encode special characters in parameters', () => { const workspace = 'test workspace' const file = 'file 123' const filename = 'my document.pdf' const url = storage.getFileUrl(workspace, file, filename) - expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}/${filename}`) + expect(url).toBe(`${baseUrl}/blob/test%20workspace/file%20123/my%20document.pdf`) + }) + + it('should encode slash in filename as a single path segment', () => { + const workspace = 'test-workspace' + const file = 'file-123' + const filename = 'folder/document.pdf' + + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/blob/${workspace}/${file}/folder%2Fdocument.pdf`) }) it('should handle base URL with trailing slash', () => { diff --git a/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts b/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts index 5c8e80c6ef..fe8f127f98 100644 --- a/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts +++ b/foundations/core/packages/storage-client/src/__tests__/front-storage.test.ts @@ -55,14 +55,24 @@ describe('FrontStorage', () => { expect(url).toBe(`${baseUrl}/${workspace}/${filename}?file=${file}&workspace=${workspace}`) }) - it('should handle special characters in workspace and file names', () => { + it('should encode special characters in workspace, file, and filename', () => { const workspace = 'test workspace' const file = 'file 123' const filename = 'my document.pdf' const url = storage.getFileUrl(workspace, file, filename) - expect(url).toBe(`${baseUrl}/${workspace}/${filename}?file=${file}&workspace=${workspace}`) + expect(url).toBe(`${baseUrl}/test%20workspace/my%20document.pdf?file=file%20123&workspace=test%20workspace`) + }) + + it('should encode slash in filename as a single path segment', () => { + const workspace = 'test-workspace' + const file = 'file-123' + const filename = 'folder/document.pdf' + + const url = storage.getFileUrl(workspace, file, filename) + + expect(url).toBe(`${baseUrl}/${workspace}/folder%2Fdocument.pdf?file=${file}&workspace=${workspace}`) }) it('should handle base URL with trailing slash', () => { diff --git a/foundations/core/packages/storage-client/src/client/datalake.ts b/foundations/core/packages/storage-client/src/client/datalake.ts index ad1dc1f7aa..97ef5884ae 100644 --- a/foundations/core/packages/storage-client/src/client/datalake.ts +++ b/foundations/core/packages/storage-client/src/client/datalake.ts @@ -16,6 +16,7 @@ import { concatLink } from '@hcengineering/core' import { FileStorage, FileStorageUploadOptions } from '../types' import { uploadMultipart, uploadXhr } from '../upload' +import { encodePathSegment } from './utils' const getPathname = (url: string): string => { const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost' @@ -27,7 +28,12 @@ export class DatalakeStorage implements FileStorage { constructor (private readonly baseUrl: string) {} getFileUrl (workspace: string, file: string, filename?: string): string { - const path = filename !== undefined ? `/blob/${workspace}/${file}/${filename}` : `/blob/${workspace}/${file}` + const encodedWorkspace = encodePathSegment(workspace) + const encodedFile = encodePathSegment(file) + const path = + filename !== undefined + ? `/blob/${encodedWorkspace}/${encodedFile}/${encodePathSegment(filename)}` + : `/blob/${encodedWorkspace}/${encodedFile}` return concatLink(this.baseUrl, path) } diff --git a/foundations/core/packages/storage-client/src/client/front.ts b/foundations/core/packages/storage-client/src/client/front.ts index 54fa4cd0b9..7dfa5d6896 100644 --- a/foundations/core/packages/storage-client/src/client/front.ts +++ b/foundations/core/packages/storage-client/src/client/front.ts @@ -16,6 +16,7 @@ import { concatLink } from '@hcengineering/core' import { FileStorage, FileStorageUploadOptions } from '../types' import { uploadXhr } from '../upload' +import { encodePathSegment } from './utils' const getPathname = (url: string): string => { const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost' @@ -27,7 +28,10 @@ export class FrontStorage implements FileStorage { constructor (private readonly baseUrl: string) {} getFileUrl (workspace: string, file: string, filename?: string): string { - const path = `/${workspace}/${filename ?? file}?file=${file}&workspace=${workspace}` + const encodedWorkspace = encodePathSegment(workspace) + const encodedFile = encodePathSegment(file) + const encodedFilename = encodePathSegment(filename ?? file) + const path = `/${encodedWorkspace}/${encodedFilename}?file=${encodedFile}&workspace=${encodedWorkspace}` return concatLink(this.baseUrl, path) } diff --git a/foundations/core/packages/storage-client/src/client/utils.ts b/foundations/core/packages/storage-client/src/client/utils.ts new file mode 100644 index 0000000000..e6e41b9a6b --- /dev/null +++ b/foundations/core/packages/storage-client/src/client/utils.ts @@ -0,0 +1,18 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export function encodePathSegment (value: string): string { + return encodeURIComponent(value) +}