본문 바로가기

오픈소스/노드

[Node] Express 서버에서 Form을 이용하여 여러 개 데이터 전송

 Express를 이용하여 서버 쪽에서 파일을 공유 하는 방법입니다. 보통은 클라이언트 단에서 서버 쪽으로 폼 데이터를 전송하여 메시지를 요청하는데, 어느 서버에서는 클라이언트가 처리하도록 서버에서 폼 데이터를 전송하는 그러한 자료를 본 적이 있습니다.

 물론 이러한 방식은 여러 파일을 한꺼번에 처리하게 되어 서버 측 입장에서는 아주 간단하고 편리 하지만, 클라이언트 측 입장에서는 바운더리 처리와 더불어 로직을 새로 짜야하는 상황이 존재하기 때문에 혹시 이러한 상황이 발생하시더라도 기본적인 틀을 이용하여 코드를 작성하면 도움이 되지 않을까 해서 작성하게 되었습니다.

 

# Contents


  • 서버
  • 클라이언트

 

 

# 서버


 서버 쪽 코드는 아주 간단합니다. 해당 라우터에 get 메소드를 통하여 폼 데이터 를 전송하면 끝이죠. 예제 코드는 아래와 같습니다.

 

router.get("/download", multipart_download);

function multipart_download(req, res) {
    let file2_path = "./uploadedFiles/test2.txt";
    let file3_path = "./uploadedFiles/ment.wav";

    let form = new formData();
    form.append("text_file", fs.createReadStream(file2_path));
    form.append("wav_file", fs.createReadStream(file3_path));
    res.setHeader("Content-Type", `multipart/form-data; boundary=${form.getBoundary()}`);

    form.pipe(res);
}

 

 처리해야 하는 구간은 클라이언트 입장입니다. 사실 상 데이터가 이렇게 들어오게 되면 파싱하지 않고서야 text 파일과 wav 파일이 한꺼번에 들어오면서 이상한 값들이 나오게 됩니다. 클라이언트 측에서는 파싱을 반드시 해야합니다. 아래 예제를 보시죠

 

 

# 클라이언트


 위와 같은 데이터 형식으로 전송되기 때문에 클라이언트 입장에서는 바운더리 파싱을 필수적 으로 해야합니다. 코드 예제는 아래와 같습니다.

 

import { Transform, PassThrough, pipeline } from "stream";
import got from "got";

export async function multipart_download2(req, res) {
    const contentStream = new PassThrough();

    pipeline(got.stream("http://localhost:3000/download/"), new byteToArray(), contentStream, (err) => {
        if (err) {
            console.error(err);
        }
    });

    const return_str = await helloworld(contentStream);
    console.log(return_str);
}

class byteToArray extends Transform {
    constructor(options = {}) {
        (options.objectMode = true), super(options);
        this.status = 0;
        this.iswav = 0;
        this.writeStream = fs.createWriteStream("C:/tmp/" + "gdgd.wav");
        this.tail = "";
    }

    _transform(chunk, encoding, cb) {
        /* Content-Type 여기 부분을 자른다. */

        this.tail = this.tail + chunk; // 짤라진 부분을 다시 합쳐서
        const piece = this.tail.split("\r\n");
        const piece_save_length = piece.length - 1;
        this.tail = piece[-1] ? piece[-1] : "";
        let i = 0;

        while (i <= piece_save_length) {
            /* 그냥 간단히 처리하게하기위해서.. */
            piece[i].includes("text") ? (this.iswav = 0) : "";
            piece[i].includes("wav") ? (this.iswav = 1) : "";

            if (this.status == 0) {
                if (piece[i] === "") {
                    this.status = 1;
                }
            } else {
                if (piece[i].includes("--")) {
                    this.status = 0;
                } else {
                    if (this.iswav == 0) {
                        this.push(Buffer.from(piece[i], "utf-8").toString());
                    } else {
                        this.writeStream.write(Buffer.from(piece[i], "latin1"));
                    }
                }
            }
            i++;
        }
        cb();
    }

    _flush(cb) {
        this.writeStream.end();
        cb();
    }
}

function helloworld(contentStream) {
    return new Promise((resolve, reject) => {
        let str_data = "";
        contentStream.on("data", (chunk) => {
            str_data += chunk.toString();
        });
        contentStream.on("end", (chunk) => {
            return resolve(str_data);
        });
    });
}

 

아래 코드는 제 서버의 클라이언트 쪽 에서 사용하고 있는 코드입니다.

 

const Transform = require("stream").Transform;
const pipeline = require("stream").pipeline;
const PassThrough = require("stream").PassThrough;
const got = require("got");
const fs = require("fs");
const logger = require("./logger");
const memoryMap = require("../utils/memoryMaplist");
const AUDIO_DIR = require("../config/config").AUDIO_DIR; // TODO 항상 바꿔줘야 합니다.
function multipart_download(options, jsName, ticketId, pFileName, channel, usedGetAudio = "N") {
    return new Promise(async (resolve) => {
        const contentStream = new PassThrough();
        let fName = null;
        let fileName = null;
        let url = options.url;
        delete options.url;
        if (usedGetAudio == "N") {
            fName = `${AUDIO_DIR}${ticketId}.raw`;
            fileName = `${AUDIO_DIR}${ticketId}`;
        } else {
            fName = `${AUDIO_DIR}${ticketId}rr.raw`;
            fileName = `${AUDIO_DIR}${ticketId}rr`;
        }
        pipeline(got.stream(url, options).setEncoding("latin1"), new byteToArray(fName), contentStream, (err) => {
            if (err) {
                // error 무시
            }
        });
        endStreamRedisRecord(contentStream, fName, channel, jsName);
        resolve(fileName);
    });
}
class byteToArray extends Transform {
    constructor(filename, options = {}) {
        (options.objectMode = true), super(options);
        this.status = 0;
        this.iswav = 0;
        this.writeStream = fs.createWriteStream(filename, { encoding: "binary" });
        this.tail = "";
        this.clearBuffer = Buffer.alloc(500);
    }
    _transform(chunk, encoding, cb) {
        /* 스트림 형식으로 다운받게 하기 */
        if (this.iswav == 1 && this.status == 1) {
            if (this.tail.includes("\r\n") == false) {
                this.writeStream.write(this.tail, "latin1");
                this.tail = "";
            }
        }
        /* Content-Type 여기 부분을 자른다. */
        this.tail = this.tail + chunk; // 짤라진 부분을 다시 합쳐서
        const piece = this.tail.split("\r\n");
        const piece_save_length = piece.length - 1;
        this.tail = piece[-1] ? piece[-1] : "";
        let i = 0;
        /* 완벽한 부분을 가지고 처리함 */
        while (i <= piece_save_length) {
            /* 그냥 간단히 처리하게하기위해서.. */
            piece[i].includes('name="msg"') ? (this.iswav = 0) : "";
            piece[i].includes('name="audio"') ? (this.iswav = 1) : "";
            if (this.status == 0) {
                if (piece[i] === "") {
                    this.status = 1;
                }
            } else {
                if (piece[i].includes("--")) {
                    this.status = 0;
                } else {
                    if (this.iswav == 0) {
                        this.push(Buffer.from(piece[i], "utf-8").toString());
                    } else {
                        this.writeStream.write(Buffer.from(piece[i], "latin1"));
                    }
                }
            }
            i++;
        }
        cb();
    }
    _flush(cb) {
        this.writeStream.write(this.clearBuffer);
        this.writeStream.end();
        cb();
    }
}
function endStreamRedisRecord(contentStream, fName, channel, jsName) {
    let str_data = "";
    contentStream.on("data", (chunk) => {
        str_data += chunk.toString();
    });
    contentStream.on("end", async () => {
        const stats = fs.statSync(fName);
        let playDuration = (stats.size / 16000).toFixed(3);
        logger.info(jsName, channel, `-------------- MultiPart File Save --------------`);
        logger.info(jsName, channel, `| NOTICE : 메모리에 파일과 음성 시간을 성공적으로 저장하였습니다. `);
        logger.info(jsName, channel, `| FILENAME : ${fName}`);
        logger.info(jsName, channel, `| DURATION : ${playDuration * 1000}/ms`);
        logger.info(jsName, channel, "--------------------------------------------------");
        logger.info(jsName, channel, "\t");
        //* memoryMaplist add */
        memoryMap.setItem(channel, playDuration);
    });
}
module.exports = multipart_download;