[stat_cache] separate symlink pol from data struct
[lighttpd.git] / src / stat_cache.c
blob42cab1c03ac6612b6de112ef0461bb4959431bcf
1 #include "first.h"
3 #include "stat_cache.h"
4 #include "base.h"
5 #include "log.h"
6 #include "fdevent.h"
7 #include "etag.h"
8 #include "splaytree.h"
10 #include <sys/types.h>
11 #include <sys/stat.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <errno.h>
16 #include <unistd.h>
17 #include <fcntl.h>
19 #ifdef HAVE_ATTR_ATTRIBUTES_H
20 # include <attr/attributes.h>
21 #endif
23 #ifdef HAVE_SYS_EXTATTR_H
24 # include <sys/extattr.h>
25 #endif
27 #ifndef HAVE_LSTAT
28 # define lstat stat
29 #endif
32 * stat-cache
34 * we cache the stat() calls in our own storage
35 * the directories are cached in FAM
37 * if we get a change-event from FAM, we increment the version in the FAM->dir mapping
39 * if the stat()-cache is queried we check if the version id for the directory is the
40 * same and return immediatly.
43 * What we need:
45 * - for each stat-cache entry we need a fast indirect lookup on the directory name
46 * - for each FAMRequest we have to find the version in the directory cache (index as userdata)
48 * stat <<-> directory <-> FAMRequest
50 * if file is deleted, directory is dirty, file is rechecked ...
51 * if directory is deleted, directory mapping is removed
53 * */
55 /* the directory name is too long to always compare on it
56 * - we need a hash
57 * - the hash-key is used as sorting criteria for a tree
58 * - a splay-tree is used as we can use the caching effect of it
61 /* we want to cleanup the stat-cache every few seconds, let's say 10
63 * - remove entries which are outdated since 30s
64 * - remove entries which are fresh but havn't been used since 60s
65 * - if we don't have a stat-cache entry for a directory, release it from the monitor
69 enum {
70 STAT_CACHE_ENGINE_UNSET,
71 STAT_CACHE_ENGINE_NONE,
72 STAT_CACHE_ENGINE_SIMPLE,
73 STAT_CACHE_ENGINE_FAM
76 #ifdef HAVE_FAM_H
77 struct stat_cache_fam;
78 #endif
80 typedef struct stat_cache {
81 splay_tree *files; /* the nodes of the tree are stat_cache_entry's */
82 #ifdef HAVE_FAM_H
83 struct stat_cache_fam *scf;
84 #endif
85 } stat_cache;
88 /* the famous DJB hash function for strings */
89 static uint32_t djbhash(const char *str, const size_t len)
91 const unsigned char * const s = (const unsigned char *)str;
92 uint32_t hash = 5381;
93 for (size_t i = 0; i < len; ++i) hash = ((hash << 5) + hash) ^ s[i];
94 return hash;
98 static uint32_t hashme(const char *str, const size_t len)
100 /* strip highest bit of hash value for splaytree */
101 return djbhash(str,len) & ~(((uint32_t)1) << 31);
105 #ifdef HAVE_FAM_H
107 #include <fam.h>
109 typedef struct {
110 FAMRequest *req;
111 buffer *name;
112 int version;
113 } fam_dir_entry;
115 typedef struct stat_cache_fam {
116 splay_tree *dirs; /* the nodes of the tree are fam_dir_entry */
118 FAMConnection fam;
120 int dir_ndx;
121 fam_dir_entry *fam_dir;
122 buffer *dir_name; /* for building the dirname from the filename */
123 fdnode *fdn;
124 int fd;
125 } stat_cache_fam;
127 static fam_dir_entry * fam_dir_entry_init(void) {
128 fam_dir_entry *fam_dir = NULL;
130 fam_dir = calloc(1, sizeof(*fam_dir));
131 force_assert(NULL != fam_dir);
133 fam_dir->name = buffer_init();
135 return fam_dir;
138 static void fam_dir_entry_free(FAMConnection *fc, void *data) {
139 fam_dir_entry *fam_dir = data;
141 if (!fam_dir) return;
143 FAMCancelMonitor(fc, fam_dir->req);
145 buffer_free(fam_dir->name);
146 free(fam_dir->req);
148 free(fam_dir);
151 static handler_t stat_cache_handle_fdevent(server *srv, void *_fce, int revent) {
152 size_t i;
153 stat_cache_fam *scf = srv->stat_cache->scf;
154 size_t events;
156 UNUSED(_fce);
157 /* */
159 if (revent & FDEVENT_IN) {
160 events = FAMPending(&scf->fam);
162 for (i = 0; i < events; i++) {
163 FAMEvent fe;
164 fam_dir_entry *fam_dir;
165 splay_tree *node;
166 int ndx;
168 FAMNextEvent(&scf->fam, &fe);
170 /* handle event */
172 switch(fe.code) {
173 case FAMChanged:
174 case FAMDeleted:
175 case FAMMoved:
176 /* if the filename is a directory remove the entry */
178 fam_dir = fe.userdata;
179 fam_dir->version++;
181 /* file/dir is still here */
182 if (fe.code == FAMChanged) break;
184 ndx = hashme(fe.filename, strlen(fe.filename));
186 scf->dirs = splaytree_splay(scf->dirs, ndx);
187 node = scf->dirs;
189 if (node && (node->key == ndx)) {
190 int osize = splaytree_size(scf->dirs);
192 fam_dir_entry_free(&scf->fam, node->data);
193 scf->dirs = splaytree_delete(scf->dirs, ndx);
195 force_assert(osize - 1 == splaytree_size(scf->dirs));
197 break;
198 default:
199 break;
204 if (revent & (FDEVENT_HUP|FDEVENT_RDHUP)) {
205 /* fam closed the connection */
206 fdevent_fdnode_event_del(srv->ev, scf->fdn);
207 fdevent_unregister(srv->ev, scf->fd);
208 scf->fdn = NULL;
210 FAMClose(&scf->fam);
211 scf->fd = -1;
214 return HANDLER_GO_ON;
217 static stat_cache_fam * stat_cache_init_fam(server *srv) {
218 stat_cache_fam *scf = calloc(1, sizeof(*scf));
219 scf->dir_name = buffer_init();
220 scf->fd = -1;
222 /* setup FAM */
223 if (0 != FAMOpen2(&scf->fam, "lighttpd")) {
224 log_error_write(srv, __FILE__, __LINE__, "s",
225 "could not open a fam connection, dieing.");
226 return NULL;
228 #ifdef HAVE_FAMNOEXISTS
229 FAMNoExists(&scf->fam);
230 #endif
232 scf->fd = FAMCONNECTION_GETFD(&scf->fam);
233 fdevent_setfd_cloexec(scf->fd);
234 scf->fdn = fdevent_register(srv->ev, scf->fd, stat_cache_handle_fdevent, NULL);
235 fdevent_fdnode_event_set(srv->ev, scf->fdn, FDEVENT_IN | FDEVENT_RDHUP);
237 return scf;
240 static void stat_cache_free_fam(stat_cache_fam *scf) {
241 if (NULL == scf) return;
242 buffer_free(scf->dir_name);
244 while (scf->dirs) {
245 int osize;
246 splay_tree *node = scf->dirs;
248 osize = scf->dirs->size;
250 fam_dir_entry_free(&scf->fam, node->data);
251 scf->dirs = splaytree_delete(scf->dirs, node->key);
253 if (osize == 1) {
254 force_assert(NULL == scf->dirs);
255 } else {
256 force_assert(osize == (scf->dirs->size + 1));
260 if (-1 != scf->fd) {
261 /*scf->fdn already cleaned up in fdevent_free()*/
262 FAMClose(&scf->fam);
263 /*scf->fd = -1;*/
266 free(scf);
269 static int buffer_copy_dirname(buffer *dst, const buffer *file) {
270 size_t i;
272 if (buffer_string_is_empty(file)) return -1;
274 for (i = buffer_string_length(file); i > 0; i--) {
275 if (file->ptr[i] == '/') {
276 buffer_copy_string_len(dst, file->ptr, i);
277 return 0;
281 return -1;
284 static handler_t stat_cache_fam_dir_check(server *srv, stat_cache_fam *scf, stat_cache_entry *sce, const buffer *name) {
285 if (0 != buffer_copy_dirname(scf->dir_name, name)) {
286 log_error_write(srv, __FILE__, __LINE__, "sb",
287 "no '/' found in filename:", name);
288 return HANDLER_ERROR;
291 scf->dir_ndx = hashme(CONST_BUF_LEN(scf->dir_name));
293 scf->dirs = splaytree_splay(scf->dirs, scf->dir_ndx);
295 if ((NULL != scf->dirs) && (scf->dirs->key == scf->dir_ndx)) {
296 scf->fam_dir = scf->dirs->data;
298 /* check whether we got a collision */
299 if (buffer_is_equal(scf->dir_name, scf->fam_dir->name)) {
300 /* test whether a found file cache entry is still ok */
301 if ((NULL != sce) && (scf->fam_dir->version == sce->dir_version)) {
302 /* the stat()-cache entry is still ok */
303 return HANDLER_FINISHED;
305 } else {
306 /* hash collision, forget about the entry */
307 scf->fam_dir = NULL;
309 } else {
310 scf->fam_dir = NULL;
313 return HANDLER_GO_ON;
316 static void stat_cache_fam_dir_monitor(server *srv, stat_cache_fam *scf, stat_cache_entry *sce, const buffer *name) {
317 /* is this directory already registered ? */
318 fam_dir_entry *fam_dir = scf->fam_dir;
319 if (NULL == fam_dir) {
320 fam_dir = fam_dir_entry_init();
322 buffer_copy_buffer(fam_dir->name, scf->dir_name);
324 fam_dir->version = 1;
326 fam_dir->req = calloc(1, sizeof(FAMRequest));
327 force_assert(NULL != fam_dir);
329 if (0 != FAMMonitorDirectory(&scf->fam, fam_dir->name->ptr,
330 fam_dir->req, fam_dir)) {
332 log_error_write(srv, __FILE__, __LINE__, "sbsbs",
333 "monitoring dir failed:",
334 fam_dir->name,
335 "file:", name,
336 FamErrlist[FAMErrno]);
338 fam_dir_entry_free(&scf->fam, fam_dir);
339 fam_dir = NULL;
340 } else {
341 int osize = splaytree_size(scf->dirs);
343 /* already splayed scf->dir_ndx */
344 if ((NULL != scf->dirs) && (scf->dirs->key == scf->dir_ndx)) {
345 /* hash collision: replace old entry */
346 fam_dir_entry_free(&scf->fam, scf->dirs->data);
347 scf->dirs->data = fam_dir;
348 } else {
349 scf->dirs = splaytree_insert(scf->dirs, scf->dir_ndx, fam_dir);
350 force_assert(osize == (splaytree_size(scf->dirs) - 1));
353 force_assert(scf->dirs);
354 force_assert(scf->dirs->data == fam_dir);
355 scf->fam_dir = fam_dir;
359 /* bind the fam_fc to the stat() cache entry */
361 if (fam_dir) {
362 sce->dir_version = fam_dir->version;
366 #endif
369 stat_cache *stat_cache_init(server *srv) {
370 stat_cache *sc = NULL;
371 UNUSED(srv);
373 sc = calloc(1, sizeof(*sc));
374 force_assert(NULL != sc);
376 #ifdef HAVE_FAM_H
377 if (STAT_CACHE_ENGINE_FAM == srv->srvconf.stat_cache_engine) {
378 sc->scf = stat_cache_init_fam(srv);
379 if (NULL == sc->scf) {
380 free(sc);
381 return NULL;
384 #endif
386 return sc;
389 static stat_cache_entry * stat_cache_entry_init(void) {
390 stat_cache_entry *sce = NULL;
392 sce = calloc(1, sizeof(*sce));
393 force_assert(NULL != sce);
395 sce->name = buffer_init();
396 sce->etag = buffer_init();
397 sce->content_type = buffer_init();
399 return sce;
402 static void stat_cache_entry_free(void *data) {
403 stat_cache_entry *sce = data;
404 if (!sce) return;
406 buffer_free(sce->etag);
407 buffer_free(sce->name);
408 buffer_free(sce->content_type);
410 free(sce);
413 void stat_cache_free(stat_cache *sc) {
414 while (sc->files) {
415 int osize;
416 splay_tree *node = sc->files;
418 osize = sc->files->size;
420 stat_cache_entry_free(node->data);
421 sc->files = splaytree_delete(sc->files, node->key);
423 force_assert(osize - 1 == splaytree_size(sc->files));
426 #ifdef HAVE_FAM_H
427 stat_cache_free_fam(sc->scf);
428 #endif
429 free(sc);
432 int stat_cache_choose_engine (server *srv, const buffer *stat_cache_string) {
433 if (buffer_string_is_empty(stat_cache_string)) {
434 srv->srvconf.stat_cache_engine = STAT_CACHE_ENGINE_SIMPLE;
435 } else if (buffer_is_equal_string(stat_cache_string, CONST_STR_LEN("simple"))) {
436 srv->srvconf.stat_cache_engine = STAT_CACHE_ENGINE_SIMPLE;
437 #ifdef HAVE_FAM_H
438 } else if (buffer_is_equal_string(stat_cache_string, CONST_STR_LEN("fam"))) {
439 srv->srvconf.stat_cache_engine = STAT_CACHE_ENGINE_FAM;
440 #endif
441 } else if (buffer_is_equal_string(stat_cache_string, CONST_STR_LEN("disable"))) {
442 srv->srvconf.stat_cache_engine = STAT_CACHE_ENGINE_NONE;
443 } else {
444 log_error_write(srv, __FILE__, __LINE__, "sb",
445 "server.stat-cache-engine can be one of \"disable\", \"simple\","
446 #ifdef HAVE_FAM_H
447 " \"fam\","
448 #endif
449 " but not:", stat_cache_string);
450 return -1;
452 return 0;
455 #if defined(HAVE_XATTR)
456 static int stat_cache_attr_get(buffer *buf, char *name, char *xattrname) {
457 int attrlen;
458 int ret;
460 buffer_string_prepare_copy(buf, 1023);
461 attrlen = buf->size - 1;
462 if(0 == (ret = attr_get(name, xattrname, buf->ptr, &attrlen, 0))) {
463 buffer_commit(buf, attrlen);
465 return ret;
467 #elif defined(HAVE_EXTATTR)
468 static int stat_cache_attr_get(buffer *buf, char *name, char *xattrname) {
469 ssize_t attrlen;
471 buffer_string_prepare_copy(buf, 1023);
473 if (-1 != (attrlen = extattr_get_file(name, EXTATTR_NAMESPACE_USER, xattrname, buf->ptr, buf->size - 1))) {
474 buf->used = attrlen + 1;
475 buf->ptr[attrlen] = '\0';
476 return 0;
478 return -1;
480 #endif
482 const buffer * stat_cache_mimetype_by_ext(const connection *con, const char *name, size_t nlen)
484 const char *end = name + nlen; /*(end of string)*/
485 const size_t used = con->conf.mimetypes->used;
486 if (used < 16) {
487 for (size_t i = 0; i < used; ++i) {
488 /* suffix match */
489 const data_string *ds = (data_string *)con->conf.mimetypes->data[i];
490 const size_t klen = buffer_string_length(ds->key);
491 if (klen <= nlen && 0 == strncasecmp(end-klen, ds->key->ptr, klen))
492 return ds->value;
495 else {
496 const char *s;
497 const data_string *ds;
498 if (nlen) {
499 for (s = end-1; s != name && *s != '/'; --s) ; /*(like memrchr())*/
500 if (*s == '/') ++s;
502 else {
503 s = name;
505 /* search for basename, then longest .ext2.ext1, then .ext1, then "" */
506 ds = (data_string *)array_get_element_klen(con->conf.mimetypes, s, end - s);
507 if (NULL != ds) return ds->value;
508 while (++s < end) {
509 while (*s != '.' && ++s != end) ;
510 if (s == end) break;
511 /* search ".ext" then "ext" */
512 ds = (data_string *)array_get_element_klen(con->conf.mimetypes, s, end - s);
513 if (NULL != ds) return ds->value;
514 /* repeat search without leading '.' to handle situation where
515 * admin configured mimetype.assign keys without leading '.' */
516 if (++s < end) {
517 if (*s == '.') { --s; continue; }
518 ds = (data_string *)array_get_element_klen(con->conf.mimetypes, s, end - s);
519 if (NULL != ds) return ds->value;
522 /* search for ""; catchall */
523 ds = (data_string *)array_get_element(con->conf.mimetypes, "");
524 if (NULL != ds) return ds->value;
527 return NULL;
530 const buffer * stat_cache_content_type_get(server *srv, connection *con, const buffer *name, stat_cache_entry *sce)
532 /*(invalid caching if user config has multiple, different
533 * con->conf.mimetypes for same extension (not expected))*/
534 if (!buffer_string_is_empty(sce->content_type)) return sce->content_type;
536 if (S_ISREG(sce->st.st_mode)) {
537 /* determine mimetype */
538 buffer_clear(sce->content_type);
539 #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR)
540 if (con->conf.use_xattr) {
541 stat_cache_attr_get(sce->content_type, name->ptr, srv->srvconf.xattr_name->ptr);
543 #else
544 UNUSED(srv);
545 #endif
546 /* xattr did not set a content-type. ask the config */
547 if (buffer_string_is_empty(sce->content_type)) {
548 const buffer *type = stat_cache_mimetype_by_ext(con, CONST_BUF_LEN(name));
549 if (NULL != type) {
550 buffer_copy_buffer(sce->content_type, type);
553 return sce->content_type;
556 return NULL;
559 const buffer * stat_cache_etag_get(stat_cache_entry *sce, etag_flags_t flags) {
560 /*(invalid caching if user config has multiple, different con->etag_flags
561 * for same path (not expected, since etag flags should be by filesystem))*/
562 if (!buffer_string_is_empty(sce->etag)) return sce->etag;
564 if (S_ISREG(sce->st.st_mode) || S_ISDIR(sce->st.st_mode)) {
565 etag_create(sce->etag, &sce->st, flags);
566 return sce->etag;
569 return NULL;
573 /***
577 * returns:
578 * - HANDLER_FINISHED on cache-miss (don't forget to reopen the file)
579 * - HANDLER_ERROR on stat() failed -> see errno for problem
582 handler_t stat_cache_get_entry(server *srv, connection *con, buffer *name, stat_cache_entry **ret_sce) {
583 stat_cache_entry *sce = NULL;
584 stat_cache *sc;
585 struct stat st;
586 int fd;
587 int file_ndx;
588 UNUSED(con);
590 *ret_sce = NULL;
593 * check if the directory for this file has changed
596 sc = srv->stat_cache;
598 file_ndx = hashme(CONST_BUF_LEN(name));
599 sc->files = splaytree_splay(sc->files, file_ndx);
601 if (sc->files && (sc->files->key == file_ndx)) {
602 /* we have seen this file already and
603 * don't stat() it again in the same second */
605 sce = sc->files->data;
607 /* check if the name is the same, we might have a collision */
609 if (buffer_is_equal(name, sce->name)) {
610 if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_SIMPLE) {
611 if (sce->stat_ts == srv->cur_ts) {
612 *ret_sce = sce;
613 return HANDLER_GO_ON;
616 } else {
617 /* collision, forget about the entry */
618 sce = NULL;
622 #ifdef HAVE_FAM_H
623 /* dir-check */
624 if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_FAM) {
625 switch (stat_cache_fam_dir_check(srv, sc->scf, sce, name)) {
626 case HANDLER_GO_ON:
627 break;
628 case HANDLER_FINISHED:
629 *ret_sce = sce;
630 return HANDLER_GO_ON;
631 case HANDLER_ERROR:
632 default:
633 return HANDLER_ERROR;
636 #endif
639 * *lol*
640 * - open() + fstat() on a named-pipe results in a (intended) hang.
641 * - stat() if regular file + open() to see if we can read from it is better
643 * */
644 if (-1 == stat(name->ptr, &st)) {
645 return HANDLER_ERROR;
649 if (S_ISREG(st.st_mode)) {
650 /* fix broken stat/open for symlinks to reg files with appended slash on freebsd,osx */
651 if (name->ptr[buffer_string_length(name) - 1] == '/') {
652 errno = ENOTDIR;
653 return HANDLER_ERROR;
656 /* try to open the file to check if we can read it */
657 #ifdef O_NONBLOCK
658 fd = open(name->ptr, O_RDONLY | O_NONBLOCK, 0);
659 #else
660 fd = open(name->ptr, O_RDONLY, 0);
661 #endif
662 if (-1 == fd) {
663 return HANDLER_ERROR;
665 close(fd);
668 if (NULL == sce) {
670 sce = stat_cache_entry_init();
671 buffer_copy_buffer(sce->name, name);
673 /* already splayed file_ndx */
674 if ((NULL != sc->files) && (sc->files->key == file_ndx)) {
675 /* hash collision: replace old entry */
676 stat_cache_entry_free(sc->files->data);
677 sc->files->data = sce;
678 } else {
679 int osize = splaytree_size(sc->files);
681 sc->files = splaytree_insert(sc->files, file_ndx, sce);
682 force_assert(osize + 1 == splaytree_size(sc->files));
684 force_assert(sc->files);
685 force_assert(sc->files->data == sce);
687 } else {
689 buffer_clear(sce->etag);
690 #if defined(HAVE_XATTR) || defined(HAVE_EXTATTR)
691 buffer_clear(sce->content_type);
692 #endif
696 sce->st = st;
697 sce->stat_ts = srv->cur_ts;
699 #ifdef HAVE_FAM_H
700 if (srv->srvconf.stat_cache_engine == STAT_CACHE_ENGINE_FAM) {
701 stat_cache_fam_dir_monitor(srv, sc->scf, sce, name);
703 #endif
705 *ret_sce = sce;
707 return HANDLER_GO_ON;
710 int stat_cache_path_contains_symlink(server *srv, buffer *name) {
711 /* caller should check for symlinks only if we should block symlinks. */
713 /* catch the obvious symlinks
715 * this is not a secure check as we still have a race-condition between
716 * the stat() and the open. We can only solve this by
717 * 1. open() the file
718 * 2. fstat() the fd
720 * and keeping the file open for the rest of the time. But this can
721 * only be done at network level.
722 * */
724 #ifdef HAVE_LSTAT
725 /* we assume "/" can not be symlink,
726 * so skip the symlink stuff if path is "/" */
727 size_t len = buffer_string_length(name);
728 force_assert(0 != len);
729 force_assert(name->ptr[0] == '/');
730 if (1 == len) return 0;
731 #ifndef PATH_MAX
732 #define PATH_MAX 4096
733 #endif
734 if (len >= PATH_MAX) return -1;
736 char buf[PATH_MAX];
737 memcpy(buf, name->ptr, len);
738 char *s_cur = buf+len;
739 do {
740 *s_cur = '\0';
741 struct stat st;
742 if (0 == lstat(buf, &st)) {
743 if (S_ISLNK(st.st_mode)) return 1;
745 else {
746 log_error_write(srv, __FILE__, __LINE__, "sss",
747 "lstat failed for:", buf, strerror(errno));
748 return -1;
750 } while ((s_cur = strrchr(buf, '/')) != buf);
751 #endif
753 return 0;
756 int stat_cache_open_rdonly_fstat (buffer *name, struct stat *st, int symlinks) {
757 /*(Note: O_NOFOLLOW affects only the final path segment, the target file,
758 * not any intermediate symlinks along the path)*/
759 const int fd = fdevent_open_cloexec(name->ptr, symlinks, O_RDONLY, 0);
760 if (fd >= 0) {
761 if (0 == fstat(fd, st)) {
762 return fd;
763 } else {
764 close(fd);
767 return -1;
771 * remove stat() from cache which havn't been stat()ed for
772 * more than 10 seconds
775 * walk though the stat-cache, collect the ids which are too old
776 * and remove them in a second loop
779 static int stat_cache_tag_old_entries(server *srv, splay_tree *t, int *keys, size_t *ndx) {
780 stat_cache_entry *sce;
782 if (!t) return 0;
784 stat_cache_tag_old_entries(srv, t->left, keys, ndx);
785 stat_cache_tag_old_entries(srv, t->right, keys, ndx);
787 sce = t->data;
789 if (srv->cur_ts - sce->stat_ts > 2) {
790 keys[(*ndx)++] = t->key;
793 return 0;
796 int stat_cache_trigger_cleanup(server *srv) {
797 stat_cache *sc;
798 size_t max_ndx = 0, i;
799 int *keys;
801 sc = srv->stat_cache;
803 if (!sc->files) return 0;
805 keys = calloc(1, sizeof(int) * sc->files->size);
806 force_assert(NULL != keys);
808 stat_cache_tag_old_entries(srv, sc->files, keys, &max_ndx);
810 for (i = 0; i < max_ndx; i++) {
811 int ndx = keys[i];
812 splay_tree *node;
814 sc->files = splaytree_splay(sc->files, ndx);
816 node = sc->files;
818 if (node && (node->key == ndx)) {
819 stat_cache_entry_free(node->data);
820 sc->files = splaytree_delete(sc->files, ndx);
824 free(keys);
826 return 0;