5

I am using Express.js with Typescript and I would like to send a UInt8Array as binary data.

This is what I use so far and it works, but I would like not to save the file before, because I think it wastes performance:

const filePath = path.resolve(__dirname, 'template.docx');
const template = fs.readFileSync(filePath);
const buffer: Uint8Array = await createReport({
  template,
  data: {
    productCode: data.productCode,
  },
});
fs.writeFileSync(path.resolve(__dirname, 'output.docx'), buffer);
res.sendFile(path.resolve(__dirname, 'output.docx'));

I am using docx-templates to generate the file by the way.

2 Answers 2

5

You can use a PassThrough stream for this purpose, it'll keep the file in memory with no need to write to disk.

Something like this should do it:

    const stream = require("stream");
    const readStream = new stream.PassThrough();

    // Pass your output.docx buffer to this
    readStream.end(buffer);
    res.set("Content-disposition", 'attachment; filename=' + "output.docx");
    res.set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
    readStream.pipe(res);

The complete node.js code:

const fs = require("fs");
const express = require("express");
const port = 8000;
const app = express();
const stream = require("stream");

app.get('/download-file', (req, res) => {
    const buffer = fs.readFileSync("./test.docx");
    console.log("/download-file: Buffer length:", buffer.length);
    
    const readStream = new stream.PassThrough();
    readStream.end(buffer);
    res.set("Content-disposition", 'attachment; filename=' + "test.docx");
    res.set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
    readStream.pipe(res);
});

app.listen(port);
console.log(`Serving at http://localhost:${port}`);

To test, add a 'test.docx' file to the same directory, then point your browser to http://localhost:8000/download-file

Sign up to request clarification or add additional context in comments.

2 Comments

Hey, Terry, How am I supposed to download this on the Client side? Currently, when I make a call to this API endpoint, and have this code, it returns successful response, but nothing downloads. I am using vuejs with vuetify, if that helps.
Hi Vitomir, I've added the complete Node.js code, hope this helps.
0

Terry,

Thanks for the update of your answer and providing the full code. However, it still does not help much. I am trying to understand how I can handle this on the front-end side, in my case in Vue. Here is the following code:

router.post('/chart/word', async (req, res, next) => {
    try {
        if (!req.body.chartImage) throw new BadRequest('Missing the chart image from the request body')

        const wordTemplate = await s3GetFile('folder', 'chart-templates-export/charts-template.docx')
        const template = wordTemplate.Body

        const buffer = await createReport({
            cmdDelimiter: ["{", "}"],
            template,
            additionalJsContext: {
                chart: () => {
                    const dataUrl = req.body.chartImage.src
                    const data = dataUrl.slice("data:image/jpeg;base64,".length);
                    return { width: 18 , height: 12, data, extension: '.jpeg' }
                }
            }
        })

        const stream = require('stream')
        const readStream = new stream.PassThrough()

        readStream.end(buffer)
        res.set("Content-disposition", 'attachment; filename=' + "output.docx")
        res.set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
        readStream.pipe(res)
    } catch (err) {
        console.log(err)
        next(err)
    }
})

And here is my Vue code, tested various stuff, but nothing...:

async exportCharts() {
    console.log('this.$refs.test: ', this.$refs.test)
    let img = {
        src: this.$refs.test.getDataURL({
            type: 'jpeg',
            pixelRatio: window.devicePixelRatio || 1,
            backgroundColor: '#fff'
        }),
        width: this.$refs.test.getWidth(),
        height: this.$refs.test.getHeight()
    }
    const answersReq = await this.axios({
        method: 'post',
        url: '/pollAnswers/chart/word',
        data: {
            chartImage: img
        }
        responseType: 'arraybuffer' // 'blob' // 'document'
    })

    console.log('answersReq: ', answersReq)

    if (answersReq.data) {
        downloadURL(answersReq.data, 'report.docx')
    }
}

What I am basically doing is: sending an image to the API (taken from html vue-echart element), then inserting it in a docx template, by using docx-templates library, which returns me Uint8Array that I want to export as the new Word Document with the populated charts. Then, the user (on the UI) should be able to choose the destination.

Here is the code for the download URL:

export function downloadURL(data, fileName) {
    const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    const blob = new Blob([data], { type: mimeType })
    const url = URL.createObjectURL(blob)

    const element = document.createElement('a')

    element.href = url
    element.download = fileName

    element.style.display = 'none'

    document.body.appendChild(element)
    element.click()
    URL.revokeObjectURL(element.href)
    document.body.removeChild(element)
}

P.S. Just to mention, if I directly save the buffer (the Uint8Array returned from the createReport) in the API, it works, the file is downloaded successfully and I can read it without any problems - it populates the correct chart in the file.

UPDATE: I figured that out, but I am not sure why this is necessary and why it works that way and not the other. So, in the /chart/word endpoint, I am converting the Uint8Array buffer into a stream, then passing it as a response (the same way you used). Afterwards, in the Vue, I fetched this as responseType: 'arraybuffer', which converted the stream response into Uint8Array buffer again, then, I used the same method for the download and it works. Initially, I tried to send directly the buffer (without converting it as stream as you mentioned), but then on the front-end, the response was received as object that contained the Uint8Array buffer values, which was not what is expected and I could not create legit docx file. So, for some reason, it is required to convert the buffer as stream in the API, before sending it as response. Afterwards, on the front-end, I have to convert it back to arraybuffer and, finally, to make the docx download.

If you can explain to me why it works like that, I will be very happy.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.