[python] gitHub REST API 를 이용한 자동 업데이트 프로젝트 - (1)

KimJiHong·2023년 11월 4일
1

My Project

목록 보기
2/3

gitHub REST API

최근에 Selenium 을 이용해 네이버 블로그와 인스타그램을 Crawling 하는 프로젝트를 제작하고, 완성된 파일을 cx_Freeze 를 이용해 빌드하고 배포하고 있다.

하지만, 사용자가 늘어남에 따라 오류 수정이나 변경사항이 생기게 되면 크몽이나 텔레그램을 통해서 일일이 고객분들에게 연락과 함께 파일을 보내줘야하는 번거로운 상황이 발생하게 되었다..

프로그램 개발단계에서 이러한 문제점을 미리 생각하고 개발을 시작했어야 했는데, 너무 안일했다.

그래서 이번에 gitHub REST API 를 이용해
프로그램 실행시 현재 version 정보을 확인하고 최신버전이 아니라면, 자동으로 최신 버전으로 업데이트 되도록 구현해보려고 한다.

SET Repo

들어가기에 앞서 먼저 설정할 리포지토리에 Tag_nameRelease 파일을 등록이 되어있어야 한다.

  • Tag_name
    • v1.2.9 와 같이 버전 이름을 태그 명으로 설정
  • Release
    • 태그 push 후 릴리스에서
      "Draft a new release" 또는 "Create a new release" 버튼을 눌러
      해당 버전을 태그명의 프로그램을 제공


GET latest Releases INFO. (values)

먼저, 내가 만든 Repositories 의 공개 범위가 public 이냐 private 에 따라 릴리스 버전 확인 방법이 살짝 틀리다.

  • public : api token 이 필요없다.
  • private : api token 키 필요하다.

public 리포지토리는 api token 키 없이 릴리스나 파일을 다운받을 수 있지만
private 리포지토리는 특정 api token 키가 필요하다.

import reqeusts

# GITHUB_REPO : "gitHub 아이디/Repository-NAME"
GITHUB_REPO = "JiHongKim98/testgithubapi"
# private 용 API token 키
GITHUB_API_TOKEN = "api-token key 번호"
API_SERVER_URL = f"https://api.github.com/repos/{GITHUB_REPO}"

# public REPO
response = requests.get(f"{API_SERVER_URL}/releases/latest")

# private REPO
# response = requests.get(f"{API_SERVER_URL}/releases/latest", auth=("gitHub 아이디", GITHUB_API_TOKEN))

if response.status_code != 200:
    print("릴리스 체크 실패")
    
receive = response.json()

위 코드와 같이 public 일 경우에는 username 과 API token 의 정보가 필요없지만

private 일 경우엔 username 과 API token 정보로 접근 권한을 획득한 뒤
최신 Release 체크가 가능하다! (API token 키 발급받기)

또한, gitHub REST API 의 JSON response 형식을 gitHub REST document 에서 확인해 볼 수 있다.

여기서 우리가 필요한 정보는

  1. Latest Release Version (tag_name) : 최신버전 확인용
  2. Download Link : 릴리스에 올라온 파일을 다운로드 받기용

위 2가지 이다.

더 자세한 정보를 보고싶으면 해당 document 링크에 들어가서 확인해보자.


Check Release Version

릴리스 정보를 성공적으로 가져왔다면

현재의 버전 정보와 최근 릴리스의 버전과의 차이를 비교해
일치하지 않다면 업데이트를 진행하는 로직을 작성 해야한다.

# 현재 버전 정보 읽기!
with open("version.txt", "r") as f:
    now_current = f.read()
    print(f"현재 버전 ==> {now_current}")

if receive["tag_name"] != now_current :

	# ... 
    # 업데이트 파일 다운로드 로직 
    # ...

else :
	print("이미 최신 버전")

나는 version.txt 라는 텍스트 파일에 현재 버전에 대한 정보(v1.0.1) 를 적어놨다.


Download Release assets

만약 현재의 버전과 일치하지 않는다면 릴리스 노트에 반영된 파일을 다운로드 받아야 한다.

gitHub REST API 을 통해 다운로드 받기 위해 다시 document 에서 확인해봤다.

document 를 확인해보면 Accept 헤더에 application/octet-stream 을 포함해 요청 한다면
stream 형식으로 redirect 할 수 있다고 되어있다.

즉, Accept 헤더를 통해서 Release assets 를 데이터 stream 을 통해서 다운 받을 수 있다!

이제 다시 코드로 구현해보자

if receive["tag_name"] != now_current :
    # assets 다운 REST API 요청 url
    download_url = receive["assets"][0]["url"]
    
    # public REPO
    response = requests.get(download_url, headers={'Accept': 'application/octet-stream'}, stream=True)
    
    # private REPO : auth 정보 필요!
    #response = requests.get(download_url, auth=("gitHub 아이디", GITHUB_API_TOKEN), headers={'Accept': 'application/octet-stream'}, stream=True)
    
    if response.status_code == 200:
	    # 다운로드 받은 zip 파일명 설정하기!
        update_newFile = "newAssets.zip" 

        with open(update_newFile, "wb") as update_file:
            for chunk in response.iter_content(chunk_size=8192*1024): #8MB 씩 Stream
                update_file.write(chunk)
    else:
        print("다운로드 요청 실패")

마찬가지로 public 리포지토리와 private 리포지토리와의 요청 방식은 auth 정보

즉, API token 정보가 필요 유무에 따라 다르다.


Extracting Downloaded Release Assets

이제 다운로드 받은 new Release Assets 파일을 압축 해제해 원본 파일과 충돌이 일어나지 않도록 다른 이름으로 저장하고, 원본파일 위에 덮어씌워보자!

Extract Files

먼저 zipfile 라이브러리를 통해 압축을 풀어보자.

import zipfile

if receive["tag_name"] != now_current:
	# ...
    
    # 압축 해제 로직
    update_temp_DIR = "update_temp_DIR" # 새로운 디렉토리를 만들어 저장
    with zipfile.ZipFile(update_newFile, 'r') as zip_ref:
    	zip_ref.extractall(update_temp_DIR)

Update Files

이제 업데이트 파일을 원본 파일 위에 덮어씌워야 한다.

나는 shutilcopytree 메소드를 사용해서 원본 파일 위로 덮어씌웠다.

import shutil

if receive["tag_name"] != now_current:
	# ...
    
    # 파일 덮어씌우기 로직
    # 현재 디렉토리 경로
    current_directory = os.path.dirname(os.path.realpath(__file__))
    
    # "업데이트된 실행파일 경로" 를 복사하여 "원본 실행파일 경로" 로 붙여넣기 (덮어쓰기)
    shutil.copytree(os.path.join(current_directory, f"{update_temp_DIR}"), current_directory, dirs_exist_ok=True)

또한 copytree 메소드의 ignore 옵션을 사용해서 덮어씌우기 에서 제외할 목록도 선택 가능하다.

Delete Update temp directory

마지막으로, 파일 최신화가 완료 되었으면 다운받은 업데이트 압축 파일과 압축 해제한 디렉토리를 삭제하여 마무리 해주면 된다!

import os

if receive["tag_name"] != now_current :
	# ...
    
    # 업데이트 zip 파일과 해당 파일을 압축 해제한 디렉토리 삭제
    os.remove(update_newFile)
    shutil.rmtree(os.path.join(current_directory, f"{update_temp_DIR}"))


Save Version & Restart program

위 작업으로 실행 파일을 최신 파일로 업데이트가 완료되었다면

현재 버전 정보를 저장하는 파일인 "version.txt" 파일에 최신 버전으로 바꿔주고

현재 실행중인 main 프로그램을 재 실행하여 업데이트된 main 프로그램으로 바꿔주면 자동 업데이트 로직은 종료된다.

if receive["tag_name"] != now_current :
	
    # ...
    
    # 버전 변경
    with open("version.txt", "w") as f:
        f.write(f"{receive['tag_name']}")
        print(f"{receive['tag_name']} 버전으로 업데이트 완료")

	# 현재 실행중인 파일인 "auto_update.py" 를 재실행 하여
    # 최신 버전의 "auto_update.py" 로 실행
    update_script = os.path.join("auto_updater.py")
    os.system(f"python {update_script}")
    sys.exit(0)

만약 main.py 파일이 아닌 main.exe 파일이라면
아래처럼 사용하면 된다.

os.system("main.exe")
sys.exit(0)


END - Think ?

만약 내가 빌드한 실행파일의 크기가 1GB 이고 오류 수정을 위해 업데이트를 적용한 파일의 크기가 1MB 라고 하자.

이런 경우에는 1MB 파일을 변경하고자 1GB 파일 전체를 가져오게 된다면 아주 큰 손해라고 생각한다.

그래서 다음 게시글에서 수정사항이 있는 파일만 다운받아 업데이트를 적용시킬 수 있는 로직을 구현해볼까 한다.


전체 소스코드 gitHub

profile
대충 할거면 시작도 하지 말자.

0개의 댓글