mod_musicindex  1.4.1
cache-file.c
Go to the documentation of this file.
1 /*
2  * cache-file.c
3  * mod_musicindex
4  *
5  * $Id: cache-file.c 1011 2012-08-07 20:23:39Z varenet $
6  *
7  * Created by Thibaut VARENE on Wed Feb 23 2005.
8  * Copyright (c) 2003-2007,2009-2010 Thibaut VARENE
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU Lesser General Public License version 2.1,
12  * as published by the Free Software Foundation.
13  *
14  */
15 
33 #include "playlist.h"
34 #include "cache-file.h"
35 
36 #ifdef HAVE_DIRENT_H
37 #include <dirent.h> /* opendir & friends */
38 #endif
39 #include <errno.h>
40 #include <stdio.h> /* fprintf / fscanf / fileno */
41 #include <sys/file.h> /* flock */
42 #include <fcntl.h> /* open */
43 #ifdef HAVE_SYS_STAT_H
44 #include <sys/stat.h> /* file handling */
45 #endif
46 #ifdef HAVE_SYS_TYPES_H
47 #include <sys/types.h> /* file handling */
48 #endif
49 #ifdef HAVE_UNISTD_H
50 #include <unistd.h> /* file handling */
51 #endif
52 #ifdef HAVE_STRING_H
53 #include <string.h> /* strerror() */
54 #endif
55 
56 #define CACHE_VERS 12
57 #define CACHE_NF 15
58 
59 #define CA_OK 0
60 #define CA_FATAL 10
61 #define CA_MISSARG 20
62 #define CA_CREATE 30
63 #define CA_LOCKED 40
64 
66 #define ISDOT(x) ( (x)[0] == '.' && (!(x)[1] || ((x)[1] == '.' && !(x)[2])) )
67 
68 #define BLANKSTR(x) ( !(x) ? "#" : (x) )
69 
70 #define ISBLANK(x) ( ((x)[0] == '#') && ((x)[1] == '\0') )
71 
82 static void error_handler(request_rec *r, const char *const caller)
83 {
84  if (!r)
85  return;
86 
87  switch (errno) {
88  case EPERM:
89  /* The filesystem containing pathname does not support the creation of directories. */
90  mi_rerror("(%s) Can't create/delete directory.", caller);
91  break;
92 #if 0
93  case EISDIR:
94  /* pathname refers to a directory. (This is the non-POSIX value returned by Linux since 2.1.132.) */
95  case EINVAL:
96  /* mode requested creation of something other than a normal file, device special file or FIFO */
97  case EEXIST:
98  /* pathname already exists (not necessarily as a directory). */
99  case EFAULT:
100  /* pathname points outside your accessible address space. */
101 #endif
102  case EACCES:
103  /* The parent directory does not allow write permission to the process, or one of the
104  directories in pathname did not allow search (execute) permission. */
105  mi_rerror("(%s) Permission denied.", caller);
106  break;
107  case EMFILE:
108  /* Too many file descriptors in use by process. */
109  case ENFILE:
110  /* Too many files are currently open in the system. */
111  mi_rerror("(%s) Too many open files!", caller);
112  break;
113  case ENAMETOOLONG:
114  /* pathname was too long. */
115  mi_rerror("(%s) Pathname was too long.", caller);
116  break;
117  case ENOENT:
118  /* A directory component in pathname does not exist or is a dangling symbolic link. */
119  break;
120 #if 0
121  case ENOTDIR:
122  /* A component used as a directory in pathname is not, in fact, a directory. */
123  case ENOTEMPTY:
124  /* pathname contains entries other than . and .. . */
125 #endif
126  case ENOMEM:
127  /* Insufficient kernel memory was available. */
128  mi_rerror("(%s) Out Of Memory!", caller);
129  break;
130  case EROFS:
131  /* pathname refers to a file on a read-only filesystem. */
132  mi_rerror("(%s) Read-Only filesystem!", caller);
133  break;
134  case ELOOP:
135  /* Too many symbolic links were encountered in resolving pathname. */
136  mi_rerror("(%s) Too many symbolic links.", caller);
137  break;
138  case EIO:
139  /* An I/O error occured. */
140  mi_rerror("(%s) I/O error.", caller);
141  break;
142  case ENOSPC:
143  /* The device containing pathname has no room for the new directory.
144  The new directory cannot be created because the user's disk quota is exhausted. */
145  mi_rerror("(%s) No space left on device!", caller);
146  break;
147  default:
148  mi_rerror("(%s) - error_handler! %s", caller, strerror(errno));
149  break;
150  }
151  return;
152 }
153 
167 static short file_cache_make_dir(request_rec *r, const char *const dirpath)
168 {
169  register unsigned short l = 0, m = 0;
170  char *tempdir = NULL;
171  short ret = CA_FATAL;
172 
173  do { /* We build the path subdirs by subdirs, in a "mkdir -p" fashion */
174  tempdir = realloc(tempdir, (m + (l = strcspn(dirpath + m, "/")) + 1));
175  if (!tempdir)
176  goto error_out;
177 
178  /* XXX TODO make better use of realloc() by using strncat() here */
179  strncpy(tempdir, dirpath, m + l);
180  tempdir[m+l] = '\0';
181  m += l;
182 
183  if (!l)
184  break;
185 
186  /* skipping (potentially multiple) slashes */
187  while (dirpath[m] == '/')
188  m++;
189 
190  if (mkdir(tempdir, S_IRWXU)) {
191  if (errno != EEXIST)
192  goto error_out;
193  }
194  } while (1);
195 
196  ret = CA_OK;
197 
198 error_out:
199  free(tempdir);
200  if (ret == CA_FATAL) // on est susceptible d'avoir de la merde en cas d'out of mem... pas grave
201  error_handler(r, __FUNCTION__);
202  return ret;
203 }
204 
216 static void file_cache_remove_dir(request_rec *r, DIR *cachedir, const char *const curdir)
217 {
218  DIR *subdir = NULL;
219  struct dirent *cachedirent = NULL;
220  struct stat origdirstat;
221  const char *origdir = NULL;
222 
223  if (unlikely(fchdir(dirfd(cachedir))))
224  return; /* on se place dans le repertoire de cache */
225 
226  while ((cachedirent = readdir(cachedir))) { /* on parcourt le repertoire */
227  if (ISDOT(cachedirent->d_name)) /* We'd rather avoid trying to remove the whole filesystem... */
228  continue;
229 
230  if (unlink(cachedirent->d_name)) { /* We try to remove any entry (actually we will only remove regular files) */
231  if ((errno == EISDIR) || (errno == EPERM)) {
232  /* On BSDs unlink() returns EPERM on non empty directories.
233  * This shouldn't lead to infloop because of subsequent tests.
234  * If it's a directory, we check that the "original" still exists.
235  * If not, we remove it recursively.
236  * Reminder: "errno == (EISDIR || EPERM)" doesn't work */
237  origdir = apr_pstrcat(r->pool, curdir, "/", cachedirent->d_name, NULL);
238  if (stat(origdir, &origdirstat)) {
239  if (rmdir(cachedirent->d_name)) { /* stat() sets errno. We have to split */
240  if (errno == ENOTEMPTY) { /* il est pas vide, bigre! */
241  subdir = opendir(cachedirent->d_name); /* on ouvre le vilain repertoire pour en supprimer le contenu */
242  file_cache_remove_dir(r, subdir, origdir); /* en rappelant recursivement la fonction sur son contenu. */
243  closedir(subdir); /* a noter que dans ce cas la il y a un test inutile, celui qui verifie si l'original existe tjrs. Mais bon. */
244  if (fchdir(dirfd(cachedir))); /* on retourne au repertoire precedent */
245  rmdir(cachedirent->d_name); /* maintenant il est vide, et on peut pas avoir d'erreur vu les tests precedants */
246  }
247  else
248  error_handler(r, __FUNCTION__); /* Oops, on est tombe sur une merde */
249  }
250  }
251  }
252  else
253  error_handler(r, __FUNCTION__); /* Oops, on est tombe sur une merde, mais plus tot */
254  }
255  }
256 
257  return;
258 }
259 
280 static void* cache_file_opendir(request_rec *r, mu_pack *const pack,
281  const char * const filename, const char * uri, unsigned long soptions)
282 {
283  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
284  DIR *cachedir = NULL;
285  struct stat cachedirstat, dirstat;
286 
287  if (!filename || !conf->cache_setup)
288  return NULL;
289 
290  /* Bear in mind we're chdir'd from now on. */
291  if (unlikely(chdir((char *)(conf->cache_setup))))
292  return NULL;
293 
294  /* Actually check for the directory in the cache, create it if needed.
295  * "+ 1" offset to suppress leading '/'. */
296  if (!(cachedir = opendir(filename + 1))) { /* on essaye d'ouvrir le repertoire concerne dans le cache (on supprime le leading "/" */
297  if (errno == ENOENT) { /* il n'existe pas mais on peut le creer (ca correspond a ENOENT, a verifier) */
298  if (file_cache_make_dir(r, filename + 1)) /* on envoie le chemin prive du leading '/' */
299  goto error_out;
300  }
301  else
302  goto error_out; /* un autre probleme, on degage */
303  }
304  else { /* Checking for cache sanity. Has it expired for that folder ? If so, delete its content. */
305  fstat(dirfd(cachedir), &cachedirstat); /* recuperons les stats du repertoire cache. XXX On considere cet appel sans echec vu les tests qu'on a fait avant. */
306  stat(filename, &dirstat); /* recuperons les stats du rep d'origine. XXX pas de test ici, a priori ya pas de raison qu'on puisse pas les recuperer */
307  if (cachedirstat.st_mtime < dirstat.st_mtime) /* si la date de modif du rep de cache est plus vieille que celle du rep original, alors qqc a ete ajoute ou retire ou ecrit */
308  file_cache_remove_dir(r, cachedir, filename); /* alors on le vide proprement de son contenu */
309  closedir(cachedir); /* On en a fini avec le repertoire, on le referme */
310  if (file_cache_make_dir(r, filename + 1)) /* on recree le rep */
311  goto error_out;
312  }
313 
314  return NULL;
315 
316 error_out:
317  error_handler(r, __FUNCTION__);
318  return NULL;
319 }
320 
327 };
328 
345 static mu_ent *file_make_cache_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
346  const char *const filename)
347 {
348  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
349  mu_ent *p = NULL;
350  short result = 0;
351  unsigned short track, posn, flags, cvers = 0;
352  signed short filetype;
353  int fdesc;
354  FILE *cache_file = NULL;
355  struct mi_data_buffer *data_buffer = NULL;
356 
357  /* Bear in mind we're chdir'd from now on. */
358  if (unlikely(chdir((char *)(conf->cache_setup))))
359  return p;
360 
361  /* Actually check for the file in the cache, open it if possible.
362  * "+ 1" offset to suppress leading '/'.
363  * Dev note: O_SHLOCK is BSD specific */
364  fdesc = open(filename + 1, O_RDONLY|O_NONBLOCK);
365  if (unlikely(fdesc < 0)) {
366  if (likely((errno == ENOENT) || (errno == EWOULDBLOCK) || (errno == EAGAIN)))
367  return p; /* Creation of the file is handled separately (playlist.c) */
368  else
369  goto error_out; /* game over */
370  }
371 
372  /* We acquire a shared advisory lock on the file to be (almost) certain of its integrity.
373  * This will prevent reading from incomplete cache files. The lock in non blocking:
374  * if we can't get it, we won't wait to read the file, we'll delegate to the original handler. */
375  if (flock(fdesc, LOCK_SH|LOCK_NB)) {
376  close(fdesc);
377  return p;
378  }
379 
380  cache_file = fdopen(fdesc, "r");
381  if (unlikely(!cache_file))
382  goto error_out;
383 
384  /* Dev note: mixing unix and std IO is ugly, but there's no flockfile() counterpart to the shared
385  * advisory lock, alas, see flockfile(3), flock(2) and lockf(3). Besides, we have to lock, since
386  * fread()/fwrite() are thread safe, but not fscanf() and fprintf() */
387 
388  p = NEW_ENT(pool);
389  if (likely(p)) {
390  data_buffer = (struct mi_data_buffer *)malloc(sizeof(struct mi_data_buffer)); /* This should save some memory */
391  if (likely(data_buffer)) {
392  result = fscanf(cache_file, "album: %[^\n]\nartist: %[^\n]\n"
393  "title: %[^\n]\ndate: %hu\ntrack: %hu\nposn: %hu\n"
394  "length: %hu\nbitrate: %lu\nfreq: %hu\nsize: %lu\n"
395  "filetype: %hi\ngenre: %[^\n]\nmtime: %lu\nflags: %hx\n"
396  "cvers: %hu\n",
397  data_buffer->album, data_buffer->artist, data_buffer->title, &p->date, &track, &posn, &p->length,
398  &p->bitrate, &p->freq, &p->size, &filetype, data_buffer->genre, &p->mtime, &flags, &cvers);
399 
400  /* Check whether the cache is somehow corrupted */
401  if (unlikely((result != CACHE_NF) || (cvers != CACHE_VERS))) { /* fscanf() returns the number of input items assigned */
402  p = NULL; /* hopefuly p allocs should be cleaned by apache */
403  }
404  else {
405  p->title = apr_pstrdup(pool, data_buffer->title);
406  if (!ISBLANK(data_buffer->album))
407  p->album = apr_pstrdup(pool, data_buffer->album);
408  if (!ISBLANK(data_buffer->artist))
409  p->artist = apr_pstrdup(pool, data_buffer->artist);
410  if (!ISBLANK(data_buffer->genre))
411  p->genre = apr_pstrdup(pool, data_buffer->genre);
412 
413  /* We have to use that trick, fscanf won't work on char variables */
414  p->filetype = filetype;
415  p->flags = flags;
416  p->track = track;
417  p->posn = posn;
418  }
419 
420  free(data_buffer);
421  }
422  else
423  p = NULL; /* something failed, return non-bogus data. p will be cleaned up by apache's GC */
424  }
425 
426  /* fclose() will also close() fdesc and thus release the lock */
427  fclose(cache_file);
428 
429  if (likely(p))
430  fclose(in); /* this part of the cache subsystem is (uglily) seen as part of the playlist system,
431  and has to behave as such. This is why we close the input file if we took advantage of it. */
432 
433  return p;
434 
435 error_out:
436  error_handler(r, __FUNCTION__);
437  close(fdesc);
438  return p;
439 }
440 
451 static void cache_file_write(request_rec *r, const mu_ent *const p, const char *const filename)
452 {
453  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
454  int fdesc;
455  FILE *cache_file = NULL;
456 
457  /* we don't deal with directories themselves */
458  if (p->filetype < 0)
459  return;
460 
461  if (chdir((char *)conf->cache_setup))
462  return;
463 
464  /* Dev note: O_EXLOCK is BSD specific. */
465  fdesc = open(filename + 1, O_WRONLY|O_NONBLOCK|O_CREAT, S_IRUSR|S_IWUSR);
466  if (fdesc < 0) {
467  if ((errno == EWOULDBLOCK) || (errno == EAGAIN)) /* does this work? */
468  return; /* Creation of the file is handled separately (playlist.c) */
469  else
470  goto error_out; /* game over */
471  }
472 
473  /* We acquire an exclusive advisory lock on the file to avoid corruption by another process.
474  * This will also prevent reading from incomplete cache. see cache_read_file() comments. */
475  if (flock(fdesc, LOCK_EX|LOCK_NB)) {
476  fclose(cache_file);
477  return;
478  }
479 
480  cache_file = fdopen(fdesc, "w"); /* now open the fdesc with stdio routines */
481 
482  /* let's check if something bad happened */
483  if (!cache_file)
484  goto error_out;
485 
486  fprintf(cache_file, "album: %s\nartist: %s\ntitle: %s\ndate: %hu\n"
487  "track: %hhu\nposn: %hhu\nlength: %hu\nbitrate: %lu\nfreq: %hu\n"
488  "size: %lu\nfiletype: %hi\ngenre: %s\nmtime: %lu\nflags: %hhx\ncvers: %hu\n",
489  BLANKSTR(p->album), BLANKSTR(p->artist), p->title, p->date,
490  p->track, p->posn, p->length, p->bitrate, p->freq, p->size, p->filetype,
492 
493  /* fclose() will also close() fdesc and thus release the lock */
494  fclose(cache_file);
495 
496  return;
497 
498 error_out:
499  error_handler(r, __FUNCTION__);
500  close(fdesc);
501 }
502 
505  .readdir = NULL,
506  .closedir = NULL,
507  .make_entry = file_make_cache_entry,
508  .write = cache_file_write,
509  .prologue = NULL,
510  .epilogue = NULL,
511 };
512 
513 int cache_file_setup(cmd_parms *cmd, const char *const setup_string, mu_config *const conf)
514 {
515  server_rec *s = cmd->server;
516  static const char biniou[] = "file://";
517  int ret = 1;
518 
519  if (strncmp(biniou, setup_string, 7) == 0) {
520  ret = -1;
521  char *restrict csetup = apr_pstrdup(cmd->pool, setup_string+6);
522  if (!csetup)
523  goto exit;
524 #if 0 /* this is never happening since we check against "file://" */
525  if (csetup[0] != '/') {
526  /* for now we only work with absolute paths */
527  mi_serror("Non absolute cache directory path: %s", csetup);
528  goto exit;
529  }
530 #endif
531  if ( (access(csetup, X_OK|W_OK)) || (chdir((char *)(csetup))) ) {
532  mi_serror("%s", strerror(errno));
533  goto exit;
534  }
535  conf->cache_setup = csetup;
536  conf->cache = &cache_backend_file;
537  ret = 0;
538  }
539 
540 exit:
541  if (-1 == ret)
542  mi_serror("Error setting up %s cache!", "file");
543  return ret;
544 }