@@ -10,51 +10,158 @@ import (
10
10
"log"
11
11
"os"
12
12
"os/signal"
13
+ "strconv"
13
14
"strings"
14
15
15
16
"dario.cat/mergo"
16
17
simpleuploadserver "github.com/mayth/go-simple-upload-server/v2/pkg"
17
18
)
18
19
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
+ }
20
60
21
- func (s * commaSeparatedString ) String () string {
22
- return strings . Join ( * s , "," )
61
+ func (f boolOptFlag ) IsSet () bool {
62
+ return f . isSet
23
63
}
24
64
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
27
74
return nil
28
75
}
29
76
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
+
30
124
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 )
57
163
}
164
+ log .Printf ("configured: %+v" , config )
58
165
59
166
if config .EnableAuth && len (config .ReadOnlyTokens ) == 0 && len (config .ReadWriteTokens ) == 0 {
60
167
log .Print ("[NOTICE] Authentication is enabled but no tokens provided. generating random tokens" )
@@ -72,10 +179,10 @@ func main() {
72
179
log .Printf ("generated read write token: %s" , readWriteToken )
73
180
}
74
181
75
- s := simpleuploadserver .NewServer (config )
182
+ s := simpleuploadserver .NewServer (* config )
76
183
ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt )
77
184
defer stop ()
78
- err : = s .Start (ctx , nil )
185
+ err = s .Start (ctx , nil )
79
186
log .Printf ("server stopped: %v" , err )
80
187
}
81
188
@@ -87,3 +194,55 @@ func generateToken() (string, error) {
87
194
b := crypto .SHA256 .New ().Sum (randBytes )
88
195
return fmt .Sprintf ("%x" , b ), nil
89
196
}
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