255 lines
6.3 KiB
Go
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())
|
|
}
|
|
}
|