Skip to content

Commit 55c5995

Browse files
authored
Merge pull request #43 from mayth/fix-config-parse
fix config overriding (#42)
2 parents 0ff7861 + a5e3f2b commit 55c5995

File tree

5 files changed

+356
-476
lines changed

5 files changed

+356
-476
lines changed

app.go

+192-33
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,158 @@ import (
1010
"log"
1111
"os"
1212
"os/signal"
13+
"strconv"
1314
"strings"
1415

1516
"dario.cat/mergo"
1617
simpleuploadserver "github.com/mayth/go-simple-upload-server/v2/pkg"
1718
)
1819

19-
type commaSeparatedString []string
20+
var DefaultConfig = ServerConfig{
21+
DocumentRoot: ".",
22+
Addr: simpleuploadserver.DefaultAddr,
23+
EnableCORS: nil,
24+
MaxUploadSize: 1024 * 1024,
25+
FileNamingStrategy: "uuid",
26+
ShutdownTimeout: 15000,
27+
EnableAuth: nil,
28+
ReadOnlyTokens: []string{},
29+
ReadWriteTokens: []string{},
30+
}
31+
32+
func BoolPointer(v bool) *bool {
33+
return &v
34+
}
35+
36+
type triBool struct {
37+
value bool
38+
isSet bool
39+
}
40+
41+
type boolOptFlag triBool
42+
43+
func (f *boolOptFlag) Set(value string) error {
44+
v, err := strconv.ParseBool(value)
45+
if err != nil {
46+
return err
47+
}
48+
f.value = v
49+
f.isSet = true
50+
return nil
51+
}
52+
53+
func (f boolOptFlag) String() string {
54+
return strconv.FormatBool(f.value)
55+
}
56+
57+
func (f boolOptFlag) IsBoolFlag() bool {
58+
return true
59+
}
2060

21-
func (s *commaSeparatedString) String() string {
22-
return strings.Join(*s, ",")
61+
func (f boolOptFlag) IsSet() bool {
62+
return f.isSet
2363
}
2464

25-
func (s *commaSeparatedString) Set(value string) error {
26-
*s = strings.Split(value, ",")
65+
func (f boolOptFlag) Get() any {
66+
return f.value
67+
}
68+
69+
type stringArrayFlag []string
70+
71+
func (f *stringArrayFlag) Set(value string) error {
72+
ss := strings.Split(value, ",")
73+
*f = ss
2774
return nil
2875
}
2976

77+
func (f stringArrayFlag) String() string {
78+
return strings.Join(f, ",")
79+
}
80+
81+
// ServerConfig wraps simpleuploadserver.ServerConfig to provide JSON marshaling.
82+
type ServerConfig struct {
83+
// Address where the server listens on.
84+
Addr string `json:"addr"`
85+
// Path to the document root.
86+
DocumentRoot string `json:"document_root"`
87+
// Determines whether to enable CORS header.
88+
EnableCORS *bool `json:"enable_cors"`
89+
// Maximum upload size in bytes.
90+
MaxUploadSize int64 `json:"max_upload_size"`
91+
// File naming strategy.
92+
FileNamingStrategy string `json:"file_naming_strategy"`
93+
// Graceful shutdown timeout in milliseconds.
94+
ShutdownTimeout int `json:"shutdown_timeout"`
95+
// Enable authentication.
96+
EnableAuth *bool `json:"enable_auth"`
97+
// Authentication tokens for read-only access.
98+
ReadOnlyTokens []string `json:"read_only_tokens"`
99+
// Authentication tokens for read-write access.
100+
ReadWriteTokens []string `json:"read_write_tokens"`
101+
}
102+
103+
func (c *ServerConfig) AsConfig() simpleuploadserver.ServerConfig {
104+
if c.EnableCORS == nil {
105+
c.EnableCORS = BoolPointer(true)
106+
}
107+
if c.EnableAuth == nil {
108+
c.EnableAuth = BoolPointer(false)
109+
}
110+
111+
return simpleuploadserver.ServerConfig{
112+
Addr: c.Addr,
113+
DocumentRoot: c.DocumentRoot,
114+
EnableCORS: *c.EnableCORS,
115+
MaxUploadSize: c.MaxUploadSize,
116+
FileNamingStrategy: c.FileNamingStrategy,
117+
ShutdownTimeout: c.ShutdownTimeout,
118+
EnableAuth: *c.EnableAuth,
119+
ReadOnlyTokens: c.ReadOnlyTokens,
120+
ReadWriteTokens: c.ReadWriteTokens,
121+
}
122+
}
123+
30124
func main() {
31-
configFile := flag.String("config", "", "path to config file")
32-
config := simpleuploadserver.ServerConfig{}
33-
flag.StringVar(&config.DocumentRoot, "document_root", ".", "path to document root directory")
34-
flag.StringVar(&config.Addr, "addr", simpleuploadserver.DefaultAddr, "address to listen")
35-
flag.BoolVar(&config.EnableCORS, "enable_cors", true, "enable CORS header")
36-
flag.Int64Var(&config.MaxUploadSize, "max_upload_size", 1024*1024, "max upload size in bytes")
37-
flag.StringVar(&config.FileNamingStrategy, "file_naming_strategy", "uuid", "File naming strategy")
38-
flag.IntVar(&config.ShutdownTimeout, "shutdown_timeout", 15000, "graceful shutdown timeout in milliseconds")
39-
flag.BoolVar(&config.EnableAuth, "enable_auth", false, "enable authentication")
40-
flag.Var((*commaSeparatedString)(&config.ReadOnlyTokens), "read_only_tokens", "comma separated list of read only tokens")
41-
flag.Var((*commaSeparatedString)(&config.ReadWriteTokens), "read_write_tokens", "comma separated list of read write tokens")
42-
flag.Parse()
43-
44-
if *configFile != "" {
45-
f, err := os.Open(*configFile)
46-
if err != nil {
47-
log.Fatalf("failed to load config: %v", err)
48-
}
49-
fileConfig := simpleuploadserver.ServerConfig{}
50-
if err := json.NewDecoder(f).Decode(&fileConfig); err != nil {
51-
log.Fatalf("failed to load config: %v", err)
52-
}
53-
if err := mergo.Merge(&fileConfig, config, mergo.WithOverride); err != nil {
54-
log.Fatalf("failed to merge config: %v", err)
55-
}
56-
config = fileConfig
125+
NewApp(os.Args[0]).Run(os.Args[1:])
126+
}
127+
128+
type app struct {
129+
flagSet *flag.FlagSet
130+
configFilePath string
131+
documentRoot string
132+
addr string
133+
enableCORS boolOptFlag
134+
maxUploadSize int64
135+
fileNamingStrategy string
136+
shutdownTimeout int
137+
enableAuth boolOptFlag
138+
readOnlyTokens stringArrayFlag
139+
readWriteTokens stringArrayFlag
140+
}
141+
142+
func NewApp(name string) *app {
143+
a := &app{}
144+
fs := flag.NewFlagSet(name, flag.ExitOnError)
145+
fs.StringVar(&a.configFilePath, "config", "", "path to config file")
146+
fs.StringVar(&a.documentRoot, "document_root", "", "path to document root directory")
147+
fs.StringVar(&a.addr, "addr", "", "address to listen")
148+
fs.Var(&a.enableCORS, "enable_cors", "enable CORS header")
149+
fs.Int64Var(&a.maxUploadSize, "max_upload_size", 0, "max upload size in bytes")
150+
fs.StringVar(&a.fileNamingStrategy, "file_naming_strategy", "", "File naming strategy")
151+
fs.IntVar(&a.shutdownTimeout, "shutdown_timeout", 0, "graceful shutdown timeout in milliseconds")
152+
fs.Var(&a.enableAuth, "enable_auth", "enable authentication")
153+
fs.Var(&a.readOnlyTokens, "read_only_tokens", "comma separated list of read only tokens")
154+
fs.Var(&a.readWriteTokens, "read_write_tokens", "comma separated list of read write tokens")
155+
a.flagSet = fs
156+
return a
157+
}
158+
159+
func (a *app) Run(args []string) {
160+
config, err := a.ParseConfig(args)
161+
if err != nil {
162+
log.Fatalf("failed to parse config: %v", err)
57163
}
164+
log.Printf("configured: %+v", config)
58165

59166
if config.EnableAuth && len(config.ReadOnlyTokens) == 0 && len(config.ReadWriteTokens) == 0 {
60167
log.Print("[NOTICE] Authentication is enabled but no tokens provided. generating random tokens")
@@ -72,10 +179,10 @@ func main() {
72179
log.Printf("generated read write token: %s", readWriteToken)
73180
}
74181

75-
s := simpleuploadserver.NewServer(config)
182+
s := simpleuploadserver.NewServer(*config)
76183
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
77184
defer stop()
78-
err := s.Start(ctx, nil)
185+
err = s.Start(ctx, nil)
79186
log.Printf("server stopped: %v", err)
80187
}
81188

@@ -87,3 +194,55 @@ func generateToken() (string, error) {
87194
b := crypto.SHA256.New().Sum(randBytes)
88195
return fmt.Sprintf("%x", b), nil
89196
}
197+
198+
// parseConfig parses the configuration from the `src` and merges it with the `orig` configuration.
199+
func (a *app) ParseConfig(args []string) (*simpleuploadserver.ServerConfig, error) {
200+
if err := a.flagSet.Parse(args); err != nil {
201+
return nil, err
202+
}
203+
204+
config := DefaultConfig
205+
206+
if a.configFilePath != "" {
207+
f, err := os.Open(a.configFilePath)
208+
if err != nil {
209+
log.Fatalf("failed to open config file: %v", err)
210+
}
211+
defer f.Close()
212+
fileConfig := ServerConfig{}
213+
if err := json.NewDecoder(f).Decode(&fileConfig); err != nil {
214+
return nil, fmt.Errorf("failed to load config: %w", err)
215+
}
216+
log.Printf("loaded config from source file: %+v", fileConfig)
217+
if err := mergo.Merge(&config, fileConfig, mergo.WithOverride); err != nil {
218+
return nil, fmt.Errorf("failed to merge config from file: %w", err)
219+
}
220+
log.Printf("merged file config: %+v", config)
221+
} else {
222+
log.Print("no config file provided")
223+
}
224+
225+
configFromFlags := ServerConfig{
226+
DocumentRoot: a.documentRoot,
227+
Addr: a.addr,
228+
MaxUploadSize: a.maxUploadSize,
229+
FileNamingStrategy: a.fileNamingStrategy,
230+
ShutdownTimeout: a.shutdownTimeout,
231+
ReadOnlyTokens: a.readOnlyTokens,
232+
ReadWriteTokens: a.readWriteTokens,
233+
}
234+
if a.enableCORS.IsSet() {
235+
configFromFlags.EnableCORS = &a.enableCORS.value
236+
}
237+
if a.enableAuth.IsSet() {
238+
configFromFlags.EnableAuth = &a.enableAuth.value
239+
}
240+
log.Printf("config from flag: %+v", configFromFlags)
241+
if err := mergo.Merge(&config, configFromFlags, mergo.WithOverride); err != nil {
242+
return nil, fmt.Errorf("failed to merge config from flags: %w", err)
243+
}
244+
log.Printf("merged flag config: %+v", config)
245+
246+
v := config.AsConfig()
247+
return &v, nil
248+
}

0 commit comments

Comments
 (0)