git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/httpx/webapphandler_old.go (about) 1 package httpx 2 3 import "encoding/base64" 4 5 // import ( 6 // "bytes" 7 // "crypto/sha256" 8 // "encoding/base64" 9 // "errors" 10 // "fmt" 11 // "io" 12 // "io/fs" 13 // "mime" 14 // "net/http" 15 // "path/filepath" 16 // "strconv" 17 // "strings" 18 // "sync" 19 20 // "git.sr.ht/~pingoo/stdx/log/slogx" 21 // ) 22 23 // type webappFileInfo struct { 24 // hash [32]byte 25 // size int64 26 // } 27 28 // // webappFileInfoCache is used to cache the metadata about a file 29 // // these metdata are used to send StatusNotModified response if the request has an If-None-Match HTTP 30 // // header 31 // type webappFileInfoCache struct { 32 // files map[string]webappFileInfo 33 // mutex sync.RWMutex 34 // } 35 36 // func (cache *webappFileInfoCache) Get(path string) (record webappFileInfo, exists bool) { 37 // cache.mutex.RLock() 38 // record, exists = cache.files[path] 39 // cache.mutex.RUnlock() 40 // return 41 // } 42 43 // func (cache *webappFileInfoCache) Set(path string, info webappFileInfo) { 44 // cache.mutex.Lock() 45 // cache.files[path] = info 46 // cache.mutex.Unlock() 47 // } 48 49 // func WebappHandlerOld(folder fs.FS) func(w http.ResponseWriter, r *http.Request) { 50 // cache := webappFileInfoCache{ 51 // files: make(map[string]webappFileInfo, 100), 52 // mutex: sync.RWMutex{}, 53 // } 54 // return func(w http.ResponseWriter, req *http.Request) { 55 // ctx := req.Context() 56 // logger := slogx.FromCtx(ctx) 57 58 // if req.Method != http.MethodGet && req.Method != http.MethodHead { 59 // w.WriteHeader(http.StatusMethodNotAllowed) 60 // w.Write([]byte("Method not allowed.\n")) 61 // return 62 // } 63 64 // ok, err := tryRead(folder, req.URL.Path, &cache, w, req) 65 // if err != nil && !errors.Is(err, ErrDir) && !errors.Is(err, ErrInvalidPath) { 66 // logger.Error("httpx.WebappHandler: reading file", slogx.Err(err)) 67 // w.Header().Set(HeaderCacheControl, CacheControlNoCache) 68 // handleError(http.StatusInternalServerError, ErrInternalError.Error(), w) 69 // return 70 // } 71 // if ok { 72 // return 73 // } 74 75 // _, err = tryRead(folder, "index.html", &cache, w, req) 76 // if err != nil { 77 // logger.Error("httpx.WebappHandler: reading index.html", slogx.Err(err)) 78 // w.Header().Set(HeaderCacheControl, CacheControlNoCache) 79 // handleError(http.StatusInternalServerError, ErrInternalError.Error(), w) 80 // return 81 // } 82 // } 83 // } 84 85 // // alternatively, we could pre-load all the files with their metadata like here: https://github.com/go-chi/chi/issues/611 86 // func tryRead(fs fs.FS, path string, cache *webappFileInfoCache, w http.ResponseWriter, req *http.Request) (ok bool, err error) { 87 // // path = filepath.Clean(path) 88 // if path == "" || strings.Contains(path, "..") { 89 // err = ErrInvalidPath 90 // return 91 // } 92 // // logger := slogx.FromCtx(r.Context()) 93 94 // // TrimLeft is efficient here as we only trim 1 character so only bytes comparison, no UTF-8 95 // path = strings.TrimLeft(path, "/") 96 97 // extension := filepath.Ext(path) 98 // contentType := mime.TypeByExtension(extension) 99 100 // cacheControl := CacheControlDynamic 101 // switch extension { 102 // case ".js", ".css", ".woff", ".woff2": 103 // // some webapp's assets files can be cached for very long time because they are versionned by 104 // // the webapp's bundler 105 // cacheControl = CacheControlImmutable 106 // } 107 108 // w.Header().Set(HeaderContentType, contentType) 109 // w.Header().Set(HeaderCacheControl, cacheControl) 110 111 // // first, we handle caching 112 // requestEtag := decodeEtag(strings.TrimSpace(req.Header.Get(HeaderIfNoneMatch))) 113 114 // cachedFileInfo, isCached := cache.Get(path) 115 // if isCached && bytes.Equal(requestEtag, cachedFileInfo.hash[:]) { 116 // // logger.Debug("httpx.WebappHandler: cache HIT") 117 // w.Header().Set(HeaderETag, encodeEtagOptimized(cachedFileInfo.hash)) 118 // w.Header().Set(HeaderContentLength, strconv.FormatInt(cachedFileInfo.size, 10)) 119 // w.WriteHeader(http.StatusNotModified) 120 // ok = true 121 // return 122 // } 123 124 // file, err := fs.Open(path) 125 // if err != nil { 126 // err = ErrInvalidPath 127 // return 128 // } 129 // defer file.Close() 130 131 // // use fs.Stat instead? 132 // // embed.FS does not implement FS.Stat, so the file need to be Open / closed anyway 133 // fileInfo, err := file.Stat() 134 // if err != nil { 135 // err = ErrInternalError 136 // return 137 // } 138 // if fileInfo.IsDir() { 139 // err = ErrDir 140 // return 141 // } 142 143 // seeker, isSeeker := file.(io.Seeker) 144 // if !isSeeker { 145 // err = ErrInternalError 146 // return 147 // } 148 149 // // we hash the file to get its Etag 150 // hasher := blake3.New() 151 // _, err = io.Copy(hasher, file) 152 // if err != nil { 153 // err = ErrInternalError 154 // return 155 // } 156 // fileHash := hasher.Sum(nil) 157 158 // cachedFileInfo.size = fileInfo.Size() 159 // cachedFileInfo.hash = [32]byte(fileHash) 160 // cache.Set(path, cachedFileInfo) 161 162 // w.Header().Set(HeaderETag, encodeEtagOptimized(cachedFileInfo.hash)) 163 // w.Header().Set(HeaderContentLength, strconv.FormatInt(cachedFileInfo.size, 10)) 164 165 // if bytes.Equal(requestEtag, cachedFileInfo.hash[:]) { 166 // // logger.Debug("httpx.WebappHandler: etag HIT") 167 // w.WriteHeader(http.StatusNotModified) 168 // ok = true 169 // return 170 // } 171 172 // // logger.Debug("httpx.WebappHandler: MISS") 173 174 // w.WriteHeader(http.StatusOK) 175 // // finally, we can send the file 176 // seeker.Seek(0, io.SeekStart) 177 // _, err = io.Copy(w, file) 178 // if err != nil { 179 // err = fmt.Errorf("httpx.tryRead: copying content to HTTP response: %w", err) 180 // return 181 // } 182 183 // ok = true 184 185 // return 186 // } 187 188 // func decodeEtag(requestEtag string) (etagBytes []byte) { 189 // // sometimes, a CDN may add the weak Etag prefix: W/ 190 // requestEtag = strings.TrimPrefix(requestEtag, "W/") 191 // requestEtag = strings.TrimPrefix(requestEtag, `"`) 192 // requestEtag = strings.TrimSuffix(requestEtag, `"`) 193 // etagBytes, err := base64.RawURLEncoding.DecodeString(requestEtag) 194 // if err != nil { 195 // etagBytes = nil 196 // return 197 // } 198 // return 199 // } 200 201 // // func toEtagPlus(hash [32]byte) string { 202 // // etag := base64.RawURLEncoding.EncodeToString(hash[:]) 203 // // return `"` + etag + `"` 204 // // } 205 206 func encodeEtagOptimized(hash [32]byte) string { 207 base64Len := base64.RawURLEncoding.EncodedLen(32) 208 buf := make([]byte, base64Len+2) 209 buf[0] = '"' 210 base64.RawURLEncoding.Encode(buf[1:], hash[:]) 211 buf[base64Len+1] = '"' 212 return string(buf) 213 }