Skip to content

Commit fbe001c

Browse files
committed
add PUT handler
1 parent 7bac991 commit fbe001c

File tree

5 files changed

+156
-13
lines changed

5 files changed

+156
-13
lines changed

pkg/api/api.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Package api provides an HTTP API for listing, fetching, and deleting files
1+
// Package api provides an HTTP API for listing, fetching, uploading, and deleting files
22
// written by the UDP ingest path. All operations are scoped to output_path.
33
package api
44

@@ -24,18 +24,19 @@ func New(outputPath, password string) *API {
2424
}
2525
}
2626

27-
// Register mounts all API routes onto smuthe server muxx and is intended to be passed as the
27+
// Register mounts all API routes onto the server mux and is intended to be passed as the
2828
// register callback to httpserver.New.
2929
func (a *API) Register(smx *http.ServeMux) {
3030
router := mux.NewRouter()
31-
apiRouter := router.PathPrefix("/api").Subrouter()
31+
apiRouter := router.PathPrefix("/api/").Subrouter()
3232
apiRouter.Use(a.authenticate)
3333

34-
apiRouter.HandleFunc("/list/{path:.+}", a.listHandler).Methods(http.MethodGet)
35-
apiRouter.HandleFunc("/file/{path:.+}", a.fileHandler).Methods(http.MethodGet)
36-
apiRouter.HandleFunc("/all", a.deleteAllHandler).Methods(http.MethodDelete)
37-
apiRouter.HandleFunc("/{path:.+}", a.deleteHandler).Methods(http.MethodDelete)
34+
apiRouter.HandleFunc("/list/{path:.+}", a.listHandler).Methods(http.MethodGet) // /api/list/some/path
35+
apiRouter.HandleFunc("/file/{path:.+}", a.fileHandler).Methods(http.MethodGet) // /api/file/some/path
36+
apiRouter.HandleFunc("/file/{path:.+}", a.putFileHandler).Methods(http.MethodPut) // /api/file/some/path
37+
apiRouter.HandleFunc("/all", a.deleteAllHandler).Methods(http.MethodDelete) // /api/all
38+
apiRouter.HandleFunc("/glob/{path:.+}", a.deleteHandler).Methods(http.MethodDelete) // /api/glob/some/path
3839

3940
smx.Handle("/api/", router)
40-
smx.Handle("/api", router)
41+
smx.Handle("/api", router) // this 404s.
4142
}

pkg/api/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (a *API) fileHandler(resp http.ResponseWriter, req *http.Request) {
111111
http.ServeContent(resp, req, info.Name(), info.ModTime(), fileHandle)
112112
}
113113

114-
// deleteHandler handles DELETE /api/{path} and removes all files or directories
114+
// deleteHandler handles DELETE /api/glob/{path} and removes all files or directories
115115
// matching the path. The path may contain * wildcards in any segment.
116116
func (a *API) deleteHandler(resp http.ResponseWriter, r *http.Request) {
117117
rawPath := mux.Vars(r)["path"]

pkg/api/uploads.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package api
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
12+
"github.com/gorilla/mux"
13+
)
14+
15+
const (
16+
// FileMode is the mode for the file.
17+
FileMode = 0o640
18+
// DirMode is the mode for the directory.
19+
DirMode = 0o750
20+
)
21+
22+
// statusError carries an HTTP status for errors returned to clients as JSON.
23+
type statusError struct {
24+
Code int
25+
Msg string
26+
}
27+
28+
func (e *statusError) Error() string {
29+
return e.Msg
30+
}
31+
32+
// putFileHandler handles PUT /api/file/{path} and writes the request body to that path.
33+
// Parent directories are created as needed. Wildcards are not permitted.
34+
func (a *API) putFileHandler(resp http.ResponseWriter, req *http.Request) {
35+
var se *statusError
36+
switch relPath, created, err := a.writeUploadedFile(mux.Vars(req)["path"], req.Body); {
37+
case errors.As(err, &se):
38+
writeError(resp, se.Code, se.Msg)
39+
case err != nil:
40+
writeError(resp, http.StatusInternalServerError, err.Error())
41+
case created:
42+
writeJSON(resp, http.StatusCreated, map[string]string{"path": relPath})
43+
default:
44+
writeJSON(resp, http.StatusOK, map[string]string{"path": relPath})
45+
}
46+
}
47+
48+
func (a *API) validatePutFilePath(rawPath string) (string, bool, error) {
49+
path, err := a.safePath(rawPath)
50+
if err != nil {
51+
return "", false, &statusError{Code: http.StatusBadRequest, Msg: err.Error()}
52+
}
53+
54+
if strings.ContainsRune(path, '*') {
55+
return "", false, &statusError{
56+
Code: http.StatusBadRequest,
57+
Msg: "wildcards not allowed for file upload",
58+
}
59+
}
60+
61+
info, statErr := os.Stat(path)
62+
if statErr == nil && info.IsDir() {
63+
return "", false, &statusError{Code: http.StatusBadRequest, Msg: "path is a directory; use /api/list/"}
64+
}
65+
66+
created := errors.Is(statErr, os.ErrNotExist)
67+
if statErr != nil && !created {
68+
return "", false, fmt.Errorf("stat: %w", statErr)
69+
}
70+
71+
return path, created, nil
72+
}
73+
74+
func commitUploadedFile(tmpPath, destPath string, created bool) error {
75+
err := os.Chmod(tmpPath, FileMode) //nolint:gosec // temp path from CreateTemp under validated directory
76+
if err != nil {
77+
return fmt.Errorf("chmod: %w", err)
78+
}
79+
80+
if !created {
81+
err = os.Remove(destPath) //nolint:gosec // destPath returned from safePath under outputPath
82+
if err != nil {
83+
return fmt.Errorf("removing existing file: %w", err)
84+
}
85+
}
86+
87+
err = os.Rename(tmpPath, destPath) //nolint:gosec // paths validated; atomic replace after write
88+
if err != nil {
89+
return fmt.Errorf("renaming file: %w", err)
90+
}
91+
92+
return nil
93+
}
94+
95+
func (a *API) writeUploadedFile(rawPath string, body io.Reader) (string, bool, error) {
96+
filePath, created, err := a.validatePutFilePath(rawPath)
97+
if err != nil {
98+
return "", false, err
99+
}
100+
101+
dir := filepath.Dir(filePath)
102+
103+
err = os.MkdirAll(dir, DirMode) //nolint:gosec // dir is filepath.Dir of a path from safePath under outputPath
104+
if err != nil {
105+
return "", false, fmt.Errorf("creating parent directories: %w", err)
106+
}
107+
108+
tmpFile, err := os.CreateTemp(dir, ".fogwillow-upload-*")
109+
if err != nil {
110+
return "", false, fmt.Errorf("temp file: %w", err)
111+
}
112+
113+
tmpPath := tmpFile.Name()
114+
cleanupTmp := true
115+
116+
defer func() {
117+
if cleanupTmp {
118+
_ = os.Remove(tmpPath) //nolint:gosec // temp file created in this function under validated dir
119+
}
120+
}()
121+
122+
_, err = io.Copy(tmpFile, body)
123+
if err != nil {
124+
_ = tmpFile.Close()
125+
return "", false, fmt.Errorf("writing body: %w", err)
126+
}
127+
128+
err = tmpFile.Close()
129+
if err != nil {
130+
return "", false, fmt.Errorf("closing temp file: %w", err)
131+
}
132+
133+
err = commitUploadedFile(tmpPath, filePath, created)
134+
if err != nil {
135+
return "", false, err
136+
}
137+
138+
cleanupTmp = false
139+
140+
return strings.TrimPrefix(filePath, a.outputPath+string(filepath.Separator)), created, nil
141+
}

pkg/fog/start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (c *Config) Start() error {
115115
}
116116

117117
// setup makes sure configurations are sound and sane.
118-
func (c *Config) setup() {
118+
func (c *Config) setup() { //nolint:cyclop
119119
// Protect uint->int64 conversions.
120120
if c.LogFileMB > httpserver.MaxStupidValue {
121121
c.LogFileMB = httpserver.MaxStupidValue

pkg/httpserver/accesslog.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import (
1010
)
1111

1212
const (
13-
// Access log file mode.
13+
// LogFileMode is the mode for the access log file.
1414
LogFileMode = 0o644
15-
// Apache Combined Log Format: host ident user time "request" status bytes "referer" "user-agent".
15+
// CombinedLogFormat is from Apache: host ident user time "request" status bytes "referer" "user-agent".
1616
CombinedLogFormat = `%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"`
17-
MaxStupidValue = uint(9999999) // for comparing with config.
17+
// MaxStupidValue is a stupid big value for minimizing config inputs.
18+
MaxStupidValue = uint(9999999) // for comparing with config.
1819
)
1920

2021
// newAccessLog creates a rotating access log writer from config.

0 commit comments

Comments
 (0)