Skip to content

Commit 2b8e907

Browse files
author
Tian Xiaoqiang
committed
feat: auth flags, FS features, Web UI polish, Vite 5
- Add -auth user:pass and -auth-scope write|all; remove legacy -user/-pass - Basic Auth: write-only by default when auth is set; scope=all protects reads - FS: batch upload, mkdir, optional delete, path hardening tests - Web: MUI theme, layout, breadcrumbs, mobile-friendly tables, empty state - Build: Vite 5, cross-env for clean NODE_OPTIONS, Makefile yarn build fix - Docs and README updates for API and curl examples Made-with: Cursor
1 parent 4643c14 commit 2b8e907

32 files changed

Lines changed: 2126 additions & 1166 deletions

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
- uses: actions/setup-go@v5
1818
with:
19-
go-version: '1.23'
19+
go-version: '1.24'
2020

2121
- uses: actions/setup-node@v4
2222
with:
@@ -26,7 +26,7 @@ jobs:
2626
run: yarn install --registry https://registry.npmmirror.com
2727

2828
- name: Build frontend
29-
run: yarn build web
29+
run: yarn build
3030

3131
- name: Build binaries
3232
run: |
@@ -46,7 +46,7 @@ jobs:
4646
for target in "${targets[@]}"; do
4747
os="${target%/*}"
4848
arch="${target#*/}"
49-
output="dist/fileserver-${VERSION}-${os}-${arch}"
49+
output="dist/fileserver-${os}-${arch}"
5050
if [ "$os" = "windows" ]; then
5151
output="${output}.exe"
5252
fi

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
build-web:
2-
@yarn && yarn build web
2+
@yarn && yarn build
33

44
build-server:
55
@go build -o ./tmp/fileserver .

README.md

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The `fileserver` can be used as a static file server to share your file and also
77
- File download
88
- File upload (batch upload supported)
99
- Directory creation
10+
- File/directory deletion (opt-in via `-allow-delete`)
1011
- HTTPS supported
1112
- Basic Auth
1213
- Web UI
@@ -20,14 +21,20 @@ The `fileserver` can be used as a static file server to share your file and also
2021
# Basic usage
2122
./fileserver -port 8880 -basedir /path/to/files
2223

23-
# With basic auth
24-
./fileserver -port 8880 -basedir /path/to/files -user admin -pass secret
24+
# With Basic Auth (default: only uploads / mkdir / delete need credentials; browsing is public)
25+
./fileserver -port 8880 -basedir /path/to/files -auth "admin:secret"
26+
27+
# Require Basic Auth for every request (including reads)
28+
./fileserver -port 8880 -basedir /path/to/files -auth "admin:secret" -auth-scope all
2529

2630
# With TLS
2731
./fileserver -port 8443 -basedir /path/to/files -tls-cert cert.pem -tls-key key.pem
2832

29-
# With TLS and basic auth
30-
./fileserver -port 8443 -basedir /path/to/files -tls-cert cert.pem -tls-key key.pem -user admin -pass secret
33+
# With TLS and Basic Auth
34+
./fileserver -port 8443 -basedir /path/to/files -tls-cert cert.pem -tls-key key.pem -auth "admin:secret"
35+
36+
# Enable deletion
37+
./fileserver -port 8880 -basedir /path/to/files -allow-delete
3138
```
3239

3340
#### Flags
@@ -38,14 +45,13 @@ The `fileserver` can be used as a static file server to share your file and also
3845
| `-basedir` | `.` | Which directory to serve |
3946
| `-tls-cert`| `""` | TLS cert file location |
4047
| `-tls-key` | `""` | TLS key file location |
41-
| `-user` | `""` | Basic auth username |
42-
| `-pass` | `""` | Basic auth password |
43-
44-
> When `-user` and `-pass` are both set, all requests (WebUI and API) require HTTP Basic Authentication.
48+
| `-auth` | `""` | Basic auth as `username:password` (password may contain `:`). Empty disables Basic auth |
49+
| `-auth-scope` | `write` | With `-auth`: `write` = only POST/PUT/PATCH/DELETE need auth; `all` = every request needs auth |
50+
| `-allow-delete` | `false` | Enable file/directory deletion |
4551

4652
### API usage
4753

48-
When basic auth is enabled, add `-u user:pass` to all curl commands.
54+
When Basic Auth is enabled, add `-u user:pass` to curl for requests that require credentials. With the default `-auth-scope write`, **GET/HEAD** (download, JSON listing) are usually anonymous; **POST** (upload, mkdir), **PUT**, and **DELETE** need `-u`. With `-auth-scope all`, add `-u` to every request.
4955

5056
**File upload - Using default file name**
5157

@@ -55,7 +61,7 @@ Following command will upload the `img.png` to the directory `/image/a/b/c/` on
5561
$ # or
5662
$ curl -F 'file=@img.png' http://localhost:8880/image/a/b/c/
5763

58-
# With basic auth
64+
# With Basic Auth (upload is a write)
5965
$ curl -u admin:secret -T img.png http://localhost:8880/image/a/b/c/
6066
```
6167

@@ -76,24 +82,33 @@ Following command will upload the `img.png` to the directory `/image/a/b/c/` on
7682
```bash
7783
$ curl -X POST 'http://localhost:8880/path/to/?action=mkdir&name=new-folder'
7884

79-
# With basic auth
85+
# With Basic Auth
8086
$ curl -u admin:secret -X POST 'http://localhost:8880/path/to/?action=mkdir&name=new-folder'
8187
```
8288

8389
**File download**
8490
```bash
8591
$ curl http://localhost:8880/image/a/b/c/another.png
8692

87-
# With basic auth
88-
$ curl -u admin:secret http://localhost:8880/image/a/b/c/another.png
93+
# Only needed if `-auth-scope all`; with default `write`, download is anonymous
94+
$ curl http://localhost:8880/image/a/b/c/another.png
95+
```
96+
97+
**Delete file or directory** (requires `-allow-delete`; with Basic Auth, DELETE is a write — add `-u` when `-auth` is set)
98+
```bash
99+
# Delete a file
100+
$ curl -u admin:secret -X DELETE http://localhost:8880/path/to/file.txt
101+
102+
# Delete a directory (recursively)
103+
$ curl -u admin:secret -X DELETE http://localhost:8880/path/to/dir/
89104
```
90105

91106
**List directory (JSON)**
92107
```bash
93108
$ curl http://localhost:8880/path/to/dir/
94109

95-
# With basic auth
96-
$ curl -u admin:secret http://localhost:8880/path/to/dir/
110+
# Only needed if `-auth-scope all`; with default `write`, JSON listing is anonymous
111+
$ curl http://localhost:8880/path/to/dir/
97112
```
98113

99114
### UI usage

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/aix3/fileserver
22

3-
go 1.17
3+
go 1.24

main.go

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,109 @@ package main
33
import (
44
"flag"
55
"fmt"
6+
"log"
67
"net/http"
78
"runtime/debug"
9+
"strings"
810

911
"github.com/aix3/fileserver/server"
1012
)
1113

1214
type config struct {
13-
port int
14-
basedir string
15-
certFile string
16-
keyFile string
17-
username string
18-
password string
15+
port int
16+
basedir string
17+
certFile string
18+
keyFile string
19+
username string
20+
password string
21+
authWriteOnly bool
22+
allowDelete bool
1923
}
2024

2125
var defaultConfig = config{
22-
port: 8880,
23-
basedir: ".",
24-
certFile: "",
25-
keyFile: "",
26-
username: "",
27-
password: "",
26+
port: 8880,
27+
basedir: ".",
28+
certFile: "",
29+
keyFile: "",
30+
username: "",
31+
password: "",
32+
authWriteOnly: true,
33+
allowDelete: false,
2834
}
2935

36+
var (
37+
flagAuth string
38+
flagAuthScope string
39+
)
40+
3041
func init() {
3142
flag.IntVar(&defaultConfig.port, "port", defaultConfig.port, "which port to listen on")
3243
flag.StringVar(&defaultConfig.basedir, "basedir", defaultConfig.basedir, "which directory to serve on")
3344
flag.StringVar(&defaultConfig.certFile, "tls-cert", defaultConfig.certFile, "TLS cert file location")
3445
flag.StringVar(&defaultConfig.keyFile, "tls-key", defaultConfig.keyFile, "TLS key file location")
35-
flag.StringVar(&defaultConfig.username, "user", defaultConfig.username, "basic auth username")
36-
flag.StringVar(&defaultConfig.password, "pass", defaultConfig.password, "basic auth password")
46+
47+
flag.StringVar(&flagAuth, "auth", "", `HTTP Basic credentials as "username:password" (password may contain ":"). Empty disables Basic auth`)
48+
flag.StringVar(&flagAuthScope, "auth-scope", "write", `with -auth: "write" = only mutations need Basic Auth (default); "all" = every request needs Basic Auth`)
49+
50+
flag.BoolVar(&defaultConfig.allowDelete, "allow-delete", defaultConfig.allowDelete, "enable file/directory deletion")
51+
}
52+
53+
func applyAuthFlags() {
54+
auth := strings.TrimSpace(flagAuth)
55+
scope := strings.TrimSpace(flagAuthScope)
56+
if scope == "" {
57+
scope = "write"
58+
}
59+
60+
if auth != "" {
61+
user, pass, err := parseAuthPair(auth)
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
if user == "" {
66+
log.Fatal(`-auth: username must not be empty (use "username:password")`)
67+
}
68+
defaultConfig.username = user
69+
defaultConfig.password = pass
70+
switch strings.ToLower(scope) {
71+
case "write":
72+
defaultConfig.authWriteOnly = true
73+
case "all":
74+
defaultConfig.authWriteOnly = false
75+
default:
76+
log.Fatalf(`-auth-scope: want "write" or "all", got %q`, scope)
77+
}
78+
}
79+
}
80+
81+
// parseAuthPair splits on the first ":" only so the password may contain colons.
82+
func parseAuthPair(s string) (user, pass string, err error) {
83+
i := strings.IndexByte(s, ':')
84+
if i < 0 {
85+
return "", "", fmt.Errorf(`-auth: expected "username:password"`)
86+
}
87+
return s[:i], s[i+1:], nil
3788
}
3889

3990
func main() {
4091
flag.Parse()
92+
applyAuthFlags()
4193

4294
mux := http.NewServeMux()
4395

4496
fs := &server.FSHandler{
45-
Basedir: defaultConfig.basedir,
97+
Basedir: defaultConfig.basedir,
98+
AllowDelete: defaultConfig.allowDelete,
4699
}
47100
ui := &server.UIHandler{
48101
Fs: fs,
49102
}
50103

51104
handler := &server.BasicAuthHandler{
52-
Username: defaultConfig.username,
53-
Password: defaultConfig.password,
54-
Next: server.NewCompHandler(ui, fs),
105+
Username: defaultConfig.username,
106+
Password: defaultConfig.password,
107+
AuthWriteOnly: defaultConfig.authWriteOnly,
108+
Next: server.NewCompHandler(ui, fs),
55109
}
56110
mux.Handle("/", handler)
57111

main_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import "testing"
4+
5+
func Test_parseAuthPair(t *testing.T) {
6+
t.Parallel()
7+
cases := []struct {
8+
in string
9+
wantUser string
10+
wantPass string
11+
wantErr bool
12+
}{
13+
{"admin:secret", "admin", "secret", false},
14+
{"a:b:c:d", "a", "b:c:d", false},
15+
{"user:", "user", "", false},
16+
{"u", "", "", true},
17+
}
18+
for _, tc := range cases {
19+
u, p, err := parseAuthPair(tc.in)
20+
if tc.wantErr {
21+
if err == nil {
22+
t.Fatalf("parseAuthPair(%q): want error", tc.in)
23+
}
24+
continue
25+
}
26+
if err != nil {
27+
t.Fatalf("parseAuthPair(%q): %v", tc.in, err)
28+
}
29+
if u != tc.wantUser || p != tc.wantPass {
30+
t.Fatalf("parseAuthPair(%q) = (%q,%q), want (%q,%q)", tc.in, u, p, tc.wantUser, tc.wantPass)
31+
}
32+
}
33+
}

package.json

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
"name": "fileserver",
33
"version": "1.0.0",
44
"private": true,
5+
"engines": {
6+
"node": ">=18"
7+
},
8+
"type": "module",
59
"scripts": {
6-
"dev": "vite web",
7-
"build": "vite build web",
8-
"serve": "vite preview web"
10+
"dev": "cross-env NODE_OPTIONS= vite web",
11+
"build": "cross-env NODE_OPTIONS= vite build web",
12+
"serve": "cross-env NODE_OPTIONS= vite preview web"
913
},
1014
"dependencies": {
1115
"@emotion/react": "^11.9.0",
@@ -15,15 +19,17 @@
1519
"@mui/material": "^5.6.0",
1620
"axios": "^0.26.1",
1721
"humanize": "^0.0.9",
18-
"react-dropzone": "^12.0.5",
1922
"react": "^17.0.2",
20-
"react-dom": "^17.0.2"
23+
"react-dom": "^17.0.2",
24+
"react-dropzone": "^12.0.5"
2125
},
2226
"devDependencies": {
27+
"@types/react": "^17.0.0",
2328
"@types/react-dom": "^17.0.2",
24-
"@vitejs/plugin-react": "^1.3.0",
29+
"@vitejs/plugin-react": "^4.3.4",
30+
"cross-env": "^7.0.3",
2531
"less": "^4.1.2",
2632
"rollup-plugin-visualizer": "^5.6.0",
27-
"vite": "^2.9.1"
33+
"vite": "^5.4.21"
2834
}
2935
}

server/auth.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,33 @@ import (
55
"net/http"
66
)
77

8+
// BasicAuthHandler enforces HTTP Basic authentication when Username is set
9+
// (Password may be empty). When AuthWriteOnly is true, only state-changing
10+
// methods require a valid Authorization header; GET/HEAD (and other reads)
11+
// are allowed without credentials.
812
type BasicAuthHandler struct {
9-
Username string
10-
Password string
11-
Next http.Handler
13+
Username string
14+
Password string
15+
AuthWriteOnly bool
16+
Next http.Handler
17+
}
18+
19+
func writeLikeRequest(r *http.Request) bool {
20+
switch r.Method {
21+
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
22+
return true
23+
default:
24+
return false
25+
}
1226
}
1327

1428
func (h *BasicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
15-
if h.Username == "" && h.Password == "" {
29+
if h.Username == "" {
30+
h.Next.ServeHTTP(w, r)
31+
return
32+
}
33+
34+
if h.AuthWriteOnly && !writeLikeRequest(r) {
1635
h.Next.ServeHTTP(w, r)
1736
return
1837
}

0 commit comments

Comments
 (0)