]> Sergey Matveev's repositories - uploader.git/blob - src/uploader/main.go
0416fdfc9d12c79f92e2e733f484c3d77f404a8c
[uploader.git] / src / uploader / main.go
1 /*
2 uploader -- simplest form file uploader
3 Copyright (C) 2018-2019 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 package main
20
21 import (
22         "bufio"
23         "encoding/base64"
24         "encoding/hex"
25         "flag"
26         "fmt"
27         "html/template"
28         "io"
29         "io/ioutil"
30         "log"
31         "mime"
32         "net"
33         "net/http"
34         "os"
35         "os/exec"
36         "strings"
37         "time"
38
39         "golang.org/x/crypto/blake2b"
40         "golang.org/x/net/netutil"
41 )
42
43 const (
44         WriteBufSize     = 1 << 20
45         FileFieldName    = "fileupload"
46         CommentFieldName = "comment"
47
48         SendmailCmd = "/usr/sbin/sendmail"
49 )
50
51 var (
52         Index = template.Must(template.New("index").Parse(`<html>
53 <head><title>Upload</title></head><body>
54 <form enctype="multipart/form-data" action="/upload/" method="post">
55 <input type="file" name="{{.}}" /><br/>
56 <label for="comment">Optional comment:</label>
57 <textarea name="comment" cols="80" rows="25" name="comment"></textarea><br/>
58 <input type="submit" />
59 </form></body></html>`))
60         NotifyFromAddr *string
61         NotifyToAddr   *string
62 )
63
64 func notify(filename, timestamp string, size int64, comment string) {
65         if *NotifyToAddr == "" {
66                 return
67         }
68         cmd := exec.Command(SendmailCmd, *NotifyToAddr)
69         cmd.Stdin = io.MultiReader(
70                 strings.NewReader(fmt.Sprintf(
71                         `From: %s
72 To: %s
73 Subject: %s
74 MIME-Version: 1.0
75 Content-Type: text/plain; charset=utf-8
76 Content-Transfer-Encoding: base64
77
78 `,
79                         *NotifyFromAddr,
80                         *NotifyToAddr,
81                         mime.BEncoding.Encode("UTF-8", fmt.Sprintf(
82                                 "%s (%d KiB)", filename, size/1024,
83                         )),
84                 )),
85                 strings.NewReader(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(
86                         "Timestamp: %s\nSize: %d bytes\nComment: %s\n",
87                         timestamp,
88                         size,
89                         comment,
90                 )))),
91         )
92         cmd.Run()
93 }
94
95 func upload(w http.ResponseWriter, r *http.Request) {
96         log.Println(r.RemoteAddr, "connected")
97         if r.Method == http.MethodGet {
98                 if err := Index.Execute(w, FileFieldName); err != nil {
99                         log.Println(r.RemoteAddr, err)
100                 }
101                 return
102         }
103         mr, err := r.MultipartReader()
104         if err != nil {
105                 log.Println(r.RemoteAddr, err)
106                 return
107         }
108         p, err := mr.NextPart()
109         if err != nil {
110                 log.Println(r.RemoteAddr, err)
111                 return
112         }
113         if p.FormName() != FileFieldName {
114                 log.Println(r.RemoteAddr, "non file form field")
115                 return
116         }
117         h, err := blake2b.New512(nil)
118         if err != nil {
119                 panic(err)
120         }
121         fn := time.Now().Format(time.RFC3339Nano)
122         fnOrig := p.FileName()
123         fd, err := os.OpenFile(fn+".part", os.O_WRONLY|os.O_CREATE, 0600)
124         if err != nil {
125                 log.Println(r.RemoteAddr, fn, fnOrig, err)
126                 return
127         }
128         fdBuf := bufio.NewWriterSize(fd, WriteBufSize)
129         mw := io.MultiWriter(fdBuf, h)
130         n, err := io.Copy(mw, p)
131         if err != nil {
132                 log.Println(r.RemoteAddr, fn, fnOrig, err)
133                 fd.Close()
134                 return
135         }
136         if n == 0 {
137                 log.Println(r.RemoteAddr, fn, fnOrig, "empty")
138                 os.Remove(fn + ".part")
139                 fd.Close()
140                 fmt.Fprintf(w, "Empty file")
141                 return
142         }
143         if err = fdBuf.Flush(); err != nil {
144                 log.Println(r.RemoteAddr, fn, fnOrig, err)
145                 fd.Close()
146                 return
147         }
148         fd.Close()
149         sum := hex.EncodeToString(h.Sum(nil))
150         if err = os.Rename(fn+".part", fn); err != nil {
151                 log.Println(r.RemoteAddr, fn, fnOrig, n, sum, err)
152                 return
153         }
154         fmt.Fprintf(w, "Timestamp: %s\nBytes: %d\nBLAKE2b: %s\n", fn, n, sum)
155         log.Println(r.RemoteAddr, fn, fnOrig, n, sum)
156         p, err = mr.NextPart()
157         if err != nil || p.FormName() != CommentFieldName {
158                 go notify(fnOrig, fn, n, "")
159                 return
160         }
161         comment, err := ioutil.ReadAll(p)
162         if err != nil || len(comment) == 0 {
163                 go notify(fnOrig, fn, n, "")
164                 return
165         }
166         ioutil.WriteFile(fn+".txt", comment, os.FileMode(0600))
167         go notify(fnOrig, fn, n, string(comment))
168 }
169
170 func main() {
171         bind := flag.String("bind", "[::]:8086", "Address to bind to")
172         conns := flag.Int("conns", 2, "Maximal number of connections")
173         NotifyFromAddr = flag.String("notify-from", "uploader@example.com", "Address notifications are send to")
174         NotifyToAddr = flag.String("notify-to", "", "Address notifications are send from")
175         flag.Parse()
176         if len(*NotifyFromAddr) == 0 && len(*NotifyToAddr) > 0 {
177                 log.Fatalln("notify-from address can not be empty, if notify-to is set")
178         }
179         ln, err := net.Listen("tcp", *bind)
180         if err != nil {
181                 log.Fatalln(err)
182         }
183         log.Println("listening", *bind)
184         ln = netutil.LimitListener(ln, *conns)
185         s := &http.Server{
186                 ReadHeaderTimeout: 10 * time.Second,
187                 MaxHeaderBytes:    10 * (1 << 10),
188         }
189         http.HandleFunc("/upload/", upload)
190         s.Serve(ln)
191 }