From 0a0cccedd999e0aee01faf7707098b392635c036 Mon Sep 17 00:00:00 2001 From: Max Grollmann <48564150+maxgr0@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:12:54 +0200 Subject: [PATCH] strip query strings when deriving file names from media URLs (#12993) fixes #12992 Co-authored-by: mg --- .../editors/TemplateEditor/PdfPreview/PdfPreview.tsx | 3 ++- .../editors/TemplateEditor/TemplateEditor.tsx | 3 ++- src/frontend/src/components/items/AttachmentLink.tsx | 7 ++++--- src/frontend/src/functions/urls.tsx | 12 ++++++++++++ src/frontend/src/tables/settings/SnippetTable.tsx | 6 ++---- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/frontend/src/components/editors/TemplateEditor/PdfPreview/PdfPreview.tsx b/src/frontend/src/components/editors/TemplateEditor/PdfPreview/PdfPreview.tsx index 3536f92c2e..100fca3ce1 100644 --- a/src/frontend/src/components/editors/TemplateEditor/PdfPreview/PdfPreview.tsx +++ b/src/frontend/src/components/editors/TemplateEditor/PdfPreview/PdfPreview.tsx @@ -5,6 +5,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; import { t } from '@lingui/core/macro'; import { api } from '../../../../App'; +import { fileNameFromUrl } from '../../../../functions/urls'; import type { PreviewAreaComponent } from '../TemplateEditor'; export const PdfPreviewComponent: PreviewAreaComponent = forwardRef( @@ -22,7 +23,7 @@ export const PdfPreviewComponent: PreviewAreaComponent = forwardRef( const formData = new FormData(); const filename = - template.template?.split('/').pop() ?? 'template.html'; + fileNameFromUrl(template.template) || 'template.html'; formData.append('template', new File([code], filename)); diff --git a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx index ef5a0e4587..803fc331ef 100644 --- a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx +++ b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx @@ -30,6 +30,7 @@ import Split from '@uiw/react-split'; import type React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../../../App'; +import { fileNameFromUrl } from '../../../functions/urls'; import type { TemplateI } from '../../../tables/settings/TemplateTable'; import { SplitButton } from '../../buttons/SplitButton'; import { StandaloneField } from '../../forms/StandaloneField'; @@ -263,7 +264,7 @@ export function TemplateEditor(props: Readonly) { if (code === undefined) return; const filename = - (template as any)[fileField]?.split('/').pop() ?? 'template.html'; + fileNameFromUrl((template as any)[fileField]) || 'template.html'; const formData = new FormData(); formData.append(fileField, new File([code], filename)); diff --git a/src/frontend/src/components/items/AttachmentLink.tsx b/src/frontend/src/components/items/AttachmentLink.tsx index 4836cd122b..0995ee20c0 100644 --- a/src/frontend/src/components/items/AttachmentLink.tsx +++ b/src/frontend/src/components/items/AttachmentLink.tsx @@ -11,7 +11,7 @@ import { IconPhoto } from '@tabler/icons-react'; import { type ReactNode, useMemo } from 'react'; -import { generateUrl } from '../../functions/urls'; +import { fileNameFromUrl, generateUrl } from '../../functions/urls'; import { Thumbnail } from '../images/Thumbnail'; /** @@ -24,7 +24,8 @@ export function attachmentIcon(attachment: string): ReactNode { return ; } - const suffix = attachment.split('.').pop()?.toLowerCase() ?? ''; + const suffix = + fileNameFromUrl(attachment).split('.').pop()?.toLowerCase() ?? ''; switch (suffix) { case 'pdf': return ; @@ -80,7 +81,7 @@ export function AttachmentLink({ return '-'; } - return external ? attachment : (attachment.split('/').pop() ?? '-'); + return external ? attachment : fileNameFromUrl(attachment) || '-'; }, [attachment, external]); return ( diff --git a/src/frontend/src/functions/urls.tsx b/src/frontend/src/functions/urls.tsx index 830b82b215..21389dff73 100644 --- a/src/frontend/src/functions/urls.tsx +++ b/src/frontend/src/functions/urls.tsx @@ -24,3 +24,15 @@ export function generateUrl(url: string | URL, base?: string): string { return newUrl.toString(); } + +/** + * Return the file name part of a file URL or storage path. + * Storage backends such as S3 return presigned URLs, so any query string or fragment is dropped before taking the last path segment. + */ +export function fileNameFromUrl(url?: string | null): string { + if (!url) { + return ''; + } + + return url.split('?')[0].split('#')[0].split('/').pop() ?? ''; +} diff --git a/src/frontend/src/tables/settings/SnippetTable.tsx b/src/frontend/src/tables/settings/SnippetTable.tsx index b89e8de010..a31057d741 100644 --- a/src/frontend/src/tables/settings/SnippetTable.tsx +++ b/src/frontend/src/tables/settings/SnippetTable.tsx @@ -30,6 +30,7 @@ import { import { AttachmentLink } from '../../components/items/AttachmentLink'; import { DescriptionColumn } from '../../components/tables/ColumnRenderers'; import { InvenTreeTable } from '../../components/tables/InvenTreeTable'; +import { fileNameFromUrl } from '../../functions/urls'; import { useCreateApiFormModal, useDeleteApiFormModal, @@ -59,10 +60,7 @@ export function SnippetDrawer({ pk: id }); - const filename = useMemo( - () => snippet?.snippet?.split('/').pop() ?? '', - [snippet] - ); + const filename = useMemo(() => fileNameFromUrl(snippet?.snippet), [snippet]); if (isFetching) { return ;