diff --git a/docs/c-api/util.h/intro.md b/docs/c-api/util.h/intro.md
index 9b30d680091764b2d58fe6f3c7cbf72788314b1d..4a390e48465f374ca183d4fac813e272c24bd16e 100644
--- a/docs/c-api/util.h/intro.md
+++ b/docs/c-api/util.h/intro.md
@@ -8,6 +8,8 @@ items:
   - { name: mg_basic_auth_header.md }
   - { name: mg_conn_addr_to_str.md }
   - { name: mg_fopen.md }
+  - { name: mg_fread.md }
+  - { name: mg_fwrite.md }
   - { name: mg_hexdump.md }
   - { name: mg_hexdump_connection.md }
   - { name: mg_hexdumpf.md }
diff --git a/docs/c-api/util.h/mg_fread.md b/docs/c-api/util.h/mg_fread.md
new file mode 100644
index 0000000000000000000000000000000000000000..0ff4994ad470bc9d79e7745fcbaf8213e9bf6145
--- /dev/null
+++ b/docs/c-api/util.h/mg_fread.md
@@ -0,0 +1,12 @@
+---
+title: "mg_fread()"
+decl_name: "mg_fread"
+symbol_kind: "func"
+signature: |
+  size_t mg_fread(void *ptr, size_t size, size_t count, FILE *f);
+---
+
+Reads data from the given file stream.
+
+Return value is a number of bytes readen. 
+
diff --git a/docs/c-api/util.h/mg_fwrite.md b/docs/c-api/util.h/mg_fwrite.md
new file mode 100644
index 0000000000000000000000000000000000000000..5ddbbbcea9b6030e56cc7b3ca993416b469c88ea
--- /dev/null
+++ b/docs/c-api/util.h/mg_fwrite.md
@@ -0,0 +1,12 @@
+---
+title: "mg_fwrite()"
+decl_name: "mg_fwrite"
+symbol_kind: "func"
+signature: |
+  size_t mg_fwrite(const void *ptr, size_t size, size_t count, FILE *f);
+---
+
+Writes data to the given file stream.
+
+Return value is a number of bytes wtitten. 
+
diff --git a/docs/overview/build-options/tunables.md b/docs/overview/build-options/tunables.md
index e1ca4c63c1c7702dac15e07c57f774f3d1c1cc44..c9944c4c26e162dc99a3513b175f0b23472952dd 100644
--- a/docs/overview/build-options/tunables.md
+++ b/docs/overview/build-options/tunables.md
@@ -9,3 +9,4 @@ title: Tunables
 - `MG_SSL_CRYPTO_MODERN`, `MG_SSL_CRYPTO_OLD` - choose either "Modern" or "Old" ciphers
   instead of the default "Intermediate" setting.
   See [this article](https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations) for details.
+- `MG_USER_FILE_FUNCTIONS` allow you to use custom file operation, by defining you own `mg_stat`, `mg_fopen`, `mg_open`, `mg_fread` and `mg_fwrite` functions
\ No newline at end of file
diff --git a/mongoose.c b/mongoose.c
index e4c210e7b49fdff2aef7341beae9134d751dbc8b..19c75e19f1daa826d2361083421597c12ce12a89 100644
--- a/mongoose.c
+++ b/mongoose.c
@@ -5374,7 +5374,7 @@ static void mg_http_transfer_file_data(struct mg_connection *nc) {
     if (to_read == 0) {
       /* Rate limiting. send_mbuf is too full, wait until it's drained. */
     } else if (pd->file.sent < pd->file.cl &&
-               (n = fread(buf, 1, to_read, pd->file.fp)) > 0) {
+               (n = mg_fread(buf, 1, to_read, pd->file.fp)) > 0) {
       mg_send(nc, buf, n);
       pd->file.sent += n;
     } else {
@@ -5384,7 +5384,7 @@ static void mg_http_transfer_file_data(struct mg_connection *nc) {
   } else if (pd->file.type == DATA_PUT) {
     struct mbuf *io = &nc->recv_mbuf;
     size_t to_write = left <= 0 ? 0 : left < io->len ? (size_t) left : io->len;
-    size_t n = fwrite(io->buf, 1, to_write, pd->file.fp);
+    size_t n = mg_fwrite(io->buf, 1, to_write, pd->file.fp);
     if (n > 0) {
       mbuf_remove(io, n);
       pd->file.sent += n;
@@ -7515,7 +7515,7 @@ void mg_file_upload_handler(struct mg_connection *nc, int ev, void *ev_data,
       struct file_upload_state *fus =
           (struct file_upload_state *) mp->user_data;
       if (fus == NULL || fus->fp == NULL) break;
-      if (fwrite(mp->data.p, 1, mp->data.len, fus->fp) != mp->data.len) {
+      if (mg_fwrite(mp->data.p, 1, mp->data.len, fus->fp) != mp->data.len) {
         LOG(LL_ERROR, ("Failed to write to %s: %d, wrote %d", fus->lfn,
                        mg_get_errno(), (int) fus->num_recd));
         if (mg_get_errno() == ENOSPC
@@ -8328,7 +8328,7 @@ static void mg_send_ssi_file(struct mg_connection *nc, struct http_message *hm,
 static void mg_send_file_data(struct mg_connection *nc, FILE *fp) {
   char buf[BUFSIZ];
   size_t n;
-  while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
+  while ((n = mg_fread(buf, 1, sizeof(buf), fp)) > 0) {
     mg_send(nc, buf, n);
   }
 }
@@ -9209,7 +9209,7 @@ static int lowercase(const char *s) {
   return tolower(*(const unsigned char *) s);
 }
 
-#if MG_ENABLE_FILESYSTEM
+#if MG_ENABLE_FILESYSTEM && !defined(MG_USER_FILE_FUNCTIONS)
 int mg_stat(const char *path, cs_stat_t *st) {
 #ifdef _WIN32
   wchar_t wpath[MAX_PATH_SIZE];
@@ -9241,6 +9241,14 @@ int mg_open(const char *path, int flag, int mode) { /* LCOV_EXCL_LINE */
   return open(path, flag, mode); /* LCOV_EXCL_LINE */
 #endif
 }
+
+size_t mg_fread(void *ptr, size_t size, size_t count, FILE *f) {
+  return fread(ptr, size, count, f);
+}
+
+size_t mg_fwrite(const void *ptr, size_t size, size_t count, FILE *f) {
+  return fwrite(ptr, size, count, f);
+}
 #endif
 
 void mg_base64_encode(const unsigned char *src, int src_len, char *dst) {
diff --git a/mongoose.h b/mongoose.h
index 192677ad616a1b6cade347d40729c93fc10f3ce6..45fab927b6283d84d74b9a8f04c80b60981367a1 100644
--- a/mongoose.h
+++ b/mongoose.h
@@ -3898,6 +3898,21 @@ FILE *mg_fopen(const char *path, const char *mode);
  * Return value is the same as for the `open()` syscall.
  */
 int mg_open(const char *path, int flag, int mode);
+
+/*
+ * Reads data from the given file stream.
+ *
+ * Return value is a number of bytes readen.
+ */
+size_t mg_fread(void *ptr, size_t size, size_t count, FILE *f);
+
+/*
+ * Writes data to the given file stream.
+ *
+ * Return value is a number of bytes wtitten.
+ */
+size_t mg_fwrite(const void *ptr, size_t size, size_t count, FILE *f);
+
 #endif /* MG_ENABLE_FILESYSTEM */
 
 #if MG_ENABLE_THREADS