github.com/aarzilli/tools@v0.0.0-20151123112009-0d27094f75e0/net/http/fileserver/1_fileserver.go (about) 1 // Package fileserver replaces http.Fileserver 2 package fileserver 3 4 import ( 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime" 11 "net/http" 12 "net/url" 13 "path" 14 "strings" 15 "time" 16 17 "github.com/golang/snappy" 18 "github.com/pbberlin/tools/net/http/htmlfrag" 19 "github.com/pbberlin/tools/net/http/loghttp" 20 "github.com/pbberlin/tools/net/http/tplx" 21 "github.com/pbberlin/tools/os/fsi" 22 "github.com/pbberlin/tools/os/fsi/common" 23 "github.com/pbberlin/tools/stringspb" 24 ) 25 26 var wpf = func(w io.Writer, format string, a ...interface{}) (int, error) { 27 fmt.Fprintf(w, format, a...) 28 fmt.Fprintf(w, "\n") 29 return 0, nil 30 } 31 32 var spf = fmt.Sprintf 33 34 type Options struct { 35 FS fsi.FileSystem 36 Prefix string 37 Replacements map[string][]byte 38 Cutout bool 39 } 40 41 // We cannot use http.FileServer(http.Dir("./css/") 42 // to dispatch our dsfs files. 43 // We need the appengine context to initialize dsfs. 44 // Thus we have to re-implement a serveFile method: 45 func FsiFileServer(w http.ResponseWriter, r *http.Request, opt Options) { 46 47 r.Header.Set("X-Custom-Header-Counter", "nocounter") 48 49 lg, b1 := loghttp.BuffLoggerUniversal(w, r) 50 51 fclose := func() { 52 // Only upon error. 53 // If everything is fine, we reset fclose at the end. 54 w.Write(b1.Bytes()) 55 } 56 defer fclose() 57 58 wpf(b1, tplx.ExecTplHelper(tplx.Head, map[string]interface{}{"HtmlTitle": "Half-Static-File-Server"})) 59 wpf(b1, "<pre>") 60 61 err := r.ParseForm() 62 if err != nil { 63 wpf(b1, "err parsing request (ParseForm)%v", err) 64 } 65 66 p := r.URL.Path 67 68 if strings.HasPrefix(p, opt.Prefix) { 69 // p = p[len(prefix):] 70 p = strings.TrimPrefix(p, opt.Prefix) 71 } else { 72 wpf(b1, "route must start with prefix %v - but is %v", opt.Prefix, p) 73 } 74 75 if strings.HasPrefix(p, "/") { 76 p = p[1:] 77 } 78 wpf(b1, "effective path = %q", p) 79 80 // fullP := path.Join(docRootDataStore, p) 81 fullP := p 82 83 f, err := opt.FS.Open(fullP) 84 if err != nil { 85 wpf(b1, "err opening file %v - %v", fullP, err) 86 return 87 } 88 defer f.Close() 89 90 inf, err := f.Stat() 91 if err != nil { 92 wpf(b1, "err opening fileinfo %v - %v", fullP, err) 93 return 94 } 95 96 if inf.IsDir() { 97 98 wpf(b1, "%v is a directory - trying index.html...", fullP) 99 100 fullP += "/index.html" 101 102 fIndex, err := opt.FS.Open(fullP) 103 if err == nil { 104 defer fIndex.Close() 105 inf, err = fIndex.Stat() 106 if err != nil { 107 wpf(b1, "err opening index fileinfo %v - %v", fullP, err) 108 return 109 } 110 111 f = fIndex 112 } else { 113 114 wpf(b1, "err opening index file %v - %v", fullP, err) 115 116 if r.FormValue("fmt") == "html" { 117 dirListHtml(w, r, f) 118 } else { 119 dirListJson(w, r, f) 120 } 121 122 b1 = new(bytes.Buffer) // success => reset the message log => dumps an empty buffer 123 return 124 } 125 126 } 127 128 wpf(b1, "opened file %v - %v - %v", f.Name(), inf.Size(), err) 129 130 bts1, err := ioutil.ReadAll(f) 131 if err != nil { 132 wpf(b1, "err with ReadAll %v - %v", fullP, err) 133 return 134 } 135 136 ext := path.Ext(fullP) 137 ext = strings.ToLower(ext) 138 if ext == ".snappy" { 139 btsDec, err := snappy.Decode(nil, bts1) 140 if err != nil { 141 wpf(b1, "err decoding snappy: "+err.Error()) 142 } else { 143 lg("decoded from %vkB to %vkB", len(bts1)/1024, len(btsDec)/1024) 144 bts1 = btsDec 145 } 146 fullP = strings.TrimSuffix(fullP, path.Ext(fullP)) 147 ext = path.Ext(fullP) 148 ext = strings.ToLower(ext) 149 lg("new extension is %v", ext) 150 } 151 152 tp := mime.TypeByExtension(ext) 153 154 w.Header().Set("Content-Type", tp) 155 156 // 157 // caching 158 // either explicitly discourage 159 // or explicitly encourage 160 if false || 161 ext == ".css" || ext == ".js" || 162 ext == "css" || ext == "js" || 163 ext == ".jpg" || ext == ".gif" || 164 ext == "jpg" || ext == "gif" || 165 false { 166 167 if strings.Contains(fullP, "tamper-monkey") { 168 htmlfrag.SetNocacheHeaders(w) 169 } else { 170 htmlfrag.CacheHeaders(w) 171 } 172 } else { 173 htmlfrag.SetNocacheHeaders(w) 174 } 175 176 for k, v := range opt.Replacements { 177 bts1 = bytes.Replace(bts1, []byte(k), v, -1) 178 } 179 if opt.Cutout { 180 sep := []byte("<span id='CUTOUT'></span>") 181 spl := bytes.Split(bts1, sep) 182 if len(spl) > 1 { 183 bts2 := []byte{} 184 for i, part := range spl { 185 if i%2 == 0 { 186 bts2 = append(bts2, part...) 187 } 188 } 189 bts1 = bts2 190 } 191 } 192 193 w.Write(bts1) 194 195 b1 = new(bytes.Buffer) // success => reset the message log => dumps an empty buffer 196 197 } 198 199 // inspired by https://golang.org/src/net/http/fs.go 200 // 201 // name may contain '?' or '#', which must be escaped to remain 202 // part of the URL path, and not indicate the start of a query 203 // string or fragment. 204 var htmlReplacer = strings.NewReplacer( 205 "&", "&", 206 "<", "<", 207 ">", ">", 208 209 `"`, """, 210 211 "'", "'", 212 ) 213 214 func dirListJson(w http.ResponseWriter, r *http.Request, f fsi.File) { 215 216 r.Header.Set("Content-Type", "application/json") 217 218 mp := []map[string]string{} 219 220 for { 221 dirs, err := f.Readdir(100) 222 if err != nil || len(dirs) == 0 { 223 break 224 } 225 for _, d := range dirs { 226 name := d.Name() 227 if d.IsDir() { 228 name = common.Directorify(name) 229 } 230 name = htmlReplacer.Replace(name) 231 232 url := url.URL{Path: name} 233 234 mpl := map[string]string{ 235 "path": url.String(), 236 "mod": d.ModTime().Format(time.RFC1123Z), 237 } 238 239 mp = append(mp, mpl) 240 } 241 } 242 243 bdirListHtml, err := json.MarshalIndent(mp, "", "\t") 244 if err != nil { 245 wpf(w, "marshalling to []byte failed - mp was %v", mp) 246 return 247 } 248 w.Write(bdirListHtml) 249 250 } 251 252 func dirListHtml(w http.ResponseWriter, r *http.Request, f fsi.File) { 253 254 w.Header().Set("Content-Type", "text/html; charset=utf-8") 255 256 for { 257 dirs, err := f.Readdir(100) 258 if err != nil || len(dirs) == 0 { 259 break 260 } 261 for _, d := range dirs { 262 name := d.Name() 263 264 suffix := "" 265 if d.IsDir() { 266 suffix = "/" 267 } 268 269 linktitle := htmlReplacer.Replace(name) 270 linktitle = stringspb.Ellipsoider(linktitle, 40) 271 if d.IsDir() { 272 linktitle = common.Directorify(linktitle) 273 } 274 275 surl := path.Join(r.URL.Path, name) + suffix + "?fmt=html" 276 277 oneLine := spf("<a style='display:inline-block;min-width:600px;' href=\"%s\">%s</a>", surl, linktitle) 278 // wpf(w, " %v", d.ModTime().Format("2006-01-02 15:04:05 MST")) 279 oneLine += spf(" %v<br>", d.ModTime().Format(time.RFC1123Z)) 280 wpf(w, oneLine) 281 } 282 } 283 284 }