koa icon indicating copy to clipboard operation
koa copied to clipboard

[fix] Download file

Open aellert opened this issue 2 years ago • 1 comments

Describe the bug

Node.js version: 16

OS version: docker (alpine)

Description: WhenI go to http://locahost:9000/test, a download of /app/file.xml should be initiated in my browser.

Actual behavior

I don't get the file content but the string "OK" (same behavior with another file or path)

$ curl http://localhost:9000/test
OK
$ curl -I http://localhost:9000/test
Content-Disposition: attachment; filename="numeezy.com.mobileconfig"
Content-Type: application/xml
Date: Sat, 03 Sep 2022 19:55:26 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Code to reproduce

const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();


router.get('/test', (ctx) => {
	ctx.response.attachment('/app/file.xml');
	ctx.response.set('Content-Type', 'application/xml');
	ctx.response.status = 200;

});


app.use(router.routes());
app.use(router.allowedMethods());

app.listen(process.env.PORT || 9000);

Thanks for your help

aellert avatar Sep 03 '22 19:09 aellert

This isn't a bug. ctx.attachment() (delegated to ctx.response.attachment) only correctly sets Content-Disposition headerfield. You actually needs to provide the file you want the client to download yourself.

Try assigning a stream to body, e.g.:

ctx.body = fs.createReadStream(path)

iwanofski avatar Sep 13 '22 11:09 iwanofski

@aellert For now, you can solve your problem by replacing the 'attachment' method with 2 lines;

router.get('/test', (ctx) => {
  const filename = '/app/file.xml';
  ctx.response.set('Content-disposition', 'attachment; filename=' + filename);
  ctx.response.set('Content-Type', 'application/xml');
  ctx.response.status = 200;
  ctx.body = fs.createReadStream(filename); // don't forget to import the file system module
});

And you can add your own utility like this if you want a re-usable logic;

function download(ctx) {
  return (sourceFileNamePath, config = {}) => {
    const { status = 200, headers = {} } = config;
    const responseHeaders = {
      'content-Type': 'application/force-download',
      ...headers,
      'content-disposition': `attachment; filename=${sourceFileNamePath}`
    }
  
    ctx.response.set(responseHeaders);
    ctx.response.status = status;
    ctx.response.body = fs.createReadStream(sourceFileNamePath); 
  }
}

I'm not sure but maybe we will need to create middleware or add a download method to make life easier here.

3imed-jaberi avatar Jun 29 '23 09:06 3imed-jaberi