게시판 - 파일첨부 기능 만들기 2 (다운로드)

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 게시판 만들기(고급) / 게시판 - 파일첨부 기능 만들기 1 (업로드)에서 이어집니다.

board.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard ccd0c59
git reset --soft 6ba2aa3
npm install
atom .

board.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 board.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/board.git
cd board
git reset --hard ccd0c59
git reset --soft 6ba2aa3
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/board/tree/ccd0c59b9896e9b324d06e83b5e40216cee55b99


이전강의에서 html form으로 부터 웹사이트 이용자의 파일을 받아서 서버의 폴더(uploadedFiles)에 저장하는 방법을 알아보았습니다. 이번 강의에서는 이렇게 upload된 파일을 route을 통해서 다운받은 방법에 대해 알아봅니다.

route을 통하지 않고 다운받는 방법은 이미 우리가 알고 있습니다. 파일이 업로드된 서버의 폴더를 static 폴더로 지정해주면 해당 폴더의 모든 파일들이 누구에게나 접근가능해집니다.(static 폴더 지정하는 법은 Node JS 첫걸음/Hello World/Static 폴더 추가하기 강의에서 배웠습니다. )

반면 route을 통해서 파일을 받게 하면, 코드를 통해서 그 과정을 관리할 수 있습니다. (로그인 하지 않은 이용자의 다운로드를 제한한다든지, 다운로드 횟수를 센다든지 등)

폴더 구조

프로젝트에 files route가 추가됩니다.

코드 - js

// models/File.js

var mongoose = require('mongoose');
var fs = require('fs'); // 1
var path = require('path'); // 2

...

// instance methods // 3
fileSchema.methods.processDelete = function(){ // 4
  this.isDeleted = true;
  this.save();
};
fileSchema.methods.getFileStream = function(){
  var stream;
  var filePath = path.join(__dirname,'..','uploadedFiles',this.serverFileName); // 5-1
  var fileExists = fs.existsSync(filePath); // 5-2
  if(fileExists){ // 5-3
    stream = fs.createReadStream(filePath);
  }
  else { // 5-4
    this.processDelete();
  }
  return stream; // 5-5
};

// model & export
...

1. fs는 File System의 약어로 컴퓨터의 파일을 조작할 수 있는 node module입니다. package를 따로 설치할 필요없이 node.js에서 기본적으로 제공됩니다.

2. path 역시 node.js에서 기본적으로 제공되며, 폴더및 파일의 path를 조작할 수 있습니다.

3. file모델의 인스턴스(instance) 함수들을 추가합니다. (모델과 인스턴스의 차이점은 이전 강의에서 자세히 설명하였으니 참고하세요.) 인스턴스의 함수는 Schema.methods 객체에 추가할 수 있습니다. 또한 이 함수들안에서 this는 인스턴스 자체를 가리킵니다.

4. processDelete함수는 삭제요청을 처리합니다. 실제 파일을 지우지는 않고, file의 isDeleted항목을 true로 변경하여 저장하는 일만 합니다.

5. getFileStream함수는 서버 파일의 스트림(stream)을 생성하여 return합니다. 스트림은 binary 데이터를 조작할 수 있는 다리로 생각하면 됩니다. 더 깊게 알고 싶은 사람들은 따로 인터넷으로 공부해 봅시다. 일단 컴퓨터에 있는 binary 파일을 프로그램에서 읽거나 수정하려면 스트림이라는 것을 만들어야 한다는 것만 꼭 기억해 줍시다.

5-1. path.join함수를 사용해 서버 파일위치의 절대주소 만들어 filePath에 string으로 저장합니다. node js에서 __dirname는 현재 파일이 있는 위치를 담고 있는 변수입니다.

5-2. fs.existsSync(파일_위치)함수는 파일_위치의 파일이 존재하면 true를, 아니면 false를 return합니다.

** fs.existsSync처럼 이름이 Sync로 끝나는 함수들은 주로 async함수와 sync함수가 나누어져 있는 경우가 많습니다. 함수이름에서 Sync를 때서(fs.exists) async함수로 사용할 수도 있죠. 현재 중요한 내용은 아니지만, 함수 이름에 의문을 가지는 사람이 있을까봐 적어봅시다.

5-3. 만약 파일이 존재하면 fs.createReadStream함수를 사용해 해당 파일의 일기 전용 스트림을 생성하여 stream변수에 담습니다.

5-4. 만약 파일이 존재하지 않으면 4번의 processDelete함수를 호출합니다.

5-5. stream을 return합니다. 만약 파일이 존재하지 않으면 stream은 undefined 상태이겠지요.

// routes/files.js

var express  = require('express');
var router = express.Router();
var File = require('../models/File');

router.get('/:serverFileName/:originalFileName', function(req, res){ // 3
  File.findOne({serverFileName:req.params.serverFileName, originalFileName:req.params.originalFileName}, function(err, file){ // 3-1
    if(err) return res.json(err);

    var stream = file.getFileStream(); // 3-2
    if(stream){ // 3-3
      res.writeHead(200, {
        'Content-Type': 'application/octet-stream',
        'Content-Disposition': 'attachment; filename=' + file.originalFileName
      });
      stream.pipe(res);
    }
    else { // 3-4
      res.statusCode = 404;
      res.end();
    }
  });
});

module.exports = router;

3. :을 사용해 server의 파일 이름(server file name)과 업로드될 당시의 파일 이름(original file name)을 파라메터로 받는 file route입니다.

3-1. serverFileName, originalFileName 파라메터를 사용해 DB에 저장된 file 데이터를 찾습니다.

3-2. file 인스턴스의 getFileStream함수로 서버 파일의 스트림을 가져옵니다.

3-3. 스트림이 존재하면 response 해더를 파일다운로드에 맞도록 'Content-Type'와 'Content-Disposition'를 설정하고, stream.pipe(res)로 파일스트림과 response를 연결해 줍니다. 이렇게 서버파일의 스트림을 response에 연결해서 client에 파일을 보낼 수 있습니다.

3-4. 만약 스트림이 존재하지 않으면 response의 code를 404 (Not Found, 찾을수 없음)으로 설정하고 res.end()함수를 써서 아무것도 전달하지 않고 response를 만듭니다.

// index.js

...

// Routes
...
app.use('/comments', util.getPostQueryString, require('./routes/comments'));
app.use('/files', require('./routes/files')); // 1

...

1. files route에 files.js를 연결하는 코드입니다.

코드 - ejs

<!-- views/posts/show.ejs -->

...
            <% if(post.attachment) { %>
              <div class="ml-2">
     <!-- 1 --> <small>📁 Attachment: <a href="/files/<%= post.attachment.serverFileName %>/<%= post.attachment.originalFileName %>"><%= post.attachment.originalFileName %></a> (<%= util.bytesToSize(post.attachment.size) %>)</small>
              </div>
            <% } %>

...

1. a 태그를 사용해서 파일에 맞는 files route를 만들어줍니다.

실행 결과

첨부파일의 파일명이 클릭할 수 있는 링크로 변경되었습니다.

링크를 클릭하면 브라우저에서 파일을 다운로드합니다.

마치며...

이번 강의에서는 서버의 파일을 조작하는 fs 모듈을 처음으로 사용해 보았고, 파일 스트림이라는 것을 처음으로 만들어 보았습니다. 스트림이라는 새로운 개념이 소개된 만큼 약간 머리가 아프실 것으로 생각되는데, 일부러 이론을 너무 깊게 들어가진 않았습니다. 내용을 이해 못하더라도 이런 식으로 코딩하면 서버에서 파일을 받을 수 있다는 아이디어만 가져도 지금은 충분합니다. 혹시 이 내용관련해서 질문있으시면 답글 남겨주세요.

첨부파일 강의는 아직 끝나지 않았습니다. 다음 강의에서는 post index view에 첨부파일이 있는 경우 표시하는 법을 알아보고, 그 다음은 첨부파일을 삭제, 수정하는 방법을 알아봅니다.

댓글

특대갈비 2020.07.15
혹시 파일 명이 한글로 되어있으면 다운로드가 안되는데 이것에 해결 방법이 있나요? TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["Content-Disposition"] 파일명이 한글이라서 이런에러가 생기는것 같습니다
I
Ian H 2020.07.15
@특대갈비,
구글링해보니 요렇게 고치시면 될 것같습니다.
https://stackoverflow.com/questions/46105398/header-content-contains-invalid-characters-error-when-piping-multipart-upload
한번 해보시고 저에게도 어떻게 되는지 알려주세요!
특대갈비 2020.07.16
제가 해보니 
res.writeHead(200, {         'Content-Type': 'application/octet-stream; charset=utf-8',         'Content-Disposition': 'attachment; filename=' + encodeURI(file.originalFileName)       });
여기서 저 encodeURI 만 바꾸니까 정상적으로 됩니다!  감사합니당
특대갈비 2020.07.16
route/file.js입니다
I
Ian H 2020.07.16
@특대갈비,
와 알려주셔서 감사합니다!
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP