source: code/trunk/partage.go@ 64

Last change on this file since 64 was 64, checked in by dev, 3 years ago

Add support for DELETE method

File size: 8.6 KB
Line 
1package main
2
3import (
4 "encoding/json"
5 "flag"
6 "fmt"
7 "html/template"
8 "io"
9 "io/ioutil"
10 "log"
11 "net"
12 "net/http"
13 "net/http/fcgi"
14 "os"
15 "os/signal"
16 "os/user"
17 "path"
18 "path/filepath"
19 "strconv"
20 "syscall"
21 "time"
22
23 "github.com/dustin/go-humanize"
24 "gopkg.in/ini.v1"
25)
26
27type templatedata struct {
28 Links []string
29 Size string
30 Maxsize string
31}
32
33type metadata struct {
34 Filename string
35 Size int64
36 Expiry int64
37}
38
39var conf struct {
40 user string
41 group string
42 chroot string
43 listen string
44 baseuri string
45 rootdir string
46 tmplpath string
47 filepath string
48 metapath string
49 filectx string
50 maxsize int64
51 expiry int64
52}
53
54var verbose bool
55
56func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
57 buffer := make([]byte, 4096)
58 eof := false
59 sz := int64(0)
60
61 defer f.Sync()
62
63 for !eof {
64 n, err := s.Read(buffer)
65 if err != nil && err != io.EOF {
66 return err
67 } else if err == io.EOF {
68 eof = true
69 }
70
71 /* ensure we don't write more than expected */
72 r := int64(n)
73 if sz+r > contentlength {
74 r = contentlength - sz
75 eof = true
76 }
77
78 _, err = f.Write(buffer[:r])
79 if err != nil {
80 return err
81 }
82 sz += r
83 }
84
85 return nil
86}
87
88func writemeta(filename string, expiry int64) error {
89
90 f, _ := os.Open(filename)
91 stat, _ := f.Stat()
92 size := stat.Size()
93 f.Close()
94
95 if expiry < 0 {
96 expiry = conf.expiry
97 }
98
99 meta := metadata{
100 Filename: filepath.Base(filename),
101 Size: size,
102 Expiry: time.Now().Unix() + expiry,
103 }
104
105 if verbose {
106 log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
107 }
108
109 f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
110 if err != nil {
111 return err
112 }
113 defer f.Close()
114
115 j, err := json.Marshal(meta)
116 if err != nil {
117 return err
118 }
119
120 _, err = f.Write(j)
121
122 return err
123}
124
125func servetemplate(w http.ResponseWriter, f string, d templatedata) {
126 t, err := template.ParseFiles(conf.tmplpath + "/" + f)
127 if err != nil {
128 http.Error(w, "Internal error", http.StatusInternalServerError)
129 return
130 }
131
132 if verbose {
133 log.Printf("Serving template %s", t.Name())
134 }
135
136 err = t.Execute(w, d)
137 if err != nil {
138 fmt.Println(err)
139 }
140}
141
142func uploaderPut(w http.ResponseWriter, r *http.Request) {
143 /* limit upload size */
144 if r.ContentLength > conf.maxsize {
145 http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
146 }
147
148 tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
149 f, err := os.Create(tmp.Name())
150 if err != nil {
151 fmt.Println(err)
152 return
153 }
154 defer f.Close()
155
156 if verbose {
157 log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
158 }
159
160 if err = writefile(f, r.Body, r.ContentLength); err != nil {
161 http.Error(w, "Internal error", http.StatusInternalServerError)
162 defer os.Remove(tmp.Name())
163 return
164 }
165 writemeta(tmp.Name(), conf.expiry)
166
167 resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
168 w.Write([]byte(resp + "\r\n"))
169}
170
171func uploaderPost(w http.ResponseWriter, r *http.Request) {
172 /* read 32Mb at a time */
173 r.ParseMultipartForm(32 << 20)
174
175 links := []string{}
176 for _, h := range r.MultipartForm.File["file"] {
177 if h.Size > conf.maxsize {
178 http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
179 return
180 }
181
182 post, err := h.Open()
183 if err != nil {
184 http.Error(w, "Internal error", http.StatusInternalServerError)
185 return
186 }
187 defer post.Close()
188
189 tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
190 f, err := os.Create(tmp.Name())
191 if err != nil {
192 http.Error(w, "Internal error", http.StatusInternalServerError)
193 return
194 }
195 defer f.Close()
196
197 if err = writefile(f, post, h.Size); err != nil {
198 http.Error(w, "Internal error", http.StatusInternalServerError)
199 defer os.Remove(tmp.Name())
200 return
201 }
202
203 expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
204 if err != nil || expiry < 0 {
205 expiry = int(conf.expiry)
206 }
207 writemeta(tmp.Name(), int64(expiry))
208
209 link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
210 links = append(links, link)
211 }
212
213 switch r.PostFormValue("output") {
214 case "html":
215 data := templatedata{
216 Maxsize: humanize.IBytes(uint64(conf.maxsize)),
217 Links: links,
218 }
219 servetemplate(w, "/index.html", data)
220 case "json":
221 data, _ := json.Marshal(links)
222 w.Write(data)
223 default:
224 for _, link := range links {
225 w.Write([]byte(link + "\r\n"))
226 }
227 }
228}
229
230func uploaderGet(w http.ResponseWriter, r *http.Request) {
231 // r.URL.Path is sanitized regarding "." and ".."
232 filename := r.URL.Path
233 if r.URL.Path == "/" || r.URL.Path == "/index.html" {
234 data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
235 servetemplate(w, "/index.html", data)
236 return
237 }
238
239 if verbose {
240 log.Printf("Serving file %s", conf.rootdir+filename)
241 }
242
243 http.ServeFile(w, r, conf.rootdir+filename)
244}
245
246func uploaderDelete(w http.ResponseWriter, r *http.Request) {
247 // r.URL.Path is sanitized regarding "." and ".."
248 filename := r.URL.Path
249 filepath := conf.filepath + filename
250
251 if verbose {
252 log.Printf("Deleting file %s", filepath)
253 }
254
255 f, err := os.Open(filepath)
256 if err != nil {
257 http.NotFound(w, r)
258 return
259 }
260 f.Close()
261
262 // Force file expiration
263 writemeta(filepath, 0)
264 w.WriteHeader(http.StatusNoContent)
265}
266
267func uploader(w http.ResponseWriter, r *http.Request) {
268 if verbose {
269 log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
270 }
271
272 switch r.Method {
273 case "DELETE":
274 uploaderDelete(w, r)
275 case "POST":
276 uploaderPost(w, r)
277 case "PUT":
278 uploaderPut(w, r)
279 case "GET":
280 uploaderGet(w, r)
281 }
282}
283
284func parseconfig(file string) error {
285 cfg, err := ini.Load(file)
286 if err != nil {
287 return err
288 }
289
290 conf.listen = cfg.Section("").Key("listen").String()
291 conf.user = cfg.Section("").Key("user").String()
292 conf.group = cfg.Section("").Key("group").String()
293 conf.baseuri = cfg.Section("").Key("baseuri").String()
294 conf.filepath = cfg.Section("").Key("filepath").String()
295 conf.metapath = cfg.Section("").Key("metapath").String()
296 conf.filectx = cfg.Section("").Key("filectx").String()
297 conf.rootdir = cfg.Section("").Key("rootdir").String()
298 conf.chroot = cfg.Section("").Key("chroot").String()
299 conf.tmplpath = cfg.Section("").Key("tmplpath").String()
300 conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
301 conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
302
303 return nil
304}
305
306func usergroupids(username string, groupname string) (int, int, error) {
307 u, err := user.Lookup(username)
308 if err != nil {
309 return -1, -1, err
310 }
311
312 uid, _ := strconv.Atoi(u.Uid)
313 gid, _ := strconv.Atoi(u.Gid)
314
315 if conf.group != "" {
316 g, err := user.LookupGroup(groupname)
317 if err != nil {
318 return uid, -1, err
319 }
320 gid, _ = strconv.Atoi(g.Gid)
321 }
322
323 return uid, gid, nil
324}
325
326func main() {
327 var err error
328 var configfile string
329 var listener net.Listener
330
331 /* default values */
332 conf.listen = "0.0.0.0:8080"
333 conf.baseuri = "http://127.0.0.1:8080"
334 conf.rootdir = "static"
335 conf.tmplpath = "templates"
336 conf.filepath = "files"
337 conf.metapath = "meta"
338 conf.filectx = "/f/"
339 conf.maxsize = 34359738368
340 conf.expiry = 86400
341
342 flag.StringVar(&configfile, "f", "", "Configuration file")
343 flag.BoolVar(&verbose, "v", false, "Verbose logging")
344 flag.Parse()
345
346 if configfile != "" {
347 if verbose {
348 log.Printf("Reading configuration %s", configfile)
349 }
350 parseconfig(configfile)
351 }
352
353 if conf.chroot != "" {
354 if verbose {
355 log.Printf("Changing root to %s", conf.chroot)
356 }
357 syscall.Chroot(conf.chroot)
358 }
359
360 if conf.listen[0] == '/' {
361 /* Remove any stale socket */
362 os.Remove(conf.listen)
363 if listener, err = net.Listen("unix", conf.listen); err != nil {
364 log.Fatal(err)
365 }
366 defer listener.Close()
367
368 /*
369 * Ensure unix socket is removed on exit.
370 * Note: this might not work when dropping privileges…
371 */
372 defer os.Remove(conf.listen)
373 sigs := make(chan os.Signal, 1)
374 signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
375 go func() {
376 _ = <-sigs
377 listener.Close()
378 if err = os.Remove(conf.listen); err != nil {
379 log.Fatal(err)
380 }
381 os.Exit(0)
382 }()
383 } else {
384 if listener, err = net.Listen("tcp", conf.listen); err != nil {
385 log.Fatal(err)
386 }
387 defer listener.Close()
388 }
389
390 if conf.user != "" {
391 if verbose {
392 log.Printf("Dropping privileges to %s", conf.user)
393 }
394 uid, gid, err := usergroupids(conf.user, conf.group)
395 if err != nil {
396 log.Fatal(err)
397 }
398
399 if listener.Addr().Network() == "unix" {
400 os.Chown(conf.listen, uid, gid)
401 }
402
403 syscall.Setuid(uid)
404 syscall.Setgid(gid)
405 }
406
407 http.HandleFunc("/", uploader)
408 http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
409
410 if verbose {
411 log.Printf("Listening on %s", conf.listen)
412 }
413
414 if listener.Addr().Network() == "unix" {
415 err = fcgi.Serve(listener, nil)
416 log.Fatal(err) /* NOTREACHED */
417 }
418
419 err = http.Serve(listener, nil)
420 log.Fatal(err) /* NOTREACHED */
421}
Note: See TracBrowser for help on using the repository browser.