대용량 영상을 올리려면 많은 시간이 발생한다.
현재는 업로드가 완료 되기전에 중지 되면 처음부터 다시 업로드를 해야한다.
만약 중간에 영상 업로드가 중지 되더라도 처음부터 업로드 하는 것이 아닌 업로드가 중지된 파일부터 다시 업로드를 재개 하도록 수정해보자.
const getKey = (file) => {
const id = file.name + file.size + file.type;
const encoded_id = new TextEncoder().encode(id);
return crypto.subtle.digest('SHA-256', encoded_id)
.then(hash => {
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
})
.catch(error => {
console.error(error);
});
}
ChunkUploadController
...
@ResponseBody
@PostMapping("/chunk/upload/{key}")
public ResponseEntity<String> chunkUpload(@RequestParam("chunk") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks,
@PathVariable String key) throws IOException {
boolean isDone = chunkUploadService.chunkUpload(file, chunkNumber, totalChunks, key);
return isDone ?
ResponseEntity.ok("File uploaded successfully") :
ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
}
...
video/{key}
경로에 파일 조각 저장ChunkUploadService
@Slf4j
@Service
public class ChunkUploadService {
public boolean chunkUpload(MultipartFile file, int chunkNumber, int totalChunks, String key) throws IOException {
String uploadDir = "video"; // 업로드 완료 후 저장 경로
String tempDir = "video/" + key; // 파일 조각 저장 경로
File dir = new File(tempDir);
if (!dir.exists()) {
dir.mkdirs();
}
String filename = file.getOriginalFilename() + ".part" + chunkNumber;
Path filePath = Paths.get(tempDir, filename);
Files.write(filePath, file.getBytes());
if (chunkNumber == totalChunks-1) {
String[] split = file.getOriginalFilename().split("\\.");
String outputFilename = UUID.randomUUID() + "." + split[split.length-1];
Path outputFile = Paths.get(uploadDir, outputFilename);
Files.createFile(outputFile);
for (int i = 0; i < totalChunks; i++) {
Path chunkFile = Paths.get(tempDir, file.getOriginalFilename() + ".part" + i);
Files.write(outputFile, Files.readAllBytes(chunkFile), StandardOpenOption.APPEND);
}
deleteDirectory(Paths.get(tempDir));
log.info("File uploaded successfully");
return true;
} else {
return false;
}
}
}
chunk.html
...
const sendNextChunk = () => {
...
const formData = new FormData();
formData.append("chunk", chunk, file.name);
formData.append("chunkNumber", currentChunk);
formData.append("totalChunks", totalChunks);
fetch("/chunk/upload/" + key, { // key 추가
method: "POST",
body: formData
}).then(resp => {
...
}).catch(err => {
...
});
};
...
ChunkUploadController
@Controller
@RequiredArgsConstructor
public class ChunkUploadController {
private final ChunkUploadService chunkUploadService;
...
@ResponseBody
@GetMapping("/chunk/upload/{key}")
public ResponseEntity<?> getLastChunkNumber(@PathVariable String key) {
return ResponseEntity.ok(chunkUploadService.getLastChunkNumber(key));
}
...
}
ex) 저장된 파일 조각 10개라면 마지막 인덱스 9이지만 하나 뺀 값인 8을 리턴
ChunkUploadService
@Slf4j
@Service
public class ChunkUploadService {
...
public int getLastChunkNumber(String key) {
Path temp = Paths.get("video", key);
String[] list = temp.toFile().list();
return list == null ? 0 : Math.max(list.length-2, 0);
}
}
chunk.html
const sendVideoChunks = async () => {
const chunkSize = 1024 * 1024; // 1MB
const file = document.getElementById("video-file").files[0];
const resultElement = document.getElementById("result");
const key = await getKey(file)
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = await getLastChunkNumber(key); // 최종 인덱스 조회
const sendNextChunk = () => {
...
};
sendNextChunk();
}
const getKey = (file) => {
...
}
const getLastChunkNumber = (key) => {
return fetch("/chunk/upload/" + key, {
method: "GET",
}).then(resp => resp.text()).then(data => data);
}
GET http://localhost:8080/chunk
GET http://localhost:8080/chunk/upload/{key}
해당 키값으로 된 디렉토리 아래에 파일 조각들이 저장 되어있다.
다시 같은 파일을 업로드 하면 업로드 시작 전에
GET http://localhost:8080/chunk/upload/{key}
를 요청하여 마지막 조각의 인덱스를 조회한다.