strip query strings when deriving file names from media URLs (#12993)

fixes #12992

Co-authored-by: mg <mg@layerx-defence.com>
This commit is contained in:
Max Grollmannandmg authored and GitHub committed 2026-10-12 14:12:54 +11:00
1 parent ead118cec5
commit 0a0cccedd9
5 files changed
+22 -9

No files matched your search

@@ -5,6 +5,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { api } from '../../../../App'; import { api } from '../../../../App';
import { fileNameFromUrl } from '../../../../functions/urls';
import type { PreviewAreaComponent } from '../TemplateEditor'; import type { PreviewAreaComponent } from '../TemplateEditor';
export const PdfPreviewComponent: PreviewAreaComponent = forwardRef( export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
@@ -22,7 +23,7 @@ export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
const formData = new FormData(); const formData = new FormData();
const filename = const filename =
template.template?.split('/').pop() ?? 'template.html'; fileNameFromUrl(template.template) || 'template.html';
formData.append('template', new File([code], filename)); formData.append('template', new File([code], filename));
@@ -30,6 +30,7 @@ import Split from '@uiw/react-split';
import type React from 'react'; import type React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api } from '../../../App'; import { api } from '../../../App';
import { fileNameFromUrl } from '../../../functions/urls';
import type { TemplateI } from '../../../tables/settings/TemplateTable'; import type { TemplateI } from '../../../tables/settings/TemplateTable';
import { SplitButton } from '../../buttons/SplitButton'; import { SplitButton } from '../../buttons/SplitButton';
import { StandaloneField } from '../../forms/StandaloneField'; import { StandaloneField } from '../../forms/StandaloneField';
@@ -263,7 +264,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
if (code === undefined) return; if (code === undefined) return;
const filename = const filename =
(template as any)[fileField]?.split('/').pop() ?? 'template.html'; fileNameFromUrl((template as any)[fileField]) || 'template.html';
const formData = new FormData(); const formData = new FormData();
formData.append(fileField, new File([code], filename)); formData.append(fileField, new File([code], filename));
@@ -11,7 +11,7 @@ import {
IconPhoto IconPhoto
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import { generateUrl } from '../../functions/urls'; import { fileNameFromUrl, generateUrl } from '../../functions/urls';
import { Thumbnail } from '../images/Thumbnail'; import { Thumbnail } from '../images/Thumbnail';
/** /**
@@ -24,7 +24,8 @@ export function attachmentIcon(attachment: string): ReactNode {
return <IconFileUnknown size={sz} />; return <IconFileUnknown size={sz} />;
} }
const suffix = attachment.split('.').pop()?.toLowerCase() ?? ''; const suffix =
fileNameFromUrl(attachment).split('.').pop()?.toLowerCase() ?? '';
switch (suffix) { switch (suffix) {
case 'pdf': case 'pdf':
return <IconFileTypePdf size={sz} />; return <IconFileTypePdf size={sz} />;
@@ -80,7 +81,7 @@ export function AttachmentLink({
return '-'; return '-';
} }
return external ? attachment : (attachment.split('/').pop() ?? '-'); return external ? attachment : fileNameFromUrl(attachment) || '-';
}, [attachment, external]); }, [attachment, external]);
return ( return (
+12
View File
@@ -24,3 +24,15 @@ export function generateUrl(url: string | URL, base?: string): string {
return newUrl.toString(); 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() ?? '';
}
@@ -30,6 +30,7 @@ import {
import { AttachmentLink } from '../../components/items/AttachmentLink'; import { AttachmentLink } from '../../components/items/AttachmentLink';
import { DescriptionColumn } from '../../components/tables/ColumnRenderers'; import { DescriptionColumn } from '../../components/tables/ColumnRenderers';
import { InvenTreeTable } from '../../components/tables/InvenTreeTable'; import { InvenTreeTable } from '../../components/tables/InvenTreeTable';
import { fileNameFromUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useDeleteApiFormModal, useDeleteApiFormModal,
@@ -59,10 +60,7 @@ export function SnippetDrawer({
pk: id pk: id
}); });
const filename = useMemo( const filename = useMemo(() => fileNameFromUrl(snippet?.snippet), [snippet]);
() => snippet?.snippet?.split('/').pop() ?? '',
[snippet]
);
if (isFetching) { if (isFetching) {
return <LoadingOverlay visible={true} />; return <LoadingOverlay visible={true} />;