BlockNote icon indicating copy to clipboard operation
BlockNote copied to clipboard

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

Open ryendu opened this issue 1 year ago • 6 comments

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.

ryendu avatar Dec 16 '23 21:12 ryendu

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.

i-am-chitti avatar Dec 23 '23 16:12 i-am-chitti

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!

YousefED avatar Dec 29 '23 15:12 YousefED

Hey, any updates on deleteFile functionality?

ahmedbna avatar Jun 09 '24 23:06 ahmedbna

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.

matthewlipski avatar Jun 11 '24 10:06 matthewlipski

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?

HenrikBechmann avatar Sep 22 '24 12:09 HenrikBechmann

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

HenrikBechmann avatar Sep 22 '24 13:09 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.

HenrikBechmann avatar Sep 23 '24 23:09 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
    }

HenrikBechmann avatar Sep 23 '24 23:09 HenrikBechmann