feat: unified modpack creation and upload form in admin panel
- Объединены формы создания модпака и загрузки файлов в одну - Slug генерируется автоматически из названия с возможностью редактирования - Поддержка загрузки файлов сразу при создании/обновлении модпака - Автоматическое распаковывание ZIP архивов - Сохранение файлов в CAS с дедупликацией по SHA-1 - Улучшен UI с прогресс баром и валидацией форм
This commit is contained in:
@@ -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"})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user