영상 스트리밍 #3. 파일 이어서 업로드 하기

Bobby·2023년 3월 2일
1

streaming

목록 보기
3/9

대용량 영상을 올리려면 많은 시간이 발생한다.
현재는 업로드가 완료 되기전에 중지 되면 처음부터 다시 업로드를 해야한다.

만약 중간에 영상 업로드가 중지 되더라도 처음부터 업로드 하는 것이 아닌 업로드가 중지된 파일부터 다시 업로드를 재개 하도록 수정해보자.


🎥 업로드 키 생성

  • 현재 업로드 중인 파일인지 여부를 판단하는 키를 생성한다.
  • 파일의 길이, 타입, 이름을 더한 값의 SHA-256 해시함수를 사용했다.
  • 참고
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);
        });
}

🎥 업로드 키 추가

컨트롤러

  • 위에서 생성한 키를 PathVariable로 전송하여 사용했다.

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();
    }
    
    ...

서비스

  • 업로드하는 경로를 변경했다.
  • 기존 경로의 하위 경로에 key 값을 지정
    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 => {
                
              	...
                
            });
        };
                    
    ...

🎥 마지막 Chunk number 조회

  • 영상 업로드 시작 전에 마지막으로 업로드한 지점을 조회 한다.
  • 해당 key값의 경로 하위에 파일이 몇개 있는지 조회하여 리턴한다.
  • 업로드가 처음이라면 0을 리턴한다.

컨트롤러

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}
    를 요청하여 마지막 조각의 인덱스를 조회한다.
  • 처음 전송이므로 0을 리턴 받는다.
  • 업로드 중간에 새로고침을 한다.
  • 해당 키값으로 된 디렉토리 아래에 파일 조각들이 저장 되어있다.

  • 다시 같은 파일을 업로드 하면 업로드 시작 전에
    GET http://localhost:8080/chunk/upload/{key}
    를 요청하여 마지막 조각의 인덱스를 조회한다.

  • 파일조각이 9개이므로 인덱스 7을 리턴받았다.
  • 7번 파일 조각부터 전송을 재개한다.
  • 전송이 완료되면 임시 파일 조각들을 삭제하고 하나로 합친다.

코드

profile
물흐르듯 개발하다 대박나기

0개의 댓글

관련 채용 정보