httpdir - cmd/httpdir/main.go
1 package main // import "vimagination.zapto.org/httpdir/cmd/httpdir"
2
3 import (
4 "compress/flate"
5 "compress/gzip"
6 "flag"
7 "fmt"
8 "io"
9 "os"
10 "sort"
11 "strconv"
12 "strings"
13 "time"
14 "unicode/utf8"
15
16 "github.com/foobaz/go-zopfli/zopfli"
17 "github.com/google/brotli/go/cbrotli"
18 "vimagination.zapto.org/memio"
19 )
20
21 var (
22 pkg = flag.String("p", "main", "package name")
23 in = flag.String("i", "", "input filename")
24 out = flag.String("o", "", "output filename")
25 odate = flag.String("d", "", "modified date [seconds since epoch]")
26 path = flag.String("w", "", "http path")
27 varname = flag.String("v", "httpdir.Default", "http dir variable name")
28 cvarname = flag.String("c", "httpdir.Default", "http dir compressed variable name")
29 help = flag.Bool("h", false, "show help")
30 gzcomp = flag.Bool("g", false, "compress using gzip")
31 brcomp = flag.Bool("b", false, "compress using brotli")
32 flcomp = flag.Bool("f", false, "compress using flate/deflate")
33 single = flag.Bool("s", false, "use single source var and decompress/compress for others")
34 zpfcomp = flag.Bool("z", false, "replace gzip with zopfli compression")
35 )
36
37 type replacer struct {
38 f *os.File
39 }
40
41 var hexArr = [16]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}
42
43 func (r replacer) Write(p []byte) (int, error) {
44 n := 0
45 toWrite := make([]byte, 0, 5)
46 for len(p) > 0 {
47 rn, s := utf8.DecodeRune(p)
48 if rn == utf8.RuneError {
49 s = 1
50 }
51 if s > 1 || (p[0] > 0 && p[0] < 0x7f && p[0] != '\n' && p[0] != '\\' && p[0] != '"') {
52 toWrite = append(toWrite[:0], p[:s]...)
53 } else {
54 switch p[0] {
55 case '\n':
56 toWrite = append(toWrite[:0], '\\', 'n')
57 case '\\':
58 toWrite = append(toWrite[:0], '\\', '\\')
59 case '"':
60 toWrite = append(toWrite[:0], '\\', '"')
61 default:
62 toWrite = append(toWrite[:0], '\\', 'x', hexArr[p[0]>>4], hexArr[p[0]&15])
63 }
64 }
65 _, err := r.f.Write(toWrite)
66 n += s
67 if err != nil {
68 return n, err
69 }
70 p = p[s:]
71 }
72 return n, nil
73 }
74
75 type tickReplacer struct {
76 f *os.File
77 }
78
79 func (r tickReplacer) Write(p []byte) (int, error) {
80 var (
81 m, n int
82 err error
83 )
84 hexes := make([]byte, 0, 32)
85 toWrite := make([]byte, 0, 1024)
86 for len(p) > 0 {
87 dr, s := utf8.DecodeRune(p)
88 if dr == utf8.RuneError || dr == '`' || dr == '\r' {
89 hexes = append(hexes, p[0])
90 } else {
91 if len(hexes) > 0 {
92 toWrite = toWrite[:0]
93 toWrite = append(toWrite, '`', '+', '"')
94 for _, b := range hexes {
95 if b == '`' {
96 toWrite = append(toWrite, '`')
97 } else if b == '\r' {
98 toWrite = append(toWrite, '\\', 'r')
99 } else {
100 toWrite = append(toWrite, '\\', 'x', hexArr[b>>4], hexArr[b&15])
101 }
102 }
103 toWrite = append(toWrite, '"', '+', '`')
104 _, err = r.f.Write(toWrite)
105 n += len(hexes)
106 if err != nil {
107 break
108 }
109 hexes = hexes[:0]
110 }
111 m, err = r.f.Write(p[:s])
112 n += m
113 if err != nil {
114 break
115 }
116 }
117 p = p[s:]
118 }
119 return n, err
120 }
121
122 func e(err error) {
123 if err != nil {
124 fmt.Fprintln(os.Stderr, err.Error())
125 os.Exit(1)
126 }
127 }
128
129 func ne(_ int, err error) {
130 e(err)
131 }
132
133 type imports []string
134
135 func (im imports) Len() int {
136 return len(im)
137 }
138
139 func (im imports) Less(i, j int) bool {
140 si := strings.HasPrefix(im[i], "\"github.com")
141 sj := strings.HasPrefix(im[j], "\"github.com")
142 vi := strings.HasPrefix(im[i], "\"vimagination.zapto.org")
143 vj := strings.HasPrefix(im[j], "\"vimagination.zapto.org")
144 if si == sj && vi == vj {
145 return im[i] < im[j]
146 }
147 return !si && !vi || si && vj
148 }
149
150 func (im imports) Swap(i, j int) {
151 im[i], im[j] = im[j], im[i]
152 }
153
154 type encoding struct {
155 Buffer []byte
156 Compress, Decompress, Ext string
157 }
158
159 type encodings []encoding
160
161 func (e encodings) Len() int {
162 return len(e)
163 }
164
165 func (e encodings) Less(i, j int) bool {
166 return len(e[i].Buffer) < len(e[j].Buffer)
167 }
168
169 func (e encodings) Swap(i, j int) {
170 e[i], e[j] = e[j], e[i]
171 }
172
173 func main() {
174 flag.Parse()
175 if *help {
176 flag.Usage()
177 return
178 }
179 var (
180 f *os.File
181 err error
182 date int64
183 )
184 if *in == "-" || *in == "" {
185 f = os.Stdin
186 date = time.Now().Unix()
187 } else {
188 f, err = os.Open(*in)
189 e(err)
190 fi, err := f.Stat()
191 e(err)
192 date = fi.ModTime().Unix()
193 }
194 if *odate != "" {
195 date, err = strconv.ParseInt(*odate, 10, 64)
196 e(err)
197 }
198
199 data := make(memio.Buffer, 0, 1<<20)
200 _, err = io.Copy(&data, f)
201 e(err)
202 e(f.Close())
203
204 im := imports{"\"vimagination.zapto.org/httpdir\"", "\"time\""}
205
206 encs := make(encodings, 1, 4)
207 encs[0] = encoding{
208 Buffer: data,
209 Compress: identCompress,
210 Decompress: identDecompress,
211 Ext: "",
212 }
213
214 if *brcomp {
215 var b memio.Buffer
216 br := cbrotli.NewWriter(&b, cbrotli.WriterOptions{Quality: 11})
217 br.Write(data)
218 br.Close()
219 if *single {
220 im = append(im, brotliImport)
221 }
222 encs = append(encs, encoding{
223 Buffer: b,
224 Compress: brotliCompress,
225 Decompress: brotliDecompress,
226 Ext: ".br",
227 })
228 }
229 if *flcomp {
230 var b memio.Buffer
231 fl, _ := flate.NewWriter(&b, flate.BestCompression)
232 fl.Write(data)
233 fl.Close()
234 if *single {
235 im = append(im, strings.Split(flateImport, "\n")...)
236 }
237 encs = append(encs, encoding{
238 Buffer: b,
239 Compress: flateCompress,
240 Decompress: flateDecompress,
241 Ext: ".fl",
242 })
243 }
244 if *gzcomp || *zpfcomp {
245 var b memio.Buffer
246 if *zpfcomp {
247 zopfli.GzipCompress(&zopfli.Options{
248 NumIterations: 100,
249 BlockSplitting: true,
250 BlockType: 2,
251 }, data, &b)
252 } else {
253 gz, _ := gzip.NewWriterLevel(&b, gzip.BestCompression)
254 gz.Write(data)
255 gz.Close()
256 }
257 if *single {
258 im = append(im, gzipImport)
259 }
260 encs = append(encs, encoding{
261 Buffer: b,
262 Compress: gzipCompress,
263 Decompress: gzipDecompress,
264 Ext: ".gz",
265 })
266 }
267
268 sort.Sort(encs)
269
270 if *single && (*gzcomp || *brcomp || *flcomp) {
271 im = append(im, "\"vimagination.zapto.org/memio\"")
272 if encs[0].Ext != "" {
273 im = append(im, "\"strings\"")
274 }
275 }
276 if encs[0].Ext == ".fl" {
277 im = append(im, "\"io\"")
278 }
279
280 sort.Sort(im)
281 var (
282 imports string
283 ext bool
284 )
285 for _, i := range im {
286 if !ext && (strings.HasPrefix(i, "\"github.com") || strings.HasPrefix(i, "\"vimagination")) {
287 imports += "\n"
288 ext = true
289 }
290 imports += " " + i + "\n"
291 }
292 if *out == "-" || *out == "" {
293 f = os.Stdout
294 } else {
295 f, err = os.Create(*out)
296 e(err)
297 }
298 fmt.Fprintf(f, packageStart, *pkg, imports, date)
299 if *single {
300 f.WriteString(stringStart)
301 if encs[0].Ext == "" {
302 ne(f.WriteString("`"))
303 ne(tickReplacer{f}.Write(encs[0].Buffer))
304 ne(f.WriteString("`"))
305 } else {
306 ne(f.WriteString("\""))
307 ne(replacer{f}.Write(encs[0].Buffer))
308 ne(f.WriteString("\""))
309 }
310 f.WriteString(stringEnd)
311 for n, enc := range encs {
312 var (
313 templ string
314 vars = []interface{}{0, *cvarname, *path + enc.Ext}
315 )
316 if enc.Ext == "" {
317 vars = vars[1:]
318 vars[0] = *varname
319 if n == 0 {
320 templ = identDecompress
321 } else {
322 templ = identCompress
323 }
324 } else {
325 if n == 0 {
326 vars[0] = len(data)
327 templ = enc.Decompress
328 } else {
329 vars[0] = len(enc.Buffer)
330 templ = enc.Compress
331 }
332 }
333 fmt.Fprintf(f, templ, vars...)
334 }
335 } else {
336 for _, enc := range encs {
337 filename := *path + enc.Ext
338 ne(fmt.Fprintf(f, soloStart, *varname, filename))
339 if enc.Ext == "" {
340 ne(f.WriteString("`"))
341 ne(tickReplacer{f}.Write(enc.Buffer))
342 ne(f.WriteString("`"))
343 } else {
344 ne(f.WriteString("\""))
345 ne(replacer{f}.Write(enc.Buffer))
346 ne(f.WriteString("\""))
347 }
348 ne(f.WriteString(soloEnd))
349 }
350 }
351 ne(fmt.Fprint(f, packageEnd))
352 e(f.Close())
353 }
354
355 const (
356 packageStart = `package %s
357
358 import (
359 %s)
360
361 func init() {
362 date := time.Unix(%d, 0)
363 `
364 stringStart = ` s := `
365 stringEnd = `
366 `
367 packageEnd = `}
368 `
369 soloStart = ` %s.Create(%q, httpdir.FileString(`
370 soloEnd = `, date))
371 `
372 identDecompress = ` b := []byte(s)
373 %s.Create(%q, httpdir.FileString(s, date))
374 `
375 identCompress = ` %s.Create(%q, httpdir.FileBytes(b, date))
376 `
377
378 brotliImport = "\"github.com/google/brotli/go/cbrotli\""
379 brotliDecompress = ` b := make([]byte, %d)
380 br := cbrotli.NewReader(strings.NewReader(s))
381 br.Read(b)
382 br.Close()
383 %s.Create(%q, httpdir.FileString(s, date))
384 `
385 brotliCompress = ` brb := make(memio.Buffer, 0, %d)
386 br := cbrotli.NewWriter(&brb, cbrotli.WriterOptions{Quality: 11})
387 br.Write(b)
388 br.Close()
389 %s.Create(%q, httpdir.FileBytes(brb, date))
390 `
391 flateImport = "\"compress/flate\""
392 flateDecompress = ` b := make([]byte, %d)
393 fl := flate.NewReader(strings.NewReader(s))
394 io.ReadFull(fl, b)
395 fl.Close()
396 %s.Create(%q, httpdir.FileString(s, date))
397 `
398 flateCompress = ` flb := make(memio.Buffer, 0, %d)
399 fl, _ := flate.NewWriter(&flb, flate.BestCompression)
400 fl.Write(b)
401 fl.Close()
402 %s.Create(%q, httpdir.FileBytes(flb, date))
403 `
404 gzipImport = "\"compress/gzip\""
405 gzipDecompress = ` b := make([]byte, %d)
406 gz, _ := gzip.NewReader(strings.NewReader(s))
407 gz.Read(b)
408 gz.Close()
409 %s.Create(%q, httpdir.FileString(s, date))
410 `
411 gzipCompress = ` gzb := make(memio.Buffer, 0, %d)
412 gz, _ := gzip.NewWriterLevel(&gzb, gzip.BestCompression)
413 gz.Write(b)
414 gz.Close()
415 %s.Create(%q, httpdir.FileBytes(gzb, date))
416 `
417 )
418