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 "strconv" 11 "sync" 12 "time" 13 14 "vimagination.zapto.org/httpencoding" 15 ) 16 17 var empty = [20]byte{0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x03} 18 19 // Type file represents an http.Handler upon which you can set static data. 20 type File struct { 21 name string 22 23 mu sync.RWMutex 24 modtime time.Time 25 data, compressed []byte 26 } 27 28 // New creates a new File with the given name, which is used to apply 29 // Content-Type headers. 30 func New(name string) *File { 31 return &File{name: name, modtime: time.Now(), compressed: empty[:]} 32 } 33 34 // NewWithData create a new File with the given name, and sets the initial 35 // uncompressed data to that provided. 36 func NewWithData(name string, data []byte) *File { 37 var buf bytes.Buffer 38 39 f := New(name) 40 f.data = data 41 g := gzip.NewWriter(&buf) 42 43 g.Write(data) 44 g.Close() 45 46 f.compressed = buf.Bytes() 47 48 return f 49 } 50 51 type requestGzip bool 52 53 func (r *requestGzip) Handle(enc httpencoding.Encoding) bool { 54 if enc == "gzip" || httpencoding.IsWildcard(enc) && !httpencoding.IsDisallowedInWildcard(enc, "gzip") { 55 *r = true 56 57 return true 58 } 59 60 return enc == "" || httpencoding.IsWildcard(enc) && !httpencoding.IsDisallowedInWildcard(enc, "") 61 } 62 63 // ServeHTTP implements the http.Handler interface. 64 func (f *File) ServeHTTP(w http.ResponseWriter, r *http.Request) { 65 var ( 66 buf bytes.Reader 67 requestGzip requestGzip 68 ) 69 70 if !httpencoding.HandleEncoding(r, &requestGzip) { 71 httpencoding.InvalidEncoding(w) 72 73 return 74 } 75 76 f.mu.RLock() 77 78 modtime := f.modtime 79 80 if requestGzip { 81 buf.Reset(f.compressed) 82 w.Header().Add("Content-Encoding", "gzip") 83 84 w = &wrapResponseWriter{ 85 ResponseWriter: w, 86 size: int64(len(f.compressed)), 87 } 88 } else { 89 buf.Reset(f.data) 90 } 91 92 f.mu.RUnlock() 93 94 http.ServeContent(w, r, f.name, modtime, &buf) 95 } 96 97 type wrapResponseWriter struct { 98 http.ResponseWriter 99 size int64 100 } 101 102 func (w *wrapResponseWriter) WriteHeader(code int) { 103 if w.Header().Get("Content-Length") == "" { 104 w.Header().Set("Content-Length", strconv.FormatInt(w.size, 10)) 105 } 106 107 w.ResponseWriter.WriteHeader(code) 108 } 109 110 // ReadFrom reads all of the data from io.Reader and applies it to the file, 111 // overwriting any existing data and setting the modtime to Now. 112 func (f *File) ReadFrom(r io.Reader) (int64, error) { 113 file := f.Create() 114 defer file.Close() 115 116 return io.Copy(file, r) 117 } 118 119 // WriteTo writes the uncompressed data to the given writer. 120 func (f *File) WriteTo(w io.Writer) (int64, error) { 121 f.mu.RLock() 122 data := f.data 123 f.mu.RUnlock() 124 125 n, err := w.Write(data) 126 127 return int64(n), err 128 } 129 130 // Chtime sets the modtime to the given time. 131 func (f *File) Chtime(t time.Time) { 132 f.mu.Lock() 133 f.modtime = t 134 f.mu.Unlock() 135 } 136 137 // Name returns the name given during File creation. 138 func (f *File) Name() string { 139 return f.name 140 } 141 142 // A Writer is bound to the File is was created from, buffering data that is 143 // written to it. Upon Closing, that data will be compressed and both the 144 // uncompressed and compressed data will be replaced on the File. 145 type Writer struct { 146 file *File 147 data []byte 148 } 149 150 // Create opens a Writer that can be used to write the data for the File. Close 151 // must be called on the resulting Writer for the data to be accepted. 152 func (f *File) Create() *Writer { 153 return &Writer{ 154 file: f, 155 } 156 } 157 158 // Write is an implementation of the io.Writer interface. 159 func (f *Writer) Write(p []byte) (int, error) { 160 if f.file == nil { 161 return 0, fs.ErrClosed 162 } 163 164 f.data = append(f.data, p...) 165 166 return len(p), nil 167 } 168 169 // WriteString is an implementation of the io.StringWriter interface. 170 func (f *Writer) WriteString(str string) (int, error) { 171 if f.file == nil { 172 return 0, fs.ErrClosed 173 } 174 175 f.data = append(f.data, str...) 176 177 return len(str), nil 178 } 179 180 // Close is an implementation of the io.Close interface. 181 // 182 // This method must be called for the written data to be accepted on the File. 183 func (f *Writer) Close() error { 184 if f.file == nil { 185 return fs.ErrClosed 186 } 187 188 var compressed bytes.Buffer 189 190 g := gzip.NewWriter(&compressed) 191 192 g.Write(f.data) 193 g.Close() 194 195 f.file.mu.Lock() 196 197 f.file.data = f.data 198 f.file.compressed = compressed.Bytes() 199 f.file.modtime = time.Now() 200 201 f.file.mu.Unlock() 202 203 *f = Writer{} 204 205 return nil 206 } 207