rfc/proposal: Add support for a content type map in ServeDir
- Dominant language
- Rust
- Stars
- 5.1k
- Forks
- 329
- PR merge metrics
- No merged PRs in 30d
Description
# Use case:
To ergonomically serve arbitrary files within a directory with specific mime types that may not be recognized by whatever best-effort mime guessing strategy tide uses
# Prior work:
- [:content_types key in elixir's Plug.Static](https://hexdocs.pm/plug/Plug.Static.html#module-options) accepts a simple map of file names to mime types. Advantages: Simple, straightforward, efficient. Disadvantages: No way to express "anything that ends with this extension should have this mime type." Possible improvements: Accept globs in the keys
- Ruby's [Rack::Static](https://www.rubydoc.info/github/rack/rack/Rack/Static) has no special affordances for mime type aside from the ability to set an application-wide MIME::Type extension->mime type override, but instead has a fairly elaborate DSL for defining arbitrary header rules based on extension, regex local path matches, etc. This allows for individual file Content-Type header overrides, among many other things. Disadvantage: Maintaining a complex path/request matching system.
- [setHeaders option in node's express serve-static library](http://expressjs.com/en/resources/middleware/serve-static.html) This is a function with the signature `(res, path, stat) → undefined`. Advantages: Useful for more than just setting content types, is a general purpose map on the response. Disadvantage: Potentially a lot of boilerplate for just "I want this custom file extension to have this mime type."
- [nginx](http://nginx.org/en/docs/http/ngx_http_core_module.html#types) just takes a map from file extension to mime type, but it's easy enough to carve out a specific path glob in nginx config to override this for a file that doesn't have an extension
- [apache mod_mime](https://httpd.apache.org/docs/2.4/mod/mod_mime.html#addtype) also takes a mime type and extension, but does some fancy stuff regarding configuring mime negotiation regarding the client-provided Accepts header. They also provide [typesconfig](https://httpd.apache.org/docs/2.4/mod/mod_mime.html#typesconfig), which lets users provide a global base mapping from extension to mime type. Similar to nginx in this regard
---
# Proposal for tide:
Add something along the lines of:
```rust
app.at("/static")
.serve_dir("./files")? // ← returns the ServeStatic or a builder instead of ()
.with_content_type_mapping("*.custom", "text/custom")
.with_content_type_mapping("special-file", "application/just-this-file")
```
Informing this proposal is an assumption based on other static file middlewares that there will likely need to be many options passed to ServeDir eventually. [Plug.Static](https://hexdocs.pm/plug/Plug.Static.html#module-options) is a great example of the affordances users might reasonably expect from a directory endpoint eventually, including in-memory caching, different compression schemes, and cache control settings, including the configuration mentioned in #447
---
## Update after discussion on discord:
Italics indicate tbd or in need of confirmation.
Proposal is to add `tide::fs::Dir` and `tide::fs::File` to _replace_ ServeDir. `Dir` provides Iterator-like functionality, such that directory listings can be filtered at time of request.
Mapping responses after the Dir Endpoint, including content-type mapping, can be done with a middleware, provided that in addition to setting the body to the File, the Path is put into response `local` data. This allows mime guessing to be performed or any other conditional headers to be set based on the file system provenance of the file.
This would either entail the ability to add middlewares to an Endpoint, or making the ServeDir a nested HttpService.
```rust
app.at("/files").serve_dir("./public");
// which is sugar for
app.at("/files").serve_dir(Dir::from_path("./public"));
```
and if mime guessing isn't default behavior,
```rust
app.at("/only_some_files").serve_dir(Dir::from_path("./public")
.filter(|path: Path| path.extension() == "css")
.guess_mime()
);
//which might be equivalent to something like
app.at("/only_some_files").serve_dir(Dir::from_path("./public")
.filter(|path: Path| path.extension() == "css")
.add_middleware(MimeMiddleware::new())
);
```
for mime overrides, the interface might look something like
```rust
app.at("/only_some_files").serve_dir(Dir::from_path("./public")
.with_mime_type(".custom", "application/my-custom-type")
);
//which might be equivalent to something like
app.at("/only_some_files").serve_dir(Dir::from_path("./public")
.add_middleware(MimeMiddleware::new()
.override(".custom", "application/my-custom-type")
)
);
```
and there also would be a File endpoint, such that
```rust
app.at("/just_this_file").serve_file("./file.txt")
```
which _might_ be identical to
```rust
app.at("/just_this_file").get(tide::fs::File::from_path("./file.txt"))
```
Contributor guide
Assessment
This issue has not been assessed yet.