refactor(media): store filenames, use pre-signed s3/azure URLs, UUIDs

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-06-12 18:45:49 +02:00 committed by Philip Molares
parent 4132833b5d
commit 157a0fe278
47 changed files with 869 additions and 389 deletions

View file

@ -3,8 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
const imageId = 'non-existing.png'
const fakeUuid = '77fdcf1c-35fa-4a65-bdcf-1c35fa8a65d5'
describe('File upload', () => {
beforeEach(() => {
@ -22,7 +21,8 @@ describe('File upload', () => {
{
statusCode: 201,
body: {
id: imageId
uuid: fakeUuid,
fileName: 'demo.png'
}
}
)
@ -38,7 +38,7 @@ describe('File upload', () => {
},
{ force: true }
)
cy.get('.cm-line').contains(`![demo.png](http://127.0.0.1:3001/api/private/media/${imageId})`)
cy.get('.cm-line').contains(`![demo.png](http://127.0.0.1:3001/media/${fakeUuid})`)
})
it('via paste', () => {
@ -51,7 +51,7 @@ describe('File upload', () => {
}
}
cy.get('.cm-content').trigger('paste', pasteEvent)
cy.get('.cm-line').contains(`![](http://127.0.0.1:3001/api/private/media/${imageId})`)
cy.get('.cm-line').contains(`![](http://127.0.0.1:3001/media/${fakeUuid})`)
})
})
@ -65,7 +65,7 @@ describe('File upload', () => {
},
{ action: 'drag-drop', force: true }
)
cy.get('.cm-line').contains(`![demo.png](http://127.0.0.1:3001/api/private/media/${imageId})`)
cy.get('.cm-line').contains(`![demo.png](http://127.0.0.1:3001/media/${fakeUuid})`)
})
})

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -31,7 +31,7 @@ export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyRespons
* @return The URL of the uploaded media object.
* @throws {Error} when the api request wasn't successful.
*/
export const uploadFile = async (noteIdOrAlias: string, media: Blob): Promise<MediaUpload> => {
export const uploadFile = async (noteIdOrAlias: string, media: File): Promise<MediaUpload> => {
const postData = new FormData()
postData.append('file', media)
const response = await new PostApiRequestBuilder<MediaUpload, void>('media')

View file

@ -4,10 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface MediaUpload {
id: string
uuid: string
fileName: string
noteId: string | null
createdAt: string
username: string
username: string | null
}
export interface ImageProxyResponse {

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -60,8 +60,8 @@ export const useHandleUpload = (): handleUploadSignature => {
return replaceSelection(cursorSelection ?? currentSelection, uploadPlaceholder, false)
})
uploadFile(noteId, file)
.then(({ id }) => {
const fullUrl = `${baseUrl}api/private/media/${id}`
.then(({ uuid }) => {
const fullUrl = `${baseUrl}media/${uuid}`
const replacement = `![${description ?? file.name ?? ''}](${fullUrl}${additionalUrlText ?? ''})`
changeContent(({ markdownContent }) => [
replaceInContent(markdownContent, uploadPlaceholder, replacement),

View file

@ -49,7 +49,7 @@ export const MediaBrowserSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
if (loading || error || !value) {
return []
}
return value.map((entry) => <MediaEntry entry={entry} key={entry.id} onDelete={setMediaEntryForDeletion} />)
return value.map((entry) => <MediaEntry entry={entry} key={entry.uuid} onDelete={setMediaEntryForDeletion} />)
}, [value, loading, error, setMediaEntryForDeletion])
const cancelDeletion = useCallback(() => {

View file

@ -25,7 +25,7 @@ export const MediaEntryDeletionModal: React.FC<MediaEntryDeletionModalProps> = (
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
const handleDelete = useCallback(() => {
deleteUploadedMedia(entry.id)
deleteUploadedMedia(entry.uuid)
.then(() => {
dispatchUiNotification('common.success', 'editor.mediaBrowser.mediaDeleted', {})
})

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.preview {
max-width: 100%;
max-height: 150px;
height: auto;
width: auto;
}

View file

@ -11,13 +11,15 @@ import {
Trash as IconTrash,
FileRichtextFill as IconFileRichtextFill,
Person as IconPerson,
Clock as IconClock
Clock as IconClock,
FileText as IconFileText
} from 'react-bootstrap-icons'
import { useIsOwner } from '../../../../../hooks/common/use-is-owner'
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
import { UserAvatarForUsername } from '../../../../common/user-avatar/user-avatar-for-username'
import { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../../../editor-pane/tool-bar/formatters/replace-selection'
import styles from './media-entry.module.css'
export interface MediaEntryProps {
entry: MediaUpload
@ -37,7 +39,7 @@ export const MediaEntry: React.FC<MediaEntryProps> = ({ entry, onDelete }) => {
const isOwner = useIsOwner()
const imageUrl = useMemo(() => {
return `${baseUrl}api/private/media/${entry.id}`
return `${baseUrl}media/${entry.uuid}`
}, [entry, baseUrl])
const textCreatedTime = useMemo(() => {
return new Date(entry.createdAt).toLocaleString()
@ -47,7 +49,7 @@ export const MediaEntry: React.FC<MediaEntryProps> = ({ entry, onDelete }) => {
changeEditorContent?.(({ currentSelection }) => {
return replaceSelection(
{ from: currentSelection.to ?? currentSelection.from },
`![${entry.id}](${imageUrl})`,
`![${entry.fileName}](${imageUrl})`,
true
)
})
@ -61,10 +63,15 @@ export const MediaEntry: React.FC<MediaEntryProps> = ({ entry, onDelete }) => {
<div className={'p-2 border-bottom border-opacity-50'}>
<a href={imageUrl} target={'_blank'} rel={'noreferrer'} className={'text-center d-block mb-2'}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={imageUrl} alt={`Upload ${entry.id}`} height={100} className={'mw-100'} />
<img src={imageUrl} alt={`Upload ${entry.fileName}`} className={styles.preview} />
</a>
<div className={'w-100 d-flex flex-row align-items-center justify-content-between'}>
<div>
<small>
<IconFileText className={'me-1'} />
{entry.fileName}
</small>
<br />
<small className={'d-inline-flex flex-row align-items-center'}>
<IconPerson className={'me-1'} />
<UserAvatarForUsername username={entry.username} size={'sm'} />

View file

@ -12,13 +12,15 @@ const handler = (req: NextApiRequest, res: NextApiResponse) => {
{
username: 'tilman',
createdAt: '2022-03-20T20:36:32Z',
id: 'dummy.png',
uuid: '5355ed83-7e12-4db0-95ed-837e124db08c',
fileName: 'dummy.png',
noteId: 'features'
},
{
username: 'tilman',
createdAt: '2022-03-20T20:36:57+0000',
id: 'dummy.png',
uuid: '656745ab-fbf9-47f1-a745-abfbf9a7f10c',
fileName: 'dummy2.png',
noteId: null
}
])

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -20,7 +20,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse): Promise<void>
req,
res,
{
id: '/public/img/avatar.png',
uuid: 'e81f57cd-5866-4253-9f57-cd5866a253ca',
fileName: 'avatar.png',
noteId: null,
username: 'test',
createdAt: '2022-02-27T21:54:23.856Z'