1 // Package httpfile provides an easy way to create HTTP handlers that respond with static data, possibly gzip compressed if requested by the client. 2 package httpfile // import "vimagination.zapto.org/httpfile" 3 4 import ( 5 "bytes" 6 "compress/gzip" 7 "io" 8 "io/fs" 9 "net/http" 10 "sync" 11 "time" 12 13 "vimagination.zapto.org/httpencoding" 14 ) 15 16 var empty = [20]byte{0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x03} 17 18 // Type file represents an http.Handler upon which you can set static data. 19 type File struct { 20 name string 21 22 mu sync.RWMutex 23 modtime time.Time 24 data, compressed []byte 25 } 26 27 // New creates a new File with the given name, which is used to apply 28 // Content-Type headers. 29 func New(name string) *File { 30 return &File{name: name, modtime: time.Now(), compressed: empty[:]} 31 } 32 33 // NewWithData create a new File with the given name, and sets the initial 34 // uncompressed data to that provided. 35 func NewWithData(name string, data []byte) *File { 36 var buf bytes.Buffer 37 38 f := New(name) 39 f.data = data 40 g := gzip.NewWriter(&buf) 41 42 g.Write(data) 43 g.Close() 44 45 f.compressed = buf.Bytes() 46 47 return f 48 } 49 50 var isGzip = httpencoding.HandlerFunc(func(enc httpencoding.Encoding) bool { return enc == "gzip" }) 51 52 // ServeHTTP implements the http.Handler interface. 53 func (f *File) ServeHTTP(w http.ResponseWriter, r *http.Request) { 54 var buf bytes.Reader 55 56 wantsGzip := httpencoding.HandleEncoding(r, isGzip) 57 58 f.mu.RLock() 59 60 modtime := f.modtime 61 62 if wantsGzip { 63 buf.Reset(f.compressed) 64 w.Header().Add("Content-Encoding", "gzip") 65 } else { 66 buf.Reset(f.data) 67 } 68 69 f.mu.RUnlock() 70 71 http.ServeContent(w, r, f.name, modtime, &buf) 72 } 73 74 // ReadFrom reads all of the data from io.Reader and applies it to the file, 75 // overwriting any existing data and setting the modtime to Now. 76 func (f *File) ReadFrom(r io.Reader) (int64, error) { 77 file := f.Create() 78 defer file.Close() 79 80 return io.Copy(file, r) 81 } 82 83 // WriteTo writes the uncompressed data to the given writer. 84 func (f *File) WriteTo(w io.Writer) (int64, error) { 85 f.mu.RLock() 86 data := f.data 87 f.mu.RUnlock() 88 89 n, err := w.Write(data) 90 91 return int64(n), err 92 } 93 94 // Chtime sets the modtime to the given time. 95 func (f *File) Chtime(t time.Time) { 96 f.mu.Lock() 97 f.modtime = t 98 f.mu.Unlock() 99 } 100 101 // Name returns the name given during File creation. 102 func (f *File) Name() string { 103 return f.name 104 } 105 106 // A Writer is bound to the File is was created from, buffering data that is 107 // written to it.Writer. Upon Closing, that data will be compressed and both 108 // the uncompressed and compressed data will be replaced on the File. 109 type Writer struct { 110 file *File 111 data []byte 112 } 113 114 // Create opens a Writer that can be used to write the data for the File. Close 115 // must be called on the resulting Writer for the data to be accepted. 116 func (f *File) Create() *Writer { 117 return &Writer{ 118 file: f, 119 } 120 } 121 122 // Write is an implementation of the io.Writer interface. 123 func (f *Writer) Write(p []byte) (int, error) { 124 if f.file == nil { 125 return 0, fs.ErrClosed 126 } 127 128 f.data = append(f.data, p...) 129 130 return len(p), nil 131 } 132 133 // WriteString is an implementation of the io.StringWriter interface. 134 func (f *Writer) WriteString(str string) (int, error) { 135 if f.file == nil { 136 return 0, fs.ErrClosed 137 } 138 139 f.data = append(f.data, str...) 140 141 return len(str), nil 142 } 143 144 // Close is an implementation of the io.Close interface. 145 // 146 // This method must be called for the written data to be accepted on the File. 147 func (f *Writer) Close() error { 148 if f.file == nil { 149 return fs.ErrClosed 150 } 151 152 var compressed bytes.Buffer 153 154 g := gzip.NewWriter(&compressed) 155 156 g.Write(f.data) 157 g.Close() 158 159 f.file.mu.Lock() 160 161 f.file.data = f.data 162 f.file.compressed = compressed.Bytes() 163 f.file.modtime = time.Now() 164 165 f.file.mu.Unlock() 166 167 *f = Writer{} 168 169 return nil 170 } 171