최근에 Selenium 을 이용해 네이버 블로그와 인스타그램을 Crawling 하는 프로젝트를 제작하고, 완성된 파일을 cx_Freeze
를 이용해 빌드하고 배포하고 있다.
하지만, 사용자가 늘어남에 따라 오류 수정이나 변경사항이 생기게 되면 크몽이나 텔레그램을 통해서 일일이 고객분들에게 연락과 함께 파일을 보내줘야하는 번거로운 상황이 발생하게 되었다..
프로그램 개발단계에서 이러한 문제점을 미리 생각하고 개발을 시작했어야 했는데, 너무 안일했다.
그래서 이번에 gitHub REST API 를 이용해
프로그램 실행시 현재 version 정보을 확인하고 최신버전이 아니라면, 자동으로 최신 버전으로 업데이트 되도록 구현해보려고 한다.
들어가기에 앞서 먼저 설정할 리포지토리에 Tag_name 과 Release 파일을 등록이 되어있어야 한다.
먼저, 내가 만든 Repositories 의 공개 범위가 public 이냐 private 에 따라 릴리스 버전 확인 방법이 살짝 틀리다.
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 에서 확인해 볼 수 있다.
여기서 우리가 필요한 정보는
위 2가지 이다.
더 자세한 정보를 보고싶으면 해당 document 링크에 들어가서 확인해보자.
릴리스 정보를 성공적으로 가져왔다면
현재의 버전 정보와 최근 릴리스의 버전과의 차이를 비교해
일치하지 않다면 업데이트를 진행하는 로직을 작성 해야한다.
# 현재 버전 정보 읽기!
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) 를 적어놨다.
만약 현재의 버전과 일치하지 않는다면 릴리스 노트에 반영된 파일을 다운로드 받아야 한다.
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 정보가 필요 유무에 따라 다르다.
이제 다운로드 받은 new Release Assets 파일을 압축 해제해 원본 파일과 충돌이 일어나지 않도록 다른 이름으로 저장하고, 원본파일 위에 덮어씌워보자!
먼저 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)
이제 업데이트 파일을 원본 파일 위에 덮어씌워야 한다.
나는 shutil
의 copytree
메소드를 사용해서 원본 파일 위로 덮어씌웠다.
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
옵션을 사용해서 덮어씌우기 에서 제외할 목록도 선택 가능하다.
마지막으로, 파일 최신화가 완료 되었으면 다운받은 업데이트 압축 파일과 압축 해제한 디렉토리를 삭제하여 마무리 해주면 된다!
import os
if receive["tag_name"] != now_current :
# ...
# 업데이트 zip 파일과 해당 파일을 압축 해제한 디렉토리 삭제
os.remove(update_newFile)
shutil.rmtree(os.path.join(current_directory, f"{update_temp_DIR}"))
위 작업으로 실행 파일을 최신 파일로 업데이트가 완료되었다면
현재 버전 정보를 저장하는 파일인 "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)
만약 내가 빌드한 실행파일의 크기가 1GB 이고 오류 수정을 위해 업데이트를 적용한 파일의 크기가 1MB 라고 하자.
이런 경우에는 1MB 파일을 변경하고자 1GB 파일 전체를 가져오게 된다면 아주 큰 손해라고 생각한다.
그래서 다음 게시글에서 수정사항이 있는 파일만 다운받아 업데이트를 적용시킬 수 있는 로직을 구현해볼까 한다.