]> Sergey Matveev's repositories - paster.git/blob - main.go
Trivial style fixes
[paster.git] / main.go
1 // paster -- paste service
2 // Copyright (C) 2021-2024 Sergey Matveev <stargrave@stargrave.org>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 3 of the License.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16 package main
17
18 import (
19         "bufio"
20         "bytes"
21         "crypto/rand"
22         _ "embed"
23         "encoding/base32"
24         "encoding/hex"
25         "flag"
26         "fmt"
27         "html/template"
28         "io"
29         "os"
30
31         "go.cypherpunks.ru/netstring/v2"
32         "lukechampine.com/blake3"
33 )
34
35 const MaxExtLen = 9
36
37 var (
38         //go:embed asciicast.tmpl
39         ASCIICastHTMLTmplRaw string
40         ASCIICastHTMLTmpl    = template.Must(template.New("asciicast").Parse(
41                 ASCIICastHTMLTmplRaw,
42         ))
43 )
44
45 func fatal(s string) {
46         fmt.Println(s)
47         os.Exit(1)
48 }
49
50 func asciicastHTML(playerPath, cast string) error {
51         var buf bytes.Buffer
52         err := ASCIICastHTMLTmpl.Execute(&buf, struct {
53                 PlayerPath string
54                 Cast       string
55         }{
56                 PlayerPath: playerPath,
57                 Cast:       cast,
58         })
59         if err != nil {
60                 return err
61         }
62         fn := cast + ".html"
63         fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
64         if err != nil {
65                 return err
66         }
67         if _, err = fd.Write(buf.Bytes()); err != nil {
68                 os.Remove(fn)
69                 return err
70         }
71         return fd.Close()
72 }
73
74 func main() {
75         maxSize := flag.Uint64("max-size", 1<<20, "Maximal upload size")
76         asciicastPath := flag.String("asciicast-path", "", "Generate HTMLs for .cast asciicasts, specify \"asciinema-player-v2.6.1\"")
77         flag.Usage = func() {
78                 fmt.Fprintf(os.Stderr, "Usage: paster [options] URL [URL ...]\n")
79                 flag.PrintDefaults()
80         }
81         flag.Parse()
82         if len(flag.Args()) == 0 {
83                 flag.Usage()
84                 os.Exit(1)
85         }
86         r := netstring.NewReader(os.Stdin)
87         size, err := r.Next()
88         if err != nil {
89                 fatal(err.Error())
90         }
91         if size > MaxExtLen {
92                 fatal("too long extension length")
93         }
94         data, err := io.ReadAll(r)
95         if err != nil {
96                 fatal(err.Error())
97         }
98         var ext string
99         if len(data) == 0 {
100                 ext = ".txt"
101         } else {
102                 ext = "." + string(data)
103         }
104         size, err = r.Next()
105         if err != nil {
106                 fatal(err.Error())
107         }
108         if size == 0 {
109                 fatal("empty paste")
110         }
111         if size > *maxSize {
112                 fatal("too big")
113         }
114         rnd := make([]byte, 12)
115         if _, err = io.ReadFull(rand.Reader, rnd); err != nil {
116                 fatal(err.Error())
117         }
118         fn := "." + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rnd) + ext
119         fd, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
120         if err != nil {
121                 fatal(err.Error())
122         }
123         h := blake3.New(32, nil)
124         bfd := bufio.NewWriter(fd)
125         mw := io.MultiWriter(bfd, h)
126         buf := make([]byte, 1)
127         _, err = io.CopyN(mw, r, int64(size-1))
128         if err != nil {
129                 goto Failed
130         }
131         _, err = r.Read(buf)
132         if err != nil {
133                 goto Failed
134         }
135         _, err = mw.Write(buf)
136         if err != nil {
137                 goto Failed
138         }
139         if (ext == ".txt" || ext == ".url") && buf[0] != '\n' {
140                 err = bfd.WriteByte('\n')
141                 if err != nil {
142                         goto Failed
143                 }
144         }
145         err = bfd.Flush()
146         if err != nil {
147                 goto Failed
148         }
149         err = fd.Close()
150         if err != nil {
151                 goto Failed
152         }
153         err = os.Rename(fn, fn[1:])
154         if err != nil {
155                 goto Failed
156         }
157         for _, u := range flag.Args() {
158                 fmt.Println(u + fn[1:])
159         }
160         fmt.Println("BLAKE3-256:", hex.EncodeToString(h.Sum(nil)))
161         if ext == ".cast" && *asciicastPath != "" {
162                 if err = asciicastHTML(*asciicastPath, fn[1:]); err != nil {
163                         goto Failed
164                 }
165                 for _, u := range flag.Args() {
166                         fmt.Println(u + fn[1:] + ".html")
167                 }
168         }
169         fmt.Fprintf(
170                 os.Stderr,
171                 "[%s]:%s %s %d\n",
172                 os.Getenv("TCPREMOTEIP"),
173                 os.Getenv("TCPREMOTEPORT"),
174                 fn[1:], size,
175         )
176         return
177 Failed:
178         os.Remove(fn)
179         fatal(err.Error())
180 }