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;
'오픈소스 > 노드' 카테고리의 다른 글
[Node] PM2 설치 및 사용법 (0) | 2021.10.06 |
---|---|
[Node] Audio Raw PCM data 에서 amplitude 추출 (0) | 2021.10.06 |
[Node] Express 404 Not Found 처리 (0) | 2021.10.06 |
[Node] 04. 콜백을 사용한 비동기 제어 흐름 패턴 (0) | 2021.10.06 |
[Node] 03. 콜백과 이벤트 (0) | 2021.10.05 |