package download import ( "archive/zip" "crypto/sha256" "encoding/hex" "fmt" "io" "os" "path/filepath" "regexp" "strings" ) var versionRe = regexp.MustCompile(`v\d+[\.\d]*`) type Service struct { repo *Repository gameDir string } func NewService(repo *Repository, gameDir string) *Service { return &Service{repo: repo, gameDir: gameDir} } func (s *Service) GetInfo() (*Info, error) { return s.repo.GetLatest() } func (s *Service) GameFilePath() string { return filepath.Join(s.gameDir, "game.zip") } func (s *Service) LauncherFilePath() string { return filepath.Join(s.gameDir, "launcher.exe") } func (s *Service) UploadLauncher(body io.Reader, baseURL string) (*Info, error) { if err := os.MkdirAll(s.gameDir, 0755); err != nil { return nil, fmt.Errorf("디렉토리 생성 실패: %w", err) } finalPath := s.LauncherFilePath() tmpPath := finalPath + ".tmp" f, err := os.Create(tmpPath) if err != nil { return nil, fmt.Errorf("파일 생성 실패: %w", err) } n, err := io.Copy(f, body) f.Close() if err != nil { os.Remove(tmpPath) return nil, fmt.Errorf("파일 저장 실패: %w", err) } if err := os.Rename(tmpPath, finalPath); err != nil { os.Remove(tmpPath) return nil, fmt.Errorf("파일 이동 실패: %w", err) } launcherSize := "" if n > 0 { launcherSize = fmt.Sprintf("%.1f MB", float64(n)/1024/1024) } info, err := s.repo.GetLatest() if err != nil { info = &Info{} } info.LauncherURL = baseURL + "/api/download/launcher" info.LauncherSize = launcherSize return info, s.repo.Save(info) } // Upload streams the body directly to disk, then extracts metadata from the zip. func (s *Service) Upload(filename string, body io.Reader, baseURL string) (*Info, error) { if err := os.MkdirAll(s.gameDir, 0755); err != nil { return nil, fmt.Errorf("디렉토리 생성 실패: %w", err) } finalPath := s.GameFilePath() tmpPath := finalPath + ".tmp" f, err := os.Create(tmpPath) if err != nil { return nil, fmt.Errorf("파일 생성 실패: %w", err) } n, err := io.Copy(f, body) f.Close() if err != nil { os.Remove(tmpPath) return nil, fmt.Errorf("파일 저장 실패: %w", err) } if err := os.Rename(tmpPath, finalPath); err != nil { os.Remove(tmpPath) return nil, fmt.Errorf("파일 이동 실패: %w", err) } version := "" if m := versionRe.FindString(filename); m != "" { version = m } fileSize := "" if n > 0 { mb := float64(n) / 1024 / 1024 if mb >= 1000 { fileSize = fmt.Sprintf("%.1f GB", mb/1024) } else { fileSize = fmt.Sprintf("%.1f MB", mb) } } fileHash := hashGameExeFromZip(finalPath) if fileHash == "" { os.Remove(finalPath) return nil, fmt.Errorf("zip 파일에 %s이(가) 포함되어 있지 않습니다", "A301.exe") } info, err := s.repo.GetLatest() if err != nil { info = &Info{} } info.URL = baseURL + "/api/download/file" info.Version = version info.FileName = filename info.FileSize = fileSize info.FileHash = fileHash return info, s.repo.Save(info) } func hashGameExeFromZip(zipPath string) string { r, err := zip.OpenReader(zipPath) if err != nil { return "" } defer r.Close() for _, f := range r.File { if strings.EqualFold(filepath.Base(f.Name), "A301.exe") { rc, err := f.Open() if err != nil { return "" } h := sha256.New() _, err = io.Copy(h, rc) rc.Close() if err != nil { return "" } return hex.EncodeToString(h.Sum(nil)) } } return "" }