Copyright (c) 2009, FRiCKLE Piotr Sikora <info@frickle.com>
All rights reserved.

This project was fully funded by yo.se.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY FRiCKLE PIOTR SIKORA AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FRiCKLE PIOTR
SIKORA OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

--- src/http/modules/ngx_http_fastcgi_module.c.orig	Tue Nov 17 06:21:25 2009
+++ src/http/modules/ngx_http_fastcgi_module.c	Tue Nov 17 06:31:14 2009
@@ -7,6 +7,7 @@
 #include <ngx_config.h>
 #include <ngx_core.h>
 #include <ngx_http.h>
+#include <nginx.h>
 
 
 typedef struct {
@@ -156,6 +157,9 @@
     void *conf);
 static char *ngx_http_fastcgi_cache_key(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
+static char *ngx_http_fastcgi_cache_purge(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
+static ngx_int_t ngx_http_fastcgi_cache_purge_handler(ngx_http_request_t *r);
 #endif
 
 static char *ngx_http_fastcgi_lowat_check(ngx_conf_t *cf, void *post,
@@ -368,6 +372,13 @@
       offsetof(ngx_http_fastcgi_loc_conf_t, upstream.cache_methods),
       &ngx_http_upstream_cache_method_mask },
 
+    { ngx_string("fastcgi_cache_purge"),
+      NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2,
+      ngx_http_fastcgi_cache_purge,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      0,
+      NULL },
+
 #endif
 
     { ngx_string("fastcgi_temp_path"),
@@ -2465,6 +2476,14 @@
 
     clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
 
+#if (NGX_HTTP_CACHE)
+
+    if (clcf->handler == ngx_http_fastcgi_cache_purge_handler) {
+        return "is incompatible with \"fastcgi_cache_purge\"";
+    }
+
+#endif
+
     clcf->handler = ngx_http_fastcgi_handler;
 
     if (clcf->name.data[clcf->name.len - 1] == '/') {
@@ -2582,7 +2601,7 @@
     if (flcf->upstream.cache != NGX_CONF_UNSET_PTR
         && flcf->upstream.cache != NULL)
     {
-        return "is incompatible with \"fastcgi_cache\"";
+        return "is incompatible with \"fastcgi_cache\" and \"fastcgi_cache_purge\"";
     }
 
 #endif
@@ -2625,7 +2644,7 @@
     value = cf->args->elts;
 
     if (flcf->upstream.cache != NGX_CONF_UNSET_PTR) {
-        return "is duplicate";
+        return "is either duplicate or collides with \"fastcgi_cache_purge\"";
     }
 
     if (ngx_strcmp(value[1].data, "off") == 0) {
@@ -2658,7 +2677,7 @@
     value = cf->args->elts;
 
     if (flcf->cache_key.value.len) {
-        return "is duplicate";
+        return "is either duplicate or collides with \"fastcgi_cache_purge\"";
     }
 
     ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
@@ -2672,6 +2691,144 @@
     }
 
     return NGX_CONF_OK;
+}
+
+static char *
+ngx_http_fastcgi_cache_purge(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_http_fastcgi_loc_conf_t       *flcf = conf;
+
+    ngx_http_compile_complex_value_t   ccv;
+    ngx_http_core_loc_conf_t          *clcf;
+    ngx_str_t                         *value;
+
+    /* check for duplicates / collisions */
+    if (flcf->upstream.cache != NGX_CONF_UNSET_PTR
+        && flcf->upstream.cache != NULL)
+    {
+        return "is either duplicate or collides with \"fastcgi_cache\"";
+    }
+
+    if (flcf->upstream.upstream || flcf->fastcgi_lengths) {
+        return "is incompatible with \"fastcgi_pass\"";
+    }
+
+    if (flcf->upstream.store > 0 || flcf->upstream.store_lengths) {
+        return "is incompatible with \"fastcgi_store\"";
+    }
+
+    value = cf->args->elts;
+
+    /* set fastcgi_cache part */
+    flcf->upstream.cache = ngx_shared_memory_add(cf, &value[1], 0,
+                                                 &ngx_http_fastcgi_module);
+    if (flcf->upstream.cache == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    /* set fastcgi_cache_key part */
+    ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
+
+    ccv.cf = cf;
+    ccv.value = &value[2];
+    ccv.complex_value = &flcf->cache_key;
+
+    if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
+    /* set handler */
+    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
+
+    clcf->handler = ngx_http_fastcgi_cache_purge_handler;
+
+    return NGX_CONF_OK;
+}
+
+static char ngx_http_cache_purge_success_page_top[] =
+"<html>" CRLF
+"<head><title>Successful purge</title></head>" CRLF
+"<body bgcolor=\"white\">" CRLF
+"<center><h1>Successful purge</h1>" CRLF
+;
+
+static char ngx_http_cache_purge_success_page_tail[] =
+CRLF "</center>" CRLF
+"<hr><center>" NGINX_VER "</center>" CRLF
+"</body>" CRLF
+"</html>" CRLF
+;
+
+static ngx_int_t
+ngx_http_fastcgi_cache_purge_handler(ngx_http_request_t *r)
+{
+    ngx_http_fastcgi_loc_conf_t  *flcf;
+    ngx_chain_t                   out;
+    ngx_buf_t                    *b;
+    ngx_str_t                    *key;
+    ngx_int_t                     rc;
+    size_t                        len;
+
+    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD|NGX_HTTP_DELETE))) {
+        return NGX_HTTP_NOT_ALLOWED;
+    }
+
+    flcf = ngx_http_get_module_loc_conf(r, ngx_http_fastcgi_module);
+
+    rc = ngx_http_file_cache_purge(r, flcf->upstream.cache->data,
+                                   &flcf->cache_key);
+
+    if (rc == NGX_ERROR) {
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    } else if (rc == NGX_DECLINED) {
+        return NGX_HTTP_NOT_FOUND;
+    }
+
+    key = r->cache->keys.elts;
+
+    len = sizeof(ngx_http_cache_purge_success_page_top) - 1
+          + sizeof(ngx_http_cache_purge_success_page_tail) - 1
+          + sizeof("<br>Key : ") - 1 + sizeof(CRLF "<br>Path: ") - 1
+          + key[0].len + r->cache->file.name.len;
+
+    r->headers_out.content_type.len = sizeof("text/html") - 1;
+    r->headers_out.content_type.data = (u_char *) "text/html";
+    r->headers_out.status = NGX_HTTP_OK;
+    r->headers_out.content_length_n = len;
+
+    if (r->method == NGX_HTTP_HEAD) {
+        rc = ngx_http_send_header(r);
+        if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
+            return rc;
+        }
+    }
+
+    b = ngx_create_temp_buf(r->pool, len);
+    if (b == NULL) {
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    out.buf = b;
+    out.next = NULL;
+
+    b->last = ngx_cpymem(b->last, ngx_http_cache_purge_success_page_top,
+                         sizeof(ngx_http_cache_purge_success_page_top) - 1);
+    b->last = ngx_cpymem(b->last, "<br>Key : ", sizeof("<br>Key : ") - 1);
+    b->last = ngx_cpymem(b->last, key[0].data, key[0].len);
+    b->last = ngx_cpymem(b->last, CRLF "<br>Path: ",
+                         sizeof(CRLF "<br>Path: ") - 1);
+    b->last = ngx_cpymem(b->last, r->cache->file.name.data,
+                         r->cache->file.name.len);
+    b->last = ngx_cpymem(b->last, ngx_http_cache_purge_success_page_tail,
+                         sizeof(ngx_http_cache_purge_success_page_tail) - 1);
+    b->last_buf = 1;
+
+    rc = ngx_http_send_header(r);
+    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
+       return rc;
+    }
+
+    return ngx_http_output_filter(r, &out);
 }
 
 #endif
--- src/http/modules/ngx_http_proxy_module.c.orig	Tue Nov 17 06:21:35 2009
+++ src/http/modules/ngx_http_proxy_module.c	Tue Nov 17 06:31:19 2009
@@ -7,6 +7,7 @@
 #include <ngx_config.h>
 #include <ngx_core.h>
 #include <ngx_http.h>
+#include <nginx.h>
 
 
 typedef struct ngx_http_proxy_redirect_s  ngx_http_proxy_redirect_t;
@@ -138,6 +139,9 @@
     void *conf);
 static char *ngx_http_proxy_cache_key(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
+static char *ngx_http_proxy_cache_purge(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
+static ngx_int_t ngx_http_proxy_cache_purge_handler(ngx_http_request_t *r);
 #endif
 
 static char *ngx_http_proxy_lowat_check(ngx_conf_t *cf, void *post, void *data);
@@ -392,6 +396,13 @@
       offsetof(ngx_http_proxy_loc_conf_t, upstream.cache_methods),
       &ngx_http_upstream_cache_method_mask },
 
+    { ngx_string("proxy_cache_purge"),
+      NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2,
+      ngx_http_proxy_cache_purge,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      0,
+      NULL },
+
 #endif
 
     { ngx_string("proxy_temp_path"),
@@ -2597,6 +2608,14 @@
 
     clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
 
+#if (NGX_HTTP_CACHE)
+
+    if (clcf->handler == ngx_http_proxy_cache_purge_handler) {
+        return "is incompatible with \"proxy_cache_purge\"";
+    }
+
+#endif
+
     clcf->handler = ngx_http_proxy_handler;
 
     if (clcf->name.data[clcf->name.len - 1] == '/') {
@@ -2836,7 +2855,7 @@
     if (plcf->upstream.cache != NGX_CONF_UNSET_PTR
         && plcf->upstream.cache != NULL)
     {
-        return "is incompatible with \"proxy_cache\"";
+        return "is incompatible with \"proxy_cache\" and \"proxy_cache_purge\"";
     }
 
 #endif
@@ -2879,7 +2898,7 @@
     value = cf->args->elts;
 
     if (plcf->upstream.cache != NGX_CONF_UNSET_PTR) {
-        return "is duplicate";
+        return "is either duplicate or collides with \"proxy_cache_purge\"";
     }
 
     if (ngx_strcmp(value[1].data, "off") == 0) {
@@ -2912,7 +2931,7 @@
     value = cf->args->elts;
 
     if (plcf->cache_key.value.len) {
-        return "is duplicate";
+        return "is either duplicate or collides with \"proxy_cache_purge\"";
     }
 
     ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
@@ -2926,6 +2945,144 @@
     }
 
     return NGX_CONF_OK;
+}
+
+static char *
+ngx_http_proxy_cache_purge(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_http_proxy_loc_conf_t         *plcf = conf;
+
+    ngx_http_compile_complex_value_t   ccv;
+    ngx_http_core_loc_conf_t          *clcf;
+    ngx_str_t                         *value;
+
+    /* check for duplicates / collisions */
+    if (plcf->upstream.cache != NGX_CONF_UNSET_PTR
+        && plcf->upstream.cache != NULL)
+    {
+        return "is either duplicate or collides with \"proxy_cache\"";
+    }
+
+    if (plcf->upstream.upstream || plcf->proxy_lengths) {
+        return "is incompatible with \"proxy_pass\"";
+    }
+
+    if (plcf->upstream.store > 0 || plcf->upstream.store_lengths) {
+        return "is incompatible with \"proxy_store\"";
+    }
+
+    value = cf->args->elts;
+
+    /* set proxy_cache part */
+    plcf->upstream.cache = ngx_shared_memory_add(cf, &value[1], 0,
+                                                 &ngx_http_proxy_module);
+    if (plcf->upstream.cache == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    /* set proxy_cache_key part */
+    ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
+
+    ccv.cf = cf;
+    ccv.value = &value[2];
+    ccv.complex_value = &plcf->cache_key;
+
+    if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
+    /* set handler */
+    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
+
+    clcf->handler = ngx_http_proxy_cache_purge_handler;
+
+    return NGX_CONF_OK;
+}
+
+static char ngx_http_cache_purge_success_page_top[] =
+"<html>" CRLF
+"<head><title>Successful purge</title></head>" CRLF
+"<body bgcolor=\"white\">" CRLF
+"<center><h1>Successful purge</h1>" CRLF
+;
+
+static char ngx_http_cache_purge_success_page_tail[] =
+CRLF "</center>" CRLF
+"<hr><center>" NGINX_VER "</center>" CRLF
+"</body>" CRLF
+"</html>" CRLF
+;
+
+static ngx_int_t
+ngx_http_proxy_cache_purge_handler(ngx_http_request_t *r)
+{
+    ngx_http_proxy_loc_conf_t  *plcf;
+    ngx_chain_t                 out;
+    ngx_buf_t                  *b;
+    ngx_str_t                  *key;
+    ngx_int_t                   rc;
+    size_t                      len;
+
+    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD|NGX_HTTP_DELETE))) {
+        return NGX_HTTP_NOT_ALLOWED;
+    }
+
+    plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module);
+
+    rc = ngx_http_file_cache_purge(r, plcf->upstream.cache->data,
+                                   &plcf->cache_key);
+
+    if (rc == NGX_ERROR) {
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    } else if (rc == NGX_DECLINED) {
+        return NGX_HTTP_NOT_FOUND;
+    }
+
+    key = r->cache->keys.elts;
+
+    len = sizeof(ngx_http_cache_purge_success_page_top) - 1
+          + sizeof(ngx_http_cache_purge_success_page_tail) - 1
+          + sizeof("<br>Key : ") - 1 + sizeof(CRLF "<br>Path: ") - 1
+          + key[0].len + r->cache->file.name.len;
+
+    r->headers_out.content_type.len = sizeof("text/html") - 1;
+    r->headers_out.content_type.data = (u_char *) "text/html";
+    r->headers_out.status = NGX_HTTP_OK;
+    r->headers_out.content_length_n = len;
+
+    if (r->method == NGX_HTTP_HEAD) {
+        rc = ngx_http_send_header(r);
+        if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
+            return rc;
+        }
+    }
+
+    b = ngx_create_temp_buf(r->pool, len);
+    if (b == NULL) {
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    out.buf = b;
+    out.next = NULL;
+
+    b->last = ngx_cpymem(b->last, ngx_http_cache_purge_success_page_top,
+                         sizeof(ngx_http_cache_purge_success_page_top) - 1);
+    b->last = ngx_cpymem(b->last, "<br>Key : ", sizeof("<br>Key : ") - 1);
+    b->last = ngx_cpymem(b->last, key[0].data, key[0].len);
+    b->last = ngx_cpymem(b->last, CRLF "<br>Path: ",
+                         sizeof(CRLF "<br>Path: ") - 1);
+    b->last = ngx_cpymem(b->last, r->cache->file.name.data,
+                         r->cache->file.name.len);
+    b->last = ngx_cpymem(b->last, ngx_http_cache_purge_success_page_tail,
+                         sizeof(ngx_http_cache_purge_success_page_tail) - 1);
+    b->last_buf = 1;
+
+    rc = ngx_http_send_header(r);
+    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
+       return rc;
+    }
+
+    return ngx_http_output_filter(r, &out);
 }
 
 #endif
--- src/http/ngx_http_cache.h.orig	Tue Nov 17 06:20:50 2009
+++ src/http/ngx_http_cache.h	Tue Nov 17 06:30:16 2009
@@ -134,6 +134,9 @@
 char *ngx_http_file_cache_valid_set_slot(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 
+ngx_int_t ngx_http_file_cache_purge(ngx_http_request_t *r,
+    ngx_http_file_cache_t *cache, ngx_http_complex_value_t *cache_key);
+
 extern ngx_str_t  ngx_http_cache_status[];
 
 
--- src/http/ngx_http_file_cache.c.orig	Tue Nov 17 06:21:00 2009
+++ src/http/ngx_http_file_cache.c	Tue Nov 17 07:50:00 2009
@@ -1620,6 +1620,90 @@
     return NGX_CONF_OK;
 }
 
+ngx_int_t
+ngx_http_file_cache_purge(ngx_http_request_t *r, ngx_http_file_cache_t *cache,
+    ngx_http_complex_value_t *cache_key)
+{
+    ngx_http_cache_t           *c;
+    ngx_str_t                  *key;
+    ngx_int_t                   rc;
+
+    rc = ngx_http_discard_request_body(r);
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    c = ngx_pcalloc(r->pool, sizeof(ngx_http_cache_t));
+    if (c == NULL) {
+        return NGX_ERROR;
+    }
+
+    rc = ngx_array_init(&c->keys, r->pool, 1, sizeof(ngx_str_t));
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    key = ngx_array_push(&c->keys);
+    if (key == NULL) {
+        return NGX_ERROR;
+    }
+
+    rc = ngx_http_complex_value(r, cache_key, key);
+    if (rc != NGX_OK) {
+        return NGX_ERROR;
+    }
+
+    r->cache = c;
+    c->body_start = ngx_pagesize;
+    c->file_cache = cache;
+    c->file.log = r->connection->log;
+
+    ngx_http_file_cache_create_key(r);
+
+    rc = ngx_http_file_cache_open(r);
+    if (rc == NGX_HTTP_CACHE_UPDATING || rc == NGX_HTTP_CACHE_STALE) {
+        rc = NGX_OK;
+    }
+
+    if (rc != NGX_OK) {
+        if (rc == NGX_DECLINED) {
+            return rc;
+        } else {
+            return NGX_ERROR;
+        }
+    }
+
+    /*
+     * delete file from disk but *keep* in-memory node,
+     * because other requests might still point to it.
+     */
+
+    ngx_shmtx_lock(&cache->shpool->mutex);
+
+    if (!c->node->exists) {
+        /* race between concurrent purges, backoff */
+        ngx_shmtx_unlock(&cache->shpool->mutex);
+        return NGX_DECLINED;
+    }
+
+    cache->sh->size -= (c->node->length + cache->bsize - 1) / cache->bsize;
+    c->node->exists = 0;
+
+    ngx_shmtx_unlock(&cache->shpool->mutex);
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ngx_cycle->log, 0,
+                   "http file cache purge: \"%s\"", c->file.name.data);
+
+    if (ngx_delete_file(c->file.name.data) == NGX_FILE_ERROR) {
+        /* entry in error log is enough, don't notice client */
+        ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno,
+                      ngx_delete_file_n " \"%s\" failed", c->file.name.data);
+    }
+
+    /* file deleted from cache */
+    return NGX_OK;
+}
+
 
 char *
 ngx_http_file_cache_valid_set_slot(ngx_conf_t *cf, ngx_command_t *cmd,
