feat: launcher.exe와 game.zip 별도 업로드/서빙 분리
All checks were successful
Server CI/CD / deploy (push) Successful in 36s

- POST /api/download/upload/game - 게임 zip 업로드
- POST /api/download/upload/launcher - launcher.exe 업로드
- GET /api/download/launcher - launcher.exe 서빙
- Info 모델에 LauncherURL, LauncherSize 필드 추가
- Content-Disposition 헤더로 올바른 파일명 설정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 23:34:09 +09:00
parent 18c39bd4c5
commit f547593c6f
4 changed files with 73 additions and 2 deletions

View File

@@ -45,5 +45,29 @@ func (h *Handler) ServeFile(c *fiber.Ctx) error {
if _, err := os.Stat(path); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "파일이 없습니다"})
}
info, _ := h.svc.GetInfo()
filename := "game.zip"
if info != nil && info.FileName != "" {
filename = info.FileName
}
c.Set("Content-Disposition", `attachment; filename="`+filename+`"`)
return c.SendFile(path)
}
func (h *Handler) UploadLauncher(c *fiber.Ctx) error {
body := c.Request().BodyStream()
info, err := h.svc.UploadLauncher(body, h.baseURL)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "업로드 실패: " + err.Error()})
}
return c.JSON(info)
}
func (h *Handler) ServeLauncher(c *fiber.Ctx) error {
path := h.svc.LauncherFilePath()
if _, err := os.Stat(path); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "파일이 없습니다"})
}
c.Set("Content-Disposition", `attachment; filename="launcher.exe"`)
return c.SendFile(path)
}

View File

@@ -15,5 +15,7 @@ type Info struct {
Version string `json:"version" gorm:"not null"`
FileName string `json:"fileName" gorm:"not null"`
FileSize string `json:"fileSize" gorm:"not null"`
FileHash string `json:"fileHash" gorm:"not null;default:''"`
FileHash string `json:"fileHash" gorm:"not null;default:''"`
LauncherURL string `json:"launcherUrl" gorm:"not null;default:''"`
LauncherSize string `json:"launcherSize" gorm:"not null;default:''"`
}

View File

@@ -31,6 +31,49 @@ 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 {

View File

@@ -39,5 +39,7 @@ func Register(
dl := api.Group("/download")
dl.Get("/info", dlH.GetInfo)
dl.Get("/file", dlH.ServeFile)
dl.Post("/upload", middleware.Auth, middleware.AdminOnly, dlH.Upload)
dl.Get("/launcher", dlH.ServeLauncher)
dl.Post("/upload/game", middleware.Auth, middleware.AdminOnly, dlH.Upload)
dl.Post("/upload/launcher", middleware.Auth, middleware.AdminOnly, dlH.UploadLauncher)
}