Skip to content

Feature Request: more secure way to get image URL and deleteFile callback #482

Closed

Description

@ryendu

I love the image block feature and the uploadFile method in the editor that allows you to define a custom function for uploading images to your server. To improve on this, I wish there would be an equivalent deleteFile callback invoked when the image block is deleted so we can delete the image from our database to save resources. I also think that defining the image as just a public URL is not the most secure, and we could also define a getFile method that will allow developers to define a custom function to retrieve a signed URL that expires after a certain duration.

Activity

  1. i-am-chitti commented on Dec 23, 2023

    @i-am-chitti
    Contributor

    I recently used the library and felt the same blocker. Requirement of deleteFile is ➕ ➕ .
    @matthewlipski I'm interested in contributing to it. Let me know if I can take this.

  2. YousefED commented on Dec 29, 2023

    @YousefED
    Collaborator

    Thanks. Both deleteFile and getFile are good suggestions.

    What's tricky with deleteFile this is that a user could delete a block and then re-add it using cmd/ctrl+z. How would you go about this? Happy to accept contributions ofc!

  3. ahmedbna commented on Jun 9, 2024

    @ahmedbna

    Hey, any updates on deleteFile functionality?

  4. matthewlipski commented on Jun 11, 2024

    @matthewlipski
    Collaborator

    Not at the moment unfortunately:/ the updates to file blocks were done as part of a client request so most likely deleteFile will be added also from client feedback or when we have time to implement it after some higher prio issues.

  5. HenrikBechmann commented on Sep 22, 2024

    @HenrikBechmann

    I think the most direct approach to the undo problem is to provide a message to the user if a file is not successfully loaded indicating that the file is not available, and that the block should be removed and a new one added.

    A more sophisticated approach would be to provide a reload option, which would replace an existing file or supply a missing file. This would require a fileDelete trigger for any replaced file.

    How do you handle duplicate file names?

  6. HenrikBechmann commented on Sep 22, 2024

    @HenrikBechmann

    In the meantime, is there a way to extend the editor specs for useCreateBlockNote to include a onDeleteFile property?

  7. HenrikBechmann commented on Sep 23, 2024

    @HenrikBechmann

    Here's what I'm doing in the meantime (using firebase):
    My editor is modal ('display', 'edit').

    When uploading a file, I record the filename in an array, saved to the same firestore document as the editor content

        async function uploadFile(file:File) {
    
            if (editorUploadedFiles.includes(file.name)) { // avoid invalidation of the first
                alert(file.name + ' has already been uploaded') // placeholder
                return null
            }
    
            const fileRef = ref(storage, workboxHandler.editRecord.profile.workbox.id + '/document/' + file.name)
            try {
                await uploadBytes(fileRef, file)
            } catch (error) {
                console.log('An error occured uploading file.name', file.name)
                alert (error.message) // placeholder
                return null
            }
    
            editorUploadedFiles.push(file.name) // <<== save uploaded file name
    
            const url = await getDownloadURL(fileRef)
    
            return url
    
        }
    

    Deletions in edit mode before save make no change to the file list, or the storage files (which takes care of the Ctrl-Z problem).

    On save, I do this (function tucked away in a class)

        // deletes storage files not in editor files, before firestore save of edited document
        reconcileUploadedFiles = (uploadedFiles, editorFiles) => {
    
            // avoid duplicates
            const uploadedFileSet = new Set(uploadedFiles)
            const editorFileSet = new Set(editorFiles)
    
            this.removeOrphans(uploadedFileSet, editorFileSet, this)
    
            // refreshes firestore document files list to match editor files, in place
            uploadedFiles.length = 0
            editorFileSet.forEach((filename) => {
                uploadedFiles.push(filename)
            })
    
        }
    

    On cancel I do something similar, but with the cancelled uploadedFiles list compared to the previous media editor blocks

        // takes edited file list and compares with pre-edit editor.content
        revertUploadedFiles = (uploadedFiles, editorFiles) => {
    
            // avoid duplicates
            const uploadedFileSet = new Set(uploadedFiles)
            const editorFileSet = new Set(editorFiles)
    
            this.removeOrphans(uploadedFileSet, editorFileSet, this)
    
        }
    

    Here is the common removeOrphans

        private removeOrphans = (uploadedFileSet, editorFileSet, self) => {
            // delete storage files not referenced in editor files
            uploadedFileSet.forEach(async function (filename) {
                if (!editorFileSet.has(filename)) {
                    const fileRef = ref(self.internal.storage,self.workboxRecord.profile.workbox.id + '/document/' + filename )
                    try {
                        await deleteObject(fileRef)
                    } catch (error) {
                        console.log('error deleting file for revert', filename, error.message)
                    }
                }
            })
        }
    

    Still needs a recovery routine in case an internet break scrambles things, but I think this will do.

  8. HenrikBechmann commented on Sep 23, 2024

    @HenrikBechmann

    Getting the editor media file names is of course trivial

        getEditorFiles = (editorDocument) => {
            const editorFiles = []
            editorDocument.forEach((block) => {
                if (['image','video','audio','file'].includes(block.type)) {
                    editorFiles.push(block.props.name)
                }
            })
            return editorFiles
        }
    
  9. HenrikBechmann commented on Oct 8, 2024

    @HenrikBechmann

    There's a wrinkle to this. It turns out that an image can be saved to the editor using "url":"data:image/jpeg;base64...", which is to say not saved to a file at all. (Drag an image from another web page to the editor directly).

    I plan to search for these on editor save and save the image to a separate file, naming it by microsecond time.

  10. HenrikBechmann commented on Oct 12, 2024

    @HenrikBechmann

    FWIW here is the code for saving data urls to files. The idea is to save database space (firestore "documents" have a hard limit of ~1MB)

    Comments are welcome.

    So before doing other save processing, I check for data urls with

    const results = await saveDataUrlsToFiles(workboxHandler.editoreditcontent) // TODO: add error handling in results
    

    and here's the function...

    async function saveDataUrlsToFiles (editorContent) {
    
        const dataurlblocks = []
        const promises = []
    
        editorContent.forEach((block) => {
    
            if (block.type == 'image') {
                const url = block.props.url
                const urlprotocol = url.split(':')[0]
                if (urlprotocol =='data') {
                    dataurlblocks.push(block)
                }
            }
        })
    
        dataurlblocks.forEach((block) => {
    
            const promise = new Promise((resolve,reject)=>{
    
                let extension
    
                const 
                    url = block.props.url,
                    type = url.split(':')[1].split(';')[0],
                    pixelRatio = window.devicePixelRatio
    
                extension = type.split('/')[1]
                // obtain dimensions
                const image = document.createElement('img') as HTMLImageElement
    
                image.onload = onLoad
                image.src = url // trigger onload
    
                function onLoad() { // tranlsate image to canvas
                    const 
                        image = this,
                        { naturalWidth, naturalHeight} = image,
                        canvas = document.createElement('canvas') as HTMLCanvasElement,
                        ctx = canvas.getContext('2d')
    
                    canvas.width = Math.floor(naturalWidth * pixelRatio)
                    canvas.height = Math.floor(naturalHeight * pixelRatio) 
    
                    ctx.scale(pixelRatio, pixelRatio)
                    ctx.imageSmoothingQuality = 'high'
                    ctx.save()
    
                    ctx.drawImage(
                        image,
                        0,0,
                        image.naturalWidth,
                        image.naturalHeight,
                        0,0,
                        image.naturalWidth,
                        image.naturalHeight
                    )
    
                    ctx.restore()
    
                    setTimeout(()=>{ // guarantee unique filename, based on milliseconds
                        canvas.toBlob(blobCallback, type)
                    },1)
                }
    
                async function blobCallback(blob) { // get and save blob; update editor block data
    
                    const 
                        fileName = Date.now() + '.' + extension, // filename = milliseconds + extension
                        { editRecord } = workboxHandler,
                        fileRef = ref(storage, editRecord.profile.workbox.id + '/document/' + fileName)
    
                    if (!fileRef) {
                        console.log('no fileRef')
                        reject()
                        return
                    }
    
                    let url
                    try {
                        await uploadBytes(fileRef, blob)
                        url = await getDownloadURL(fileRef)
    
                    } catch (error) {
                        console.log(error.message)
                        reject()
                    }
                    // update block
                    block.props.url = url
                    block.props.name = fileName
                    editRecord.document.files.push(fileName)
                    resolve(true)
                }
            })
    
            promises.push(promise)
    
        })
    
        return Promise.allSettled(promises)
    }
    
  11. pumuckelo commented on Feb 2, 2025

    @pumuckelo

    Hey @YousefED , is there any update on this?

    Ideally we should still expose something like onDeleteBlock or similar.
    A user can then decide what he wants to do with that information (e.g. set a timeout to delete the image if a user hasn't pressed ctrl+z after a few seconds)

    +1 to the getFile method for signed urls.

    How are you currently dealing with the signed url issue? The only workarounds that I can currently think of are to use a proxy service that returns the signed url or to add a custom image block that handles this.

  12. self-assigned this
    on Apr 7, 2025
  13. hunterkiernan commented on Oct 28, 2025

    @hunterkiernan

    Hi everyone. I too would love a getFile like solution for signed urls. Thanks!

  14. added
    needs-triageIssue has not yet been reviewed or classified by maintainers.
    on Feb 3, 2026
  15. locked and limited conversation to collaborators on Feb 25, 2026
  16. converted this issue into a discussion #2515 on Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

needs-triageIssue has not yet been reviewed or classified by maintainers.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions