Node.js와 GridFS를 이용한 파일 저장하기!

g_choi·2021년 7월 5일
0

Tutorial

목록 보기
1/1

Node.js로 오디오 파일 스트리밍하기

Node.js는 여러모로 많은 라이브러리와 빠른 생산성으로 아이디어를 작성하기 좋은 도구라고 생각해요. 이번에 제작 중인 개인 웹페이지의 메인 부분인 오디오 스트리밍을 담당할 메인 코드를 간단히 작성해보았어요.

아래 미디움에서 작성된 글을 보고, 해당 라이브러리가 deprecated되어 라이브러리만 교체하여 재작성된 글임을 알립니다.

source

의존성

/**
 * Module dependencies.
 */
const express = require('express');
const trackRoute = express.Router();
const multer = require('multer');
const mongoose = require('mongoose');
const { ObjectID } = require('mongodb');
const { createModel } = require('mongoose-gridfs');
const { Readable } = require('stream');

mongoose-gridfs는 mongoose에서 GridFS기능을 이용할 수 있게 도와주는 기능이에요. 기본적으로 MongoDB는 한 도큐먼트의 사이즈가 16MB를 초과할 수 없기 때문에 대용량을 저장 시 GridFS란 기능을 사용함으로써 분산저장을 하고, 그 저장된 파일들을 따로따로 불러오는 기능을 수행해요.

기본 설정

/**
 * Create Express server && Express Router configuration.
 */
const app = express();
// GridFS Bucket variable
let Attachment;
app.use('/audio', trackRoute);

/**
 * Connect Mongo Driver to MongoDB.
 */
mongoose.connect("mongodb://localhost:27017/trackDB", {
    useNewUrlParser: true,
    useCreateIndex: true,
}).then(()=> {
    console.log("Connected to MongoDB");
  	// Allocate model into Attachment
    Attachment = createModel();
}).catch((error) => {
    console.error(error);
});

DB와 연결하고 express를 설정해줘요. DB와 연결된 이후, createModel을 통해 GridFS bucket 모델을 생성하고, 그 모델을 변수 Attachment에 할당해줘요.

POST Route

trackRoute.post('/', (req, res) => {
    const storage = multer.memoryStorage()
    const upload = multer({ storage: storage, limits: { fields: 1, fileSize: 6000000, files: 1, parts: 2 }});
    upload.single('track')(req, res, (err) => {
        if (err) {
            return res.status(400).json({ message: "Upload Request Validation Failed" });
        } else if(!req.body.name) {
            return res.status(400).json({ message: "No track name in request body" });
        }

        const readStream = Readable.from(req.file.buffer);
        const options = ({ filename: req.body.name, contenttype: 'audio/mp3'});
        Attachment.write(options, readStream, (err, file) => {
            if (err)
                return res.status(400).json({message: "Bad Request"});
            else {
                console.log("Posted! \n" + file.toString());
                return res.status(200).json({
                    message: "Successfully Saved!",
                    file: file,
            });
            }
        })
    });
});

요청받은 파일을 DB에 저장하는 작업이에요. 해당 URL로 들어온 요청에서 file의 크기가 multer를 통해 크기와 필드가 일치하게 되면, 이후 body.name 필드가 존재하는 지 확인하게 되요(Validation). req.file.buffer를 가져와 readableStream에 넣는 과정을 통해 해당 스트림을 GridFS로 넣을 준비를 해요. Attachment.write를 통해서 만약 write가 성공적이라면 200과 file의 내용들을, 아니라면 400을 보내 해당 요청에 대한 response를 마무리합니다.

GET Route

trackRoute.get('/:trackID', (req, res) => {
    if(!req.params.trackID) {
        return res.status(400).json({
            message: "Invalid trackID in URL parameter."
        });
    }
    res.set('content-type', 'audio/mp3');
    res.set('accept-ranges', 'bytes');
    
    try {
        const reader = Attachment.read({_id: ObjectID(req.params.trackID)});   
        reader.on('data', (chunk)=> {
            res.write(chunk);
        });
        reader.on('close', () => {
            console.log("All Sent!");
            res.end();
        });
    } catch(err) {
        console.log(err);
        res.status(404).json({
            message:"Cannot find files that have the ID",
        });
    }
});

GridFS에서 파일을 찾아내어 client에게 전송해주는 작업을 하는 Router에요.
1. trackID가 존재한지 요청이 유효한 것을 확인한다.
2. header에 타입을 mp3로 지정하여 해당 response는 audio인 것을 선언한다.
3. Attachment에 해당 id가 존재하는 지 여부를 확인, 만약 존재한다면 chuck로 쪼개어 client에게 전달한다.
4. 만약 없다면 error를 response한다.

DELETE Route

trackRoute.delete("/:trackID", (req, res)=> {
    if(!req.params.trackID) {
        return res.status(400).json({
            message: "Invalid trackID in URL parameter."
        });
    }
    
    Attachment.unlink({_id: ObjectID(req.params.trackID)}, (err, file)=> {
        if (err) {
            console.log("Failed to delete\n" + err);
            return res.status(400).json({
                message: "Wrong Request",
                error: err.message,
            });
        }
        
        console.log('Deleted\n' + file);
        return res.status(200).json({
            message: "Successfully Deleted",
            file: file,
        });
    });   
});

Attachment.unlink는 해당 id를 가진 도큐먼트 뿐만이 아닌, 해당 도큐먼트와 연결된 모든 분산저장된 바이너리들을 일괄 삭제해줍니다.

Test

  1. POST

    해당 response에 있는 _id는 모든 파일들을 일괄관리하는 도큐먼트의 번호이자, 앞으로 해당 파일을 관리할 키에요.

    DB에 fs.files에는 해당 곡의 도큐먼트가 생성되었어요.

    그리고 chucks엔 바이너리들이 18개로 나누어져 관리되요.

  2. GET

    해당 id를 URL에 넣어주면 audio타입의 곡을 Postman에서 들을 수 있어요!

  3. DELETE

    삭제도 동일하게 id를 URL에 넣어주면 DB에서 모든 chuck까지 지워지게 되요.

전체 코드

/**
 * Module dependencies.
 */
const express = require('express');
const trackRoute = express.Router();
const multer = require('multer');
const mongoose = require('mongoose');
const { ObjectID } = require('mongodb');
const { createModel } = require('mongoose-gridfs');
const { Readable } = require('stream');

/**
 * Create Express server && Express Router configuration.
 */
const app = express();
//global
let Attachment;
app.use('/audio', trackRoute);

/**
 * Connect Mongo Driver to MongoDB.
 */
mongoose.connect("mongodb://localhost:27017/trackDB", {
    useNewUrlParser: true,
    useCreateIndex: true,
}).then(()=> {
    console.log("Connected to MongoDB");
    Attachment = createModel();
}).catch((error) => {
    console.error(error);
});

/**
 * GET /audio/:trackID
 */
trackRoute.get('/:trackID', (req, res) => {
    if(!req.params.trackID) {
        return res.status(400).json({
            message: "Invalid trackID in URL parameter."
        });
    }
    res.set('content-type', 'audio/mp3');
    res.set('accept-ranges', 'bytes');
    
    try {
        const reader = Attachment.read({_id: ObjectID(req.params.trackID)});   
        reader.on('data', (chunk)=> {
            res.write(chunk);
        });
        reader.on('close', () => {
            console.log("All Sent!");
            res.end();
        });
    } catch(err) {
        console.log(err);
        res.status(404).json({
            message:"Cannot find files that have the ID",
        });
    }
});

/**
 * POST /audio
 */
trackRoute.post('/', (req, res) => {
    const storage = multer.memoryStorage()
    const upload = multer({ storage: storage, limits: { fields: 1, fileSize: 6000000, files: 1, parts: 2 }});
    upload.single('track')(req, res, (err) => {
        if (err) {
            return res.status(400).json({ message: "Upload Request Validation Failed" });
        } else if(!req.body.name) {
            return res.status(400).json({ message: "No track name in request body" });
        }

        const readStream = Readable.from(req.file.buffer);
        const options = ({ filename: req.body.name, contenttype: 'audio/mp3'});
        Attachment.write(options, readStream, (err, file) => {
            if (err)
                return res.status(400).json({message: "Bad Request"});
            else {
                console.log("Posted! \n" + file.toString());
                return res.status(200).json({
                    message: "Successfully Saved!",
                    file: file,
            });
            }
        })
    });
});

/**
 * DELETE /audio/:trackID
 */
trackRoute.delete("/:trackID", (req, res)=> {
    if(!req.params.trackID) {
        return res.status(400).json({
            message: "Invalid trackID in URL parameter."
        });
    }
    
    Attachment.unlink({_id: ObjectID(req.params.trackID)}, (err, file)=> {
        if (err) {
            console.log("Failed to delete\n" + err);
            return res.status(400).json({
                message: "Wrong Request",
                error: err.message,
            });
        }
        
        console.log('Deleted\n' + file);
        return res.status(200).json({
            message: "Successfully Deleted",
            file: file,
        });
    });   
});
  
app.listen(3005, () => {
    console.log("App listening on port 3005!");
});

Github

profile
해외에서 공부중인, 다양한 걸 배울려 하는, 항상 모자름을 느끼기에 성장하는 학생 :D

0개의 댓글