forked from bots-garden/ori
| ✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme | 1 | package files |
| 2 | ||
| 3 | import ( | |
| 4 | "mime" | |
| 5 | "net/http" | |
| 6 | "os" | |
| 7 | "path/filepath" | |
| 8 | "strings" | |
| 9 | ) | |
| 10 | ||
| 11 | // imageTypes pins the Content-Type of the image formats the preview pane | |
| 12 | // renders, independently of the host's mime database (which may lack | |
| 13 | // entries such as .webp or .avif, or spell .ico differently). | |
| 14 | var imageTypes = map[string]string{ | |
| 15 | ".avif": "image/avif", | |
| 16 | ".bmp": "image/bmp", | |
| 17 | ".gif": "image/gif", | |
| 18 | ".ico": "image/x-icon", | |
| 19 | ".jpeg": "image/jpeg", | |
| 20 | ".jpg": "image/jpeg", | |
| 21 | ".png": "image/png", | |
| 22 | ".svg": "image/svg+xml", | |
| 23 | ".webp": "image/webp", | |
| 24 | } | |
| 25 | ||
| 26 | // ContentTypeFor returns the Content-Type the raw endpoint serves a file | |
| 27 | // under: pinned image types first, then the host mime database, and a | |
| 28 | // content sniff of the first bytes as a last resort. | |
| 29 | // | |
| 30 | // Example: | |
| 31 | // | |
| 32 | // files.ContentTypeFor("logo.webp", nil) // "image/webp" | |
| 33 | func ContentTypeFor(path string, head []byte) string { | |
| 34 | ext := strings.ToLower(filepath.Ext(path)) | |
| 35 | if typ, ok := imageTypes[ext]; ok { | |
| 36 | return typ | |
| 37 | } | |
| 38 | if typ := mime.TypeByExtension(ext); typ != "" { | |
| 39 | return typ | |
| 40 | } | |
| 41 | return http.DetectContentType(head) | |
| 42 | } | |
| 43 | ||
| 44 | // rawFile streams a file's bytes as-is, for the browser to render (images) | |
| 45 | // or download. Unlike readFile it has no size cap and no text requirement: | |
| 46 | // http.ServeContent streams and honours Range requests. | |
| 47 | func (s *Service) rawFile(w http.ResponseWriter, r *http.Request) { | |
| 48 | if r.URL.Query().Get("path") == "" { | |
| 49 | writeError(w, http.StatusBadRequest, "path query parameter is required") | |
| 50 | return | |
| 51 | } | |
| 52 | path := s.resolve(r.URL.Query().Get("path")) | |
| 53 | info, err := os.Stat(path) | |
| 54 | if err != nil { | |
| 55 | writeFSError(w, err) | |
| 56 | return | |
| 57 | } | |
| 58 | if info.IsDir() { | |
| 59 | writeError(w, http.StatusBadRequest, "path is a directory") | |
| 60 | return | |
| 61 | } | |
| 62 | file, err := os.Open(path) | |
| 63 | if err != nil { | |
| 64 | writeFSError(w, err) | |
| 65 | return | |
| 66 | } | |
| 67 | // Read-only handle: a Close error carries no information the client | |
| 68 | // could act on, and the response headers are already committed. | |
| 69 | defer func() { _ = file.Close() }() | |
| 70 | ||
| 71 | head := make([]byte, 512) | |
| 72 | n, _ := file.Read(head) | |
| 73 | if _, err := file.Seek(0, 0); err != nil { | |
| 74 | writeFSError(w, err) | |
| 75 | return | |
| 76 | } | |
| 77 | w.Header().Set("Content-Type", ContentTypeFor(path, head[:n])) | |
| 78 | w.Header().Set("X-Content-Type-Options", "nosniff") | |
| 79 | http.ServeContent(w, r, info.Name(), info.ModTime(), file) | |
| 80 | } |