diff --git a/mongoose.c b/mongoose.c
index 94fe4ffe0855f1f8cad89c551e2a382d9ed65cdd..ae528bea10bfbbc287542c4c7aa8f917ffe6f4f3 100644
--- a/mongoose.c
+++ b/mongoose.c
@@ -407,7 +407,7 @@ void mg_error(struct mg_connection *c, const char *fmt, ...) {
 }
 
 #ifdef MG_ENABLE_LINES
-#line 1 "src/fs.c"
+#line 1 "src/fs_packed.c"
 #endif
 
 
@@ -423,58 +423,202 @@ const char *mg_unpack(const char *path, size_t *size) {
   return NULL;
 }
 
-#if defined(MG_FOPENCOOKIE)
-ssize_t packed_read(void *cookie, char *buf, size_t size) {
-  struct packed_file *fp = (struct packed_file *) cookie;
-  if (size > fp->size - fp->pos) size = fp->size - fp->pos;
-  memcpy(buf, &fp->data[fp->pos], size);
-  fp->pos += size;
-  return (ssize_t) size;
+static char *packed_realpath(const char *path, char *resolved_path) {
+  if (resolved_path == NULL) resolved_path = malloc(strlen(path) + 1);
+  strcpy(resolved_path, path);
+  return resolved_path;
 }
 
-ssize_t packed_write(void *cookie, const char *buf, size_t size) {
-  (void) cookie, (void) buf, (void) size;
-  return -1;
+static int packed_stat(const char *path, size_t *size, unsigned *mtime) {
+  const char *data = mg_unpack(path, size);
+  if (mtime) *mtime = 0;
+  return data == NULL ? 0 : MG_FS_READ;
 }
 
-int packed_seek(void *cookie, long *offset, int whence) {
-  struct packed_file *fp = (struct packed_file *) cookie;
-  if (whence == SEEK_SET) fp->pos = (size_t) *offset;
-  if (whence == SEEK_END) fp->pos = (size_t)((long) fp->size + *offset);
-  if (whence == SEEK_CUR) fp->pos = (size_t)((long) fp->pos + *offset);
-  if (fp->pos > fp->size) fp->pos = fp->size;
-  *offset = (long) fp->pos;
-  return 0;
-}
-
-int packed_close(void *cookie) {
-  free(cookie);
-  return 0;
+static void packed_list(const char *path, void (*fn)(const char *, void *),
+                        void *userdata) {
+  (void) path, (void) fn, (void) userdata;
 }
 
-FILE *mg_fopen_packed(const char *path, const char *mode) {
-  cookie_io_functions_t funcs = {
-      .read = packed_read,
-      .write = packed_write,
-      .seek = packed_seek,
-      .close = packed_close,
-  };
-  struct packed_file *cookie = NULL;
+static struct mg_fd *packed_open(const char *path, int flags) {
   size_t size = 0;
   const char *data = mg_unpack(path, &size);
+  struct packed_file *fp = NULL;
+  struct mg_fd *fd = NULL;
   if (data == NULL) return NULL;
-  if ((cookie = calloc(1, sizeof(*cookie))) == NULL) return NULL;
-  cookie->data = data;
-  cookie->size = size;
-  return fopencookie(cookie, mode, funcs);
+  if (flags & MG_FS_WRITE) return NULL;
+  fp = calloc(1, sizeof(*fp));
+  fd = calloc(1, sizeof(*fd));
+  fp->size = size;
+  fp->data = data;
+  fd->fd = fp;
+  fd->fs = &mg_fs_packed;
+  return fd;
+}
+
+static void packed_close(struct mg_fd *fd) {
+  if (fd) free(fd->fd), free(fd);
+}
+
+static size_t packed_read(void *fd, void *buf, size_t len) {
+  struct packed_file *fp = (struct packed_file *) fd;
+  if (fp->pos + len > fp->size) len = fp->size - fp->pos;
+  memcpy(buf, &fp->data[fp->pos], len);
+  fp->pos += len;
+  return len;
 }
+
+static size_t packed_write(void *fd, const void *buf, size_t len) {
+  (void) fd, (void) buf, (void) len;
+  return 0;
+}
+
+static size_t packed_seek(void *fd, size_t offset) {
+  struct packed_file *fp = (struct packed_file *) fd;
+  fp->pos = offset;
+  if (fp->pos > fp->size) fp->pos = fp->size;
+  return fp->pos;
+}
+
+struct mg_fs mg_fs_packed = {packed_realpath, packed_stat,  packed_list,
+                             packed_open,     packed_close, packed_read,
+                             packed_write,    packed_seek};
+
+#ifdef MG_ENABLE_LINES
+#line 1 "src/fs_posix.c"
+#endif
+
+
+#if defined(O_READ)
+static char *posix_realpath(const char *path, char *resolved_path) {
+#ifdef _WIN32
+  return _fullpath(path, resolved_path, PATH_MAX);
+#else
+  return realpath(path, resolved_path);
+#endif
+}
+
+static int posix_stat(const char *path, size_t *size, unsigned *mtime) {
+#ifdef _WIN32
+  struct _stati64 st;
+  wchar_t tmp[PATH_MAX];
+  MultiByteToWideChar(CP_UTF8, 0, path, -1, tmp, sizeof(tmp) / sizeof(tmp[0]));
+  if (_wstati64(tmp, &st) != 0) return 0;
 #else
-FILE *mg_fopen_packed(const char *path, const char *mode) {
-  (void) path, (void) mode;
+  struct stat st;
+  if (stat(path, &st) != 0) return 0;
+#endif
+  if (size) *size = (size_t) st.st_size;
+  if (mtime) *mtime = (unsigned) st.st_mtime;
+  return MG_FS_READ | MG_FS_WRITE | (S_ISDIR(st.st_mode) ? MG_FS_DIR : 0);
+}
+
+static void posix_list(const char *dir, void (*fn)(const char *, void *),
+                       void *userdata) {
+  // char path[MG_PATH_MAX], *p = &dir[strlen(dir) - 1], tmp[10];
+  struct dirent *dp;
+  DIR *dirp;
+
+  // while (p > dir && *p != '/') *p-- = '\0';
+  if ((dirp = (opendir(dir))) != NULL) {
+    size_t off, n;
+    while ((dp = readdir(dirp)) != NULL) {
+      // Do not show current dir and hidden files
+      if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue;
+      fn(dp->d_name, &st);
+    }
+    closedir(dirp);
+  }
+}
+
+static struct mg_fd *posix_open(const char *path, int flags) {
+  const char *mode =
+      flags & (MG_FS_READ | MG_FS_WRITE)
+          ? "r+b"
+          : flags & MG_FS_READ ? "rb" : flags & MG_FS_WRITE ? "wb" : "";
+  void *fp = NULL;
+  struct mg_fd *fd = NULL;
+#ifdef _WIN32
+  wchar_t b1[PATH_MAX], b2[10];
+  MultiByteToWideChar(CP_UTF8, 0, path, -1, b1, sizeof(b1) / sizeof(b1[0]));
+  MultiByteToWideChar(CP_UTF8, 0, mode, -1, b2, sizeof(b2) / sizeof(b2[0]));
+  fp = (void *) _wfopen(b1, b2);
+#else
+  fp = (void *) fopen(path, mode);
+#endif
+  if (fp == NULL) return NULL;
+  fd = calloc(1, sizeof(*fd));
+  fd->fd = fp;
+  fd->fs = &mg_fs_posix;
+  return fd;
+}
+
+static void posix_close(struct mg_fd *fd) {
+  if (fd) fclose((FILE *) fd->fd), free(fd);
+}
+
+static size_t posix_read(void *fp, void *buf, size_t len) {
+  return fread(buf, 1, len, (FILE *) fp);
+}
+
+static size_t posix_write(void *fp, const void *buf, size_t len) {
+  return fwrite(buf, 1, len, (FILE *) fp);
+}
+
+static size_t posix_seek(void *fp, size_t offset) {
+#if _FILE_OFFSET_BITS == 64 || _POSIX_C_SOURCE >= 200112L || \
+    _XOPEN_SOURCE >= 600
+  fseeko((FILE *) fp, (off_t) offset, SEEK_SET);
+#else
+  fseek((FILE *) fp, (long) offset, SEEK_SET);
+#endif
+  return (size_t) ftell((FILE *) fp);
+}
+#else
+static char *posix_realpath(const char *path, char *resolved_path) {
+  (void) path, (void) resolved_path;
+  return NULL;
+}
+
+static int posix_stat(const char *path, size_t *size, unsigned *mtime) {
+  (void) path, (void) size, (void) mtime;
+  return 0;
+}
+
+static void posix_list(const char *path, void (*fn)(const char *, void *),
+                       void *userdata) {
+  (void) path, (void) fn, (void) userdata;
+}
+
+static struct mg_fd *posix_open(const char *path, int flags) {
+  (void) path, (void) flags;
   return NULL;
 }
+
+static void posix_close(struct mg_fd *fd) {
+  (void) fd;
+}
+
+static size_t posix_read(void *fd, void *buf, size_t len) {
+  (void) fd, (void) buf, (void) len;
+  return 0;
+}
+
+static size_t posix_write(void *fd, const void *buf, size_t len) {
+  (void) fd, (void) buf, (void) len;
+  return 0;
+}
+
+static size_t posix_seek(void *fd, size_t offset) {
+  (void) fd, (void) offset;
+  return (size_t) ~0;
+}
 #endif
 
+struct mg_fs mg_fs_posix = {posix_realpath, posix_stat,  posix_list,
+                            posix_open,     posix_close, posix_read,
+                            posix_write,    posix_seek};
+
 #ifdef MG_ENABLE_LINES
 #line 1 "src/http.c"
 #endif
@@ -900,73 +1044,83 @@ static void static_cb(struct mg_connection *c, int ev, void *ev_data,
   (void) ev_data;
 }
 
-static const char *guess_content_type(const char *filename) {
-  size_t n = strlen(filename);
-#define MIME_ENTRY(_ext, _type) \
-  { _ext, sizeof(_ext) - 1, _type }
-  const struct {
-    const char *ext;
-    size_t ext_len;
-    const char *type;
-  } * t, types[] = {
-             MIME_ENTRY("html", "text/html; charset=utf-8"),
-             MIME_ENTRY("htm", "text/html; charset=utf-8"),
-             MIME_ENTRY("css", "text/css; charset=utf-8"),
-             MIME_ENTRY("js", "text/javascript; charset=utf-8"),
-             MIME_ENTRY("gif", "image/gif"),
-             MIME_ENTRY("png", "image/png"),
-             MIME_ENTRY("woff", "font/woff"),
-             MIME_ENTRY("ttf", "font/ttf"),
-             MIME_ENTRY("aac", "audio/aac"),
-             MIME_ENTRY("avi", "video/x-msvideo"),
-             MIME_ENTRY("azw", "application/vnd.amazon.ebook"),
-             MIME_ENTRY("bin", "application/octet-stream"),
-             MIME_ENTRY("bmp", "image/bmp"),
-             MIME_ENTRY("bz", "application/x-bzip"),
-             MIME_ENTRY("bz2", "application/x-bzip2"),
-             MIME_ENTRY("csv", "text/csv"),
-             MIME_ENTRY("doc", "application/msword"),
-             MIME_ENTRY("epub", "application/epub+zip"),
-             MIME_ENTRY("exe", "application/octet-stream"),
-             MIME_ENTRY("gz", "application/gzip"),
-             MIME_ENTRY("ico", "image/x-icon"),
-             MIME_ENTRY("json", "application/json"),
-             MIME_ENTRY("mid", "audio/mid"),
-             MIME_ENTRY("mjs", "text/javascript"),
-             MIME_ENTRY("mov", "video/quicktime"),
-             MIME_ENTRY("mp3", "audio/mpeg"),
-             MIME_ENTRY("mp4", "video/mp4"),
-             MIME_ENTRY("mpeg", "video/mpeg"),
-             MIME_ENTRY("mpg", "video/mpeg"),
-             MIME_ENTRY("ogg", "application/ogg"),
-             MIME_ENTRY("pdf", "application/pdf"),
-             MIME_ENTRY("rar", "application/rar"),
-             MIME_ENTRY("rtf", "application/rtf"),
-             MIME_ENTRY("shtml", "text/html; charset=utf-8"),
-             MIME_ENTRY("svg", "image/svg+xml"),
-             MIME_ENTRY("tar", "application/tar"),
-             MIME_ENTRY("tgz", "application/tar-gz"),
-             MIME_ENTRY("txt", "text/plain; charset=utf-8"),
-             MIME_ENTRY("wasm", "application/wasm"),
-             MIME_ENTRY("wav", "audio/wav"),
-             MIME_ENTRY("weba", "audio/webm"),
-             MIME_ENTRY("webm", "video/webm"),
-             MIME_ENTRY("webp", "image/webp"),
-             MIME_ENTRY("xls", "application/excel"),
-             MIME_ENTRY("xml", "application/xml"),
-             MIME_ENTRY("xsl", "application/xml"),
-             MIME_ENTRY("zip", "application/zip"),
-             MIME_ENTRY("3gp", "video/3gpp"),
-             MIME_ENTRY("7z", "application/x-7z-compressed"),
-             {NULL, 0, NULL},
-         };
-
-  for (t = types; t->ext != NULL; t++) {
-    if (n < t->ext_len + 2) continue;
-    if (mg_ncasecmp(t->ext, &filename[n - t->ext_len], t->ext_len)) continue;
-    return t->type;
-  }
-  return "text/plain; charset=utf-8";
+static struct mg_str guess_content_type(struct mg_str path, const char *extra) {
+  // clang-format off
+  struct mimeentry { struct mg_str extension, value; };
+  #define MIME_ENTRY(a, b) {{a, sizeof(a) - 1 }, { b, sizeof(b) - 1 }}
+  // clang-format on
+  const struct mimeentry tab[] = {
+      MIME_ENTRY("html", "text/html; charset=utf-8"),
+      MIME_ENTRY("htm", "text/html; charset=utf-8"),
+      MIME_ENTRY("css", "text/css; charset=utf-8"),
+      MIME_ENTRY("js", "text/javascript; charset=utf-8"),
+      MIME_ENTRY("gif", "image/gif"),
+      MIME_ENTRY("png", "image/png"),
+      MIME_ENTRY("woff", "font/woff"),
+      MIME_ENTRY("ttf", "font/ttf"),
+      MIME_ENTRY("aac", "audio/aac"),
+      MIME_ENTRY("avi", "video/x-msvideo"),
+      MIME_ENTRY("azw", "application/vnd.amazon.ebook"),
+      MIME_ENTRY("bin", "application/octet-stream"),
+      MIME_ENTRY("bmp", "image/bmp"),
+      MIME_ENTRY("bz", "application/x-bzip"),
+      MIME_ENTRY("bz2", "application/x-bzip2"),
+      MIME_ENTRY("csv", "text/csv"),
+      MIME_ENTRY("doc", "application/msword"),
+      MIME_ENTRY("epub", "application/epub+zip"),
+      MIME_ENTRY("exe", "application/octet-stream"),
+      MIME_ENTRY("gz", "application/gzip"),
+      MIME_ENTRY("ico", "image/x-icon"),
+      MIME_ENTRY("json", "application/json"),
+      MIME_ENTRY("mid", "audio/mid"),
+      MIME_ENTRY("mjs", "text/javascript"),
+      MIME_ENTRY("mov", "video/quicktime"),
+      MIME_ENTRY("mp3", "audio/mpeg"),
+      MIME_ENTRY("mp4", "video/mp4"),
+      MIME_ENTRY("mpeg", "video/mpeg"),
+      MIME_ENTRY("mpg", "video/mpeg"),
+      MIME_ENTRY("ogg", "application/ogg"),
+      MIME_ENTRY("pdf", "application/pdf"),
+      MIME_ENTRY("rar", "application/rar"),
+      MIME_ENTRY("rtf", "application/rtf"),
+      MIME_ENTRY("shtml", "text/html; charset=utf-8"),
+      MIME_ENTRY("svg", "image/svg+xml"),
+      MIME_ENTRY("tar", "application/tar"),
+      MIME_ENTRY("tgz", "application/tar-gz"),
+      MIME_ENTRY("txt", "text/plain; charset=utf-8"),
+      MIME_ENTRY("wasm", "application/wasm"),
+      MIME_ENTRY("wav", "audio/wav"),
+      MIME_ENTRY("weba", "audio/webm"),
+      MIME_ENTRY("webm", "video/webm"),
+      MIME_ENTRY("webp", "image/webp"),
+      MIME_ENTRY("xls", "application/excel"),
+      MIME_ENTRY("xml", "application/xml"),
+      MIME_ENTRY("xsl", "application/xml"),
+      MIME_ENTRY("zip", "application/zip"),
+      MIME_ENTRY("3gp", "video/3gpp"),
+      MIME_ENTRY("7z", "application/x-7z-compressed"),
+      MIME_ENTRY("7z", "application/x-7z-compressed"),
+      {{0, 0}, {0, 0}},
+  };
+  size_t i = 0;
+  struct mg_str k, v, s = mg_str(extra);
+
+  // Shrink path to its extension only
+  while (i < path.len && path.ptr[path.len - i - 1] != '.') i++;
+  path.ptr += path.len - i;
+  path.len = i;
+
+  // Process user-provided mime type overrides, if any
+  while (mg_next_comma_entry(&s, &k, &v)) {
+    if (mg_strcmp(path, k) == 0) return v;
+  }
+
+  // Process built-in mime types
+  for (i = 0; tab[i].extension.ptr != NULL; i++) {
+    if (mg_strcmp(path, tab[i].extension) == 0) return tab[i].value;
+  }
+
+  return mg_str("text/plain; charset=utf-8");
 }
 
 static int getrange(struct mg_str *s, int64_t *a, int64_t *b) {
@@ -990,7 +1144,7 @@ static int getrange(struct mg_str *s, int64_t *a, int64_t *b) {
 }
 
 void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm,
-                        const char *path, const char *mime, const char *hdrs) {
+                        const char *path, struct mg_http_serve_opts *opts) {
   struct mg_str *inm = mg_http_get_header(hm, "If-None-Match");
   struct stat st;
   char etag[64];
@@ -1008,6 +1162,7 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm,
     int n, status = 200;
     char range[100] = "";
     int64_t r1 = 0, r2 = 0, cl = st.st_size;
+    struct mg_str mime = guess_content_type(mg_str(path), opts->mime_types);
 
     // Handle Range header
     struct mg_str *rh = mg_http_get_header(hm, "Range");
@@ -1038,10 +1193,10 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm,
     }
 
     mg_printf(c,
-              "HTTP/1.1 %d %s\r\nContent-Type: %s\r\n"
+              "HTTP/1.1 %d %s\r\nContent-Type: %.*s\r\n"
               "Etag: %s\r\nContent-Length: " MG_INT64_FMT "\r\n%s%s\r\n",
-              status, mg_http_status_code_str(status), mime, etag, cl, range,
-              hdrs ? hdrs : "");
+              status, mg_http_status_code_str(status), (int) mime.len, mime.ptr,
+              etag, cl, range, opts->extra_headers ? opts->extra_headers : "");
     if (mg_vcasecmp(&hm->method, "HEAD") == 0) {
       fclose(fp);
     } else {
@@ -1345,40 +1500,39 @@ static bool uri_to_local_path(struct mg_connection *c,
 
 void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm,
                        struct mg_http_serve_opts *opts) {
-  char root_dir[MG_PATH_MAX], full_path[sizeof(root_dir)];
+  char root_dir[MG_PATH_MAX], path[sizeof(root_dir)];
   bool is_index = false, exists;
   struct stat st;
-  root_dir[0] = full_path[0] = '\0';
+  root_dir[0] = path[0] = '\0';
 
-  if (!uri_to_local_path(c, hm, opts, root_dir, sizeof(root_dir), full_path,
-                         sizeof(full_path), &is_index))
+  if (!uri_to_local_path(c, hm, opts, root_dir, sizeof(root_dir), path,
+                         sizeof(path), &is_index))
     return;
 
-  exists = stat(full_path, &st) == 0;
+  exists = stat(path, &st) == 0;
 #if MG_ENABLE_SSI
   if (is_index && !exists) {
-    char *p = full_path + strlen(full_path);
-    while (p > full_path && p[-1] != '/') p--;
-    strncpy(p, "index.shtml", (size_t)(&full_path[sizeof(full_path)] - p - 2));
-    full_path[sizeof(full_path) - 1] = '\0';
-    exists = stat(full_path, &st) == 0;
+    char *p = path + strlen(path);
+    while (p > path && p[-1] != '/') p--;
+    strncpy(p, "index.shtml", (size_t)(&path[sizeof(path)] - p - 2));
+    path[sizeof(path) - 1] = '\0';
+    exists = stat(path, &st) == 0;
   }
 #endif
   if (is_index && !exists) {
 #if MG_ENABLE_DIRECTORY_LISTING
-    listdir(c, hm, opts, full_path);
+    listdir(c, hm, opts, path);
 #else
     mg_http_reply(c, 403, "", "%s", "Directory listing not supported");
 #endif
 #if MG_ENABLE_SSI
   } else if (opts->ssi_pattern != NULL &&
-             mg_globmatch(opts->ssi_pattern, strlen(opts->ssi_pattern),
-                          full_path, strlen(full_path))) {
-    mg_http_serve_ssi(c, root_dir, full_path);
+             mg_globmatch(opts->ssi_pattern, strlen(opts->ssi_pattern), path,
+                          strlen(path))) {
+    mg_http_serve_ssi(c, root_dir, path);
 #endif
   } else {
-    mg_http_serve_file(c, hm, full_path, guess_content_type(full_path),
-                       opts->extra_headers);
+    mg_http_serve_file(c, hm, path, opts);
   }
 }
 
diff --git a/mongoose.h b/mongoose.h
index 4a09f906b2d3f935a9da49e642d793fe43fa79b5..24e35ed7363198188e2e9719f0694e3eb047c15c 100644
--- a/mongoose.h
+++ b/mongoose.h
@@ -606,14 +606,28 @@ void mg_usleep(unsigned long usecs);
 
 
 
-FILE *mg_fopen_packed(const char *path, const char *mode);
+enum { MG_FS_READ = 1, MG_FS_WRITE = 2, MG_FS_DIR = 4 };
+
+// Filesystem API functions
+struct mg_fs {
+  char *(*realpath)(const char *path, char *resolved_path);
+  int (*stat)(const char *path, size_t *size, unsigned *mtime);
+  void (*list)(const char *path, void (*fn)(const char *, void *), void *);
+  struct mg_fd *(*open)(const char *path, int flags);
+  void (*close)(struct mg_fd *fd);
+  size_t (*read)(void *fd, void *buf, size_t len);
+  size_t (*write)(void *fd, const void *buf, size_t len);
+  size_t (*seek)(void *fd, size_t offset);
+};
 
-#if (defined(__linux__) && defined(__GNUC__)) || defined(__NEWLIB__)
-#define MG_ENABLE_PACKED_FS 1
-#define MG_FOPENCOOKIE
-#else
-#define MG_ENABLE_PACKED_FS 0
-#endif
+// File descriptor
+struct mg_fd {
+  void *fd;
+  struct mg_fs *fs;
+};
+
+extern struct mg_fs mg_fs_posix;   // POSIX open/close/read/write/seek
+extern struct mg_fs mg_fs_packed;  // Packed FS, see examples/complete
 
 
 
@@ -781,6 +795,7 @@ char *mg_ntoa(const struct mg_addr *addr, char *buf, size_t len);
 
 
 
+
 struct mg_http_header {
   struct mg_str name;
   struct mg_str value;
@@ -800,7 +815,8 @@ struct mg_http_serve_opts {
   const char *root_dir;       // Web root directory, must be non-NULL
   const char *ssi_pattern;    // SSI file name pattern, e.g. #.shtml
   const char *extra_headers;  // Extra HTTP headers to add in responses
-  bool use_packed_fs;         // Serve files embedded into binary
+  const char *mime_types;     // Extra mime types, ext1=type1,ext2=type2,..
+  struct mg_fs *fs;           // Filesystem implementation. Use NULL for POSIX
 };
 
 // Parameter for mg_http_next_multipart
@@ -820,9 +836,9 @@ struct mg_connection *mg_http_listen(struct mg_mgr *, const char *url,
 struct mg_connection *mg_http_connect(struct mg_mgr *, const char *url,
                                       mg_event_handler_t fn, void *fn_data);
 void mg_http_serve_dir(struct mg_connection *, struct mg_http_message *hm,
-                       struct mg_http_serve_opts *);
-void mg_http_serve_file(struct mg_connection *, struct mg_http_message *,
-                        const char *, const char *mime, const char *headers);
+                       struct mg_http_serve_opts *opts);
+void mg_http_serve_file(struct mg_connection *, struct mg_http_message *hm,
+                        const char *path, struct mg_http_serve_opts *opts);
 void mg_http_reply(struct mg_connection *, int status_code, const char *headers,
                    const char *body_fmt, ...);
 struct mg_str *mg_http_get_header(struct mg_http_message *, const char *name);
diff --git a/src/fs.c b/src/fs.c
deleted file mode 100644
index fe2064f7add5e21f71c9f3f9d08820442854e9f6..0000000000000000000000000000000000000000
--- a/src/fs.c
+++ /dev/null
@@ -1,65 +0,0 @@
-#include "fs.h"
-
-struct packed_file {
-  const char *data;
-  size_t size;
-  size_t pos;
-};
-
-const char *mg_unpack(const char *, size_t *) WEAK;
-const char *mg_unpack(const char *path, size_t *size) {
-  (void) path, (void) size;
-  return NULL;
-}
-
-#if defined(MG_FOPENCOOKIE)
-ssize_t packed_read(void *cookie, char *buf, size_t size) {
-  struct packed_file *fp = (struct packed_file *) cookie;
-  if (size > fp->size - fp->pos) size = fp->size - fp->pos;
-  memcpy(buf, &fp->data[fp->pos], size);
-  fp->pos += size;
-  return (ssize_t) size;
-}
-
-ssize_t packed_write(void *cookie, const char *buf, size_t size) {
-  (void) cookie, (void) buf, (void) size;
-  return -1;
-}
-
-int packed_seek(void *cookie, long *offset, int whence) {
-  struct packed_file *fp = (struct packed_file *) cookie;
-  if (whence == SEEK_SET) fp->pos = (size_t) *offset;
-  if (whence == SEEK_END) fp->pos = (size_t)((long) fp->size + *offset);
-  if (whence == SEEK_CUR) fp->pos = (size_t)((long) fp->pos + *offset);
-  if (fp->pos > fp->size) fp->pos = fp->size;
-  *offset = (long) fp->pos;
-  return 0;
-}
-
-int packed_close(void *cookie) {
-  free(cookie);
-  return 0;
-}
-
-FILE *mg_fopen_packed(const char *path, const char *mode) {
-  cookie_io_functions_t funcs = {
-      .read = packed_read,
-      .write = packed_write,
-      .seek = packed_seek,
-      .close = packed_close,
-  };
-  struct packed_file *cookie = NULL;
-  size_t size = 0;
-  const char *data = mg_unpack(path, &size);
-  if (data == NULL) return NULL;
-  if ((cookie = calloc(1, sizeof(*cookie))) == NULL) return NULL;
-  cookie->data = data;
-  cookie->size = size;
-  return fopencookie(cookie, mode, funcs);
-}
-#else
-FILE *mg_fopen_packed(const char *path, const char *mode) {
-  (void) path, (void) mode;
-  return NULL;
-}
-#endif
diff --git a/src/fs.h b/src/fs.h
index 9fd0b957481dc98548a8e7fcb6bfef7e5c90a818..c7f862e6d54e538490bdb3bea7deef894e29699c 100644
--- a/src/fs.h
+++ b/src/fs.h
@@ -2,11 +2,25 @@
 
 #include "arch.h"
 
-FILE *mg_fopen_packed(const char *path, const char *mode);
-
-#if (defined(__linux__) && defined(__GNUC__)) || defined(__NEWLIB__)
-#define MG_ENABLE_PACKED_FS 1
-#define MG_FOPENCOOKIE
-#else
-#define MG_ENABLE_PACKED_FS 0
-#endif
+enum { MG_FS_READ = 1, MG_FS_WRITE = 2, MG_FS_DIR = 4 };
+
+// Filesystem API functions
+struct mg_fs {
+  char *(*realpath)(const char *path, char *resolved_path);
+  int (*stat)(const char *path, size_t *size, unsigned *mtime);
+  void (*list)(const char *path, void (*fn)(const char *, void *), void *);
+  struct mg_fd *(*open)(const char *path, int flags);
+  void (*close)(struct mg_fd *fd);
+  size_t (*read)(void *fd, void *buf, size_t len);
+  size_t (*write)(void *fd, const void *buf, size_t len);
+  size_t (*seek)(void *fd, size_t offset);
+};
+
+// File descriptor
+struct mg_fd {
+  void *fd;
+  struct mg_fs *fs;
+};
+
+extern struct mg_fs mg_fs_posix;   // POSIX open/close/read/write/seek
+extern struct mg_fs mg_fs_packed;  // Packed FS, see examples/complete
diff --git a/src/fs_packed.c b/src/fs_packed.c
new file mode 100644
index 0000000000000000000000000000000000000000..75add662f0a301071fffd54ca929afdac840d4a6
--- /dev/null
+++ b/src/fs_packed.c
@@ -0,0 +1,74 @@
+#include "fs.h"
+
+struct packed_file {
+  const char *data;
+  size_t size;
+  size_t pos;
+};
+
+const char *mg_unpack(const char *, size_t *) WEAK;
+const char *mg_unpack(const char *path, size_t *size) {
+  (void) path, (void) size;
+  return NULL;
+}
+
+static char *packed_realpath(const char *path, char *resolved_path) {
+  if (resolved_path == NULL) resolved_path = malloc(strlen(path) + 1);
+  strcpy(resolved_path, path);
+  return resolved_path;
+}
+
+static int packed_stat(const char *path, size_t *size, unsigned *mtime) {
+  const char *data = mg_unpack(path, size);
+  if (mtime) *mtime = 0;
+  return data == NULL ? 0 : MG_FS_READ;
+}
+
+static void packed_list(const char *path, void (*fn)(const char *, void *),
+                        void *userdata) {
+  (void) path, (void) fn, (void) userdata;
+}
+
+static struct mg_fd *packed_open(const char *path, int flags) {
+  size_t size = 0;
+  const char *data = mg_unpack(path, &size);
+  struct packed_file *fp = NULL;
+  struct mg_fd *fd = NULL;
+  if (data == NULL) return NULL;
+  if (flags & MG_FS_WRITE) return NULL;
+  fp = calloc(1, sizeof(*fp));
+  fd = calloc(1, sizeof(*fd));
+  fp->size = size;
+  fp->data = data;
+  fd->fd = fp;
+  fd->fs = &mg_fs_packed;
+  return fd;
+}
+
+static void packed_close(struct mg_fd *fd) {
+  if (fd) free(fd->fd), free(fd);
+}
+
+static size_t packed_read(void *fd, void *buf, size_t len) {
+  struct packed_file *fp = (struct packed_file *) fd;
+  if (fp->pos + len > fp->size) len = fp->size - fp->pos;
+  memcpy(buf, &fp->data[fp->pos], len);
+  fp->pos += len;
+  return len;
+}
+
+static size_t packed_write(void *fd, const void *buf, size_t len) {
+  (void) fd, (void) buf, (void) len;
+  return 0;
+}
+
+static size_t packed_seek(void *fd, size_t offset) {
+  struct packed_file *fp = (struct packed_file *) fd;
+  fp->pos = offset;
+  if (fp->pos > fp->size) fp->pos = fp->size;
+  return fp->pos;
+}
+
+struct mg_fs mg_fs_packed = {packed_realpath, packed_stat,  packed_list,
+                             packed_open,     packed_close, packed_read,
+                             packed_write,    packed_seek};
diff --git a/src/fs_posix.c b/src/fs_posix.c
new file mode 100644
index 0000000000000000000000000000000000000000..d93f2c3b5301756e719bae24b9902cc40b1339ac
--- /dev/null
+++ b/src/fs_posix.c
@@ -0,0 +1,131 @@
+#include "fs.h"
+
+#if defined(O_READ)
+static char *posix_realpath(const char *path, char *resolved_path) {
+#ifdef _WIN32
+  return _fullpath(path, resolved_path, PATH_MAX);
+#else
+  return realpath(path, resolved_path);
+#endif
+}
+
+static int posix_stat(const char *path, size_t *size, unsigned *mtime) {
+#ifdef _WIN32
+  struct _stati64 st;
+  wchar_t tmp[PATH_MAX];
+  MultiByteToWideChar(CP_UTF8, 0, path, -1, tmp, sizeof(tmp) / sizeof(tmp[0]));
+  if (_wstati64(tmp, &st) != 0) return 0;
+#else
+  struct stat st;
+  if (stat(path, &st) != 0) return 0;
+#endif
+  if (size) *size = (size_t) st.st_size;
+  if (mtime) *mtime = (unsigned) st.st_mtime;
+  return MG_FS_READ | MG_FS_WRITE | (S_ISDIR(st.st_mode) ? MG_FS_DIR : 0);
+}
+
+static void posix_list(const char *dir, void (*fn)(const char *, void *),
+                       void *userdata) {
+  // char path[MG_PATH_MAX], *p = &dir[strlen(dir) - 1], tmp[10];
+  struct dirent *dp;
+  DIR *dirp;
+
+  // while (p > dir && *p != '/') *p-- = '\0';
+  if ((dirp = (opendir(dir))) != NULL) {
+    size_t off, n;
+    while ((dp = readdir(dirp)) != NULL) {
+      // Do not show current dir and hidden files
+      if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue;
+      fn(dp->d_name, &st);
+    }
+    closedir(dirp);
+  }
+}
+
+static struct mg_fd *posix_open(const char *path, int flags) {
+  const char *mode =
+      flags & (MG_FS_READ | MG_FS_WRITE)
+          ? "r+b"
+          : flags & MG_FS_READ ? "rb" : flags & MG_FS_WRITE ? "wb" : "";
+  void *fp = NULL;
+  struct mg_fd *fd = NULL;
+#ifdef _WIN32
+  wchar_t b1[PATH_MAX], b2[10];
+  MultiByteToWideChar(CP_UTF8, 0, path, -1, b1, sizeof(b1) / sizeof(b1[0]));
+  MultiByteToWideChar(CP_UTF8, 0, mode, -1, b2, sizeof(b2) / sizeof(b2[0]));
+  fp = (void *) _wfopen(b1, b2);
+#else
+  fp = (void *) fopen(path, mode);
+#endif
+  if (fp == NULL) return NULL;
+  fd = calloc(1, sizeof(*fd));
+  fd->fd = fp;
+  fd->fs = &mg_fs_posix;
+  return fd;
+}
+
+static void posix_close(struct mg_fd *fd) {
+  if (fd) fclose((FILE *) fd->fd), free(fd);
+}
+
+static size_t posix_read(void *fp, void *buf, size_t len) {
+  return fread(buf, 1, len, (FILE *) fp);
+}
+
+static size_t posix_write(void *fp, const void *buf, size_t len) {
+  return fwrite(buf, 1, len, (FILE *) fp);
+}
+
+static size_t posix_seek(void *fp, size_t offset) {
+#if _FILE_OFFSET_BITS == 64 || _POSIX_C_SOURCE >= 200112L || \
+    _XOPEN_SOURCE >= 600
+  fseeko((FILE *) fp, (off_t) offset, SEEK_SET);
+#else
+  fseek((FILE *) fp, (long) offset, SEEK_SET);
+#endif
+  return (size_t) ftell((FILE *) fp);
+}
+#else
+static char *posix_realpath(const char *path, char *resolved_path) {
+  (void) path, (void) resolved_path;
+  return NULL;
+}
+
+static int posix_stat(const char *path, size_t *size, unsigned *mtime) {
+  (void) path, (void) size, (void) mtime;
+  return 0;
+}
+
+static void posix_list(const char *path, void (*fn)(const char *, void *),
+                       void *userdata) {
+  (void) path, (void) fn, (void) userdata;
+}
+
+static struct mg_fd *posix_open(const char *path, int flags) {
+  (void) path, (void) flags;
+  return NULL;
+}
+
+static void posix_close(struct mg_fd *fd) {
+  (void) fd;
+}
+
+static size_t posix_read(void *fd, void *buf, size_t len) {
+  (void) fd, (void) buf, (void) len;
+  return 0;
+}
+
+static size_t posix_write(void *fd, const void *buf, size_t len) {
+  (void) fd, (void) buf, (void) len;
+  return 0;
+}
+
+static size_t posix_seek(void *fd, size_t offset) {
+  (void) fd, (void) offset;
+  return (size_t) ~0;
+}
+#endif
+
+struct mg_fs mg_fs_posix = {posix_realpath, posix_stat,  posix_list,
+                            posix_open,     posix_close, posix_read,
+                            posix_write,    posix_seek};
diff --git a/src/http.c b/src/http.c
index 196ba01ed05a89270b9801dd5e8fa8e90bc25700..3447f88a6dbe473b24db1932faacd815d47ef35d 100644
--- a/src/http.c
+++ b/src/http.c
@@ -420,73 +420,83 @@ static void static_cb(struct mg_connection *c, int ev, void *ev_data,
   (void) ev_data;
 }
 
-static const char *guess_content_type(const char *filename) {
-  size_t n = strlen(filename);
-#define MIME_ENTRY(_ext, _type) \
-  { _ext, sizeof(_ext) - 1, _type }
-  const struct {
-    const char *ext;
-    size_t ext_len;
-    const char *type;
-  } * t, types[] = {
-             MIME_ENTRY("html", "text/html; charset=utf-8"),
-             MIME_ENTRY("htm", "text/html; charset=utf-8"),
-             MIME_ENTRY("css", "text/css; charset=utf-8"),
-             MIME_ENTRY("js", "text/javascript; charset=utf-8"),
-             MIME_ENTRY("gif", "image/gif"),
-             MIME_ENTRY("png", "image/png"),
-             MIME_ENTRY("woff", "font/woff"),
-             MIME_ENTRY("ttf", "font/ttf"),
-             MIME_ENTRY("aac", "audio/aac"),
-             MIME_ENTRY("avi", "video/x-msvideo"),
-             MIME_ENTRY("azw", "application/vnd.amazon.ebook"),
-             MIME_ENTRY("bin", "application/octet-stream"),
-             MIME_ENTRY("bmp", "image/bmp"),
-             MIME_ENTRY("bz", "application/x-bzip"),
-             MIME_ENTRY("bz2", "application/x-bzip2"),
-             MIME_ENTRY("csv", "text/csv"),
-             MIME_ENTRY("doc", "application/msword"),
-             MIME_ENTRY("epub", "application/epub+zip"),
-             MIME_ENTRY("exe", "application/octet-stream"),
-             MIME_ENTRY("gz", "application/gzip"),
-             MIME_ENTRY("ico", "image/x-icon"),
-             MIME_ENTRY("json", "application/json"),
-             MIME_ENTRY("mid", "audio/mid"),
-             MIME_ENTRY("mjs", "text/javascript"),
-             MIME_ENTRY("mov", "video/quicktime"),
-             MIME_ENTRY("mp3", "audio/mpeg"),
-             MIME_ENTRY("mp4", "video/mp4"),
-             MIME_ENTRY("mpeg", "video/mpeg"),
-             MIME_ENTRY("mpg", "video/mpeg"),
-             MIME_ENTRY("ogg", "application/ogg"),
-             MIME_ENTRY("pdf", "application/pdf"),
-             MIME_ENTRY("rar", "application/rar"),
-             MIME_ENTRY("rtf", "application/rtf"),
-             MIME_ENTRY("shtml", "text/html; charset=utf-8"),
-             MIME_ENTRY("svg", "image/svg+xml"),
-             MIME_ENTRY("tar", "application/tar"),
-             MIME_ENTRY("tgz", "application/tar-gz"),
-             MIME_ENTRY("txt", "text/plain; charset=utf-8"),
-             MIME_ENTRY("wasm", "application/wasm"),
-             MIME_ENTRY("wav", "audio/wav"),
-             MIME_ENTRY("weba", "audio/webm"),
-             MIME_ENTRY("webm", "video/webm"),
-             MIME_ENTRY("webp", "image/webp"),
-             MIME_ENTRY("xls", "application/excel"),
-             MIME_ENTRY("xml", "application/xml"),
-             MIME_ENTRY("xsl", "application/xml"),
-             MIME_ENTRY("zip", "application/zip"),
-             MIME_ENTRY("3gp", "video/3gpp"),
-             MIME_ENTRY("7z", "application/x-7z-compressed"),
-             {NULL, 0, NULL},
-         };
-
-  for (t = types; t->ext != NULL; t++) {
-    if (n < t->ext_len + 2) continue;
-    if (mg_ncasecmp(t->ext, &filename[n - t->ext_len], t->ext_len)) continue;
-    return t->type;
+static struct mg_str guess_content_type(struct mg_str path, const char *extra) {
+  // clang-format off
+  struct mimeentry { struct mg_str extension, value; };
+  #define MIME_ENTRY(a, b) {{a, sizeof(a) - 1 }, { b, sizeof(b) - 1 }}
+  // clang-format on
+  const struct mimeentry tab[] = {
+      MIME_ENTRY("html", "text/html; charset=utf-8"),
+      MIME_ENTRY("htm", "text/html; charset=utf-8"),
+      MIME_ENTRY("css", "text/css; charset=utf-8"),
+      MIME_ENTRY("js", "text/javascript; charset=utf-8"),
+      MIME_ENTRY("gif", "image/gif"),
+      MIME_ENTRY("png", "image/png"),
+      MIME_ENTRY("woff", "font/woff"),
+      MIME_ENTRY("ttf", "font/ttf"),
+      MIME_ENTRY("aac", "audio/aac"),
+      MIME_ENTRY("avi", "video/x-msvideo"),
+      MIME_ENTRY("azw", "application/vnd.amazon.ebook"),
+      MIME_ENTRY("bin", "application/octet-stream"),
+      MIME_ENTRY("bmp", "image/bmp"),
+      MIME_ENTRY("bz", "application/x-bzip"),
+      MIME_ENTRY("bz2", "application/x-bzip2"),
+      MIME_ENTRY("csv", "text/csv"),
+      MIME_ENTRY("doc", "application/msword"),
+      MIME_ENTRY("epub", "application/epub+zip"),
+      MIME_ENTRY("exe", "application/octet-stream"),
+      MIME_ENTRY("gz", "application/gzip"),
+      MIME_ENTRY("ico", "image/x-icon"),
+      MIME_ENTRY("json", "application/json"),
+      MIME_ENTRY("mid", "audio/mid"),
+      MIME_ENTRY("mjs", "text/javascript"),
+      MIME_ENTRY("mov", "video/quicktime"),
+      MIME_ENTRY("mp3", "audio/mpeg"),
+      MIME_ENTRY("mp4", "video/mp4"),
+      MIME_ENTRY("mpeg", "video/mpeg"),
+      MIME_ENTRY("mpg", "video/mpeg"),
+      MIME_ENTRY("ogg", "application/ogg"),
+      MIME_ENTRY("pdf", "application/pdf"),
+      MIME_ENTRY("rar", "application/rar"),
+      MIME_ENTRY("rtf", "application/rtf"),
+      MIME_ENTRY("shtml", "text/html; charset=utf-8"),
+      MIME_ENTRY("svg", "image/svg+xml"),
+      MIME_ENTRY("tar", "application/tar"),
+      MIME_ENTRY("tgz", "application/tar-gz"),
+      MIME_ENTRY("txt", "text/plain; charset=utf-8"),
+      MIME_ENTRY("wasm", "application/wasm"),
+      MIME_ENTRY("wav", "audio/wav"),
+      MIME_ENTRY("weba", "audio/webm"),
+      MIME_ENTRY("webm", "video/webm"),
+      MIME_ENTRY("webp", "image/webp"),
+      MIME_ENTRY("xls", "application/excel"),
+      MIME_ENTRY("xml", "application/xml"),
+      MIME_ENTRY("xsl", "application/xml"),
+      MIME_ENTRY("zip", "application/zip"),
+      MIME_ENTRY("3gp", "video/3gpp"),
+      MIME_ENTRY("7z", "application/x-7z-compressed"),
+      MIME_ENTRY("7z", "application/x-7z-compressed"),
+      {{0, 0}, {0, 0}},
+  };
+  size_t i = 0;
+  struct mg_str k, v, s = mg_str(extra);
+
+  // Shrink path to its extension only
+  while (i < path.len && path.ptr[path.len - i - 1] != '.') i++;
+  path.ptr += path.len - i;
+  path.len = i;
+
+  // Process user-provided mime type overrides, if any
+  while (mg_next_comma_entry(&s, &k, &v)) {
+    if (mg_strcmp(path, k) == 0) return v;
   }
-  return "text/plain; charset=utf-8";
+
+  // Process built-in mime types
+  for (i = 0; tab[i].extension.ptr != NULL; i++) {
+    if (mg_strcmp(path, tab[i].extension) == 0) return tab[i].value;
+  }
+
+  return mg_str("text/plain; charset=utf-8");
 }
 
 static int getrange(struct mg_str *s, int64_t *a, int64_t *b) {
@@ -510,7 +520,7 @@ static int getrange(struct mg_str *s, int64_t *a, int64_t *b) {
 }
 
 void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm,
-                        const char *path, const char *mime, const char *hdrs) {
+                        const char *path, struct mg_http_serve_opts *opts) {
   struct mg_str *inm = mg_http_get_header(hm, "If-None-Match");
   struct stat st;
   char etag[64];
@@ -528,6 +538,7 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm,
     int n, status = 200;
     char range[100] = "";
     int64_t r1 = 0, r2 = 0, cl = st.st_size;
+    struct mg_str mime = guess_content_type(mg_str(path), opts->mime_types);
 
     // Handle Range header
     struct mg_str *rh = mg_http_get_header(hm, "Range");
@@ -558,10 +569,10 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm,
     }
 
     mg_printf(c,
-              "HTTP/1.1 %d %s\r\nContent-Type: %s\r\n"
+              "HTTP/1.1 %d %s\r\nContent-Type: %.*s\r\n"
               "Etag: %s\r\nContent-Length: " MG_INT64_FMT "\r\n%s%s\r\n",
-              status, mg_http_status_code_str(status), mime, etag, cl, range,
-              hdrs ? hdrs : "");
+              status, mg_http_status_code_str(status), (int) mime.len, mime.ptr,
+              etag, cl, range, opts->extra_headers ? opts->extra_headers : "");
     if (mg_vcasecmp(&hm->method, "HEAD") == 0) {
       fclose(fp);
     } else {
@@ -865,40 +876,39 @@ static bool uri_to_local_path(struct mg_connection *c,
 
 void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm,
                        struct mg_http_serve_opts *opts) {
-  char root_dir[MG_PATH_MAX], full_path[sizeof(root_dir)];
+  char root_dir[MG_PATH_MAX], path[sizeof(root_dir)];
   bool is_index = false, exists;
   struct stat st;
-  root_dir[0] = full_path[0] = '\0';
+  root_dir[0] = path[0] = '\0';
 
-  if (!uri_to_local_path(c, hm, opts, root_dir, sizeof(root_dir), full_path,
-                         sizeof(full_path), &is_index))
+  if (!uri_to_local_path(c, hm, opts, root_dir, sizeof(root_dir), path,
+                         sizeof(path), &is_index))
     return;
 
-  exists = stat(full_path, &st) == 0;
+  exists = stat(path, &st) == 0;
 #if MG_ENABLE_SSI
   if (is_index && !exists) {
-    char *p = full_path + strlen(full_path);
-    while (p > full_path && p[-1] != '/') p--;
-    strncpy(p, "index.shtml", (size_t)(&full_path[sizeof(full_path)] - p - 2));
-    full_path[sizeof(full_path) - 1] = '\0';
-    exists = stat(full_path, &st) == 0;
+    char *p = path + strlen(path);
+    while (p > path && p[-1] != '/') p--;
+    strncpy(p, "index.shtml", (size_t)(&path[sizeof(path)] - p - 2));
+    path[sizeof(path) - 1] = '\0';
+    exists = stat(path, &st) == 0;
   }
 #endif
   if (is_index && !exists) {
 #if MG_ENABLE_DIRECTORY_LISTING
-    listdir(c, hm, opts, full_path);
+    listdir(c, hm, opts, path);
 #else
     mg_http_reply(c, 403, "", "%s", "Directory listing not supported");
 #endif
 #if MG_ENABLE_SSI
   } else if (opts->ssi_pattern != NULL &&
-             mg_globmatch(opts->ssi_pattern, strlen(opts->ssi_pattern),
-                          full_path, strlen(full_path))) {
-    mg_http_serve_ssi(c, root_dir, full_path);
+             mg_globmatch(opts->ssi_pattern, strlen(opts->ssi_pattern), path,
+                          strlen(path))) {
+    mg_http_serve_ssi(c, root_dir, path);
 #endif
   } else {
-    mg_http_serve_file(c, hm, full_path, guess_content_type(full_path),
-                       opts->extra_headers);
+    mg_http_serve_file(c, hm, path, opts);
   }
 }
 
diff --git a/src/http.h b/src/http.h
index 7e68af61b98d0d09b4cd0a554b2b587f4800bd60..a9ffbad19fee9c1b787fd2b4221aaf7278df45d6 100644
--- a/src/http.h
+++ b/src/http.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "config.h"
+#include "fs.h"
 #include "net.h"
 #include "str.h"
 
@@ -23,7 +24,8 @@ struct mg_http_serve_opts {
   const char *root_dir;       // Web root directory, must be non-NULL
   const char *ssi_pattern;    // SSI file name pattern, e.g. #.shtml
   const char *extra_headers;  // Extra HTTP headers to add in responses
-  bool use_packed_fs;         // Serve files embedded into binary
+  const char *mime_types;     // Extra mime types, ext1=type1,ext2=type2,..
+  struct mg_fs *fs;           // Filesystem implementation. Use NULL for POSIX
 };
 
 // Parameter for mg_http_next_multipart
@@ -43,9 +45,9 @@ struct mg_connection *mg_http_listen(struct mg_mgr *, const char *url,
 struct mg_connection *mg_http_connect(struct mg_mgr *, const char *url,
                                       mg_event_handler_t fn, void *fn_data);
 void mg_http_serve_dir(struct mg_connection *, struct mg_http_message *hm,
-                       struct mg_http_serve_opts *);
-void mg_http_serve_file(struct mg_connection *, struct mg_http_message *,
-                        const char *, const char *mime, const char *headers);
+                       struct mg_http_serve_opts *opts);
+void mg_http_serve_file(struct mg_connection *, struct mg_http_message *hm,
+                        const char *path, struct mg_http_serve_opts *opts);
 void mg_http_reply(struct mg_connection *, int status_code, const char *headers,
                    const char *body_fmt, ...);
 struct mg_str *mg_http_get_header(struct mg_http_message *, const char *name);
diff --git a/test/mongoose_custom.h b/test/mongoose_custom.h
index 5963078ede3c7ac158a5795334f8d6a728a983e7..8176b245b04ca6e3bdbd23cb28bc9b8f3a6b1917 100644
--- a/test/mongoose_custom.h
+++ b/test/mongoose_custom.h
@@ -22,6 +22,4 @@
 #undef MG_ENABLE_SOCKET
 #define MG_ENABLE_SOCKET 0
 
-#define realpath(a, b) (a)
-
 int clock_gettime(clockid_t clock_id, struct timespec *tp);
diff --git a/test/unit_test.c b/test/unit_test.c
index f37d9e89e7cf43315417fb3f0816f8b93f942a03..316912d2aabbd358536d8f8cbcb1923ca5058f84 100644
--- a/test/unit_test.c
+++ b/test/unit_test.c
@@ -363,6 +363,16 @@ static void eh1(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
       sopts.root_dir = ".";
       sopts.extra_headers = "A: B\r\nC: D\r\n";
       mg_http_serve_dir(c, hm, &sopts);
+    } else if (mg_http_match_uri(hm, "/packed/#")) {
+      struct mg_http_serve_opts sopts;
+      memset(&sopts, 0, sizeof(sopts));
+      sopts.root_dir = ".";
+      mg_http_serve_dir(c, hm, &sopts);
+    } else if (mg_http_match_uri(hm, "/servefile")) {
+      struct mg_http_serve_opts sopts;
+      memset(&sopts, 0, sizeof(sopts));
+      sopts.mime_types = "foo=a/b,txt=c/d";
+      mg_http_serve_file(c, hm, "test/data/a.txt", &sopts);
     } else {
       struct mg_http_serve_opts sopts;
       memset(&sopts, 0, sizeof(sopts));
@@ -501,6 +511,17 @@ static void test_http_server(void) {
                  etag) == 304);
   }
 
+  // Text mime type override
+  ASSERT(fetch(&mgr, buf, url, "GET /servefile HTTP/1.0\n\n") == 200);
+  ASSERT(cmpbody(buf, "hello\n") == 0);
+  {
+    struct mg_http_message hm;
+    mg_http_parse(buf, strlen(buf), &hm);
+    ASSERT(mg_http_get_header(&hm, "Content-Type") != NULL);
+    ASSERT(mg_strcmp(*mg_http_get_header(&hm, "Content-Type"), mg_str("c/d")) ==
+           0);
+  }
+
   ASSERT(fetch(&mgr, buf, url, "GET /foo/1 HTTP/1.0\r\n\n") == 200);
   // LOG(LL_INFO, ("%d %.*s", (int) hm.len, (int) hm.len, hm.buf));
   ASSERT(cmpbody(buf, "uri: 1") == 0);
@@ -1336,18 +1357,14 @@ static void test_multipart(void) {
 }
 
 static void test_packed(void) {
-  const char *path = "Makefile";
-  FILE *fp = mg_fopen_packed(path, "r");
-#if MG_ENABLE_PACKED_FS
-  struct stat st;
-  ASSERT(stat(path, &st) == 0);
-  ASSERT(fp != NULL);
-  fseek(fp, 0, SEEK_END);
-  ASSERT(ftell(fp) == st.st_size);
-  fclose(fp);
-#else
-  ASSERT(fp == NULL);
-#endif
+  struct mg_mgr mgr;
+  const char *url = "http://127.0.0.1:12351";
+  char buf[FETCH_BUF_SIZE];
+  mg_mgr_init(&mgr);
+  mg_http_listen(&mgr, url, eh1, NULL);
+  ASSERT(fetch(&mgr, buf, url, "GET /packed/ HTTP/1.0\n\n") == 404);
+  mg_mgr_free(&mgr);
+  ASSERT(mgr.conns == NULL);
 }
 
 int main(void) {