diff --git a/internal/admin/admin.go b/internal/admin/admin.go index d74a7e9..4adb00d 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -134,33 +134,101 @@ func (h *Handler) listModpacks(w http.ResponseWriter, r *http.Request) { } func (h *Handler) createModpack(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - utils.WriteError(w, http.StatusBadRequest, "Cannot read body") + // Parse multipart form (for both modpack data and file uploads) + if err := r.ParseMultipartForm(500 << 20); err != nil { // 500 MB max + utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 500 MB)") return } - var req modpackRequest - if err := json.Unmarshal(body, &req); err != nil { - utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") - return - } + // Extract modpack data from form + slug := r.FormValue("slug") + name := r.FormValue("name") + minecraftVersion := r.FormValue("minecraft_version") + javaVersionStr := r.FormValue("java_version") + serverIP := r.FormValue("server_ip") - if req.Slug == "" || req.Name == "" || req.MinecraftVersion == "" { + // Validate required fields + if slug == "" || name == "" || minecraftVersion == "" { utils.WriteError(w, http.StatusBadRequest, "slug, name, and minecraft_version are required") return } - _, err = h.db.Pool().Exec(r.Context(), + // Parse java_version + javaVersion := 8 // default + if javaVersionStr != "" { + if parsed, err := strconv.Atoi(javaVersionStr); err == nil { + javaVersion = parsed + } + } + + // Insert modpack into database + _, err := h.db.Pool().Exec(r.Context(), `INSERT INTO modpacks (slug, name, minecraft_version, java_version, server_ip, is_active) VALUES ($1, $2, $3, $4, $5, true)`, - req.Slug, req.Name, req.MinecraftVersion, req.JavaVersion, req.ServerIP, + slug, name, minecraftVersion, javaVersion, serverIP, ) if err != nil { utils.WriteError(w, http.StatusConflict, "Modpack with this slug already exists") return } + // Handle file uploads if any + files := r.MultipartForm.File["files"] + if len(files) > 0 { + // Create instance directory for this modpack + uploadDir := filepath.Join(h.cfg.CASDir, "..", "instances", slug) + os.MkdirAll(uploadDir, 0o755) + + var uploaded []string + for _, fh := range files { + src, err := fh.Open() + if err != nil { + continue + } + + data, err := io.ReadAll(src) + src.Close() + if err != nil { + continue + } + + if strings.HasSuffix(strings.ToLower(fh.Filename), ".zip") { + extracted, err := h.extractZip(data, uploadDir) + if err == nil { + uploaded = append(uploaded, extracted...) + continue + } + } + + hash := utils.SHA1Bytes(data) + destDir := filepath.Join(h.cfg.CASDir, hash[:2]) + os.MkdirAll(destDir, 0o755) + os.WriteFile(filepath.Join(destDir, hash), data, 0o644) + + h.db.Pool().Exec(r.Context(), + `INSERT INTO global_files (sha1, size_bytes, file_name) VALUES ($1, $2, $3) + ON CONFLICT (sha1) DO NOTHING`, + hash, int64(len(data)), fh.Filename, + ) + + instPath := filepath.Join(uploadDir, "mods", fh.Filename) + os.MkdirAll(filepath.Dir(instPath), 0o755) + os.WriteFile(instPath, data, 0o644) + + uploaded = append(uploaded, fh.Filename) + } + + // TODO: Optionally generate manifest after upload + // For now, just return success with upload info + utils.WriteJSON(w, http.StatusCreated, map[string]interface{}{ + "status": "created", + "uploaded": uploaded, + "count": len(uploaded), + }) + return + } + + // No files uploaded, just created modpack utils.WriteJSON(w, http.StatusCreated, map[string]string{"status": "created"}) } @@ -171,28 +239,101 @@ func (h *Handler) updateModpack(w http.ResponseWriter, r *http.Request) { return } - body, err := io.ReadAll(r.Body) - if err != nil { - utils.WriteError(w, http.StatusBadRequest, "Cannot read body") + // Parse multipart form (for both modpack data and file uploads) + if err := r.ParseMultipartForm(500 << 20); err != nil { // 500 MB max + utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 500 MB)") return } - var req modpackRequest - if err := json.Unmarshal(body, &req); err != nil { - utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") + // Extract modpack data from form + name := r.FormValue("name") + slug := r.FormValue("slug") + minecraftVersion := r.FormValue("minecraft_version") + javaVersionStr := r.FormValue("java_version") + serverIP := r.FormValue("server_ip") + + // Validate required fields + if name == "" || slug == "" || minecraftVersion == "" { + utils.WriteError(w, http.StatusBadRequest, "name, slug, and minecraft_version are required") return } + // Parse java_version + javaVersion := 8 // default + if javaVersionStr != "" { + if parsed, err := strconv.Atoi(javaVersionStr); err == nil { + javaVersion = parsed + } + } + + // Update modpack in database _, err = h.db.Pool().Exec(r.Context(), `UPDATE modpacks SET name=$1, slug=$2, minecraft_version=$3, java_version=$4, server_ip=$5 WHERE id=$6`, - req.Name, req.Slug, req.MinecraftVersion, req.JavaVersion, req.ServerIP, id, + name, slug, minecraftVersion, javaVersion, serverIP, id, ) if err != nil { utils.WriteError(w, http.StatusInternalServerError, "Update failed") return } + // Handle file uploads if any + files := r.MultipartForm.File["files"] + if len(files) > 0 { + // Create instance directory for this modpack + uploadDir := filepath.Join(h.cfg.CASDir, "..", "instances", slug) + os.MkdirAll(uploadDir, 0o755) + + var uploaded []string + for _, fh := range files { + src, err := fh.Open() + if err != nil { + continue + } + + data, err := io.ReadAll(src) + src.Close() + if err != nil { + continue + } + + if strings.HasSuffix(strings.ToLower(fh.Filename), ".zip") { + extracted, err := h.extractZip(data, uploadDir) + if err == nil { + uploaded = append(uploaded, extracted...) + continue + } + } + + hash := utils.SHA1Bytes(data) + destDir := filepath.Join(h.cfg.CASDir, hash[:2]) + os.MkdirAll(destDir, 0o755) + os.WriteFile(filepath.Join(destDir, hash), data, 0o644) + + h.db.Pool().Exec(r.Context(), + `INSERT INTO global_files (sha1, size_bytes, file_name) VALUES ($1, $2, $3) + ON CONFLICT (sha1) DO NOTHING`, + hash, int64(len(data)), fh.Filename, + ) + + instPath := filepath.Join(uploadDir, "mods", fh.Filename) + os.MkdirAll(filepath.Dir(instPath), 0o755) + os.WriteFile(instPath, data, 0o644) + + uploaded = append(uploaded, fh.Filename) + } + + // TODO: Optionally generate manifest after upload + // For now, just return success with upload info + utils.WriteJSON(w, http.StatusOK, map[string]interface{}{ + "status": "updated", + "uploaded": uploaded, + "count": len(uploaded), + }) + return + } + + // No files uploaded, just updated modpack utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } diff --git a/internal/templates/html/admin.html b/internal/templates/html/admin.html index d4f1b76..f2a78bf 100644 --- a/internal/templates/html/admin.html +++ b/internal/templates/html/admin.html @@ -29,56 +29,46 @@ - +