mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
@@ -1,6 +1,9 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
@@ -53,6 +56,162 @@ func (b *BaseApi) LoadContainerUsers(c *gin.Context) {
|
||||
helper.SuccessWithData(c, containerService.LoadUsers(req))
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary List container files
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Success 200 {array} dto.ContainerFileInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/search [post]
|
||||
func (b *BaseApi) ListContainerFiles(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
files, err := containerService.ListContainerFiles(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, files)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Upload container file
|
||||
// @Accept multipart/form-data
|
||||
// @Param containerID formData string true "containerID"
|
||||
// @Param path formData string true "path"
|
||||
// @Param file formData file true "file"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/upload [post]
|
||||
// @x-panel-log {"bodyKeys":["containerID","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"容器 [containerID] 上传文件到 [path]","formatEN":"Upload file to [path] in container [containerID]"}
|
||||
func (b *BaseApi) UploadContainerFile(c *gin.Context) {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
containerIDs := form.Value["containerID"]
|
||||
paths := form.Value["path"]
|
||||
uploadFiles := form.File["file"]
|
||||
if len(containerIDs) == 0 || len(paths) == 0 || len(uploadFiles) == 0 {
|
||||
helper.BadRequest(c, errors.New("invalid container file upload params"))
|
||||
return
|
||||
}
|
||||
req := dto.ContainerFileReq{
|
||||
ContainerID: containerIDs[0],
|
||||
Path: paths[0],
|
||||
}
|
||||
for _, uploadFile := range uploadFiles {
|
||||
file, err := uploadFile.Open()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
err = containerService.UploadContainerFile(req, path.Base(uploadFile.Filename), uploadFile.Size, file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Get container file content
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Success 200 {object} dto.ContainerFileContent
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/content [post]
|
||||
func (b *BaseApi) GetContainerFileContent(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
content, err := containerService.GetContainerFileContent(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, content)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Get container file size
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Success 200 {int} size
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/size [post]
|
||||
func (b *BaseApi) GetContainerFileSize(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
size, err := containerService.GetContainerFileSize(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, size)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Delete container file
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileBatchDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/del [post]
|
||||
// @x-panel-log {"bodyKeys":["containerID","paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除容器 [containerID] 文件 [paths]","formatEN":"Delete files [paths] in container [containerID]"}
|
||||
func (b *BaseApi) DeleteContainerFile(c *gin.Context) {
|
||||
var req dto.ContainerFileBatchDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := containerService.DeleteContainerFile(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary Download container file
|
||||
// @Accept json
|
||||
// @Param request body dto.ContainerFileReq true "request"
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/files/download [post]
|
||||
// @x-panel-log {"bodyKeys":["containerID","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载容器 [containerID] 文件 [path]","formatEN":"Download file [path] from container [containerID]"}
|
||||
func (b *BaseApi) DownloadContainerFile(c *gin.Context) {
|
||||
var req dto.ContainerFileReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.ContainerID == "" || req.Path == "" {
|
||||
helper.BadRequest(c, errors.New("invalid container file download params"))
|
||||
return
|
||||
}
|
||||
reader, fileName, contentType, err := containerService.DownloadContainerFile(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(fileName))
|
||||
c.DataFromReader(http.StatusOK, -1, contentType, reader, nil)
|
||||
}
|
||||
|
||||
// @Tags Container
|
||||
// @Summary List containers
|
||||
// @Accept json
|
||||
|
||||
@@ -48,6 +48,34 @@ type ContainerOptions struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type ContainerFileReq struct {
|
||||
ContainerID string `json:"containerID" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerFileBatchDeleteReq struct {
|
||||
ContainerID string `json:"containerID" validate:"required"`
|
||||
Paths []string `json:"paths" validate:"required,min=1,dive,required"`
|
||||
}
|
||||
|
||||
type ContainerFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsDir bool `json:"isDir"`
|
||||
IsLink bool `json:"isLink"`
|
||||
LinkTo string `json:"linkTo"`
|
||||
Size int64 `json:"size"`
|
||||
Mode string `json:"mode"`
|
||||
ModTime string `json:"modTime"`
|
||||
}
|
||||
|
||||
type ContainerFileContent struct {
|
||||
Content string `json:"content"`
|
||||
Size int64 `json:"size"`
|
||||
Truncated bool `json:"truncated"`
|
||||
IsBinary bool `json:"isBinary"`
|
||||
}
|
||||
|
||||
type ContainerStatus struct {
|
||||
Created int `json:"created"`
|
||||
Running int `json:"running"`
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -40,6 +43,7 @@ import (
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/gin-gonic/gin"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@@ -89,6 +93,12 @@ type IContainerService interface {
|
||||
Prune(req dto.ContainerPrune) error
|
||||
|
||||
LoadUsers(req dto.OperationWithName) []string
|
||||
ListContainerFiles(req dto.ContainerFileReq) ([]dto.ContainerFileInfo, error)
|
||||
UploadContainerFile(req dto.ContainerFileReq, fileName string, fileSize int64, file io.Reader) error
|
||||
GetContainerFileContent(req dto.ContainerFileReq) (*dto.ContainerFileContent, error)
|
||||
GetContainerFileSize(req dto.ContainerFileReq) (int64, error)
|
||||
DeleteContainerFile(req dto.ContainerFileBatchDeleteReq) error
|
||||
DownloadContainerFile(req dto.ContainerFileReq) (io.ReadCloser, string, string, error)
|
||||
|
||||
StreamLogs(ctx *gin.Context, params dto.StreamLog)
|
||||
}
|
||||
@@ -1166,6 +1176,444 @@ func (u *ContainerService) LoadUsers(req dto.OperationWithName) []string {
|
||||
return users
|
||||
}
|
||||
|
||||
func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.ContainerFileInfo, error) {
|
||||
if len(req.Path) == 0 {
|
||||
req.Path = "/"
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isDir := stat.Mode.IsDir()
|
||||
isLink := stat.Mode&os.ModeSymlink != 0
|
||||
if isLink && !isDir {
|
||||
linkDir, linkErr := isContainerDir(cli, req.ContainerID, req.Path)
|
||||
if linkErr == nil {
|
||||
isDir = linkDir
|
||||
}
|
||||
}
|
||||
if !isDir {
|
||||
return []dto.ContainerFileInfo{toContainerFileInfo(req.Path, stat, isDir)}, nil
|
||||
}
|
||||
|
||||
output, err := runContainerCommand(cli, req.ContainerID, []string{"ls", "-1A", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
files := make([]dto.ContainerFileInfo, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
name := strings.TrimSpace(line)
|
||||
if len(name) == 0 || name == "." || name == ".." {
|
||||
continue
|
||||
}
|
||||
childPath := req.Path
|
||||
if childPath == "/" {
|
||||
childPath = "/" + name
|
||||
} else {
|
||||
childPath = strings.TrimSuffix(childPath, "/") + "/" + name
|
||||
}
|
||||
childStat, statErr := cli.ContainerStatPath(ctx, req.ContainerID, childPath)
|
||||
if statErr != nil {
|
||||
continue
|
||||
}
|
||||
childIsDir := childStat.Mode.IsDir()
|
||||
if childStat.Mode&os.ModeSymlink != 0 && !childIsDir {
|
||||
linkDir, linkErr := isContainerDir(cli, req.ContainerID, childPath)
|
||||
if linkErr == nil {
|
||||
childIsDir = linkDir
|
||||
}
|
||||
}
|
||||
files = append(files, toContainerFileInfo(childPath, childStat, childIsDir))
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
if files[i].IsDir != files[j].IsDir {
|
||||
return files[i].IsDir
|
||||
}
|
||||
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) DeleteContainerFile(req dto.ContainerFileBatchDeleteReq) error {
|
||||
for _, item := range req.Paths {
|
||||
if strings.TrimSpace(item) == "/" {
|
||||
return buserr.New("ErrPathNotDelete")
|
||||
}
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
command := []string{"rm", "-rf", "--"}
|
||||
command = append(command, req.Paths...)
|
||||
_, err = runContainerCommand(cli, req.ContainerID, command)
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *ContainerService) UploadContainerFile(req dto.ContainerFileReq, fileName string, fileSize int64, file io.Reader) error {
|
||||
if len(req.Path) == 0 {
|
||||
req.Path = "/"
|
||||
}
|
||||
safeName := path.Base(fileName)
|
||||
if safeName == "." || safeName == "/" || len(safeName) == 0 {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
if _, mkErr := runContainerCommand(cli, req.ContainerID, []string{"mkdir", "-p", "--", req.Path}); mkErr != nil {
|
||||
return mkErr
|
||||
}
|
||||
stat, err = cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !stat.Mode.IsDir() {
|
||||
return fmt.Errorf("path %s is not directory", req.Path)
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
writeErr := make(chan error, 1)
|
||||
go func() {
|
||||
tw := tar.NewWriter(pipeWriter)
|
||||
header := &tar.Header{
|
||||
Name: safeName,
|
||||
Mode: 0644,
|
||||
Size: fileSize,
|
||||
ModTime: time.Now(),
|
||||
}
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
_ = tw.Close()
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(tw, file); err != nil {
|
||||
_ = tw.Close()
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
writeErr <- err
|
||||
return
|
||||
}
|
||||
_ = pipeWriter.Close()
|
||||
writeErr <- nil
|
||||
}()
|
||||
|
||||
err = cli.CopyToContainer(ctx, req.ContainerID, req.Path, pipeReader, container.CopyToContainerOptions{
|
||||
CopyUIDGID: true,
|
||||
})
|
||||
if err != nil {
|
||||
_ = pipeReader.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
<-writeErr
|
||||
return err
|
||||
}
|
||||
if err := <-writeErr; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) GetContainerFileContent(req dto.ContainerFileReq) (*dto.ContainerFileContent, error) {
|
||||
if len(req.Path) == 0 {
|
||||
return nil, buserr.New("ErrInvalidChar")
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stat.Mode.IsDir() {
|
||||
return nil, fmt.Errorf("path %s is directory", req.Path)
|
||||
}
|
||||
|
||||
content := &dto.ContainerFileContent{Size: stat.Size}
|
||||
headBytes, err := runContainerCommandRaw(cli, req.ContainerID, []string{"head", "-c", "4096", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bytes.IndexByte(headBytes, 0) >= 0 {
|
||||
content.IsBinary = true
|
||||
return content, nil
|
||||
}
|
||||
|
||||
const inlinePreviewMax = 512 * 1024
|
||||
if stat.Size <= inlinePreviewMax {
|
||||
raw, err := runContainerCommandRaw(cli, req.ContainerID, []string{"cat", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content.Content = string(raw)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
raw, err := runContainerCommandRaw(cli, req.ContainerID, []string{"tail", "-n", "300", "--", req.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content.Content = string(raw)
|
||||
content.Truncated = true
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) GetContainerFileSize(req dto.ContainerFileReq) (int64, error) {
|
||||
if len(req.Path) == 0 {
|
||||
return 0, buserr.New("ErrInvalidChar")
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !stat.Mode.IsDir() {
|
||||
return stat.Size, nil
|
||||
}
|
||||
output, err := runContainerCommand(cli, req.ContainerID, []string{"du", "-sb", "--", req.Path})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
parts := strings.Fields(output)
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("invalid du output")
|
||||
}
|
||||
size, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.ReadCloser, string, string, error) {
|
||||
if len(req.Path) == 0 {
|
||||
req.Path = "/"
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
|
||||
if err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
fileName := stat.Name
|
||||
if len(fileName) == 0 {
|
||||
fileName = "container-file"
|
||||
}
|
||||
if stat.Mode.IsDir() {
|
||||
if _, err := runContainerCommand(cli, req.ContainerID, []string{"sh", "-c", "command -v tar >/dev/null 2>&1"}); err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", fmt.Errorf("tar command not found in container")
|
||||
}
|
||||
|
||||
targetPath := path.Clean(req.Path)
|
||||
parentPath := path.Dir(targetPath)
|
||||
targetName := path.Base(targetPath)
|
||||
if parentPath == "." || parentPath == "" {
|
||||
parentPath = "/"
|
||||
}
|
||||
tarStream, err := runContainerCommandStream(cli, req.ContainerID, []string{
|
||||
"tar", "-czf", "-", "-C", parentPath, "--", targetName,
|
||||
})
|
||||
if err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", err
|
||||
}
|
||||
if !strings.HasSuffix(fileName, ".tar.gz") {
|
||||
fileName += ".tar.gz"
|
||||
}
|
||||
return &closeHookReader{
|
||||
ReadCloser: tarStream,
|
||||
onClose: cli.Close,
|
||||
}, fileName, "application/gzip", nil
|
||||
}
|
||||
|
||||
fileStream, err := runContainerCommandStream(cli, req.ContainerID, []string{"cat", "--", req.Path})
|
||||
if err != nil {
|
||||
_ = cli.Close()
|
||||
return nil, "", "", err
|
||||
}
|
||||
return &closeHookReader{
|
||||
ReadCloser: fileStream,
|
||||
onClose: cli.Close,
|
||||
}, fileName, "application/octet-stream", nil
|
||||
}
|
||||
|
||||
func runContainerCommand(cli *client.Client, containerID string, command []string) (string, error) {
|
||||
raw, err := runContainerCommandRaw(cli, containerID, command)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(raw)), nil
|
||||
}
|
||||
|
||||
type closeHookReader struct {
|
||||
io.ReadCloser
|
||||
onClose func() error
|
||||
}
|
||||
|
||||
func (r *closeHookReader) Close() error {
|
||||
var closeErr error
|
||||
if r.ReadCloser != nil {
|
||||
closeErr = r.ReadCloser.Close()
|
||||
}
|
||||
if r.onClose != nil {
|
||||
if err := r.onClose(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func runContainerCommandRaw(cli *client.Client, containerID string, command []string) ([]byte, error) {
|
||||
ctx := context.Background()
|
||||
resp, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||
Cmd: command,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hijack, err := cli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer hijack.Close()
|
||||
|
||||
raw, err := io.ReadAll(hijack.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
if _, err := stdcopy.StdCopy(&stdout, &stderr, bytes.NewReader(raw)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output := strings.TrimSpace(stdout.String())
|
||||
errorOutput := strings.TrimSpace(stderr.String())
|
||||
info, err := cli.ContainerExecInspect(ctx, resp.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.ExitCode != 0 {
|
||||
if len(errorOutput) != 0 {
|
||||
return nil, fmt.Errorf("%s", errorOutput)
|
||||
}
|
||||
if len(output) == 0 {
|
||||
return nil, fmt.Errorf("container command failed with exit code %d", info.ExitCode)
|
||||
}
|
||||
return nil, fmt.Errorf("%s", output)
|
||||
}
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func runContainerCommandStream(cli *client.Client, containerID string, command []string) (io.ReadCloser, error) {
|
||||
ctx := context.Background()
|
||||
resp, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||
Cmd: command,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hijack, err := cli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
go func() {
|
||||
defer hijack.Close()
|
||||
var stderr bytes.Buffer
|
||||
_, copyErr := stdcopy.StdCopy(pipeWriter, &stderr, hijack.Reader)
|
||||
if copyErr != nil {
|
||||
_ = pipeWriter.CloseWithError(copyErr)
|
||||
return
|
||||
}
|
||||
info, inspectErr := cli.ContainerExecInspect(ctx, resp.ID)
|
||||
if inspectErr != nil {
|
||||
_ = pipeWriter.CloseWithError(inspectErr)
|
||||
return
|
||||
}
|
||||
if info.ExitCode != 0 {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if len(msg) == 0 {
|
||||
msg = fmt.Sprintf("container command failed with exit code %d", info.ExitCode)
|
||||
}
|
||||
_ = pipeWriter.CloseWithError(fmt.Errorf("%s", msg))
|
||||
return
|
||||
}
|
||||
_ = pipeWriter.Close()
|
||||
}()
|
||||
return pipeReader, nil
|
||||
}
|
||||
|
||||
func toContainerFileInfo(filePath string, stat container.PathStat, isDir bool) dto.ContainerFileInfo {
|
||||
name := stat.Name
|
||||
if len(name) == 0 {
|
||||
items := strings.Split(strings.TrimSuffix(filePath, "/"), "/")
|
||||
name = items[len(items)-1]
|
||||
}
|
||||
isLink := stat.Mode&os.ModeSymlink != 0
|
||||
return dto.ContainerFileInfo{
|
||||
Name: name,
|
||||
Path: filePath,
|
||||
IsDir: isDir,
|
||||
IsLink: isLink,
|
||||
LinkTo: stat.LinkTarget,
|
||||
Size: stat.Size,
|
||||
Mode: stat.Mode.String(),
|
||||
ModTime: stat.Mtime.Format(constant.DateTimeLayout),
|
||||
}
|
||||
}
|
||||
|
||||
func isContainerDir(cli *client.Client, containerID, targetPath string) (bool, error) {
|
||||
_, err := runContainerCommand(cli, containerID, []string{
|
||||
"sh", "-c", "[ -d \"$1\" ]", "sh", targetPath,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func stringsToMap(list []string) map[string]string {
|
||||
var labelMap = make(map[string]string)
|
||||
for _, label := range list {
|
||||
|
||||
@@ -368,6 +368,36 @@
|
||||
"formatZH": "docker 服务 [operation]",
|
||||
"formatEN": "[operation] docker service"
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除容器 [containerID] 文件 [paths]",
|
||||
"formatEN": "Delete files [paths] in container [containerID]"
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载容器 [containerID] 文件 [path]",
|
||||
"formatEN": "Download file [path] from container [containerID]"
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "容器 [containerID] 上传文件到 [path]",
|
||||
"formatEN": "Upload file to [path] in container [containerID]"
|
||||
},
|
||||
"/containers/image/build": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
|
||||
@@ -35,6 +35,12 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
baRouter.POST("/prune", baseApi.ContainerPrune)
|
||||
|
||||
baRouter.POST("/users", baseApi.LoadContainerUsers)
|
||||
baRouter.POST("/files/search", baseApi.ListContainerFiles)
|
||||
baRouter.POST("/files/upload", baseApi.UploadContainerFile)
|
||||
baRouter.POST("/files/content", baseApi.GetContainerFileContent)
|
||||
baRouter.POST("/files/size", baseApi.GetContainerFileSize)
|
||||
baRouter.POST("/files/del", baseApi.DeleteContainerFile)
|
||||
baRouter.POST("/files/download", baseApi.DownloadContainerFile)
|
||||
|
||||
baRouter.GET("/repo", baseApi.ListRepo)
|
||||
baRouter.POST("/repo/status", baseApi.CheckRepoStatus)
|
||||
|
||||
@@ -523,6 +523,79 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/update": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigUpdateReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Update Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/telegram/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -739,6 +812,79 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/check": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginCheckReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Check Agent plugin installation status",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/install": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginInstallReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Install Agent plugin",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/providers": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -4095,6 +4241,225 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/content": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileContent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file content",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileBatchDeleteReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Download container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/search": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.ContainerFileInfo"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "List container files",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/size": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file size",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "containerID",
|
||||
"in": "formData",
|
||||
"name": "containerID",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "path",
|
||||
"in": "formData",
|
||||
"name": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "file",
|
||||
"in": "formData",
|
||||
"name": "file",
|
||||
"required": true,
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Upload container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/image": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -23746,6 +24111,104 @@ const docTemplate = `{
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginCheckReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginInstallReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"taskID": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"taskID",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginStatus": {
|
||||
"properties": {
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfig": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigUpdateReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"appId",
|
||||
"clientSecret"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentTelegramConfig": {
|
||||
"properties": {
|
||||
"botToken": {
|
||||
@@ -25035,6 +25498,86 @@ const docTemplate = `{
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileBatchDeleteReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"paths": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileContent": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBinary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileInfo": {
|
||||
"properties": {
|
||||
"isDir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isLink": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"linkTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"modTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerItemStats": {
|
||||
"properties": {
|
||||
"buildCacheReclaimable": {
|
||||
|
||||
@@ -519,6 +519,79 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfig"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/qqbot/update": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentQQBotConfigUpdateReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Update Agent QQ Bot channel config",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/channel/telegram/get": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -735,6 +808,79 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/check": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginCheckReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Check Agent plugin installation status",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/plugin/install": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.AgentPluginInstallReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Install Agent plugin",
|
||||
"tags": [
|
||||
"AI"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ai/agents/providers": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@@ -4091,6 +4237,225 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/content": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileContent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file content",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileBatchDeleteReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Download container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/search": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.ContainerFileInfo"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "List container files",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/size": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"in": "body",
|
||||
"name": "request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ContainerFileReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Get container file size",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"description": "containerID",
|
||||
"in": "formData",
|
||||
"name": "containerID",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "path",
|
||||
"in": "formData",
|
||||
"name": "path",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "file",
|
||||
"in": "formData",
|
||||
"name": "file",
|
||||
"required": true,
|
||||
"type": "file"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
},
|
||||
{
|
||||
"Timestamp": []
|
||||
}
|
||||
],
|
||||
"summary": "Upload container file",
|
||||
"tags": [
|
||||
"Container"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/containers/image": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -23742,6 +24107,104 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginCheckReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginInstallReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"taskID": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"qqbot"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"taskID",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentPluginStatus": {
|
||||
"properties": {
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfig": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentQQBotConfigUpdateReq": {
|
||||
"properties": {
|
||||
"agentId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"appId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"agentId",
|
||||
"appId",
|
||||
"clientSecret"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.AgentTelegramConfig": {
|
||||
"properties": {
|
||||
"botToken": {
|
||||
@@ -25031,6 +25494,86 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileBatchDeleteReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"paths": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileContent": {
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBinary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
},
|
||||
"truncated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileInfo": {
|
||||
"properties": {
|
||||
"isDir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isLink": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"linkTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"modTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerFileReq": {
|
||||
"properties": {
|
||||
"containerID": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"dto.ContainerItemStats": {
|
||||
"properties": {
|
||||
"buildCacheReclaimable": {
|
||||
|
||||
@@ -368,6 +368,36 @@
|
||||
"formatZH": "docker 服务 [operation]",
|
||||
"formatEN": "[operation] docker service"
|
||||
},
|
||||
"/containers/files/del": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"paths"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "删除容器 [containerID] 文件 [paths]",
|
||||
"formatEN": "Delete files [paths] in container [containerID]"
|
||||
},
|
||||
"/containers/files/download": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "下载容器 [containerID] 文件 [path]",
|
||||
"formatEN": "Download file [path] from container [containerID]"
|
||||
},
|
||||
"/containers/files/upload": {
|
||||
"bodyKeys": [
|
||||
"containerID",
|
||||
"path"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "容器 [containerID] 上传文件到 [path]",
|
||||
"formatEN": "Upload file to [path] in container [containerID]"
|
||||
},
|
||||
"/containers/image/build": {
|
||||
"bodyKeys": [
|
||||
"name"
|
||||
|
||||
@@ -98,6 +98,7 @@ func OperationLog() gin.HandlerFunc {
|
||||
writer := responseBodyWriter{
|
||||
ResponseWriter: c.Writer,
|
||||
body: &bytes.Buffer{},
|
||||
captureBody: shouldCaptureResponseBody(c.Request.URL.Path),
|
||||
}
|
||||
c.Writer = &writer
|
||||
now := time.Now()
|
||||
@@ -141,12 +142,24 @@ func OperationLog() gin.HandlerFunc {
|
||||
datas, _ = io.ReadAll(reader)
|
||||
}
|
||||
var res response
|
||||
_ = json.Unmarshal(datas, &res)
|
||||
if res.Code == 200 {
|
||||
record.Status = constant.StatusSuccess
|
||||
contentType := strings.ToLower(c.Writer.Header().Get("Content-Type"))
|
||||
isJSONResponse := strings.Contains(contentType, "application/json")
|
||||
if isJSONResponse {
|
||||
_ = json.Unmarshal(datas, &res)
|
||||
if res.Code == 200 {
|
||||
record.Status = constant.StatusSuccess
|
||||
} else {
|
||||
record.Status = constant.StatusFailed
|
||||
record.Message = res.Message
|
||||
}
|
||||
} else {
|
||||
record.Status = constant.StatusFailed
|
||||
record.Message = res.Message
|
||||
statusCode := c.Writer.Status()
|
||||
if statusCode >= 200 && statusCode < 400 {
|
||||
record.Status = constant.StatusSuccess
|
||||
} else {
|
||||
record.Status = constant.StatusFailed
|
||||
record.Message = http.StatusText(statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
latency := time.Since(now)
|
||||
@@ -209,6 +222,7 @@ type responseBodyWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
resolvedHeader string
|
||||
captureBody bool
|
||||
}
|
||||
|
||||
func (r *responseBodyWriter) sanitizeResolvedHeader() {
|
||||
@@ -230,10 +244,20 @@ func (r *responseBodyWriter) WriteHeaderNow() {
|
||||
|
||||
func (r *responseBodyWriter) Write(b []byte) (int, error) {
|
||||
r.sanitizeResolvedHeader()
|
||||
r.body.Write(b)
|
||||
if r.captureBody {
|
||||
r.body.Write(b)
|
||||
}
|
||||
return r.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func shouldCaptureResponseBody(reqPath string) bool {
|
||||
reqPath = strings.ToLower(reqPath)
|
||||
if strings.Contains(reqPath, "download") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func loadLogInfo(path string) string {
|
||||
path = replaceStr(path, "/api/v2", "/core", "/xpack")
|
||||
if !strings.Contains(path, "/") {
|
||||
|
||||
@@ -56,6 +56,26 @@ export namespace Container {
|
||||
name: string;
|
||||
state: string;
|
||||
}
|
||||
export interface ContainerFileReq {
|
||||
containerID: string;
|
||||
path: string;
|
||||
}
|
||||
export interface ContainerFileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
isDir: boolean;
|
||||
isLink: boolean;
|
||||
linkTo: string;
|
||||
size: number;
|
||||
mode: string;
|
||||
modTime: string;
|
||||
}
|
||||
export interface ContainerFileContent {
|
||||
content: string;
|
||||
size: number;
|
||||
truncated: boolean;
|
||||
isBinary: boolean;
|
||||
}
|
||||
export interface ResourceLimit {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
|
||||
@@ -15,6 +15,30 @@ export const listContainerByImage = (image: string) => {
|
||||
export const loadContainerUsers = (name: string) => {
|
||||
return http.post<Array<string>>(`/containers/users`, { name: name });
|
||||
};
|
||||
export const listContainerFiles = (params: Container.ContainerFileReq) => {
|
||||
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const uploadContainerFile = (params: FormData) => {
|
||||
return http.upload(`/containers/files/upload`, params, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: TimeoutEnum.T_5M,
|
||||
});
|
||||
};
|
||||
export const getContainerFileContent = (params: Container.ContainerFileReq) => {
|
||||
return http.post<Container.ContainerFileContent>(`/containers/files/content`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const getContainerFileSize = (params: Container.ContainerFileReq) => {
|
||||
return http.post<number>(`/containers/files/size`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const deleteContainerFile = (params: { containerID: string; paths: string[] }) => {
|
||||
return http.post(`/containers/files/del`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const downloadContainerFile = (params: { containerID: string; path: string }) => {
|
||||
return http.download<BlobPart>(`/containers/files/download`, params, {
|
||||
responseType: 'blob',
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
});
|
||||
};
|
||||
export const loadContainerStatus = () => {
|
||||
return http.get<Container.ContainerStatus>(`/containers/status`);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<DrawerPro
|
||||
v-model="visible"
|
||||
:header="$t('home.dir')"
|
||||
:resource="title"
|
||||
size="large"
|
||||
:fullScreen="true"
|
||||
@close="visible = false"
|
||||
>
|
||||
<template #content>
|
||||
<el-form label-position="top">
|
||||
<el-form-item>
|
||||
<div class="path-breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item>
|
||||
<el-link type="primary" @click="navigateToPath('/')">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
</el-link>
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="item in pathSegments" :key="item.path">
|
||||
<el-link type="primary" @click="navigateToPath(item.path)">
|
||||
{{ item.name }}
|
||||
</el-link>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange">
|
||||
<el-button class="mt-2" :loading="uploading" type="primary" plain>
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button class="mt-2 ml-3" plain :disabled="selectedRows.length === 0" @click="onBatchDelete">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<el-button class="mt-2" plain @click="loadContainerFiles">
|
||||
{{ $t('commons.button.refresh') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<ComplexTable
|
||||
:data="containerFiles"
|
||||
size="small"
|
||||
max-height="calc(100vh - 240px)"
|
||||
v-model:selects="selectedRows"
|
||||
>
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column :label="$t('commons.table.name')" min-width="260" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div class="name-cell">
|
||||
<el-icon>
|
||||
<FolderOpened v-if="row.isDir" />
|
||||
<Document v-else />
|
||||
</el-icon>
|
||||
<el-link v-if="row.isDir" type="primary" @click="enterDir(row.path)">
|
||||
{{ row.name }}
|
||||
</el-link>
|
||||
<el-link v-else type="primary" @click="onOpenFile(row.path)">
|
||||
{{ row.name }}
|
||||
</el-link>
|
||||
<span v-if="row.isLink && row.linkTo" class="symlink-target">-> {{ row.linkTo }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('file.size')" width="130">
|
||||
<template #default="{ row }">
|
||||
<span v-if="!row.isDir">{{ formatSize(row.size) }}</span>
|
||||
<el-button
|
||||
v-else-if="!row.sizeLoaded"
|
||||
link
|
||||
type="primary"
|
||||
:loading="row.sizeLoading"
|
||||
@click="onCalcDirSize(row)"
|
||||
>
|
||||
{{ $t('file.calculate') }}
|
||||
</el-button>
|
||||
<span v-else>{{ formatSize(row.size) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.operate')" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="onDownloadFile(row)">
|
||||
{{ $t('commons.button.download') }}
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="onDeleteFile(row)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ComplexTable>
|
||||
</el-form>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<el-dialog v-model="previewVisible" :title="previewTitle" width="70%" destroy-on-close>
|
||||
<el-alert
|
||||
v-if="previewTruncated"
|
||||
:title="$t('file.previewTruncated')"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-2"
|
||||
/>
|
||||
<pre class="preview-content">{{ previewContent }}</pre>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import {
|
||||
deleteContainerFile,
|
||||
downloadContainerFile,
|
||||
getContainerFileContent,
|
||||
getContainerFileSize,
|
||||
listContainerFiles,
|
||||
uploadContainerFile,
|
||||
} from '@/api/modules/container';
|
||||
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
|
||||
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
|
||||
import { computeSize2 } from '@/utils/util';
|
||||
|
||||
const visible = ref(false);
|
||||
const title = ref('');
|
||||
const containerID = ref('');
|
||||
const filePath = ref('/');
|
||||
const containerFiles = ref<any[]>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const uploading = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewTitle = ref('');
|
||||
const previewContent = ref('');
|
||||
const previewTruncated = ref(false);
|
||||
const selectedRows = ref<any[]>([]);
|
||||
const pathSegments = computed(() => {
|
||||
const parts = filePath.value.split('/').filter((item) => item);
|
||||
return parts.map((name, index) => ({
|
||||
name,
|
||||
path: '/' + parts.slice(0, index + 1).join('/'),
|
||||
}));
|
||||
});
|
||||
|
||||
interface DrawerProps {
|
||||
containerID: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const acceptParams = async (params: DrawerProps): Promise<void> => {
|
||||
visible.value = true;
|
||||
containerID.value = params.containerID;
|
||||
title.value = params.title;
|
||||
filePath.value = '/';
|
||||
await loadContainerFiles();
|
||||
};
|
||||
|
||||
const loadContainerFiles = async () => {
|
||||
if (!containerID.value || !filePath.value) {
|
||||
return;
|
||||
}
|
||||
await listContainerFiles({
|
||||
containerID: containerID.value,
|
||||
path: filePath.value,
|
||||
})
|
||||
.then((res) => {
|
||||
containerFiles.value = (res.data || []).map((item) => ({
|
||||
...item,
|
||||
sizeLoaded: !item.isDir,
|
||||
sizeLoading: false,
|
||||
}));
|
||||
selectedRows.value = [];
|
||||
})
|
||||
.catch(() => {
|
||||
containerFiles.value = [];
|
||||
});
|
||||
};
|
||||
|
||||
const enterDir = async (path: string) => {
|
||||
filePath.value = path;
|
||||
await loadContainerFiles();
|
||||
};
|
||||
|
||||
const navigateToPath = async (path: string) => {
|
||||
filePath.value = path || '/';
|
||||
await loadContainerFiles();
|
||||
};
|
||||
|
||||
const onUploadChange = async (uploadFile: UploadFile) => {
|
||||
if (!uploadFile.raw) {
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('containerID', containerID.value);
|
||||
formData.append('path', filePath.value);
|
||||
formData.append('file', uploadFile.raw);
|
||||
uploading.value = true;
|
||||
await uploadContainerFile(formData)
|
||||
.then(async () => {
|
||||
MsgSuccess(i18n.global.t('file.uploadSuccess'));
|
||||
await loadContainerFiles();
|
||||
})
|
||||
.catch((err) => {
|
||||
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
|
||||
})
|
||||
.finally(() => {
|
||||
uploading.value = false;
|
||||
uploadRef.value?.clearFiles();
|
||||
});
|
||||
};
|
||||
|
||||
const onOpenFile = async (path: string) => {
|
||||
await getContainerFileContent({
|
||||
containerID: containerID.value,
|
||||
path,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data?.isBinary) {
|
||||
MsgWarning(i18n.global.t('file.fileCanNotRead'));
|
||||
return;
|
||||
}
|
||||
previewTitle.value = path;
|
||||
previewContent.value = res.data?.content || '';
|
||||
previewTruncated.value = !!res.data?.truncated;
|
||||
previewVisible.value = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
|
||||
});
|
||||
};
|
||||
|
||||
const onCalcDirSize = async (row: any) => {
|
||||
row.sizeLoading = true;
|
||||
await getContainerFileSize({
|
||||
containerID: containerID.value,
|
||||
path: row.path,
|
||||
})
|
||||
.then((res) => {
|
||||
row.size = res.data || 0;
|
||||
row.sizeLoaded = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
|
||||
})
|
||||
.finally(() => {
|
||||
row.sizeLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
const formatSize = (size: number) => {
|
||||
return computeSize2(size || 0);
|
||||
};
|
||||
|
||||
const onDownloadFile = async (row: any) => {
|
||||
const blob = await downloadContainerFile({
|
||||
containerID: containerID.value,
|
||||
path: row.path,
|
||||
});
|
||||
const downloadUrl = window.URL.createObjectURL(new Blob([blob]));
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
const fileName = row.path.split('/').pop() || 'container-file';
|
||||
a.download = row.isDir ? `${fileName}.tar.gz` : fileName;
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
};
|
||||
|
||||
const onDeleteFile = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(i18n.global.t('file.deleteHelper2'), i18n.global.t('commons.button.delete'), {
|
||||
type: 'warning',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await deleteContainerFile({
|
||||
containerID: containerID.value,
|
||||
paths: [row.path],
|
||||
})
|
||||
.then(async () => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
await loadContainerFiles();
|
||||
})
|
||||
.catch((err) => {
|
||||
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
|
||||
});
|
||||
};
|
||||
|
||||
const onBatchDelete = async () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(i18n.global.t('file.deleteHelper2'), i18n.global.t('commons.button.delete'), {
|
||||
type: 'warning',
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await deleteContainerFile({
|
||||
containerID: containerID.value,
|
||||
paths: selectedRows.value.map((item) => item.path),
|
||||
})
|
||||
.then(async () => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
selectedRows.value = [];
|
||||
await loadContainerFiles();
|
||||
})
|
||||
.catch((err) => {
|
||||
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.symlink-target {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.path-breadcrumb {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
margin: 0;
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -378,6 +378,7 @@
|
||||
<CommitDialog @search="search" ref="dialogCommitRef" />
|
||||
<MonitorDialog ref="dialogMonitorRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
<ContainerFileDrawer ref="dialogFileBrowserRef" />
|
||||
|
||||
<PortJumpDialog ref="dialogPortJumpRef" />
|
||||
<Backups ref="dialogBackupRef" />
|
||||
@@ -393,6 +394,7 @@ import UpgradeDialog from '@/views/container/container/upgrade/index.vue';
|
||||
import CommitDialog from '@/views/container/container/commit/index.vue';
|
||||
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||
import ContainerFileDrawer from '@/views/container/container/file-browser/index.vue';
|
||||
import ContainerInspectDialog from '@/views/container/container/inspect/index.vue';
|
||||
import PortJumpDialog from '@/components/port-jump/index.vue';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
@@ -675,6 +677,11 @@ const onTerminal = (row: any) => {
|
||||
const title = i18n.global.t('menu.container') + ' ' + row.name;
|
||||
dialogTerminalRef.value!.acceptParams({ containerID: row.containerID, title: title });
|
||||
};
|
||||
const dialogFileBrowserRef = ref();
|
||||
const onOpenFileBrowser = (row: any) => {
|
||||
const title = i18n.global.t('menu.container') + ' ' + row.name;
|
||||
dialogFileBrowserRef.value!.acceptParams({ containerID: row.containerID, title: title });
|
||||
};
|
||||
|
||||
const onInspect = async (row: any) => {
|
||||
const res = await inspect({ id: row.containerID, type: 'container', detail: '' });
|
||||
@@ -782,6 +789,15 @@ const buttons = [
|
||||
dialogContainerLogRef.value!.acceptParams({ containerID: row.containerID, container: row.name });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('home.dir'),
|
||||
disabled: (row: Container.ContainerInfo) => {
|
||||
return row.state !== 'running';
|
||||
},
|
||||
click: (row: Container.ContainerInfo) => {
|
||||
onOpenFileBrowser(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: Container.ContainerInfo) => {
|
||||
|
||||
Reference in New Issue
Block a user