82.6 파일 추출하기

이제 아카이브 파일에 들어있는 특정 파일을 추출해보겠습니다. 다음은 파일을 추출하는 과정입니다.

그림 82-17 아카이브 파일에서 파일을 추출하는 과정

지금까지 파일 정보에 파일의 크기 size와 파일 데이터의 위치 dataOffset을 넣었으므로 파일을 추출할 때는 이 값들을 이용하면 됩니다.

소스 코드의 main 함수 윗부분에 다음과 같이 extract 함수를 추가합니다. 이 함수는 매개변수로 파일 이름을 받아서 아카이브에 파일이 있으면 추출합니다.

int extract(PARCHIVE archive, char *filename)    // 파일 추출 함수 정의
{
    // 파일 목록 연결 리스트를 순회
    PFILE_NODE curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        // 추출할 파일이 있는지 검사
        if (strcmp(curr->desc.name, filename) == 0)
        {
            int ret = 0;
            uint32_t size = curr->desc.size;
            uint8_t *buffer = malloc(size);

            // 파일 데이터가 있는 곳으로 파일 포인터를 이동시킴
            fseek(archive->fp, curr->desc.dataOffset, SEEK_SET);

            // 파일 데이터 읽기
            if (fread(buffer, size, 1, archive->fp) < 1)
            {
                printf("아카이브 파일 읽기 실패\n");
                ret = -1;       // -1은 실패
                goto Error1;    // buffer를 해제하는 에러 처리로 이동
            }

            // 추출한 파일을 저장할 새 파일 생성
            FILE *fp = fopen(filename, "wb");
            if (fp == NULL)
            {
                printf("%s 파일 열기 실패\n", filename);
                ret = -1;
                goto Error1;    // buffer를 해제하는 에러 처리로 이동
            }

            // 새로 생성된 파일에 파일 데이터 쓰기
            if (fwrite(buffer, size, 1, fp) < 1)
            {
                printf("%s 파일 쓰기 실패\n", filename);
                ret = -1;
                goto Error2;    // fp를 닫고, buffer를 해제하는 에러 처리로 이동
            }

            printf("%s 파일 추출 성공\n크기: %d\n", filename, size);

        Error2:
            fclose(fp);    // 파일 포인터 닫기

        Error1:
            free(buffer);    // 동적 메모리 해제

            return ret;      // 성공이냐 실패냐에 따라 0 또는 -1을 반환
        }

        curr = curr->next;
    }

    return -1;
}

먼저 archive->fileList를 활용하여 파일 목록 연결 리스트의 첫 번째 노드부터 순회하면서 추출할 파일이 있는지 검사합니다.

    // 파일 목록 연결 리스트를 순회
    PFILE_NODE curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        // 추출할 파일이 있는지 검사
        if (strcmp(curr->desc.name, filename) == 0)
        {

아카이브 파일 안에 추출할 파일이 있다면 파일 크기만큼 버퍼를 할당합니다. 그리고 아카이브 파일에서 파일 포인터를 desc.dataOffset만큼 이동시킵니다. 이렇게 하면 아카이브 파일에서 추출하고자 하는 파일 데이터를 읽을 준비가 끝납니다.

            int ret = 0;
            uint32_t size = curr->desc.size;
            uint8_t *buffer = malloc(size);

            // 파일 데이터가 있는 곳으로 파일 포인터를 이동시킴
            fseek(archive->fp, curr->desc.dataOffset, SEEK_SET);

이 코드를 그림으로 표현하면 다음과 같은 모양이 됩니다.

그림 82‑18 파일 포인터를 dataOffset만큼 이동시킴

이제 아카이브 파일에서 fread 함수로 추출할 파일 크기만큼 읽습니다.

            // 파일 데이터 읽기
            if (fread(buffer, size, 1, archive->fp) < 1)
            {
                printf("아카이브 파일 읽기 실패\n");
                ret = -1;       // -1은 실패
                goto Error1;    // buffer를 해제하는 에러 처리로 이동
            }

추출한 파일을 저장할 새 파일을 생성한 뒤 버퍼에 저장된 파일 데이터를 씁니다. 그리고 모든 작업이 끝났으므로 새 파일의 파일 포인터를 닫고 버퍼를 해제한 뒤 함수를 종료하면 됩니다(물론 여기서도 각 과정을 실패했을 때 사용할 레이블 Error1, Error2를 만듭니다).

            // 추출한 파일을 저장할 새 파일 생성
            FILE *fp = fopen(filename, "wb");
            if (fp == NULL)
            {
                printf("%s 파일 열기 실패\n", filename);
                ret = -1;
                goto Error1;    // buffer를 해제하는 에러 처리로 이동
            }

            // 새로 생성된 파일에 파일 데이터 쓰기
            if (fwrite(buffer, size, 1, fp) < 1)
            {
                printf("%s 파일 쓰기 실패\n", filename);
                ret = -1;
                goto Error2;    // fp를 닫고, buffer를 해제하는 에러 처리로 이동
            }

            printf("%s 파일 추출 성공\n크기: %d\n", filename, size);

        Error2:
            fclose(fp);    // 파일 포인터 닫기

        Error1:
            free(buffer);    // 동적 메모리 해제

            return ret;      // 성공이냐 실패냐에 따라 0 또는 -1을 반환
        }

        curr = curr->next;
    }

다음은 아카이브 파일에서 hello.txt 파일을 찾은 뒤 추출(파일을 생성)하는 과정입니다.

그림 82‑19 파일 추출

지금까지 만든 파일 추출 기능을 테스트합니다. 여기서는 앞에서 추가한 hello.txt 파일을 추출해봅시다. main 함수에서 list 함수 호출 부분은 지우고 다음과 같이 extract 함수 호출 부분을 추가하세요(전체 파일은 GitHub 저장소의 Unit 82/82.6 폴더에 들어있습니다).

main.c

int main
{
    // 생략 ...
        currPos = ftell(fp) + node->desc.size;
        fseek(fp, currPos, SEEK_SET);
    }

    extract(archive, "hello.txt");    // hello.txt 파일 추출

FINALIZE:
    // 파일 목록 연결 리스트를 순회하면서 메모리 해제
    curr = archive->fileList.next; // 첫 번째 노드
    while (curr != NULL)
    // 생략 ...
}

main.c 파일이 있는 폴더에 이전에 쓰던 hello.txt 파일이 있을 것입니다. 이 파일이 있으면 추출할 파일과 파일 이름이 겹치므로 삭제합니다.

이제 Visual Studio에서 Ctrl+F5 키를 눌러 프로그램을 실행해보면 다음과 같이 출력됩니다.

실행 결과

hello.txt 파일 추출 성공
크기: 13

hello.txt 파일이 새로 생성되어 있고, 파일을 열어보면 데이터도 온전히 들어있는 것을 볼 수 있습니다.

hello.txt

Hello, world!

이제 기본 기능을 구현했습니다. 하지만 매번 소스 코드를 수정해가면서 프로그램을 사용할 수는 없죠. 그래서 실행 파일에 명령을 지정하여 파일을 추가, 목록 출력, 추출하는 기능을 구현해보겠습니다.

filearchive.exe append hello.txt
filearchive.exe list
filearchive.exe extract hello.txt