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

gameap / gameap / 25960901307

16 May 2026 11:29AM UTC coverage: 76.887% (+0.2%) from 76.64%
25960901307

push

github

et-nik
audit logs tests

45395 of 59041 relevant lines covered (76.89%)

33895.16 hits per line

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

83.13
/internal/api/filemanager/upload/handler.go
1
package upload
2

3
import (
4
        "context"
5
        "io"
6
        "log/slog"
7
        "mime/multipart"
8
        "net/http"
9
        "os"
10
        "path/filepath"
11
        "strconv"
12

13
        "github.com/gameap/gameap/internal/api/base"
14
        "github.com/gameap/gameap/internal/api/filemanager/filemanagerpath"
15
        serversbase "github.com/gameap/gameap/internal/api/servers/base"
16
        "github.com/gameap/gameap/internal/audit"
17
        "github.com/gameap/gameap/internal/daemon"
18
        "github.com/gameap/gameap/internal/domain"
19
        "github.com/gameap/gameap/internal/filters"
20
        "github.com/gameap/gameap/internal/repositories"
21
        "github.com/gameap/gameap/pkg/api"
22
        "github.com/gameap/gameap/pkg/auth"
23
        "github.com/pkg/errors"
24
)
25

26
const (
27
        maxMemory     = 32 << 20 // 32 MB
28
        defaultPerms  = 0o644
29
        maxUploadSize = 100 << 20 // 100 MB
30
)
31

32
var (
33
        errUserNotAuthenticated = errors.New("user not authenticated")
34
        errNoFilesUploaded      = errors.New("no files uploaded")
35
        errInvalidFileSize      = errors.New("invalid file size")
36
)
37

38
type fileService interface {
39
        UploadStream(
40
                ctx context.Context,
41
                node *domain.Node,
42
                filePath string,
43
                r io.Reader,
44
                size uint64,
45
                perms os.FileMode,
46
                owner daemon.OwnerOptions,
47
        ) error
48
}
49

50
type Handler struct {
51
        serverFinder   *serversbase.ServerFinder
52
        abilityChecker *serversbase.AbilityChecker
53
        nodeRepo       repositories.NodeRepository
54
        daemonFiles    fileService
55
        responder      base.Responder
56
        audit          audit.Logger
57
}
58

59
func NewHandler(
60
        serverRepo repositories.ServerRepository,
61
        nodeRepo repositories.NodeRepository,
62
        rbac base.RBAC,
63
        daemonFiles fileService,
64
        responder base.Responder,
65
        auditLogger audit.Logger,
66
) *Handler {
14✔
67
        if auditLogger == nil {
26✔
68
                auditLogger = audit.NopLogger{}
12✔
69
        }
12✔
70

71
        return &Handler{
14✔
72
                serverFinder:   serversbase.NewServerFinder(serverRepo, rbac),
14✔
73
                abilityChecker: serversbase.NewAbilityChecker(rbac),
14✔
74
                nodeRepo:       nodeRepo,
14✔
75
                daemonFiles:    daemonFiles,
14✔
76
                responder:      responder,
14✔
77
                audit:          auditLogger,
14✔
78
        }
14✔
79
}
80

81
//nolint:funlen
82
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
14✔
83
        ctx := r.Context()
14✔
84

14✔
85
        session := auth.SessionFromContext(ctx)
14✔
86
        if !session.IsAuthenticated() {
15✔
87
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
88
                        errUserNotAuthenticated,
1✔
89
                        http.StatusUnauthorized,
1✔
90
                ))
1✔
91

1✔
92
                return
1✔
93
        }
1✔
94

95
        serverID, err := api.NewInputReader(r).ReadUint("server")
13✔
96
        if err != nil {
14✔
97
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
98
                        errors.WithMessage(err, "invalid server id"),
1✔
99
                        http.StatusBadRequest,
1✔
100
                ))
1✔
101

1✔
102
                return
1✔
103
        }
1✔
104

105
        server, err := h.serverFinder.FindUserServer(ctx, session.User, serverID)
12✔
106
        if err != nil {
14✔
107
                h.responder.WriteError(ctx, rw, err)
2✔
108

2✔
109
                return
2✔
110
        }
2✔
111

112
        err = h.abilityChecker.CheckOrError(
10✔
113
                ctx,
10✔
114
                session.User.ID,
10✔
115
                server.ID,
10✔
116
                []domain.AbilityName{domain.AbilityNameGameServerFiles},
10✔
117
        )
10✔
118
        if err != nil {
11✔
119
                h.responder.WriteError(ctx, rw, err)
1✔
120

1✔
121
                return
1✔
122
        }
1✔
123

124
        r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize)
9✔
125
        err = r.ParseMultipartForm(maxMemory)
9✔
126
        if err != nil {
9✔
127
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
×
128
                        errors.WithMessage(err, "failed to parse multipart form"),
×
129
                        http.StatusBadRequest,
×
130
                ))
×
131

×
132
                return
×
133
        }
×
134

135
        disk := r.FormValue("disk")
9✔
136
        path := r.FormValue("path")
9✔
137
        overwriteStr := r.FormValue("overwrite")
9✔
138

9✔
139
        if disk != "server" {
10✔
140
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
141
                        errors.Errorf("unsupported disk: %s, only 'server' disk is supported", disk),
1✔
142
                        http.StatusBadRequest,
1✔
143
                ))
1✔
144

1✔
145
                return
1✔
146
        }
1✔
147

148
        _ = overwriteStr
8✔
149

8✔
150
        if path == "" {
14✔
151
                path = "."
6✔
152
        }
6✔
153

154
        if err = filemanagerpath.ValidatePath(path); err != nil {
9✔
155
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
156
                        err,
1✔
157
                        http.StatusBadRequest,
1✔
158
                ))
1✔
159

1✔
160
                return
1✔
161
        }
1✔
162

163
        node, err := h.getNode(ctx, server.DSID)
7✔
164
        if err != nil {
8✔
165
                h.responder.WriteError(ctx, rw, err)
1✔
166

1✔
167
                return
1✔
168
        }
1✔
169

170
        files := r.MultipartForm.File["files[]"]
6✔
171
        if len(files) == 0 {
7✔
172
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
173
                        errNoFilesUploaded,
1✔
174
                        http.StatusBadRequest,
1✔
175
                ))
1✔
176

1✔
177
                return
1✔
178
        }
1✔
179

180
        err = h.processFiles(ctx, node, server, path, files)
5✔
181
        if err != nil {
5✔
182
                h.responder.WriteError(ctx, rw, err)
×
183

×
184
                return
×
185
        }
×
186

187
        audit.SensitiveOp(ctx, h.audit, audit.EventFileUpload, audit.CategoryFileOp,
5✔
188
                "server", strconv.FormatUint(uint64(serverID), 10), "upload",
5✔
189
                slog.Int("files", len(files)))
5✔
190

5✔
191
        h.responder.Write(ctx, rw, newUploadResponse())
5✔
192
}
193

194
func (h *Handler) getNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
7✔
195
        nodes, err := h.nodeRepo.Find(ctx, &filters.FindNode{
7✔
196
                IDs: []uint{nodeID},
7✔
197
        }, nil, &filters.Pagination{
7✔
198
                Limit: 1,
7✔
199
        })
7✔
200
        if err != nil {
7✔
201
                return nil, errors.WithMessage(err, "failed to find node")
×
202
        }
×
203

204
        if len(nodes) == 0 {
8✔
205
                return nil, api.NewNotFoundError("node not found")
1✔
206
        }
1✔
207

208
        return &nodes[0], nil
6✔
209
}
210

211
func (h *Handler) processFiles(
212
        ctx context.Context,
213
        node *domain.Node,
214
        server *domain.Server,
215
        targetPath string,
216
        files []*multipart.FileHeader,
217
) error {
5✔
218
        owner := daemon.OwnerFromServer(server)
5✔
219

5✔
220
        for _, fileHeader := range files {
11✔
221
                if fileHeader.Size > maxUploadSize {
6✔
222
                        return api.WrapHTTPError(
×
223
                                errors.Errorf("file %s exceeds maximum size of %d bytes", fileHeader.Filename, maxUploadSize),
×
224
                                http.StatusBadRequest,
×
225
                        )
×
226
                }
×
227

228
                if err := filemanagerpath.ValidateFilename(fileHeader.Filename); err != nil {
6✔
229
                        return api.WrapHTTPError(err, http.StatusBadRequest)
×
230
                }
×
231

232
                fullPath := filepath.Join(node.WorkPath, server.Dir, targetPath, fileHeader.Filename)
6✔
233

6✔
234
                file, err := fileHeader.Open()
6✔
235
                if err != nil {
6✔
236
                        return errors.WithMessage(err, "failed to open uploaded file")
×
237
                }
×
238

239
                if fileHeader.Size < 0 {
6✔
240
                        _ = file.Close()
×
241

×
242
                        return errInvalidFileSize
×
243
                }
×
244
                fileSize := uint64(fileHeader.Size)
6✔
245

6✔
246
                err = h.daemonFiles.UploadStream(
6✔
247
                        ctx,
6✔
248
                        node,
6✔
249
                        fullPath,
6✔
250
                        file,
6✔
251
                        fileSize,
6✔
252
                        defaultPerms,
6✔
253
                        owner,
6✔
254
                )
6✔
255

6✔
256
                _ = file.Close()
6✔
257

6✔
258
                if err != nil {
6✔
259
                        return errors.WithMessagef(err, "failed to upload file %s", fileHeader.Filename)
×
260
                }
×
261
        }
262

263
        return nil
5✔
264
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc