diff options
Diffstat (limited to 'ftpfs/dir.c')
-rw-r--r-- | ftpfs/dir.c | 859 |
1 files changed, 859 insertions, 0 deletions
diff --git a/ftpfs/dir.c b/ftpfs/dir.c new file mode 100644 index 00000000..f083848a --- /dev/null +++ b/ftpfs/dir.c @@ -0,0 +1,859 @@ +/* Directory operations + + Copyright (C) 1997, 1998 Free Software Foundation, Inc. + Written by Miles Bader <miles@gnu.ai.mit.edu> + This file is part of the GNU Hurd. + + The GNU Hurd is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2, or (at + your option) any later version. + + The GNU Hurd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA. */ + +#include <unistd.h> +#include <string.h> + +#include <hurd/netfs.h> + +#include "ftpfs.h" +#include "ccache.h" + +/* Free the directory entry E and all resources it consumes. */ +void +free_entry (struct ftpfs_dir_entry *e) +{ + assert (! e->self_p); /* We should only free deleted nodes. */ + free (e->name); + if (e->symlink_target) + free (e->symlink_target); + free (e); +} + +/* Put the directory entry E into the hash table HTABLE, of length HTABLE_LEN. */ +static void +insert (struct ftpfs_dir_entry *e, + struct ftpfs_dir_entry **htable, size_t htable_len) +{ + struct ftpfs_dir_entry **t = &htable[e->hv % htable_len]; + if (*t) + (*t)->self_p = &e->next; + e->next = *t; + e->self_p = t; + *t = e; +} + +/* Replace DIR's hashtable with a new one of length NEW_LEN, retaining all + existing entries. */ +static error_t +rehash (struct ftpfs_dir *dir, size_t new_len) +{ + int i; + size_t old_len = dir->htable_len; + struct ftpfs_dir_entry **old_htable = dir->htable; + struct ftpfs_dir_entry **new_htable = + malloc (new_len * sizeof (struct ftpfs_dir_entry *)); + + if (! new_htable) + return ENOMEM; + + bzero (new_htable, new_len * sizeof (struct ftpfs_dir_entry *)); + + for (i = 0; i < old_len; i++) + while (old_htable[i]) + { + struct ftpfs_dir_entry *e = old_htable[i]; + + /* Remove E from the old table (don't bother to fixup + e->next->self_p). */ + old_htable[i] = e->next; + + insert (e, new_htable, new_len); + } + + free (old_htable); + + dir->htable = new_htable; + dir->htable_len = new_len; + + return 0; +} + +/* Calculate NAME's hash value. */ +static size_t +hash (const char *name) +{ + size_t hv = 0; + while (*name) + hv = ((hv << 5) + *name++) & 0xFFFFFF; + return hv; +} + +/* Lookup NAME in DIR and return its entry. If there is no such entry, and + ADD is true, then a new entry is allocated and returned, otherwise 0 is + returned (if ADD is true then 0 can be returned if a memory allocation + error occurs). */ +struct ftpfs_dir_entry * +lookup (struct ftpfs_dir *dir, const char *name, int add) +{ + size_t hv = hash (name); + struct ftpfs_dir_entry *h = dir->htable[hv % dir->htable_len], *e = h; + + while (e && strcmp (name, e->name) != 0) + e = e->next; + + if (!e && add) + { + if (dir->num_entries > dir->htable_len) + /* Grow the hash table. */ + if (rehash (dir, (dir->htable_len + 1) * 2 - 1) != 0) + return 0; + + e = malloc (sizeof *e); + if (e) + { + e->hv = hv; + e->name = strdup (name); + e->node = 0; + e->dir = dir; + e->stat_timestamp = 0; + bzero (&e->stat, sizeof e->stat); + e->symlink_target = 0; + e->noent = 0; + e->valid = 0; + e->name_timestamp = e->stat_timestamp = 0; + e->ordered_next = 0; + e->ordered_self_p = 0; + e->next = 0; + e->self_p = 0; + insert (e, dir->htable, dir->htable_len); + dir->num_entries++; + } + } + + return e; +} + +/* Remove E from its position in the ordered_next chain. */ +static void +ordered_unlink (struct ftpfs_dir_entry *e) +{ + if (e->ordered_self_p) + *e->ordered_self_p = e->ordered_next; + if (e->ordered_next) + e->ordered_next->self_p = e->ordered_self_p; +} + +/* Delete E from its directory, freeing any resources it holds. */ +static void +delete (struct ftpfs_dir_entry *e, struct ftpfs_dir *dir) +{ + dir->num_entries--; + + /* Take out of the hash chain. */ + if (e->self_p) + *e->self_p = e->next; + if (e->next) + e->next->self_p = e->self_p; + + /* This indicates a deleted entry. */ + e->self_p = 0; + e->next = 0; + + /* Take out of the directory ordered list. */ + ordered_unlink (e); + + /* If there's a node attached, we'll delete the entry whenever it goes + away, otherwise, just delete it now. */ + if (! e->node) + free_entry (e); +} + +/* Clear the valid bit in all DIR's htable. */ +static void +mark (struct ftpfs_dir *dir) +{ + size_t len = dir->htable_len, i; + struct ftpfs_dir_entry **htable = dir->htable, *e; + + for (i = 0; i < len; i++) + for (e = htable[i]; e; e = e->next) + e->valid = 0; +} + +/* Delete any entries in DIR which don't have their valid bit set. */ +static void +sweep (struct ftpfs_dir *dir) +{ + size_t len = dir->htable_len, i; + struct ftpfs_dir_entry **htable = dir->htable, *e; + + for (i = 0; i < len; i++) + for (e = htable[i]; e; e = e->next) + if (!e->valid && !e->noent) + delete (e, dir); +} + +/* Update the directory entry for NAME to reflect ST and SYMLINK_TARGET. + True is returned if successful, or false if there was a memory allocation + error. TIMESTAMP is used to record the time of this update. */ +static void +update_entry (struct ftpfs_dir_entry *e, const struct stat *st, + const char *symlink_target, time_t timestamp) +{ + ino_t ino; + struct ftpfs *fs = e->dir->fs; + + if (e->stat.st_ino) + ino = e->stat.st_ino; + else + ino = fs->next_inode++; + + e->name_timestamp = timestamp; + + if (st) + /* The ST and SYMLINK_TARGET parameters are only valid if ST isn't 0. */ + { + e->stat = *st; + e->stat_timestamp = timestamp; + + if (!e->symlink_target || !symlink_target + || strcmp (e->symlink_target, symlink_target) != 0) + { + if (e->symlink_target) + free (e->symlink_target); + e->symlink_target = symlink_target ? strdup (symlink_target) : 0; + } + } + + /* The st_ino field is always valid. */ + e->stat.st_ino = ino; + e->stat.st_fsid = fs->fsid; + e->stat.st_fstype = FSTYPE_FTP; +} + +/* Add the timestamp TIMESTAMP to the set used to detect bulk stats, and + return true if there have been enough individual stats recently to call + for just refetching the whole directory. */ +static int +need_bulk_stat (time_t timestamp, struct ftpfs_dir *dir) +{ + time_t period = dir->fs->params.bulk_stat_period; + unsigned threshold = dir->fs->params.bulk_stat_threshold; + + if (timestamp > dir->bulk_stat_base_stamp + period * 3) + /* No stats done in a while, just start over. */ + { + dir->bulk_stat_count_first_half = 1; + dir->bulk_stat_count_second_half = 0; + dir->bulk_stat_base_stamp = (timestamp / period) * period; + } + else if (timestamp > dir->bulk_stat_base_stamp + period * 2) + /* Start a new period, but keep the second half of the old one. */ + { + dir->bulk_stat_count_first_half = dir->bulk_stat_count_second_half; + dir->bulk_stat_count_second_half = 1; + dir->bulk_stat_base_stamp += period; + } + else if (timestamp > dir->bulk_stat_base_stamp + period) + dir->bulk_stat_count_second_half++; + else + dir->bulk_stat_count_first_half++; + + return + (dir->bulk_stat_count_first_half + dir->bulk_stat_count_second_half) + > threshold; +} + +static void +reset_bulk_stat_info (struct ftpfs_dir *dir) +{ + dir->bulk_stat_count_first_half = 0; + dir->bulk_stat_count_second_half = 0; + dir->bulk_stat_base_stamp = 0; +} + +/* State shared between ftpfs_dir_refresh and update_ordered_entry. */ +struct dir_fetch_state +{ + struct ftpfs_dir *dir; + time_t timestamp; + + /* A pointer to the NEXT-field of the previously seen entry, or a pointer + to the ORDERED field in the directory if this is the first. */ + struct ftpfs_dir_entry **prev_entry_next_p; +}; + +/* Update the directory entry for NAME to reflect ST and SYMLINK_TARGET, also + rearranging the entries to reflect the order in which they are sent from + the server, and setting their valid bits so that obsolete entries can be + deleted. HOOK points to the state from ftpfs_dir_fetch. */ +static error_t +update_ordered_entry (const char *name, const struct stat *st, + const char *symlink_target, void *hook) +{ + struct dir_fetch_state *dfs = hook; + struct ftpfs_dir_entry *e = lookup (dfs->dir, name, 1); + + if (! e) + return ENOMEM; + + update_entry (e, st, symlink_target, dfs->timestamp); + e->valid = 1; + + if (! e->ordered_self_p) + /* Position E in the ordered chain following the previously seen entry. */ + { + /* The PREV_ENTRY_NEXT_P field holds a pointer to the NEXT-field of the + previous entry, or a pointer to the ORDERED field in the directory. */ + e->ordered_self_p = dfs->prev_entry_next_p; + + if (*e->ordered_self_p) + /* Update the self_p pointer of the previous successor. */ + (*e->ordered_self_p)->ordered_self_p = &e->ordered_next; + + /* E comes before the previous successor. */ + e->ordered_next = *e->ordered_self_p; + + *e->ordered_self_p = e; /* Put E there. */ + } + + /* Put the next entry after this one. */ + dfs->prev_entry_next_p = &e->ordered_next; + + return 0; +} + +/* Update the directory entry for NAME, rearranging the entries to reflect + the order in which they are sent from the server, and setting their valid + bits so that obsolete entries can be deleted. HOOK points to the state + from ftpfs_dir_fetch. */ +static error_t +update_ordered_name (const char *name, void *hook) +{ + /* We just do the same thing as for stats, but without the stat info. */ + return update_ordered_entry (name, 0, 0, hook); +} + +/* Refresh DIR from the directory DIR_NAME in the filesystem FS. If + UPDATE_STATS is true, then directory stat information will also be + updated. If PRESERVE_ENTRY is non-0, that entry won't be deleted if it's + not in the directory after the refresh, but instead will have its NOENT + flag turned on. */ +static error_t +refresh_dir (struct ftpfs_dir *dir, int update_stats, time_t timestamp, + struct ftpfs_dir_entry *preserve_entry) +{ + error_t err; + struct ftp_conn *conn; + struct dir_fetch_state dfs; + + if ((update_stats + ? dir->stat_timestamp + dir->fs->params.stat_timeout + : dir->name_timestamp + dir->fs->params.name_timeout) + >= timestamp) + /* We've already refreshed this directory recently. */ + return 0; + + err = ftpfs_get_ftp_conn (dir->fs, &conn); + if (err) + return err; + + /* Mark directory entries so we can GC them later using sweep. */ + mark (dir); + + if (update_stats) + /* We're doing a bulk stat now, so don't do another for a while. */ + reset_bulk_stat_info (dir); + + /* Info passed to update_ordered_entry. */ + dfs.dir = dir; + dfs.timestamp = timestamp; + dfs.prev_entry_next_p = &dir->ordered; + + /* Make sure `.' and `..' are always included (if the actual list also + includes `.' and `..', the ordered may be rearranged). */ + err = update_ordered_name (".", &dfs); + if (! err) + err = update_ordered_name ("..", &dfs); + + /* Refetch the directory from the server. */ + if (update_stats) + /* Fetch both names and stat info. */ + err = ftp_conn_get_stats (conn, dir->rmt_path, 1, + update_ordered_entry, &dfs); + else + /* Just fetch names. */ + err = ftp_conn_get_names (conn, dir->rmt_path, update_ordered_name, &dfs); + + if (! err) + /* GC any directory entries that weren't seen this time. */ + { + dir->name_timestamp = timestamp; + if (update_stats) + dir->stat_timestamp = timestamp; + if (preserve_entry && !preserve_entry->valid) + { + preserve_entry->noent = 1; + preserve_entry->name_timestamp = timestamp; + } + sweep (dir); + } + + ftpfs_release_ftp_conn (dir->fs, conn); + + return err; +} + +/* Refresh DIR. */ +error_t +ftpfs_dir_refresh (struct ftpfs_dir *dir) +{ + time_t timestamp = NOW; + return refresh_dir (dir, 0, timestamp, 0); +} + +/* State shared between ftpfs_dir_entry_refresh and update_old_entry. */ +struct refresh_entry_state +{ + struct ftpfs_dir_entry *entry; + time_t timestamp; +}; + +/* Update the directory entry for NAME to reflect ST and SYMLINK_TARGET. + HOOK points to the state from ftpfs_dir_fetch_entry. */ +static error_t +update_old_entry (const char *name, const struct stat *st, + const char *symlink_target, void *hook) +{ + struct refresh_entry_state *res = hook; + + if (strcmp (name, res->entry->name) != 0) + return EGRATUITOUS; + + update_entry (res->entry, st, symlink_target, res->timestamp); + + return 0; +} + +/* Refresh stat information for NODE. This may actually refresh the whole + directory if that is deemed desirable. NODE should be locked. */ +error_t +ftpfs_refresh_node (struct node *node) +{ + struct netnode *nn = node->nn; + struct ftpfs_dir_entry *entry = nn->dir_entry; + + if (! entry) + /* This is a deleted node, don't attempt to do anything. */ + return 0; + else + { + error_t err = 0; + time_t timestamp = NOW; + struct ftpfs_dir *dir = entry->dir; + + mutex_lock (&dir->node->lock); + + if (! entry->self_p) + /* This is a deleted entry, just awaiting disposal; do so. */ + { + nn->dir_entry = 0; + free_entry (entry); + return 0; + } + else if ((entry->name_timestamp + dir->fs->params.name_timeout + >= timestamp) + && entry->noent) + err = ENOENT; + else if (entry->stat_timestamp + dir->fs->params.stat_timeout < timestamp) + { + /* Stat information needs updating. */ + if (need_bulk_stat (timestamp, dir)) + /* Refetch the whole directory from the server. */ + { + err = refresh_dir (entry->dir, 1, timestamp, entry); + if (!err && entry->noent) + err = ENOENT; + } + else + { + struct ftp_conn *conn; + + err = ftpfs_get_ftp_conn (dir->fs, &conn); + + if (! err) + { + char *rmt_path; + + err = ftp_conn_append_name (conn, dir->rmt_path, entry->name, + &rmt_path); + if (! err) + { + struct refresh_entry_state res; + + res.entry = entry; + res.timestamp = timestamp; + + if (! err) + err = ftp_conn_get_stats (conn, rmt_path, 0, + update_old_entry, &res); + + free (rmt_path); + } + + ftpfs_release_ftp_conn (dir->fs, conn); + } + + if (err == ENOENT) + { + entry->noent = 1; /* A negative entry. */ + entry->name_timestamp = timestamp; + } + } + } + + if ((entry->stat.st_mtime < node->nn_stat.st_mtime + || entry->stat.st_size != node->nn_stat.st_size) + && nn && nn->contents) + /* The file has changed. */ + ccache_invalidate (nn->contents); + + node->nn_stat = entry->stat; + if (!nn->dir && S_ISDIR (entry->stat.st_mode)) + ftpfs_dir_create (nn->fs, node, nn->rmt_path, &nn->dir); + + mutex_unlock (&dir->node->lock); + + ftpfs_cache_node (node); + + return err; + } +} + +/* Remove NODE from its entry (if the entry is still valid, it will remain + without a node). NODE should be locked. */ +error_t +ftpfs_detach_node (struct node *node) +{ + struct netnode *nn = node->nn; + struct ftpfs_dir_entry *entry = nn->dir_entry; + + if (entry) + /* NODE is still attached to some entry, so detach it. */ + { + struct ftpfs_dir *dir = entry->dir; + + mutex_lock (&dir->node->lock); + + if (entry->self_p) + /* Just detach NODE from the still active entry. */ + entry->node = 0; + else + /* This is a deleted entry, just awaiting disposal; do so. */ + { + nn->dir_entry = 0; + free_entry (entry); + } + + if (--dir->num_live_entries == 0) + netfs_nput (dir->node); + else + mutex_unlock (&dir->node->lock); + } + + return 0; +} + +/* State shared between ftpfs_dir_lookup and update_new_entry. */ +struct new_entry_state +{ + time_t timestamp; + struct ftpfs_dir *dir; + struct ftpfs_dir_entry *entry; +}; + +/* Update the directory entry for NAME to reflect ST and SYMLINK_TARGET. + HOOK->entry will be updated to reflect the new entry. */ +static error_t +update_new_entry (const char *name, const struct stat *st, + const char *symlink_target, void *hook) +{ + struct ftpfs_dir_entry *e; + struct new_entry_state *nes = hook; + + e = lookup (nes->dir, name, 1); + if (! e) + return ENOMEM; + + update_entry (e, st, symlink_target, nes->timestamp); + nes->entry = e; + + return 0; +} + +/* Lookup NAME in DIR, returning its entry, or an error. DIR's node should + be locked, and will be unlocked after returning; *NODE will contain the + result node, locked, and with an additional reference, or 0 if an error + occurs. */ +error_t +ftpfs_dir_lookup (struct ftpfs_dir *dir, const char *name, + struct node **node) +{ + struct ftp_conn *conn; + struct ftpfs_dir_entry *e; + error_t err = 0; + char *rmt_path = 0; + time_t timestamp = NOW; + + if (*name == '\0' || strcmp (name, ".") == 0) + /* Current directory -- just add an additional reference to DIR's node + and return it. */ + { + netfs_nref (dir->node); + *node = dir->node; + return 0; + } + else if (strcmp (name, "..") == 0) + /* Parent directory. */ + { + if (dir->node->nn->dir_entry) + { + *node = dir->node->nn->dir_entry->dir->node; + mutex_lock (&(*node)->lock); + netfs_nref (*node); + } + else + { + err = ENOENT; /* No .. */ + *node = 0; + } + + mutex_unlock (&dir->node->lock); + + return err; + } + + e = lookup (dir, name, 0); + if (!e || e->name_timestamp + dir->fs->params.name_timeout < timestamp) + /* Try to fetch info about NAME. */ + { + if (need_bulk_stat (timestamp, dir)) + /* Refetch the whole directory from the server. */ + { + err = refresh_dir (dir, 1, timestamp, e); + if (!err && !e) + e = lookup (dir, name, 0); + } + else + { + err = ftpfs_get_ftp_conn (dir->fs, &conn); + if (! err) + { + err = ftp_conn_append_name (conn, dir->rmt_path, name, + &rmt_path); + if (! err) + { + struct new_entry_state nes; + + nes.dir = dir; + nes.timestamp = timestamp; + + err = ftp_conn_get_stats (conn, rmt_path, 0, + update_new_entry, &nes); + if (! err) + e = nes.entry; + else if (err == ENOENT) + { + e = lookup (dir, name, 1); + if (! e) + err = ENOMEM; + else + { + e->noent = 1; /* A negative entry. */ + e->name_timestamp = timestamp; + } + } + } + + ftpfs_release_ftp_conn (dir->fs, conn); + } + } + } + + if (! err) + { + if (e && !e->noent) + /* We've got a dir entry, get a node for it. */ + { + /* If there's already a node, add a ref so that it doesn't go + away. */ + spin_lock (&netfs_node_refcnt_lock); + if (e->node) + e->node->references++; + spin_unlock (&netfs_node_refcnt_lock); + + if (! e->node) + /* No node; make one and install it into E. */ + { + if (! rmt_path) + /* We have to cons up the absolute path. We need the + connection just for the pathname frobbing functions. */ + { + err = ftpfs_get_ftp_conn (dir->fs, &conn); + if (! err) + { + err = ftp_conn_append_name (conn, dir->rmt_path, name, + &rmt_path); + ftpfs_release_ftp_conn (dir->fs, conn); + } + } + + if (! err) + { + err = ftpfs_create_node (e, rmt_path, &e->node); + + if (!err && dir->num_live_entries++ == 0) + /* Keep a reference to dir's node corresponding to + children. */ + { + spin_lock (&netfs_node_refcnt_lock); + dir->node->references++; + spin_unlock (&netfs_node_refcnt_lock); + } + } + } + + if (! err) + { + *node = e->node; + /* We have to unlock DIR's node before locking the child node + because the locking order is always child-parent. We know + the child node won't go away because we already hold the + additional reference to it. */ + mutex_unlock (&dir->node->lock); + mutex_lock (&e->node->lock); + } + } + else + err = ENOENT; + } + + if (err) + { + *node = 0; + mutex_unlock (&dir->node->lock); + } + + if (rmt_path) + free (rmt_path); + + return err; +} + +/* Lookup the null name in DIR, and return a node for it in NODE. Unlike + ftpfs_dir_lookup, this won't attempt to validate the existance of the + entry (to avoid opening a new connection if possible) -- that will happen + the first time the entry is refreshed. Also unlink ftpfs_dir_lookup, this + function doesn't expect DIR to be locked, and won't return *NODE locked. + This function is only used for bootstrapping the root node. */ +error_t +ftpfs_dir_null_lookup (struct ftpfs_dir *dir, struct node **node) +{ + struct ftpfs_dir_entry *e; + error_t err = 0; + + e = lookup (dir, "", 1); + if (! e) + return ENOMEM; + + if (! e->noent) + /* We've got a dir entry, get a node for it. */ + { + /* If there's already a node, add a ref so that it doesn't go away. */ + spin_lock (&netfs_node_refcnt_lock); + if (e->node) + e->node->references++; + spin_unlock (&netfs_node_refcnt_lock); + + if (! e->node) + /* No node; make one and install it into E. */ + { + err = ftpfs_create_node (e, dir->rmt_path, &e->node); + + if (!err && dir->num_live_entries++ == 0) + /* Keep a reference to dir's node corresponding to children. */ + { + spin_lock (&netfs_node_refcnt_lock); + dir->node->references++; + spin_unlock (&netfs_node_refcnt_lock); + } + } + + if (! err) + *node = e->node; + } + else + err = ENOENT; + + return err; +} + +/* Return in DIR a new ftpfs directory, in the filesystem FS, with node NODE + and remote path RMT_PATH. RMT_PATH is *not copied*, so it shouldn't ever + change while this directory is active. */ +error_t +ftpfs_dir_create (struct ftpfs *fs, struct node *node, const char *rmt_path, + struct ftpfs_dir **dir) +{ + struct ftpfs_dir *new = malloc (sizeof (struct ftpfs_dir)); + + if (! new) + return ENOMEM; + + /* Hold a reference to the new dir's node. */ + spin_lock (&netfs_node_refcnt_lock); + node->references++; + spin_unlock (&netfs_node_refcnt_lock); + + new->num_entries = 0; + new->num_live_entries = 0; + new->htable_len = 5; + new->htable = malloc (new->htable_len * sizeof (struct ftpfs_dir_entry *)); + bzero (new->htable, sizeof *new->htable * new->htable_len); + new->ordered = 0; + new->rmt_path = rmt_path; + new->fs = fs; + new->node = node; + new->stat_timestamp = 0; + new->name_timestamp = 0; + new->bulk_stat_base_stamp = 0; + new->bulk_stat_count_first_half = 0; + new->bulk_stat_count_second_half = 0; + + *dir = new; + + return 0; +} + +void +ftpfs_dir_free (struct ftpfs_dir *dir) +{ + /* Free all entries. */ + mark (dir); + sweep (dir); + + if (dir->htable) + free (dir->htable); + + netfs_nrele (dir->node); + + free (dir); +} |