구글 드라이브가 점점 파일로 채워져서 용량을 업그레이드하라는 경고를 자주 받게 되었다. 가장 많은 용량을 차지하는 것은 구글 '포토'이다. 마이크로소프트 오피스 365 구독을 통해 소유하고 있는 1TB의 공간에 이를 옮기기로 결정하고 실제 실행에 옮긴 기록을 남기고자 한다. 이 파이프라인의 핵심은 구글 테이크아웃과 Windows PowerShell 스크립트이다. ChatGPT의 도움으로 개발한 이 스크립트의 특징은 글 마지막 부분에 소개하였다.
구글 테이크아웃은 구글 서비스에 등록된 모든 자료를 2GB 단위의 zip 파일로 분할하여 다운로드 링크를 제공한다. 나의 경우에는 총 93개의 파일이 생성되었다. 이 파일의 이름(001, 002...)은 연도순으로 붙여지는 것이 아님에 유의하자.
Zip 파일을 모두 다운로드하여 PC에서 압축을 해제한 뒤 그냥 원드라이브에 밀어 넣으면 되는가? 안타깝게도 구글 포토와 같이 촬영일자에 따른 미디어 정리라든가 앨범 정보 같은 것이 그대로 따라가지는 않는다. 따라서 PC에서 zip 파일을 받은 뒤 내부에 포함된 JSON 파일을 이용하여 각 미디어 파일을 연도별 폴더로 나누어 넣는 일이 핵심이다. 이 과정에서는 PowerShell 스크립트인 takeout_batch_year.ps1가 매우 중요한 역할을 하였다(개인 리포지토리로 이용하는 DokuWiki에서는 업로드할 수 있는 파일의 확장자를 제한하고 있기 때문에 zip으로 압축하여 올렸음). 이 스크립트는 여러 차례에 걸쳐 수정하면서 기능을 개선하였고 안정적인 동작을 하는 것을 최종적으로 확인하였다.... 아, 나중에 발견하였지만 꽤 심각한 문제점이 있었다. 이 글의 끝부분을 참조하기 바란다.
스크립트 작성에서는 로컬 PC에 저장 공간이 별로 많이 남지 않았다는 것을 고려하여 작동하도록 많은 신경을 썼다. 내 노트북 컴퓨터에서는 여유 공간이 별로 없어서 180GB가 넘는 총 93개의 zip 파일을 한꺼번에 다운로드하여 작업하는 것이 근본적으로 불가능하였다. 컴퓨터에 남은 여유 저장 공간을 점검하면서 일정 개수의 zip 파일을 다운로드하여 처리한 뒤 원드라이브 업로드 후에는 지워서 공간을 확보하고, 다음 차례의 zip 파일을 처리하는 방식으로 진행하는 것이 기본 아이디어였다.
그러나 ChatGPT와 더불어 개발 작업을 계속하면서 스크립트의 성능이 점점 좋아졌다. -ZipDir로 지정한 디렉토리에 다운로드한 파일을 계속 밀어 넣으면 스크립트는 자동적으로 -BatchSize 단위로 처리를 한 뒤 작업이 끝난 zip 파일을 다른 위치(-DestRoot 하위의 _processed 폴더)로 옮긴다. 반복문을 돌릴 필요도 없으며, 다음 배치 작업 전에 자동적으로 저장공간이 얼마나 남아 있는지 확인하여 안전한 수준이 아니면(-MinFreeGB로 지정한 값보다 적은 경우) 자동으로 작업을 중단한다. 사용자는 작업이 끝나서 다른 위치로 옮겨진 zip 파일을 이따금씩 지우면 된다.
스크립트는 Downloads 폴더에 두고 여기에서 PowerShell 창을 열어서 다음과 같이 명령어를 실행하면 된다. 첫 줄 명령어는 지금 열려 있는 PowerShell에서 .ps1 스크립트를 실행하기 위해 정책을 변경하는 명령으로서 현재 열린 창에서만 유효하다. 반복된 테스트에 의하면 -BatchSize는 4~8 정도가 적당한 것 같다. 빠른 압축 해제를 위해서 반드시 7-Zip이 필요하다. 이 유틸리티를 설치한 뒤 실행파일인 7z.exe의 위치를 Path 환경변수에 지정해야 한다.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass .\takeout_batch_year.ps1 ` -ZipDir "C:\Users\jeong\Downloads\Takeout_Zip" ` -DestRoot "C:\Users\jeong\Downloads\Photos_Backup\From_Google_Takeout" ` -BatchSize 4 ` -MinFreeGB 25
takeout_batch_year.ps1은 모든 미디어 파일에 대한 해시를 생성하여 한번 처리한 파일은 다시 작업하지 않는다. 따라서 작업 결과로 만들어진 _hashes_sha256.txt를 절대로 지워서는 안 된다. 1년이 지난 뒤 다시 구글 포토로부터 테이크아웃을 할 때, 증분 백업 같은 것은 지원하지 않는다. 지워지지 않고 남은 파일에 대해서 언제나 전체 백업을 할 뿐이다. 따라서 이 해시 파일은 유일한 작업 기록인 셈이니 이것 역시 작업 종류 후에는 원드라이브에 보관하는 것을 권장한다. zip 파일을 가져다가 똑같은 스크립트를 돌리면 압축 해제는 어쩔 수 없이 다시 돌겠지만, 해시에 이미 기록된 파일은 추가 작업을 하지 않는다.
그러므로 이번 이전 작업이 성공적으로 끝난 뒤에는 구글 포토에는 최근 5년 정도의 파일만 남겨 두는 것이 좋을 것이다.
한 배치에 대한 작업이 끝난 뒤 Photos_Backup/From_Google_Takeout 아래에는 다음과 같이 연도별로 정리된 폴더가 생기고 미디어 파일은 그 아래에 분류된다. JSON 파일의 분석에 실패하면 자동으로 가장 최근 연도인 2026 폴더로 들어간다.
| 스크립트 실행 후 연도별 폴더에 미디어 파일이 자동으로 분류되어 들어간다. 해시 파일과 문제점이 있는 파일의 기록이 여기에 남는다. |
너무나 당연한 이야기지만 -ZipDir이나 DestRoot에 원드라이브를 지정해서는 안 된다. 스크립트 작업이 다 끝나면 C:\Users\jeong\Downloads\Photos_Backup\From_Google_Takeout은 폴더 그대로 원드라이브의 '사진' 폴더 하위로 보내면 된다.
원드라이브는 구글 포토와 같은 수준의 앨범 작성이나 편집 기능 같은 것은 없다. 휴대폰에서 직접 원드라이브로 사진을 백업하게 만들면 휴대폰에 남아 있는 최신 사진의 경우 구글 포토와 비슷하게 관리할 수는 있다. 이는 PC에서는 원드라이브 -> 사진 -> Camera Roll에서 접근 가능하다. 오늘 글에서 설명한 아카이브 재정비 작업과는 별개이다.
PowerShell 기반 takeout_batch_years.ps1 사진 아카이브 자동화 파이프라인의 주요 특징
- Google Takeout ZIP을 배치 처리
- ZipDir에 있는
*.zip전체를 정렬해 순차 처리 -BatchSize(기본 4)개씩 묶어서 반복 실행(자동으로 다음 배치로 넘어감)
- ZipDir에 있는
- 배치별 임시 작업 폴더로 “누적 스캔” 방지
ZipDir\_work\<batchId>\...형태로 배치 전용 작업 폴더 생성- 스캔 대상은 해당 배치 폴더만(이전 실행 잔여물로 미디어 수가 튀는 문제 방지)
-KeepWork옵션이 없으면 배치 종료 후 작업 폴더 자동 삭제
- 7-Zip 우선 사용 + 미설치 시 Expand-Archive 폴백
7z.exe를 PATH에서 찾으면 7-Zip으로 빠르게 추출- 없으면 PowerShell
Expand-Archive로 추출(상대적으로 느림)
- 미디어 확장자만 선별 스캔
- JPG/JPEG/PNG/GIF/WEBP/HEIC, MP4/MOV 등(스크립트의
$MediaExt목록 기준)
- JPG/JPEG/PNG/GIF/WEBP/HEIC, MP4/MOV 등(스크립트의
- 연도 폴더 분류 로직(우선순위 명확)
- 1순위: Takeout JSON의
photoTakenTime.timestamp또는creationTime.timestamp - 2순위: (JPG/JPEG 한정) EXIF
DateTimeOriginal(0x9003)→ 없으면DateTime(0x0132) - 3순위: 최후 수단으로 파일
LastWriteTime.Year사용
- 1순위: Takeout JSON의
- JSON 매칭 성능 최적화
- 폴더 단위로 JSON을 한 번만 읽어
dir → (basename→year)캐시($JsonYearCache) 생성 - 같은 폴더 내 파일은 캐시로 빠르게 연도 조회
- 폴더 단위로 JSON을 한 번만 읽어
- 중복 제거의 핵심: 파일 내용 기반 SHA-256 해시
Get-FileHash SHA256로 파일 내용을 해싱_hashes_sha256.txt에 누적 저장하여 다음 배치/다음 실행에서도 중복 스킵- 파일명/폴더가 달라도 내용이 같으면 중복으로 제거됨
- 해시 DB 기록 안정화(버퍼링 + 재시도)
- 해시를 메모리 버퍼(
$HashBuffer)에 모았다가 배치 끝에 한 번에 기록 Add-Content실패 시 슬립을 두고 재시도(AppendTextWithRetry)
- 해시를 메모리 버퍼(
- 파일 이동 실패에 대한 복구 로직
Move-Item실패 시 재시도(MoveWithRetry)- 그래도 실패하면
Copy-Item재시도 후 원본 삭제(가능한 경우)
- 동일 파일명 충돌 방지
- 대상 경로에 같은 이름이 있으면
__1,__2…를 붙여 저장(덮어쓰기 방지)
- 대상 경로에 같은 이름이 있으면
- 문제 파일은 멈추지 않고 건너뛰며 기록
- 해시 계산 실패/해시 null/이동·복사 실패 등의 경우 처리 중단 없이 스킵
_bad_files.txt에 유형과 경로(및 일부 오류 메시지) 기록
- 디스크 여유공간 가드
-MinFreeGB(기본 25GB) 미만이면 배치를 진행하지 않음(안전 정지)- 배치 크기가 큰 경우 8→6→4로 자동 축소 시도 로직 포함
- 처리 완료 ZIP 관리
- 기본: 처리한 ZIP을
ZipDir\_processed로 이동(재처리 방지) - 옵션:
-DeleteZips지정 시 처리한 ZIP을 삭제(주의 필요)
- 기본: 처리한 ZIP을
- 운영 로그가 명확
- 배치 시작/추출/스캔된 미디어 개수/누적 해시 수/여유공간/ZIP 이동(또는 삭제) 등 타임스탬프 로그 출력
-VerboseLog켜면 중복 스킵 등 상세 로그도 출력
이 스크립트는 아직 개선할 점이 남아 있다. 중복을 제거한 미디어 파일에 대해서 원본의 JSON 파일도 같이 가져와야 하지만, 현재는 미디어 파일만 최종 결과물로 남기게 되어 있기 때문이다. 고난의 시작! 더 큰 문제는 JSON이나 EXIF 정보가 있는 원본 미디어 파일임에도 불구하고 촬영일 정보를 제대로 추출하지 못하여 2026 fallback으로 분류되는 것이 상당히 많았다는 점이다. 지금은 이에 대한 개선 작업을 진행하고 있다. 결과는 다음번 글에 쓰도록 하겠다.
댓글 없음:
댓글 쓰기