Add server file manager workflow

This commit is contained in:
npc0-hue
2026-08-24 12:56:55 +08:00
parent dc9b0eaf2a
commit 33d3a13e74
20 changed files with 1961 additions and 22 deletions
+219
View File
@@ -1,6 +1,7 @@
package api
import (
"io"
"net/http"
"strconv"
"strings"
@@ -92,6 +93,14 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/install", h.serverDependenciesInstall)
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies", h.serverDependencies)
mux.HandleFunc("/api/v1/server-instances/{id}/logs/events", h.serverLogEvents)
mux.HandleFunc("/api/v1/server-instances/{id}/files/workspace", h.serverFilesWorkspace)
mux.HandleFunc("/api/v1/server-instances/{id}/files/list", h.serverFilesList)
mux.HandleFunc("/api/v1/server-instances/{id}/files/refresh", h.serverFilesRefresh)
mux.HandleFunc("/api/v1/server-instances/{id}/files/read-snapshot", h.serverFilesReadSnapshot)
mux.HandleFunc("/api/v1/server-instances/{id}/files/read", h.serverFilesRead)
mux.HandleFunc("/api/v1/server-instances/{id}/files/write", h.serverFilesWrite)
mux.HandleFunc("/api/v1/server-instances/{id}/files/upload", h.serverFilesUpload)
mux.HandleFunc("/api/v1/server-instances/{id}/files/download", h.serverFilesDownload)
mux.HandleFunc("/api/v1/server-instances/{id}/rcon/commands", h.sourceRCONCommands)
mux.HandleFunc("/api/v1/server-instances/{id}/administrators/candidates", h.serverAdministratorCandidates)
mux.HandleFunc("/api/v1/server-instances/{id}/administrators", h.serverAdministrators)
@@ -109,6 +118,7 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/run/jobs/source-rcon-input", h.requireRunSignature(h.runSourceRCONInput))
mux.HandleFunc("/api/v1/run/jobs/update-input", h.requireRunSignature(h.runJobUpdateInput))
mux.HandleFunc("/api/v1/run/jobs/update-chunk", h.requireRunSignature(h.runJobUpdateChunk))
mux.HandleFunc("/api/v1/run/files/input-chunk", h.requireRunSignature(h.runFileInputChunk))
mux.HandleFunc("/api/v1/run/jobs/update-health", h.requireRunSignature(h.runJobUpdateHealth))
mux.HandleFunc("/api/v1/run/jobs/client-manager-input", h.requireRunSignature(h.runClientManagerLifecycleInput))
mux.HandleFunc("/api/v1/run/jobs/client-manager-chunk", h.requireRunSignature(h.runClientManagerLifecycleChunk))
@@ -1278,6 +1288,196 @@ func (h *coreHandlers) serverInstanceConfigApprove(w http.ResponseWriter, r *htt
writeJSON(w, http.StatusAccepted, dto.ServerConfigWriteDispatchFromDomain(dispatch))
}
// serverFilesWorkspace godoc
// @Summary Read server file workspace
// @Description Returns plugin-declared logical directories and transfer policy without exposing host paths.
// @Tags server-files
// @Produce json
// @Param id path string true "Server instance ID"
// @Success 200 {object} dto.ServerFileWorkspaceResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/files/workspace [get]
func (h *coreHandlers) serverFilesWorkspace(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
workspace, err := h.core.GetServerFileWorkspaceForSession(bearerToken(r), r.PathValue("id"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerFileWorkspaceFromDomain(workspace))
}
func (h *coreHandlers) serverFilesList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
directoryKey, err := h.serverFileDirectoryKey(r, r.URL.Query().Get("directoryKey"))
if err != nil {
writeServiceError(w, err)
return
}
result, err := h.core.ListServerFilesForSession(bearerToken(r), domain.ServerFileListRequest{ServerInstanceID: r.PathValue("id"), DirectoryKey: directoryKey, Path: r.URL.Query().Get("path"), Query: r.URL.Query().Get("query"), Recursive: r.URL.Query().Get("recursive") == "true"})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.ServerFileListFromDomain(result))
}
func (h *coreHandlers) serverFilesRefresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerFileListRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
request.DirectoryKey, err = h.serverFileDirectoryKey(r, request.DirectoryKey)
if err != nil {
writeServiceError(w, err)
return
}
result, err := h.core.RefreshServerFileListForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.ServerFileListFromDomain(result))
}
func (h *coreHandlers) serverFilesReadSnapshot(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
snapshot, err := h.core.GetDeclaredFileReadSnapshotForSession(bearerToken(r), r.PathValue("id"), r.URL.Query().Get("key"))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.DeclaredFileReadSnapshotFromDomain(snapshot))
}
func (h *coreHandlers) serverFilesRead(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerFileReadRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ReadServerFileForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.FileOperationDispatchFromDomain(result))
}
func (h *coreHandlers) serverFilesWrite(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerFileWriteRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.WriteServerFileForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.FileOperationDispatchFromDomain(result))
}
func (h *coreHandlers) serverFilesUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
upload, err := h.decodeServerFileUpload(r)
if err != nil {
writeServiceError(w, err)
return
}
result, err := h.core.UploadServerFileForSession(bearerToken(r), upload)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.ServerFileUploadFromDomain(result))
}
func (h *coreHandlers) serverFilesDownload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerFileDownloadRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.PrepareServerFileDownloadForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.ServerFileDownloadFromDomain(result))
}
func (h *coreHandlers) serverFileDirectoryKey(r *http.Request, requested string) (string, error) {
requested = strings.TrimSpace(requested)
if requested != "" {
return requested, nil
}
workspace, err := h.core.GetServerFileWorkspaceForSession(bearerToken(r), r.PathValue("id"))
if err != nil {
return "", err
}
return workspace.DefaultDirectoryKey, nil
}
func (h *coreHandlers) decodeServerFileUpload(r *http.Request) (domain.ServerFileUploadRequest, error) {
if err := r.ParseMultipartForm(8 * 1024 * 1024); err != nil {
return domain.ServerFileUploadRequest{}, validator.ValidationError{Violations: []string{"multipart upload is invalid"}}
}
file, header, err := r.FormFile("file")
if err != nil {
return domain.ServerFileUploadRequest{}, validator.ValidationError{Violations: []string{"file is required"}}
}
defer file.Close()
payload, err := io.ReadAll(io.LimitReader(file, validator.MaxArtifactBytes+1))
if err != nil {
return domain.ServerFileUploadRequest{}, err
}
if int64(len(payload)) > validator.MaxArtifactBytes {
return domain.ServerFileUploadRequest{}, validator.ValidationError{Violations: []string{"payload is too large"}}
}
checksum := strings.TrimSpace(r.FormValue("checksum"))
if checksum == "" {
checksum = validator.BytesChecksum(payload)
}
filename := strings.TrimSpace(r.FormValue("filename"))
if filename == "" && header != nil {
filename = header.Filename
}
return domain.ServerFileUploadRequest{ServerInstanceID: r.PathValue("id"), DirectoryKey: r.FormValue("directoryKey"), RelativePath: r.FormValue("relativePath"), Filename: filename, Payload: payload, Checksum: checksum, IdempotencyKey: r.FormValue("idempotencyKey")}, nil
}
// fileOperationDispatch godoc
// @Summary Dispatch scoped file operation
// @Description Queues a scoped files.read or files.write job using logical file keys or refs, never raw host paths.
@@ -1673,6 +1873,25 @@ func (h *coreHandlers) runJobUpdateChunk(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusOK, dto.RunUpdateChunkFromDomain(result))
}
// runFileInputChunk serves one bounded browser-uploaded file input range only to the active fenced Run attempt.
func (h *coreHandlers) runFileInputChunk(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.RunFileInputChunkRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.ReadRunFileInputChunk(request.ToDomain())
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.RunFileInputChunkFromDomain(result))
}
// runJobUpdateHealth godoc
// @Summary Confirm a reconciled Run self-update outcome
// @Description Accepts a signed current-session health or rollback report fenced to the terminal update job attempt.