// uploader -- simplest form file uploader // Copyright (C) 2018-2024 Sergey Matveev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package main import ( "bufio" "bytes" "encoding/base64" "encoding/hex" "flag" "fmt" "html/template" "io" "log" "mime" "net" "net/http" "os" "os/exec" "strconv" "strings" "sync" "time" "go.cypherpunks.ru/recfile" "go.cypherpunks.ru/tai64n/v2" "golang.org/x/net/netutil" "lukechampine.com/blake3" ) const ( WriteBufSize = 1 << 20 FileFieldName = "file" CommentFieldName = "comment" SendmailCmd = "/usr/sbin/sendmail" ) var ( Index = template.Must(template.New("index").Parse(` Upload Example command line usage:
$ curl -F file=@somedata.tar.gpg [-F comment="optional comment"] http://.../upload/
$ b3sum somedata.tar.gpg # to verify BLAKE3-256 checksum




`)) NotifyFromAddr *string NotifyToAddr *string Jobs sync.WaitGroup ) func upload(w http.ResponseWriter, r *http.Request) { log.Println(r.RemoteAddr, "connected") if r.Method == http.MethodGet { if err := Index.Execute(w, FileFieldName); err != nil { log.Println(r.RemoteAddr, err) } return } mr, err := r.MultipartReader() if err != nil { log.Println(r.RemoteAddr, err) return } p, err := mr.NextPart() if err != nil { log.Println(r.RemoteAddr, err) return } if p.FormName() != FileFieldName { log.Println(r.RemoteAddr, "non file form field") return } t := time.Now() ts := new(tai64n.TAI64N) ts.FromTime(t) tai := tai64n.Encode(ts[:])[1:] fnOrig := p.FileName() fd, err := os.OpenFile(tai+".part", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) if err != nil { log.Println(r.RemoteAddr, tai, fnOrig, err) return } fdBuf := bufio.NewWriterSize(fd, WriteBufSize) h := blake3.New(32, nil) mw := io.MultiWriter(fdBuf, h) n, err := io.Copy(mw, p) if err != nil { log.Println(r.RemoteAddr, tai, fnOrig, err) fd.Close() return } if n == 0 { log.Println(r.RemoteAddr, tai, fnOrig, "empty") os.Remove(tai + ".part") fd.Close() fmt.Fprintf(w, "Empty file") return } if err = fdBuf.Flush(); err != nil { log.Println(r.RemoteAddr, tai, fnOrig, err) fd.Close() return } if err = fd.Sync(); err != nil { log.Println(r.RemoteAddr, tai, fnOrig, err) fd.Close() return } fd.Close() sum := hex.EncodeToString(h.Sum(nil)) if err = os.Rename(tai+".part", tai); err != nil { log.Println(r.RemoteAddr, tai, fnOrig, n, sum, err) return } var rec bytes.Buffer wr := recfile.NewWriter(&rec) if _, err = wr.WriteFields( recfile.Field{Name: "TAI64N", Value: tai}, recfile.Field{Name: "Size", Value: strconv.FormatInt(n, 10)}, recfile.Field{Name: "Checksum", Value: sum}, ); err != nil { log.Println(r.RemoteAddr, tai, fnOrig, n, sum, err) return } if _, err = w.Write(rec.Bytes()); err == nil { log.Println(r.RemoteAddr, tai, fnOrig, n, sum) } else { log.Println(r.RemoteAddr, tai, fnOrig, n, sum, err) return } if _, err = wr.WriteFields( recfile.Field{Name: "Filename", Value: fnOrig}, ); err != nil { log.Println(r.RemoteAddr, tai, fnOrig, n, sum, err) return } var commentLines []string p, err = mr.NextPart() if err == nil && p.FormName() == CommentFieldName { comment, err := io.ReadAll(p) if err == nil && len(comment) > 0 { commentLines = strings.Split(string(comment), "\n") wr.WriteFieldMultiline("Comment", commentLines) } } os.WriteFile(tai+".rec", rec.Bytes(), os.FileMode(0666)) if *NotifyToAddr == "" { return } cmd := exec.Command(SendmailCmd, *NotifyToAddr) cmd.Stdin = io.MultiReader( strings.NewReader(fmt.Sprintf( `From: %s To: %s Subject: %s MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: base64 `, *NotifyFromAddr, *NotifyToAddr, mime.BEncoding.Encode("UTF-8", fmt.Sprintf("%s (%d KiB)", fnOrig, n/1024)), )), strings.NewReader(base64.StdEncoding.EncodeToString(rec.Bytes())), ) Jobs.Add(1) go func() { cmd.Run() Jobs.Done() }() } func main() { doUCSPI := flag.Bool("ucspi", false, "Work as UCSPI-TCP service") bind := flag.String("bind", "[::]:8086", "Address to bind to") conns := flag.Int("conns", 2, "Maximal number of connections") NotifyFromAddr = flag.String("notify-from", "uploader@example.com", "Address notifications are sent to") NotifyToAddr = flag.String("notify-to", "", "Address notifications are sent from") flag.Parse() log.SetFlags(log.Lshortfile) if !*doUCSPI { log.SetOutput(os.Stdout) } if len(*NotifyFromAddr) == 0 && len(*NotifyToAddr) > 0 { log.Fatalln("notify-from address can not be empty, if notify-to is set") } s := &http.Server{ ReadHeaderTimeout: 10 * time.Second, MaxHeaderBytes: 10 * (1 << 10), } http.HandleFunc("/upload/", upload) if *doUCSPI { s.SetKeepAlivesEnabled(false) ln := &UCSPI{} s.ConnState = connStater err := s.Serve(ln) if _, ok := err.(UCSPIAlreadyAccepted); !ok { log.Fatalln(err) } Jobs.Wait() } else { ln, err := net.Listen("tcp", *bind) if err != nil { log.Fatalln(err) } log.Println("listening", *bind) ln = netutil.LimitListener(ln, *conns) if err = s.Serve(ln); err != nil { log.Fatalln(err) } } }