프런트엔드: vue
백엔드: nodejs + express
우선 업로드 파일에서 파일 크기가 최대 수십만 MB에 달하면 업로드 시간이 매우 길어지고 네트워크가 안정적이지 않거나 브라우저, 프로그램 충돌, 업로드가 실패하고 이때 파일 슬라이싱 업로드와 파일을 계속 업로드하기위한 중단 점이 매우 중요하며 물론 1 초 안에 대용량 파일도 있는데, 이는 파일 해시 값이며 서버가 파일의 해시 값이 동일하면 프론트 엔드 파일 업로드 성공을 직접 알리면 업로드가 매우 좋은 아이디어가 될 것입니다. 서버에 동일한 해시 값을 가진 파일이 존재하면 파일이 성공적으로 업로드되었음을 프런트 엔드에 직접 알려주며이 기사에서는 먼저 슬라이스 업로드와슬라이스 및 병합에 대해 설명합니다.
먼저 프런트엔드 코드를 살펴보면, 우선 입력 , type="file", 업로드 버튼이 필요하며, 구체적인 코드는 다음과 같습니다.
<template>
<div>
<input type="file" @change="onFileChange" />
<button @click="upload">Upload</button>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
file: null,//업로드된 파일
filename: "",//
};
},
methods: {
onFileChange(event) {
this.file = event.target.files[0];
console.log(event.target.files[0]);
this.filename = this.file.name
},
async upload() {
if (!this.file) {
return;
}
/**
* chunkSize 는 바이트 단위의 슬라이스 크기입니다.
* totalChunks 총 슬라이스 수 수학.ceil 를 반올림하기 위해
* chunkIndex 현재 슬라이스에 대한 인덱스
* chunkPromises 모든 슬라이스에 대해 "약속
*/
const chunkSize = 10 * 1024 * 1024; // 10MB
const totalChunks = Math.ceil(this.file.size / chunkSize);
const chunkPromises = [];
// 슬라이싱
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
// FormData 객체에 바이너리 데이터와 폼 데이터를 저장할 수 있으며, 마지막에 추가, 청크의 순서에 주의하세요.
// 백엔드에서 먼저 chunkIndex, totalChunks 및 파일 이름을 구문 분석하는지 확인합니다.
const formData = new FormData();
formData.append('chunkIndex', chunkIndex); //key:chunkIndex value:각 슬라이스의 인덱스
formData.append('totalChunks', totalChunks); //key:totalChunks value:총 슬라이스 수
formData.append('filename', this.file.name); //key:filename value:
formData.append('file', chunk); //key:file value:각 슬라이스에 대한 바이너리 데이터
const uploadPromise = axios.post('"http://...1:3000"/upload', formData);
chunkPromises.push(uploadPromise);
// const fileReader = new FileReader();
// fileReader.onload = (e) => {
// console.log(e);
// }
// fileReader.readAsDataURL(chunk);
}
console.log(chunkPromises);
const res=await Promise.all(chunkPromises);
console.log('res',res);
// 모든 슬라이스가 업로드되면 서버에 파일 업로드가 완료되었음을 알리는 요청을 보낼 수 있습니다.
await axios.post('"http://...1:3000"/upload/complete', { filename: this.filename });
// 업로드 완료 후 콜백
console.log('파일 업로드 완료');
}
}
};
</script>
nodejs로 구현된 백엔드 코드로 이동하여 먼저 종속성을 살펴 보겠습니다:
"dependencies": {
"express": "^4.18.2",
"multer": "1.4.5-lts.1"
}
먼저 사용해야 하는 패키지를 소개하고 도메인 간 문제를 해결하는 것부터 시작하겠습니다.
const express = require('express');
const multer = require('multer');
const app = express();
const fs = require('fs');
const path = require('path');
app.use((req, res, next) => {
// 모든 도메인에 대한 교차 도메인 요청 허용
res.header('Access-Control-Allow-Origin', '*');
// 허용되는 HTTP 헤더 유형 설정
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
// 허용되는 HTTP 메서드 설정
if (req.method === 'OPTIONS') {
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE');
return res.status(200).json({});
}
// 요청을 계속 처리합니다.
next();
});
JSON 요청을 구문 분석하도록 미들웨어 구성하기
// express.json() 는 JSON 형식의 요청을 구문 분석하는 미들웨어입니다.
app.use(express.json());
파일 슬라이스를 수신하는 인터페이스를 작성합니다.
// 분명히 여기에는 파일을 저장하는 멀티터인 미들웨어가 사용됩니다.
app.post('/upload',upload.single('file'),(req, res) => {
console.log('데이터 반환 ---');
res.send({
code: 200,
msg: '업로드 성공',
});
});
여기서 upload.single('file') 이 미들웨어가 멀티터에서 제공하는 것으로, 이 인터페이스가 호출될 때마다 다음 구성을 구성하기 위해 다음과 같이 트리거됩니다.
서버에서 지정한 디렉터리에 파일을 자동으로 저장하도록 구성할 수 있는 타사 라이브러리인 멀티 패키지를 구성하되 미들웨어 upload.single('file') 사용해야 한다는 점을 기억하세요.
// 구성 파일 저장 인터페이스가 업로드를 사용할 때 멀티 라이브러리의 구성입니다..single('file')미들웨어는 자동으로 이 구성을 호출합니다.
const storage = multer.diskStorage({
// 파일 저장 위치를 설정하며, 콜백 함수 cb의 첫 번째 매개변수는 오류 메시지를 나타내고 두 번째 매개변수는 파일 저장 경로를 나타냅니다.
destination: function (req, file, cb) {
// 임시 폴더 만들기
try {
fs.mkdirSync(path.resolve(__dirname, 'temp'), { recursive: true });
} catch (error) {
console.log(' 임시 폴더를 만들지 못했습니다.,error);
}
cb(null, path.resolve(__dirname, 'temp'));
},
// 위와 같이 파일 이름을 설정하고, 첫 번째 매개 변수는 오류 메시지를, 두 번째 매개 변수는 파일 이름을 나타냅니다.
filename: function (req, file, cb) {
// console.log('reqbody',req.body);//인쇄 요청.body,
// console.log('file',file);
cb(null, file.fieldname + '_' + req.body.chunkIndex);
}
});
//이 코드는 멀티가 방금 수행한 구성을 사용하기 위해 호출해야 하는 코드입니다.
const upload = multer({ storage: storage });
여기서는 파일 이름 콜백 함수가 이름 지정에 사용되며, 프런트엔드에서 전달된 FormData의 슬라이스 청크가 끝에 배치되고 이름 지정 형식은 다음과 같습니다:
이렇게 하면 다음에 따라 숫자를 정렬할 수 있습니다.
슬라이스 업로드가 완료된 후 다른 인터페이스를 작성하여 슬라이스를 병합합니다.
//모든 슬라이스가 프론트엔드에 업로드된 후 이 인터페이스가 호출되어 병합됩니다.
app.post('/upload/complete', (req, res) => {
// console.log('filename',req.body.filename);
const filename = req.body.filename;
combineChunks(path.resolve(__dirname, 'acceptFiles/'+filename),path.resolve(__dirname, 'temp'))
res.json('파일 업로드 완료');
});
// combineChunks 메서드는 모든 슬라이스를 하나의 파일로 병합하는 데 사용됩니다.
// 첫 번째 매개변수: 저장 경로, 여기서 작업 디렉터리`__dirname`,파일 저장 디렉터리`acceptFiles`,파일명
// 두 번째 매개변수: 슬라이스가 저장된 디렉터리 경로
const combineChunks = (outputFilename, tempDir) => {
// fs.reddir 폴더의 파일 읽기 , files는 파일 목록을 반환합니다.
fs.readdir(tempDir, (err, files) => {
if (err) {
console.error('Error reading directory:', err);
return;
}
//각 슬라이스의 이름에 따라 파일 목록을 정렬합니다(예: hhh)..mp4, 각 슬라이스는 파일_0,file_1 ,기타,
//슬라이스 순서는 병합에 매우 중요하며, 순서가 잘못되면 병합된 파일도 잘못됩니다.
files.sort((a, b) => {
return parseInt(a.split('_').pop()) - parseInt(b.split('_').pop())
})
console.log(''' 뒤에 오는 파일을 정렬합니다.,files);
// 디렉터리 경로 가져오기
const directoryPath = path.dirname(outputFilename);
// 디렉터리가 없는 경우 디렉터리 만들기
fs.mkdir(directoryPath, { recursive: true }, (err) => {
if (err) {
return console.error('디렉터리를 만드는 동안 오류가 발생했습니다.:', err);
}
});
// 파일 출력을 위한 쓰기 가능한 스트림 만들기
const output = fs.createWriteStream(outputFilename);
(async function() {
for (const file of files) {
console.log('file',file)
// 각 파일에 대해 다음과 같은 순서로 쓰여지도록 Promise가 생성됩니다.
await new Promise((resolve, reject) => {
// 현재 파일의 전체 경로 가져오기
const filePath = path.resolve(tempDir, file);
// 현재 파일을 읽는 스트림 만들기
const readStream = fs.createReadStream(filePath);
// 스트림을 읽는 동안 오류가 발생하면 "약속"이 거부됩니다.
readStream.on('error', reject);
// 이 "약속"은 스트림에 쓰는 데 오류가 있는 경우에도 거부됩니다.
output.on('error', reject);
// 읽기 스트림이 종료되면 이 프로미스는 현재 파일이 기록되었음을 나타내기 위해 확인됩니다.
readStream.on('end', resolve);
// 파이프 메서드를 사용하여 읽기 스트림의 내용을 출력 쓰기 가능한 스트림에 씁니다.
// end: false 이 매개변수는 현재 파일이 쓰여진 후 출력 스트림이 닫히지 않음을 나타냅니다.
readStream.pipe(output, { end: false });
});
}
// 모든 파일이 기록되면 출력 스트림을 닫습니다.
output.end();
})()
.then(
// 슬라이스가 저장된 폴더 삭제하기,recursive: true 재귀적 삭제를 나타냅니다.
() => fs.rm(tempDir, { recursive: true },
(err) => {
if (err) {
// 오류 처리
console.error(err);
} else {
// 삭제 성공
console.log('Directory removed successfully.');
}
})
)
.catch(e=>{
console.log('오류가 발생했습니다.,e)
});
})
}
파일을 선택하여 시도하는 방법은 다음과 같습니다.
업로드를 클릭하면 temp 디렉터리에 슬라이스가 있고 해당 슬라이스가 acceptFiles 디렉터리에 병합된 것을 확인할 수 있습니다.
병합이 완료되면 fs.rm 메서드가 실행되어 슬라이스가 저장된 임시 디렉터리를 삭제합니다.
전체 백엔드 코드:
const express = require('express');
const multer = require('multer');
const app = express();
const fs = require('fs');
const path = require('path');
app.use((req, res, next) => {
// 모든 도메인에 대한 교차 도메인 요청 허용
res.header('Access-Control-Allow-Origin', '*');
// 허용되는 HTTP 헤더 유형 설정
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
// 허용되는 HTTP 메서드 설정
if (req.method === 'OPTIONS') {
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE');
return res.status(200).json({});
}
// 요청을 계속 처리합니다.
next();
});
// express.json() 요청 본문을 JSON 형식으로 구문 분석하는 미들웨어입니다.
app.use(express.json());
// 구성 파일 저장 인터페이스가 업로드를 사용할 때 멀티 라이브러리의 구성입니다..single('file')미들웨어는 자동으로 이 구성을 호출합니다.
const storage = multer.diskStorage({
// 콜백 함수의 첫 번째 매개 변수는 오류 메시지를 나타내고 두 번째 매개 변수는 파일 저장 경로를 나타내는 파일 저장 위치를 설정합니다.
destination: function (req, file, cb) {
// 임시 폴더 만들기
try {
fs.mkdirSync(path.resolve(__dirname, 'temp'), { recursive: true });
} catch (error) {
console.log(' 임시 폴더를 만들지 못했습니다.,error);
}
cb(null, path.resolve(__dirname, 'temp'));
},
// 위와 같이 파일 이름을 설정하고, 첫 번째 매개 변수는 오류 메시지를 나타내고, 두 번째 매개 변수는 파일 이름을 나타내며, 여기에는 다음과 같은 함정이 있습니다.
filename: function (req, file, cb) {
// console.log('reqbody',req.body);
// console.log('file',file);
cb(null, file.fieldname + '_' + req.body.chunkIndex);
}
});
//이 코드는 멀티가 방금 수행한 구성을 사용하기 위해 호출해야 하는 코드입니다.
const upload = multer({ storage: storage });
app.post('/upload',upload.single('file'),(req, res) => {
console.log('데이터 반환 ---');
res.send({
code: 200,
msg: '업로드 성공',
});
});
//모든 슬라이스가 프론트엔드에 업로드된 후 이 인터페이스가 호출되어 병합됩니다.
app.post('/upload/complete', (req, res) => {
// console.log('filename',req.body.filename);
const filename = req.body.filename;
combineChunks(path.resolve(__dirname, 'acceptFiles/'+filename),path.resolve(__dirname, 'temp'))
res.json('파일 업로드 완료');
});
app.listen(3000, () => {
console.log('서버가 가동되었습니다');
});
// 이 메서드는 모든 슬라이스를 하나의 파일로 병합하는 데 사용됩니다.
// 첫 번째 매개변수: 저장 경로, 여기서 작업 디렉터리`__dirname`,파일 저장 디렉터리`acceptFiles`,파일명
// 두 번째 매개변수: 슬라이스가 저장된 디렉터리 경로
const combineChunks = (outputFilename, tempDir) => {
// fs.reddir 폴더의 파일 읽기 , files는 파일 목록을 반환합니다.
fs.readdir(tempDir, (err, files) => {
if (err) {
console.error('Error reading directory:', err);
return;
}
/**
각 슬라이스의 이름에 따라 파일 목록을 정렬합니다(예: hhh)..mp4, 각 슬라이스는 hhh.mp4_0,hhh.mp4_1 ,기타,
슬라이스 순서는 병합에 매우 중요하며, 순서가 잘못되면 병합된 파일도 잘못됩니다.
* */
files.sort((a, b) => {
return parseInt(a.split('_').pop()) - parseInt(b.split('_').pop())
})
console.log(''' 뒤에 오는 파일을 정렬합니다.,files);
// 디렉터리 경로 가져오기
const directoryPath = path.dirname(outputFilename);
// 디렉터리가 없는 경우 디렉터리 만들기
fs.mkdir(directoryPath, { recursive: true }, (err) => {
if (err) {
return console.error('디렉터리를 만드는 동안 오류가 발생했습니다.:', err);
}
});
// 파일 출력을 위한 쓰기 가능한 스트림 만들기
const output = fs.createWriteStream(outputFilename);
(async function() {
for (const file of files) {
console.log('file',file)
// 각 파일에 대해 다음과 같은 순서로 쓰여지도록 Promise가 생성됩니다.
await new Promise((resolve, reject) => {
// 현재 파일의 전체 경로 가져오기
const filePath = path.resolve(tempDir, file);
// 현재 파일을 읽는 스트림 만들기
const readStream = fs.createReadStream(filePath);
// 스트림을 읽는 동안 오류가 발생하면 "약속"이 거부됩니다.
readStream.on('error', reject);
// 이 "약속"은 스트림에 쓰는 데 오류가 있는 경우에도 거부됩니다.
output.on('error', reject);
// 읽기 스트림이 종료되면 이 프로미스는 현재 파일이 기록되었음을 나타내기 위해 확인됩니다.
readStream.on('end', resolve);
// 파이프 메서드를 사용하여 읽기 스트림의 내용을 출력 쓰기 가능한 스트림에 씁니다.
// end: false 이 매개변수는 현재 파일이 쓰여진 후 출력 스트림이 닫히지 않음을 나타냅니다.
readStream.pipe(output, { end: false });
});
}
// 모든 파일이 기록되면 출력 스트림을 닫습니다.
output.end();
})()
.then(
// 임시 폴더 삭제하기,recursive: true 재귀적 삭제를 나타냅니다.
() => fs.rm(tempDir, { recursive: true },
(err) => {
if (err) {
// 오류 처리
console.error(err);
} else {
// 삭제 성공
console.log('Directory removed successfully.');
}
})
)
.catch(e=>{
console.log('오류가 발생했습니다.,e)
});
})
}





