s3: OneFS bulk directory enumeration support
[ira/wip.git] / source3 / modules / onefs_dir.c
1 /*
2  * Unix SMB/CIFS implementation.
3  *
4  * Support for OneFS bulk directory enumeration API
5  *
6  * Copyright (C) Steven Danneman, 2009
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, see <http://www.gnu.org/licenses/>.
20  */
21
22 #include "onefs.h"
23
24 #include <ifs/ifs_syscalls.h>
25
26 /* The OneFS filesystem provides a readdirplus() syscall, equivalent to the
27  * NFSv3 PDU, which retrieves bulk directory listings with stat information
28  * in a single syscall.
29  *
30  * This file hides this bulk interface underneath Samba's very POSIX like
31  * opendir/readdir/telldir VFS interface.  This is done to provide a
32  * significant performance improvement when listing the contents of large
33  * directories, which also require file meta information. ie a typical
34  * Windows Explorer request.
35  */
36
37 #define RDP_RESUME_KEY_START 0x1
38
39 #define RDP_BATCH_SIZE 128
40 #define RDP_DIRENTRIES_SIZE ((size_t)(RDP_BATCH_SIZE * sizeof(struct dirent)))
41
42 static char *rdp_direntries = NULL;
43 static SMB_STRUCT_STAT *rdp_stats = NULL;
44 static uint64_t *rdp_cookies = NULL;
45
46 struct rdp_dir_state {
47         struct rdp_dir_state *next, *prev;
48         SMB_STRUCT_DIR *dirp;
49         char *direntries_cursor; /* cursor to current direntry in the cache */
50         size_t stat_count;       /* number of entries stored in the cache */
51         size_t stat_cursor;      /* cursor to current stat in the cache */
52         uint64_t resume_cookie;  /* last cookie returned from the cache */
53         long location;           /* absolute location of direnty in DIR */
54 };
55
56 static struct rdp_dir_state *dirstatelist = NULL;
57
58 SMB_STRUCT_DIR *rdp_last_dirp = NULL;
59
60 /**
61  * Given a DIR pointer, return our internal state.
62  *
63  * This function also tells us whether the given DIR is the same as we saw
64  * during the last call.  Because we use a single globally allocated buffer
65  * for readdirplus entries we must check every call into this API to see if
66  * it's for the same directory listing, or a new one. If it's the same we can
67  * maintain our current cached entries, otherwise we must go to the kernel.
68  *
69  * @return 0 on success, 1 on failure
70  */
71 static int
72 rdp_retrieve_dir_state(SMB_STRUCT_DIR *dirp, struct rdp_dir_state **dir_state,
73                        bool *same_as_last)
74 {
75         struct rdp_dir_state *dsp;
76
77         /* Is this directory the same as the last call */
78         *same_as_last = (dirp == rdp_last_dirp);
79
80         for(dsp = dirstatelist; dsp; dsp = dsp->next)
81                 if (dsp->dirp == dirp) {
82                         *dir_state = dsp;
83                         return 0;
84                 }
85
86         /* Couldn't find existing dir_state for the given directory
87          * pointer. */
88         return 1;
89 }
90
91 /**
92  * Initialize the global readdirplus buffers.
93  *
94  * These same buffers are used for all calls into readdirplus.
95  *
96  * @return 0 on success, errno value on failure
97  */
98 static int
99 rdp_init(struct rdp_dir_state *dsp)
100 {
101         /* Unfortunately, there is no good way to free these buffers.  If we
102          * allocated and freed for every DIR handle performance would be
103          * adversely affected.  For now these buffers will be leaked and only
104          * freed when the smbd process dies. */
105         if (!rdp_direntries) {
106                 rdp_direntries = SMB_MALLOC(RDP_DIRENTRIES_SIZE);
107                 if (!rdp_direntries)
108                         return ENOMEM;
109         }
110
111         if (!rdp_stats) {
112                 rdp_stats =
113                     SMB_MALLOC(RDP_BATCH_SIZE * sizeof(SMB_STRUCT_STAT));
114                 if (!rdp_stats)
115                         return ENOMEM;
116         }
117
118         if (!rdp_cookies) {
119                 rdp_cookies = SMB_MALLOC(RDP_BATCH_SIZE * sizeof(uint64_t));
120                 if (!rdp_cookies)
121                         return ENOMEM;
122         }
123
124         dsp->direntries_cursor = rdp_direntries + RDP_DIRENTRIES_SIZE;
125         dsp->stat_count = RDP_BATCH_SIZE;
126         dsp->stat_cursor = RDP_BATCH_SIZE;
127         dsp->resume_cookie = RDP_RESUME_KEY_START;
128         dsp->location = 0;
129
130         return 0;
131 }
132
133 /**
134  * Call into readdirplus() to refill our global dirent cache.
135  *
136  * This function also resets all cursors back to the beginning of the cache.
137  * All stat buffers are retrieved by following symlinks.
138  *
139  * @return number of entries retrieved, -1 on error
140  */
141 static int
142 rdp_fill_cache(struct rdp_dir_state *dsp)
143 {
144         int nread, dirfd;
145
146         dirfd = dirfd(dsp->dirp);
147         if (dirfd < 0) {
148                 DEBUG(1, ("Could not retrieve fd for DIR\n"));
149                 return -1;
150         }
151
152         /* Resize the stat_count to grab as many entries as possible */
153         dsp->stat_count = RDP_BATCH_SIZE;
154
155         DEBUG(9, ("Calling readdirplus() with DIR %p, dirfd: %d, "
156                  "resume_cookie 0x%llx, location %u, size_to_read: %zu, "
157                  "direntries_size: %zu, stat_count: %u\n",
158                  dsp->dirp, dirfd, dsp->resume_cookie, dsp->location,
159                  RDP_BATCH_SIZE, RDP_DIRENTRIES_SIZE, dsp->stat_count));
160
161         nread = readdirplus(dirfd,
162                             RDP_FOLLOW,
163                             &dsp->resume_cookie,
164                             RDP_BATCH_SIZE,
165                             rdp_direntries,
166                             RDP_DIRENTRIES_SIZE,
167                             &dsp->stat_count,
168                             rdp_stats,
169                             rdp_cookies);
170         if (nread < 0) {
171                 DEBUG(1, ("Error calling readdirplus(): %s\n",
172                          strerror(errno)));
173                 return -1;
174         }
175
176         DEBUG(9, ("readdirplus() returned %u entries from DIR %p\n",
177                  dsp->stat_count, dsp->dirp));
178
179         dsp->direntries_cursor = rdp_direntries;
180         dsp->stat_cursor = 0;
181
182         return nread;
183 }
184
185 /**
186  * Open a directory for enumeration.
187  *
188  * Create a state struct to track the state of this directory for the life
189  * of this open.
190  *
191  * @param[in] handle vfs handle given in most VFS calls
192  * @param[in] fname filename of the directory to open
193  * @param[in] mask unused
194  * @param[in] attr unused
195  *
196  * @return DIR pointer, NULL if directory does not exist, NULL on error
197  */
198 SMB_STRUCT_DIR *
199 onefs_opendir(vfs_handle_struct *handle, const char *fname, const char *mask,
200               uint32 attr)
201 {
202         int ret = 0;
203         SMB_STRUCT_DIR *ret_dirp;
204         struct rdp_dir_state *dsp = NULL;
205
206         /* Fallback to default system routines if readdirplus is disabled */
207         if (!lp_parm_bool(SNUM(handle->conn), PARM_ONEFS_TYPE,
208             PARM_USE_READDIRPLUS, PARM_USE_READDIRPLUS_DEFAULT))
209         {
210                 return SMB_VFS_NEXT_OPENDIR(handle, fname, mask, attr);
211         }
212
213         /* Create a struct dir_state */
214         dsp = SMB_MALLOC_P(struct rdp_dir_state);
215         if (!dsp) {
216                 DEBUG(0, ("Error allocating struct rdp_dir_state.\n"));
217                 return NULL;
218         }
219
220         /* Open the directory */
221         ret_dirp = SMB_VFS_NEXT_OPENDIR(handle, fname, mask, attr);
222         if (!ret_dirp) {
223                 DEBUG(3, ("Unable to open directory: %s\n", fname));
224                 return NULL;
225         }
226
227         /* Initialize the dir_state structure and add it to the list */
228         ret = rdp_init(dsp);
229         if (ret) {
230                 DEBUG(0, ("Error initializing readdirplus() buffers: %s\n",
231                     strerror(ret)));
232                 return NULL;
233         }
234
235         /* Set the SMB_STRUCT_DIR in the dsp */
236         dsp->dirp = ret_dirp;
237
238         DLIST_ADD(dirstatelist, dsp);
239
240         DEBUG(9, ("Opened handle on directory: \"%s\", DIR %p\n",
241                  fname, dsp->dirp));
242
243         return ret_dirp;
244 }
245
246 /**
247  * Retrieve one direntry and optional stat buffer from our readdir cache.
248  *
249  * Increment the internal resume cookie, and refresh the cache from the
250  * kernel if necessary.
251  *
252  * @param[in] handle vfs handle given in most VFS calls
253  * @param[in] dirp system DIR handle to retrieve direntries from
254  * @param[in/out] sbuf optional stat buffer to fill, this can be NULL
255  *
256  * @return dirent structure, NULL if at the end of the directory, NULL on error
257  */
258 SMB_STRUCT_DIRENT *
259 onefs_readdir(vfs_handle_struct *handle, SMB_STRUCT_DIR *dirp,
260               SMB_STRUCT_STAT *sbuf)
261 {
262         struct rdp_dir_state *dsp = NULL;
263         SMB_STRUCT_DIRENT *ret_direntp;
264         bool same_as_last;
265         int ret = -1;
266
267         /* Set stat invalid in-case we error out */
268         if (sbuf)
269                 SET_STAT_INVALID(*sbuf);
270
271         /* Fallback to default system routines if readdirplus is disabled */
272         if (!lp_parm_bool(SNUM(handle->conn), PARM_ONEFS_TYPE,
273             PARM_USE_READDIRPLUS, PARM_USE_READDIRPLUS_DEFAULT))
274         {
275                 return sys_readdir(dirp);
276         }
277
278         /* Retrieve state based off DIR handle */
279         ret = rdp_retrieve_dir_state(dirp, &dsp, &same_as_last);
280         if (ret) {
281                 DEBUG(1, ("Could not retrieve dir_state struct for "
282                          "SMB_STRUCT_DIR pointer.\n"));
283                 ret_direntp = NULL;
284                 goto end;
285         }
286
287         /* DIR is the same, current buffer and cursors are valid.
288          * Grab the next direntry from our cache. */
289         if (same_as_last) {
290                 if ((dsp->direntries_cursor >=
291                     rdp_direntries + RDP_DIRENTRIES_SIZE) ||
292                     (dsp->stat_cursor == dsp->stat_count))
293                 {
294                         /* Cache is empty, refill from kernel */
295                         ret = rdp_fill_cache(dsp);
296                         if (ret <= 0) {
297                                 ret_direntp = NULL;
298                                 goto end;
299                         }
300                 }
301         } else {
302                 /* DIR is different from last call, reset all buffers and
303                  * cursors, and refill the global cache from the new DIR */
304                 ret = rdp_fill_cache(dsp);
305                 if (ret <= 0) {
306                         ret_direntp = NULL;
307                         goto end;
308                 }
309                 DEBUG(8, ("Switched global rdp cache to new DIR entry.\n"));
310         }
311
312         /* Return next entry from cache */
313         ret_direntp = ((SMB_STRUCT_DIRENT *)dsp->direntries_cursor);
314         dsp->direntries_cursor +=
315             ((SMB_STRUCT_DIRENT *)dsp->direntries_cursor)->d_reclen;
316         if (sbuf) {
317                 *sbuf = rdp_stats[dsp->stat_cursor];
318                 /* readdirplus() sets st_ino field to 0, if it was
319                  * unable to retrieve stat information for that
320                  * particular directory entry. */
321                 if (sbuf->st_ino == 0)
322                         SET_STAT_INVALID(*sbuf);
323         }
324
325         DEBUG(9, ("Read from DIR %p, direntry: \"%s\", location: %ld, "
326                  "resume cookie: 0x%llx, cache cursor: %zu, cache count: %zu\n",
327                  dsp->dirp, ret_direntp->d_name, dsp->location,
328                  dsp->resume_cookie, dsp->stat_cursor, dsp->stat_count));
329
330         dsp->resume_cookie = rdp_cookies[dsp->stat_cursor];
331         dsp->stat_cursor++;
332         dsp->location++;
333
334         /* FALLTHROUGH */
335 end:
336         /* Set rdp_last_dirp at the end of every VFS call where the cache was
337          * reloaded */
338         rdp_last_dirp = dirp;
339         return ret_direntp;
340 }
341
342 /**
343  * Set the location of the next direntry to be read via onefs_readdir().
344  *
345  * This function should only pass in locations retrieved from onefs_telldir().
346  *
347  * Ideally the seek point will still be in the readdirplus cache, and we'll
348  * just update our cursors.  If the seek location is outside of the current
349  * cache we must do an expensive re-enumeration of the entire directory up
350  * to the offset.
351  *
352  * @param[in] handle vfs handle given in most VFS calls
353  * @param[in] dirp system DIR handle to set offset on
354  * @param[in] offset from the start of the directory where the next read
355  *            will take place
356  *
357  * @return no return value
358  */
359 void
360 onefs_seekdir(vfs_handle_struct *handle, SMB_STRUCT_DIR *dirp, long offset)
361 {
362         struct rdp_dir_state *dsp = NULL;
363         bool same_as_last;
364         bool outside_cache = false;
365         int ret = -1, i;
366
367         /* Fallback to default system routines if readdirplus is disabled */
368         if (!lp_parm_bool(SNUM(handle->conn), PARM_ONEFS_TYPE,
369             PARM_USE_READDIRPLUS, PARM_USE_READDIRPLUS_DEFAULT))
370         {
371                 return sys_seekdir(dirp, offset);
372         }
373
374         /* Validate inputs */
375         if (offset < 0) {
376                 DEBUG(1, ("Invalid offset %ld passed.\n", offset));
377                 return;
378         }
379
380         /* Retrieve state based off DIR handle */
381         ret = rdp_retrieve_dir_state(dirp, &dsp, &same_as_last);
382         if (ret) {
383                 DEBUG(1, ("Could not retrieve dir_state struct for "
384                          "SMB_STRUCT_DIR pointer.\n"));
385                 /* XXX: we can't return an error, should we ABORT rather than
386                  * return without actually seeking? */
387                 return;
388         }
389
390         /* Short cut if no work needs to be done */
391         if (offset == dsp->location)
392                 return;
393
394         /* If DIR is different from last call, reset all buffers and cursors,
395          * and refill the global cache from the new DIR */
396         if (!same_as_last) {
397                 ret = rdp_fill_cache(dsp);
398                 if (ret <= 0)
399                         goto out;
400                 DEBUG(8, ("Switched global rdp cache to new DIR entry.\n"));
401         }
402
403         /* Check if location is outside the currently cached entries */
404         if (offset < dsp->location - dsp->stat_cursor) {
405                 /* offset is before the current cache */
406                 /* reset to the beginning of the directory */
407                 ret = rdp_init(dsp);
408                 if (ret) {
409                         DEBUG(0, ("Error initializing readdirplus() buffers: "
410                                  "%s\n", strerror(ret)));
411                         goto out;
412                 }
413                 outside_cache = true;
414         } else if (offset >
415             dsp->location + (dsp->stat_count - 1 - dsp->stat_cursor))
416         {
417                 /* offset is after the current cache
418                  * advance the cookie to the end of the cache */
419                 dsp->resume_cookie = rdp_cookies[dsp->stat_count - 1];
420                 outside_cache = true;
421         }
422
423         if (outside_cache) {
424                 /* start reading from the directory, until we have the
425                  * specified offset in our cache */
426                 do {
427                         dsp->location += dsp->stat_count - dsp->stat_cursor;
428                         ret = rdp_fill_cache(dsp);
429                         if (ret <= 0) {
430                                 DEBUG(1, ("Error seeking to offset outside the "
431                                          "cached directory entries. Offset "
432                                          "%ld \n", dsp->location));
433                                 goto out;
434                         }
435                         dsp->resume_cookie = rdp_cookies[dsp->stat_count - 1];
436                 } while (offset >= dsp->location + dsp->stat_count);
437         }
438
439         /* Location should be within the currently cached entries */
440         if (offset < dsp->location &&
441             offset >= dsp->location - dsp->stat_cursor)
442         {
443                 /* offset is within the current cache, before the cursor.
444                  * update cursors to the new location */
445                 int new_cursor = dsp->stat_cursor - (dsp->location - offset);
446
447                 dsp->direntries_cursor = rdp_direntries;
448                 for (i=0; i < new_cursor; i++) {
449                         dsp->direntries_cursor +=
450                             ((SMB_STRUCT_DIRENT *)
451                              dsp->direntries_cursor)->d_reclen;
452                 }
453                 dsp->stat_cursor = new_cursor;
454                 dsp->resume_cookie = rdp_cookies[dsp->stat_cursor];
455                 dsp->location = offset;
456         } else if (offset >= dsp->location &&
457            offset <= dsp->location + (dsp->stat_count - 1 - dsp->stat_cursor))
458         {
459                 /* offset is within the current cache, at or after the cursor.
460                  * update cursors to the new location */
461                 int add_to_cursor = offset - dsp->location - 1;
462
463                 for (i=0; i < add_to_cursor; i++) {
464                         dsp->direntries_cursor +=
465                             ((SMB_STRUCT_DIRENT *)
466                              dsp->direntries_cursor)->d_reclen;
467                 }
468                 dsp->stat_cursor += add_to_cursor;
469                 dsp->resume_cookie = rdp_cookies[dsp->stat_cursor];
470                 dsp->location = offset;
471         }
472
473         DEBUG(9, ("Seek DIR %p, location: %ld, cache cursor: %zu\n",
474                  dsp->dirp, dsp->location, dsp->stat_cursor));
475
476         /* FALLTHROUGH */
477 out:
478         /* Set rdp_last_dirp at the end of every VFS call where the cache was
479          * reloaded */
480         rdp_last_dirp = dirp;
481         return;
482 }
483
484 /**
485  * Returns the location of the next direntry to be read via onefs_readdir().
486  *
487  * This value can be passed into onefs_seekdir().
488  *
489  * @param[in] handle vfs handle given in most VFS calls
490  * @param[in] dirp system DIR handle to set offset on
491  *
492  * @return offset from the start of the directory where the next read
493  *         will take place
494  */
495 long
496 onefs_telldir(vfs_handle_struct *handle,  SMB_STRUCT_DIR *dirp)
497 {
498         struct rdp_dir_state *dsp = NULL;
499         bool same_as_last;
500         int ret = -1;
501
502         /* Fallback to default system routines if readdirplus is disabled */
503         if (!lp_parm_bool(SNUM(handle->conn), PARM_ONEFS_TYPE,
504             PARM_USE_READDIRPLUS, PARM_USE_READDIRPLUS_DEFAULT))
505         {
506                 return sys_telldir(dirp);
507         }
508
509         /* Retrieve state based off DIR handle */
510         ret = rdp_retrieve_dir_state(dirp, &dsp, &same_as_last);
511         if (ret) {
512                 DEBUG(1, ("Could not retrieve dir_state struct for "
513                          "SMB_STRUCT_DIR pointer.\n"));
514                 return -1;
515         }
516
517         DEBUG(9, ("Tell DIR %p, location: %ld, cache cursor: %zu\n",
518                  dsp->dirp, dsp->location, dsp->stat_cursor));
519
520         return dsp->location;
521 }
522
523 /**
524  * Set the next direntry to be read via onefs_readdir() to the beginning of the
525  * directory.
526  *
527  * @param[in] handle vfs handle given in most VFS calls
528  * @param[in] dirp system DIR handle to set offset on
529  *
530  * @return no return value
531  */
532 void
533 onefs_rewinddir(vfs_handle_struct *handle,  SMB_STRUCT_DIR *dirp)
534 {
535         struct rdp_dir_state *dsp = NULL;
536         bool same_as_last;
537         int ret = -1;
538
539         /* Fallback to default system routines if readdirplus is disabled */
540         if (!lp_parm_bool(SNUM(handle->conn), PARM_ONEFS_TYPE,
541             PARM_USE_READDIRPLUS, PARM_USE_READDIRPLUS_DEFAULT))
542         {
543                 return sys_rewinddir(dirp);
544         }
545
546         /* Retrieve state based off DIR handle */
547         ret = rdp_retrieve_dir_state(dirp, &dsp, &same_as_last);
548         if (ret) {
549                 DEBUG(1, ("Could not retrieve dir_state struct for "
550                          "SMB_STRUCT_DIR pointer.\n"));
551                 return;
552         }
553
554         /* Reset location and resume key to beginning */
555         ret = rdp_init(dsp);
556         if (ret) {
557                 DEBUG(0, ("Error re-initializing rdp cursors: %s\n",
558                     strerror(ret)));
559                 return;
560         }
561
562         DEBUG(9, ("Rewind DIR: %p, to location: %ld\n", dsp->dirp,
563                  dsp->location));
564
565         return;
566 }
567
568 /**
569  * Close DIR pointer and remove all state for that directory open.
570  *
571  * @param[in] handle vfs handle given in most VFS calls
572  * @param[in] dirp system DIR handle to set offset on
573  *
574  * @return -1 on failure, setting errno
575  */
576 int
577 onefs_closedir(vfs_handle_struct *handle,  SMB_STRUCT_DIR *dirp)
578 {
579         struct rdp_dir_state *dsp = NULL;
580         bool same_as_last;
581         int ret_val = -1;
582         int ret = -1;
583
584         /* Fallback to default system routines if readdirplus is disabled */
585         if (!lp_parm_bool(SNUM(handle->conn), PARM_ONEFS_TYPE,
586             PARM_USE_READDIRPLUS, PARM_USE_READDIRPLUS_DEFAULT))
587         {
588                 return SMB_VFS_NEXT_CLOSEDIR(handle, dirp);
589         }
590
591         /* Retrieve state based off DIR handle */
592         ret = rdp_retrieve_dir_state(dirp, &dsp, &same_as_last);
593         if (ret) {
594                 DEBUG(1, ("Could not retrieve dir_state struct for "
595                          "SMB_STRUCT_DIR pointer.\n"));
596                 errno = ENOENT;
597                 return -1;
598         }
599
600         /* Close DIR pointer */
601         ret_val = SMB_VFS_NEXT_CLOSEDIR(handle, dsp->dirp);
602
603         DEBUG(9, ("Closed handle on DIR %p\n", dsp->dirp));
604
605         /* Tear down state struct */
606         DLIST_REMOVE(dirstatelist, dsp);
607         SAFE_FREE(dsp);
608
609         /* Set lastp to NULL, as cache is no longer valid */
610         rdp_last_dirp = NULL;
611
612         return ret_val;
613 }
614
615 /**
616  * Initialize cache data at the beginning of every SMB search operation
617  *
618  * Since filesystem operations, such as delete files or meta data
619  * updates can occur to files in the directory we're searching
620  * between FIND_FIRST and FIND_NEXT calls we must refresh the cache
621  * from the kernel on every new search SMB.
622  *
623  * @param[in] handle vfs handle given in most VFS calls
624  * @param[in] dirp system DIR handle for the current search
625  *
626  * @return nothing
627  */
628 void
629 onefs_init_search_op(vfs_handle_struct *handle,  SMB_STRUCT_DIR *dirp)
630 {
631         /* Setting the rdp_last_dirp to NULL will cause the next readdir operation
632          * to refill the cache. */
633         rdp_last_dirp = NULL;
634
635         return;
636 }