• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gameap / gameap / 28878367588

07 Jul 2026 03:30PM UTC coverage: 82.185% (+5.7%) from 76.45%
28878367588

Pull #22

github

et-nik
e2e fixes
Pull Request #22: Develop into master

47018 of 57210 relevant lines covered (82.18%)

34985.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.44
/internal/api/filemanager/paste/handler.go
1
package paste
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "net/http"
7
        "path"
8
        "path/filepath"
9
        "strings"
10

11
        "github.com/gameap/gameap/internal/api/base"
12
        "github.com/gameap/gameap/internal/api/filemanager/filemanagerpath"
13
        serversbase "github.com/gameap/gameap/internal/api/servers/base"
14
        "github.com/gameap/gameap/internal/domain"
15
        "github.com/gameap/gameap/internal/filters"
16
        "github.com/gameap/gameap/internal/repositories"
17
        "github.com/gameap/gameap/pkg/api"
18
        "github.com/gameap/gameap/pkg/auth"
19
        "github.com/pkg/errors"
20
)
21

22
const (
23
        operationTypeCopy = "copy"
24
        operationTypeCut  = "cut"
25
)
26

27
type fileService interface {
28
        Copy(ctx context.Context, node *domain.Node, source, destination string) error
29
        Move(ctx context.Context, node *domain.Node, source, destination string) error
30
}
31

32
type Handler struct {
33
        serverFinder   *serversbase.ServerFinder
34
        abilityChecker *serversbase.AbilityChecker
35
        nodeRepo       repositories.NodeRepository
36
        daemonFiles    fileService
37
        responder      base.Responder
38
}
39

40
func NewHandler(
41
        serverRepo repositories.ServerRepository,
42
        nodeRepo repositories.NodeRepository,
43
        rbac base.RBAC,
44
        daemonFiles fileService,
45
        responder base.Responder,
46
) *Handler {
21✔
47
        return &Handler{
21✔
48
                serverFinder:   serversbase.NewServerFinder(serverRepo, rbac),
21✔
49
                abilityChecker: serversbase.NewAbilityChecker(rbac),
21✔
50
                nodeRepo:       nodeRepo,
21✔
51
                daemonFiles:    daemonFiles,
21✔
52
                responder:      responder,
21✔
53
        }
21✔
54
}
21✔
55

56
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
21✔
57
        ctx := r.Context()
21✔
58

21✔
59
        session := auth.SessionFromContext(ctx)
21✔
60
        if !session.IsAuthenticated() {
22✔
61
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
62
                        errors.New("user not authenticated"),
1✔
63
                        http.StatusUnauthorized,
1✔
64
                ))
1✔
65

1✔
66
                return
1✔
67
        }
1✔
68

69
        input := api.NewInputReader(r)
20✔
70

20✔
71
        serverID, err := input.ReadUint("server")
20✔
72
        if err != nil {
21✔
73
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
74
                        errors.WithMessage(err, "invalid server id"),
1✔
75
                        http.StatusBadRequest,
1✔
76
                ))
1✔
77

1✔
78
                return
1✔
79
        }
1✔
80

81
        server, err := h.serverFinder.FindUserServer(ctx, session.User, serverID)
19✔
82
        if err != nil {
21✔
83
                h.responder.WriteError(ctx, rw, err)
2✔
84

2✔
85
                return
2✔
86
        }
2✔
87

88
        err = h.abilityChecker.CheckOrError(
17✔
89
                ctx,
17✔
90
                session.User.ID,
17✔
91
                server.ID,
17✔
92
                []domain.AbilityName{domain.AbilityNameGameServerFiles},
17✔
93
        )
17✔
94
        if err != nil {
18✔
95
                h.responder.WriteError(ctx, rw, err)
1✔
96

1✔
97
                return
1✔
98
        }
1✔
99

100
        var req pasteRequest
16✔
101
        err = json.NewDecoder(r.Body).Decode(&req)
16✔
102
        if err != nil {
17✔
103
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
104
                        errors.WithMessage(err, "invalid request body"),
1✔
105
                        http.StatusBadRequest,
1✔
106
                ))
1✔
107

1✔
108
                return
1✔
109
        }
1✔
110

111
        if err = h.validateRequest(&req); err != nil {
19✔
112
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(err, http.StatusBadRequest))
4✔
113

4✔
114
                return
4✔
115
        }
4✔
116

117
        node, err := h.getNode(ctx, server.DSID)
11✔
118
        if err != nil {
12✔
119
                h.responder.WriteError(ctx, rw, err)
1✔
120

1✔
121
                return
1✔
122
        }
1✔
123

124
        if err = h.processItems(ctx, node, server.Dir, &req); err != nil {
13✔
125
                h.responder.WriteError(ctx, rw, err)
3✔
126

3✔
127
                return
3✔
128
        }
3✔
129

130
        h.responder.Write(ctx, rw, newPasteResponse(req.Clipboard.Type))
7✔
131
}
132

133
func (h *Handler) validateRequest(req *pasteRequest) error {
15✔
134
        if req.Disk != "server" {
16✔
135
                return errors.Errorf("unsupported disk: %s, only 'server' disk is supported", req.Disk)
1✔
136
        }
1✔
137

138
        if req.Clipboard.Disk != "server" {
15✔
139
                return errors.Errorf(
1✔
140
                        "unsupported clipboard disk: %s, only same-disk operations are supported",
1✔
141
                        req.Clipboard.Disk,
1✔
142
                )
1✔
143
        }
1✔
144

145
        if req.Clipboard.Type != operationTypeCopy && req.Clipboard.Type != operationTypeCut {
14✔
146
                return errors.Errorf("unsupported clipboard type: %s, must be 'copy' or 'cut'", req.Clipboard.Type)
1✔
147
        }
1✔
148

149
        if len(req.Clipboard.Files) == 0 && len(req.Clipboard.Directories) == 0 {
13✔
150
                return errors.New("clipboard is empty: no files or directories to paste")
1✔
151
        }
1✔
152

153
        return nil
11✔
154
}
155

156
func (h *Handler) getNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
11✔
157
        nodes, err := h.nodeRepo.Find(ctx, &filters.FindNode{
11✔
158
                IDs: []uint{nodeID},
11✔
159
        }, nil, &filters.Pagination{
11✔
160
                Limit: 1,
11✔
161
        })
11✔
162
        if err != nil {
11✔
163
                return nil, errors.WithMessage(err, "failed to find node")
×
164
        }
×
165

166
        if len(nodes) == 0 {
12✔
167
                return nil, api.NewNotFoundError("node not found")
1✔
168
        }
1✔
169

170
        return &nodes[0], nil
10✔
171
}
172

173
func (h *Handler) processItems(
174
        ctx context.Context,
175
        node *domain.Node,
176
        serverDir string,
177
        req *pasteRequest,
178
) error {
10✔
179
        destPath := strings.ReplaceAll(req.Path, "\\", "/")
10✔
180
        if err := filemanagerpath.ValidatePath(destPath); err != nil {
11✔
181
                return api.WrapHTTPError(err, http.StatusBadRequest)
1✔
182
        }
1✔
183

184
        destinationBase := filepath.Join(node.WorkPath, serverDir, destPath)
9✔
185

9✔
186
        for _, rawFilePath := range req.Clipboard.Files {
18✔
187
                filePath := strings.ReplaceAll(rawFilePath, "\\", "/")
9✔
188
                if err := filemanagerpath.ValidatePath(filePath); err != nil {
10✔
189
                        return api.WrapHTTPError(err, http.StatusBadRequest)
1✔
190
                }
1✔
191

192
                sourcePath := filepath.Join(node.WorkPath, serverDir, filePath)
8✔
193
                destinationPath := filepath.Join(destinationBase, path.Base(filePath))
8✔
194

8✔
195
                if err := h.pasteItem(ctx, node, sourcePath, destinationPath, req.Clipboard.Type); err != nil {
8✔
196
                        return errors.WithMessagef(err, "failed to paste file: %s", filePath)
×
197
                }
×
198
        }
199

200
        for _, rawDirPath := range req.Clipboard.Directories {
11✔
201
                dirPath := strings.ReplaceAll(rawDirPath, "\\", "/")
3✔
202
                if err := filemanagerpath.ValidatePath(dirPath); err != nil {
4✔
203
                        return api.WrapHTTPError(err, http.StatusBadRequest)
1✔
204
                }
1✔
205

206
                sourcePath := filepath.Join(node.WorkPath, serverDir, dirPath)
2✔
207
                destinationPath := filepath.Join(destinationBase, path.Base(dirPath))
2✔
208

2✔
209
                if err := h.pasteItem(ctx, node, sourcePath, destinationPath, req.Clipboard.Type); err != nil {
2✔
210
                        return errors.WithMessagef(err, "failed to paste directory: %s", dirPath)
×
211
                }
×
212
        }
213

214
        return nil
7✔
215
}
216

217
func (h *Handler) pasteItem(
218
        ctx context.Context,
219
        node *domain.Node,
220
        source string,
221
        destination string,
222
        operationType string,
223
) error {
10✔
224
        switch operationType {
10✔
225
        case operationTypeCopy:
9✔
226
                return h.daemonFiles.Copy(ctx, node, source, destination)
9✔
227
        case operationTypeCut:
1✔
228
                return h.daemonFiles.Move(ctx, node, source, destination)
1✔
229
        default:
×
230
                return errors.Errorf("unknown operation type: %s", operationType)
×
231
        }
232
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc