diff --git a/main.go b/main.go index 01360bf..090aea0 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -425,26 +426,44 @@ func downloadWithProgress(downloadURL, destDir string) error { return <-errCh } +// doDownloadRequest sends a GET (with Range if a partial file exists). +// If the server replies 416, it deletes the stale temp file and retries once. +func doDownloadRequest(downloadURL, tmpPath string) (*http.Response, error) { + for attempt := 0; attempt < 2; attempt++ { + var resumeOffset int64 + if fi, err := os.Stat(tmpPath); err == nil { + resumeOffset = fi.Size() + } + + req, err := http.NewRequest("GET", downloadURL, nil) + if err != nil { + return nil, fmt.Errorf("다운로드 요청 생성 실패: %w", err) + } + if resumeOffset > 0 { + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", resumeOffset)) + } + + resp, err := downloadClient.Do(req) + if err != nil { + return nil, fmt.Errorf("다운로드 연결 실패: %w", err) + } + + if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable { + resp.Body.Close() + os.Remove(tmpPath) + continue // 임시 파일 삭제 후 처음부터 재시도 + } + return resp, nil + } + return nil, fmt.Errorf("다운로드 실패: 재시도 횟수 초과") +} + func doDownload(downloadURL, destDir string) error { tmpPath := filepath.Join(os.TempDir(), "a301_game.zip") - // 이어받기: 기존 임시 파일 크기 확인 - var resumeOffset int64 - if fi, err := os.Stat(tmpPath); err == nil { - resumeOffset = fi.Size() - } - - req, err := http.NewRequest("GET", downloadURL, nil) + resp, err := doDownloadRequest(downloadURL, tmpPath) if err != nil { - return fmt.Errorf("다운로드 요청 생성 실패: %w", err) - } - if resumeOffset > 0 { - req.Header.Set("Range", fmt.Sprintf("bytes=%d-", resumeOffset)) - } - - resp, err := downloadClient.Do(req) - if err != nil { - return fmt.Errorf("다운로드 연결 실패: %w", err) + return err } defer resp.Body.Close() @@ -452,20 +471,26 @@ func doDownload(downloadURL, destDir string) error { var total int64 var tmpFile *os.File + // 이어받기: 기존 임시 파일 크기 확인 + var resumeOffset int64 + if fi, statErr := os.Stat(tmpPath); statErr == nil { + resumeOffset = fi.Size() + } + switch resp.StatusCode { case http.StatusPartialContent: // 서버가 Range 요청 수락 → 이어받기 downloaded = resumeOffset - total = resumeOffset + resp.ContentLength + if resp.ContentLength > 0 { + total = resumeOffset + resp.ContentLength + } tmpFile, err = os.OpenFile(tmpPath, os.O_WRONLY|os.O_APPEND, 0644) case http.StatusOK: // 서버가 Range 미지원이거나 파일이 변경됨 → 처음부터 - total = resp.ContentLength + if resp.ContentLength > 0 { + total = resp.ContentLength + } tmpFile, err = os.Create(tmpPath) - case http.StatusRequestedRangeNotSatisfiable: - // 임시 파일이 서버 파일보다 큼 (서버 파일 변경) → 처음부터 - os.Remove(tmpPath) - return doDownload(downloadURL, destDir) default: return fmt.Errorf("다운로드 실패 (HTTP %d)", resp.StatusCode) } @@ -501,6 +526,9 @@ func doDownload(downloadURL, destDir string) error { } if total > 0 { pct := int(downloaded * 100 / total) + if pct > 100 { + pct = 100 + } setProgress(fmt.Sprintf("다운로드 중... %d%%", pct), pct) } } @@ -554,9 +582,11 @@ func extractZip(zipPath, destDir string) error { var rel string if len(parts) == 2 && parts[1] != "" { rel = parts[1] - } else if len(parts) == 1 && parts[0] != "" { + } else if len(parts) == 1 && !strings.Contains(clean, "/") && parts[0] != "" { + // 최상위에 직접 있는 파일 (디렉토리 없는 zip) rel = parts[0] } else { + // 최상위 디렉토리 자체("A301/") 등 → 스킵 continue } @@ -630,6 +660,7 @@ func moveContents(srcDir, dstDir string) error { os.Remove(dst) // 실패한 부분 파일 제거 return err } + os.Remove(src) // 복사 성공 후 원본 삭제 } } } @@ -646,13 +677,23 @@ func copyFile(src, dst string) error { if err != nil { return err } - defer out.Close() - _, err = io.Copy(out, in) - return err + if _, err = io.Copy(out, in); err != nil { + out.Close() + return err + } + return out.Close() } // ── Server info ────────────────────────────────────────────────────────────── +// errNoRetry wraps errors that should not be retried (e.g. 4xx responses). +type errNoRetry struct { + err error +} + +func (e *errNoRetry) Error() string { return e.err.Error() } +func (e *errNoRetry) Unwrap() error { return e.err } + type downloadInfo struct { FileHash string `json:"fileHash"` URL string `json:"url"` @@ -666,10 +707,10 @@ func fetchServerInfoOnce() (*downloadInfo, error) { defer resp.Body.Close() if resp.StatusCode == 404 { - return nil, fmt.Errorf("게임이 아직 준비되지 않았습니다") + return nil, &errNoRetry{fmt.Errorf("게임이 아직 준비되지 않았습니다")} } if resp.StatusCode >= 400 { - return nil, fmt.Errorf("서버 오류 (HTTP %d)", resp.StatusCode) + return nil, &errNoRetry{fmt.Errorf("서버 오류 (HTTP %d)", resp.StatusCode)} } var info downloadInfo @@ -689,7 +730,8 @@ func fetchServerInfo() (*downloadInfo, error) { } lastErr = err // 4xx 에러는 재시도해도 의미 없음 - if strings.Contains(err.Error(), "서버 오류") || strings.Contains(err.Error(), "준비되지") { + var noRetry *errNoRetry + if errors.As(err, &noRetry) { return nil, err } time.Sleep(time.Duration(1<