zlh-agent/internal/handlers/files.go
2026-03-07 20:59:27 +00:00

255 lines
6.3 KiB
Go

package handlers
import (
"errors"
"io"
"mime"
"net/http"
"os"
"strconv"
"strings"
agentfiles "zlh-agent/internal/files"
)
func HandleGameFilesList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
return
}
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
resp, err := agentfiles.List(serverRoot, r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, resp)
}
func HandleGameFilesStat(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
return
}
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
resp, err := agentfiles.Stat(serverRoot, r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, resp)
}
func HandleGameFilesRead(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
return
}
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
resp, err := agentfiles.Read(serverRoot, r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, resp)
}
func HandleGameFilesDownload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
return
}
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
file, info, name, err := agentfiles.OpenDownload(serverRoot, r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
defer file.Close()
w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
http.ServeContent(w, r, name, info.ModTime(), file)
}
func HandleGameFilesRoot(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodDelete:
handleGameFilesDelete(w, r)
case http.MethodPut:
handleGameFilesWrite(w, r)
default:
writeJSONError(w, http.StatusMethodNotAllowed, "PUT or DELETE only")
}
}
func HandleGameFilesUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "POST only")
return
}
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || !strings.HasPrefix(mediaType, "multipart/") {
writeJSONError(w, http.StatusBadRequest, "multipart/form-data required")
return
}
normalizedPath, err := agentfiles.NormalizeVisiblePath(r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
overwrite := strings.EqualFold(r.URL.Query().Get("overwrite"), "true")
mr, err := r.MultipartReader()
if err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid multipart body")
return
}
for {
part, err := mr.NextPart()
if err == io.EOF {
writeJSONError(w, http.StatusBadRequest, "missing file part")
return
}
if err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid multipart body")
return
}
if part.FormName() != "file" {
part.Close()
continue
}
size, overwritten, err := agentfiles.Upload(serverRoot, normalizedPath, part, 0, overwrite)
part.Close()
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"uploaded": true,
"path": normalizedPath,
"size": size,
"overwritten": overwritten,
})
return
}
}
func HandleGameFilesRevert(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "POST only")
return
}
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
revertedPath, err := agentfiles.Revert(serverRoot, r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"reverted": true,
"path": revertedPath,
})
}
func handleGameFilesDelete(w http.ResponseWriter, r *http.Request) {
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
deletedPath, err := agentfiles.Delete(serverRoot, r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"deleted": true,
"path": deletedPath,
})
}
func handleGameFilesWrite(w http.ResponseWriter, r *http.Request) {
_, serverRoot, ok := requireMinecraftGame(w)
if !ok {
return
}
normalizedPath, err := agentfiles.NormalizeVisiblePath(r.URL.Query().Get("path"))
if err != nil {
writeFilesError(w, err)
return
}
limitedBody := io.LimitReader(r.Body, agentfiles.MaxWriteSize+1)
data, err := io.ReadAll(limitedBody)
if err != nil {
writeJSONError(w, http.StatusBadRequest, "invalid request body")
return
}
backupCreated, err := agentfiles.Write(serverRoot, normalizedPath, data)
if err != nil {
writeFilesError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"saved": true,
"path": normalizedPath,
"backupCreated": backupCreated,
})
}
func writeFilesError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, agentfiles.ErrInvalidPath):
writeJSONError(w, http.StatusBadRequest, err.Error())
case errors.Is(err, agentfiles.ErrPathEscape), errors.Is(err, agentfiles.ErrForbiddenPath):
writeJSONError(w, http.StatusForbidden, err.Error())
case errors.Is(err, agentfiles.ErrDeleteDenied), errors.Is(err, agentfiles.ErrWriteDenied), errors.Is(err, agentfiles.ErrUploadDenied):
writeJSONError(w, http.StatusForbidden, err.Error())
case errors.Is(err, agentfiles.ErrAlreadyExists):
writeJSONError(w, http.StatusConflict, err.Error())
case errors.Is(err, agentfiles.ErrNotDir), errors.Is(err, agentfiles.ErrNotFile), errors.Is(err, agentfiles.ErrBinaryFile):
writeJSONError(w, http.StatusBadRequest, err.Error())
case errors.Is(err, agentfiles.ErrTooLarge), errors.Is(err, agentfiles.ErrWriteTooLarge):
writeJSONError(w, http.StatusRequestEntityTooLarge, err.Error())
case errors.Is(err, os.ErrNotExist), errors.Is(err, http.ErrMissingFile):
writeJSONError(w, http.StatusNotFound, "path not found")
default:
writeJSONError(w, http.StatusInternalServerError, err.Error())
}
}