1998-09-26 02:34:59 +04:00
|
|
|
/*
|
|
|
|
* Shared code between the fish.c and the ftp.c file systems
|
|
|
|
*
|
1999-12-16 15:55:16 +03:00
|
|
|
* $Id$
|
|
|
|
*
|
1998-11-21 22:36:01 +03:00
|
|
|
* Actually, this code is not being used by fish.c any more :-).
|
|
|
|
*
|
1998-10-13 02:07:53 +04:00
|
|
|
* Namespace pollution: X_hint_reread, X_flushdir.
|
1998-09-26 02:34:59 +04:00
|
|
|
*/
|
|
|
|
static int store_file (struct direntry *fe);
|
|
|
|
static int retrieve_file (struct direntry *fe);
|
|
|
|
static int remove_temp_file (char *file_name);
|
|
|
|
static struct dir *retrieve_dir (struct connection *bucket,
|
|
|
|
char *remote_path,
|
|
|
|
int resolve_symlinks);
|
1998-09-27 23:27:58 +04:00
|
|
|
static void my_forget (char *path);
|
|
|
|
|
1998-10-13 02:07:53 +04:00
|
|
|
static int linear_start (struct direntry *fe, int from);
|
1998-09-27 23:27:58 +04:00
|
|
|
static int linear_read (struct direntry *fe, void *buf, int len);
|
|
|
|
static void linear_close (struct direntry *fe);
|
1998-09-13 14:40:43 +04:00
|
|
|
|
|
|
|
static int
|
|
|
|
select_on_two (int fd1, int fd2)
|
|
|
|
{
|
|
|
|
fd_set set;
|
|
|
|
struct timeval timeout;
|
|
|
|
int v;
|
|
|
|
int maxfd = (fd1 > fd2 ? fd1 : fd2) + 1;
|
|
|
|
|
|
|
|
timeout.tv_sec = 1;
|
|
|
|
timeout.tv_usec = 0;
|
|
|
|
FD_ZERO(&set);
|
|
|
|
FD_SET(fd1, &set);
|
|
|
|
FD_SET(fd2, &set);
|
|
|
|
v = select (maxfd, &set, 0, 0, &timeout);
|
|
|
|
if (v <= 0)
|
|
|
|
return v;
|
|
|
|
if (FD_ISSET (fd1, &set))
|
|
|
|
return 1;
|
|
|
|
if (FD_ISSET (fd2, &set))
|
|
|
|
return 2;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
get_line (int sock, char *buf, int buf_len, char term)
|
|
|
|
{
|
|
|
|
int i, status;
|
|
|
|
char c;
|
|
|
|
|
1999-08-30 10:01:37 +04:00
|
|
|
for (i = 0; i < buf_len - 1; i++, buf++) {
|
1998-09-13 14:40:43 +04:00
|
|
|
if (read(sock, buf, sizeof(char)) <= 0)
|
|
|
|
return 0;
|
|
|
|
if (logfile){
|
|
|
|
fwrite (buf, 1, 1, logfile);
|
|
|
|
fflush (logfile);
|
|
|
|
}
|
|
|
|
if (*buf == term) {
|
|
|
|
*buf = 0;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
*buf = 0;
|
|
|
|
while ((status = read(sock, &c, sizeof(c))) > 0){
|
|
|
|
if (logfile){
|
|
|
|
fwrite (&c, 1, 1, logfile);
|
|
|
|
fflush (logfile);
|
|
|
|
}
|
|
|
|
if (c == '\n')
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
direntry_destructor (void *data)
|
|
|
|
{
|
|
|
|
struct direntry *fe = data;
|
|
|
|
|
|
|
|
fe->count--;
|
|
|
|
|
|
|
|
if (fe->count > 0)
|
|
|
|
return;
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fe->name);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (fe->linkname)
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fe->linkname);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (fe->local_filename) {
|
|
|
|
if (fe->local_is_temp) {
|
|
|
|
if (!fe->local_stat.st_mtime)
|
|
|
|
unlink(fe->local_filename);
|
|
|
|
else {
|
|
|
|
struct stat sb;
|
|
|
|
|
|
|
|
if (stat (fe->local_filename, &sb) >=0 &&
|
|
|
|
fe->local_stat.st_mtime == sb.st_mtime)
|
|
|
|
unlink (fe->local_filename); /* Delete only if it hasn't changed */
|
|
|
|
}
|
|
|
|
}
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fe->local_filename);
|
1998-09-23 15:52:58 +04:00
|
|
|
fe->local_filename = NULL;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
if (fe->remote_filename)
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fe->remote_filename);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (fe->l_stat)
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fe->l_stat);
|
|
|
|
g_free(fe);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
dir_destructor(void *data)
|
|
|
|
{
|
|
|
|
struct dir *fd = data;
|
|
|
|
|
|
|
|
fd->count--;
|
|
|
|
if (fd->count > 0)
|
|
|
|
return;
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fd->remote_path);
|
1998-09-13 14:40:43 +04:00
|
|
|
linklist_destroy(fd->file_list, direntry_destructor);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fd);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
get_line_interruptible (char *buffer, int size, int fd)
|
|
|
|
{
|
|
|
|
int n;
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
for (i = 0; i < size-1; i++) {
|
|
|
|
n = read (fd, buffer+i, 1);
|
|
|
|
if (n == -1 && errno == EINTR){
|
|
|
|
buffer [i] = 0;
|
|
|
|
return EINTR;
|
|
|
|
}
|
|
|
|
if (n == 0){
|
|
|
|
buffer [i] = 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if (buffer [i] == '\n'){
|
|
|
|
buffer [i] = 0;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
buffer [size-1] = 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
free_bucket (void *data)
|
|
|
|
{
|
|
|
|
struct connection *bucket = data;
|
|
|
|
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(qhost(bucket));
|
|
|
|
g_free(quser(bucket));
|
1998-09-13 14:40:43 +04:00
|
|
|
if (qcdir(bucket))
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(qcdir(bucket));
|
1998-09-13 14:40:43 +04:00
|
|
|
if (qhome(bucket))
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(qhome(bucket));
|
1998-09-13 14:40:43 +04:00
|
|
|
if (qupdir(bucket))
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(qupdir(bucket));
|
1998-09-13 14:40:43 +04:00
|
|
|
if (bucket->password)
|
|
|
|
wipe_password (bucket->password);
|
|
|
|
linklist_destroy(qdcache(bucket), dir_destructor);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(bucket);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
connection_destructor(void *data)
|
|
|
|
{
|
|
|
|
connection_close (data);
|
|
|
|
free_bucket (data);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
flush_all_directory(struct connection *bucket)
|
|
|
|
{
|
|
|
|
linklist_delete_all(qdcache(bucket), dir_destructor);
|
|
|
|
}
|
|
|
|
|
1998-10-13 02:07:53 +04:00
|
|
|
static void X_fill_names (vfs *me, void (*func)(char *))
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct linklist *lptr;
|
|
|
|
char *path_name;
|
|
|
|
struct connection *bucket;
|
|
|
|
|
|
|
|
if (!connections_list)
|
|
|
|
return;
|
|
|
|
lptr = connections_list;
|
|
|
|
do {
|
|
|
|
if ((bucket = lptr->data) != 0){
|
|
|
|
|
1999-01-27 03:49:11 +03:00
|
|
|
path_name = g_strconcat ( X_myname, quser (bucket),
|
1998-09-13 14:40:43 +04:00
|
|
|
"@", qhost (bucket),
|
1999-01-21 01:01:11 +03:00
|
|
|
qcdir(bucket), NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
(*func)(path_name);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free (path_name);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
lptr = lptr->next;
|
|
|
|
} while (lptr != connections_list);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* get_path:
|
|
|
|
* makes BUCKET point to the connection bucket descriptor for PATH
|
|
|
|
* returns a malloced string with the pathname relative to BUCKET.
|
1998-09-18 15:02:04 +04:00
|
|
|
*
|
|
|
|
* path must _not_ contain initial /bla/#ftp:
|
1998-09-13 14:40:43 +04:00
|
|
|
*/
|
|
|
|
static char*
|
1998-09-18 15:02:04 +04:00
|
|
|
s_get_path (struct connection **bucket, char *path, char *name)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
char *user, *host, *remote_path, *pass;
|
|
|
|
int port;
|
|
|
|
|
|
|
|
#ifndef BROKEN_PATHS
|
1998-09-18 15:02:04 +04:00
|
|
|
if (strncmp (path, name, strlen (name)))
|
1998-09-13 14:40:43 +04:00
|
|
|
return NULL; /* Normal: consider cd /bla/#ftp */
|
|
|
|
#else
|
1998-09-18 15:02:04 +04:00
|
|
|
if (!(path = strstr (path, name)))
|
1998-09-13 14:40:43 +04:00
|
|
|
return NULL;
|
|
|
|
#endif
|
1998-09-18 15:02:04 +04:00
|
|
|
path += strlen (name);
|
1998-09-13 14:40:43 +04:00
|
|
|
|
|
|
|
if (!(remote_path = my_get_host_and_username (path, &host, &user, &port, &pass)))
|
|
|
|
my_errno = ENOENT;
|
|
|
|
else {
|
|
|
|
if ((*bucket = open_link (host, user, port, pass)) == NULL) {
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free (remote_path);
|
1998-09-13 14:40:43 +04:00
|
|
|
remote_path = NULL;
|
|
|
|
}
|
|
|
|
}
|
1999-01-27 03:49:11 +03:00
|
|
|
if (host)
|
|
|
|
g_free (host);
|
|
|
|
if (user)
|
|
|
|
g_free (user);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (pass)
|
|
|
|
wipe_password (pass);
|
|
|
|
|
|
|
|
if (!remote_path)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
/* NOTE: Usage of tildes is deprecated, consider:
|
|
|
|
* cd /#ftp:pavel@hobit
|
|
|
|
* cd ~
|
|
|
|
* And now: what do I want to do? Do I want to go to /home/pavel or to
|
|
|
|
* /#ftp:hobit/home/pavel? I think first has better sense...
|
|
|
|
*/
|
|
|
|
{
|
|
|
|
int f = !strcmp( remote_path, "/~" );
|
|
|
|
if (f || !strncmp( remote_path, "/~/", 3 )) {
|
|
|
|
char *s;
|
|
|
|
s = concat_dir_and_file( qhome (*bucket), remote_path +3-f );
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free (remote_path);
|
1998-09-13 14:40:43 +04:00
|
|
|
remote_path = s;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return remote_path;
|
|
|
|
}
|
|
|
|
|
1998-09-21 13:52:07 +04:00
|
|
|
void
|
1999-11-11 17:23:40 +03:00
|
|
|
ftpfs_flushdir (void)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
1998-09-21 13:52:07 +04:00
|
|
|
force_expiration = 1;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
1998-09-21 13:52:07 +04:00
|
|
|
static int
|
1998-09-27 23:27:58 +04:00
|
|
|
s_setctl (vfs *me, char *path, int ctlop, char *arg)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
switch (ctlop) {
|
|
|
|
case MCCTL_REMOVELOCALCOPY:
|
|
|
|
return remove_temp_file (path);
|
|
|
|
return 0;
|
1998-09-27 23:27:58 +04:00
|
|
|
case MCCTL_FORGET_ABOUT:
|
|
|
|
my_forget(path);
|
|
|
|
return 0;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
1998-09-27 23:27:58 +04:00
|
|
|
return 0;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
static struct direntry *
|
|
|
|
_get_file_entry(struct connection *bucket, char *file_name,
|
|
|
|
int op, int flags)
|
|
|
|
{
|
|
|
|
char *p, q;
|
|
|
|
struct direntry *ent;
|
|
|
|
struct linklist *file_list, *lptr;
|
|
|
|
struct dir *dcache;
|
|
|
|
struct stat sb;
|
|
|
|
|
|
|
|
p = strrchr(file_name, '/');
|
|
|
|
q = *p;
|
|
|
|
*p = '\0';
|
1998-09-15 23:41:22 +04:00
|
|
|
dcache = retrieve_dir(bucket, *file_name ? file_name : "/", op & DO_RESOLVE_SYMLINK);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (dcache == NULL)
|
|
|
|
return NULL;
|
|
|
|
file_list = dcache->file_list;
|
|
|
|
*p++ = q;
|
|
|
|
if (!*p)
|
|
|
|
p = ".";
|
|
|
|
for (lptr = file_list->next; lptr != file_list; lptr = lptr->next) {
|
|
|
|
ent = lptr->data;
|
|
|
|
if (strcmp(p, ent->name) == 0) {
|
|
|
|
if (S_ISLNK(ent->s.st_mode) && (op & DO_RESOLVE_SYMLINK)) {
|
1998-09-18 15:02:04 +04:00
|
|
|
if (ent->l_stat == NULL) ERRNOR (ENOENT, NULL);
|
|
|
|
if (S_ISLNK(ent->l_stat->st_mode)) ERRNOR (ELOOP, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
if (ent && (op & DO_OPEN)) {
|
|
|
|
mode_t fmode;
|
|
|
|
|
|
|
|
fmode = S_ISLNK(ent->s.st_mode)
|
|
|
|
? ent->l_stat->st_mode
|
|
|
|
: ent->s.st_mode;
|
1999-09-26 00:25:20 +04:00
|
|
|
if (S_ISDIR(fmode))
|
|
|
|
ERRNOR (EISDIR, NULL);
|
|
|
|
if (!S_ISREG(fmode))
|
|
|
|
ERRNOR (EPERM, NULL);
|
|
|
|
if ((flags & O_EXCL) && (flags & O_CREAT))
|
|
|
|
ERRNOR (EEXIST, NULL);
|
1998-09-21 13:52:07 +04:00
|
|
|
if (ent->remote_filename == NULL)
|
1999-09-26 00:25:20 +04:00
|
|
|
if (!(ent->remote_filename = g_strdup(file_name)))
|
|
|
|
ERRNOR (ENOMEM, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (ent->local_filename == NULL ||
|
|
|
|
!ent->local_stat.st_mtime ||
|
|
|
|
stat (ent->local_filename, &sb) < 0 ||
|
|
|
|
sb.st_mtime != ent->local_stat.st_mtime) {
|
|
|
|
int handle;
|
|
|
|
|
|
|
|
if (ent->local_filename){
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free (ent->local_filename);
|
1998-09-21 13:52:07 +04:00
|
|
|
ent->local_filename = NULL;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
if (flags & O_TRUNC) {
|
|
|
|
ent->local_filename = tempnam (NULL, X "fs");
|
1998-09-18 15:02:04 +04:00
|
|
|
if (ent->local_filename == NULL) ERRNOR (ENOMEM, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
handle = open(ent->local_filename, O_CREAT | O_TRUNC | O_RDWR | O_EXCL, 0600);
|
1998-09-18 15:02:04 +04:00
|
|
|
if (handle < 0) ERRNOR (EIO, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
close(handle);
|
|
|
|
if (stat (ent->local_filename, &ent->local_stat) < 0)
|
|
|
|
ent->local_stat.st_mtime = 0;
|
|
|
|
}
|
|
|
|
else {
|
1998-09-21 13:52:07 +04:00
|
|
|
if (IS_LINEAR(flags)) {
|
1998-09-13 14:40:43 +04:00
|
|
|
ent->local_is_temp = 0;
|
1998-09-21 13:52:07 +04:00
|
|
|
ent->local_filename = NULL;
|
1998-10-13 02:07:53 +04:00
|
|
|
ent->linear_state = LS_LINEAR_CLOSED;
|
1998-09-13 14:40:43 +04:00
|
|
|
return ent;
|
|
|
|
}
|
|
|
|
if (!retrieve_file(ent))
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (flags & O_TRUNC) {
|
|
|
|
truncate(ent->local_filename, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ((op & DO_OPEN) && (flags & O_CREAT)) {
|
|
|
|
int handle;
|
|
|
|
|
1999-01-21 01:01:11 +03:00
|
|
|
ent = g_new (struct direntry, 1);
|
1998-09-18 15:02:04 +04:00
|
|
|
if (ent == NULL) ERRNOR (ENOMEM, NULL);
|
1999-09-26 00:25:20 +04:00
|
|
|
ent->freshly_created = 0;
|
1998-09-13 14:40:43 +04:00
|
|
|
ent->count = 1;
|
|
|
|
ent->linkname = NULL;
|
|
|
|
ent->l_stat = NULL;
|
|
|
|
ent->bucket = bucket;
|
1999-01-21 01:01:11 +03:00
|
|
|
ent->name = g_strdup(p);
|
|
|
|
ent->remote_filename = g_strdup(file_name);
|
1998-09-13 14:40:43 +04:00
|
|
|
ent->local_filename = tempnam (NULL, X "fs");
|
1999-09-26 00:25:20 +04:00
|
|
|
if (!ent->name || !ent->remote_filename || !ent->local_filename) {
|
1998-09-13 14:40:43 +04:00
|
|
|
direntry_destructor(ent);
|
1998-09-18 15:02:04 +04:00
|
|
|
ERRNOR (ENOMEM, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
1999-09-26 00:25:20 +04:00
|
|
|
handle = open (ent->local_filename, O_CREAT | O_EXCL | O_RDWR | O_TRUNC, 0700);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (handle == -1) {
|
|
|
|
my_errno = EIO;
|
1998-09-18 15:02:04 +04:00
|
|
|
goto error;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
fstat(handle, &ent->s);
|
|
|
|
close(handle);
|
|
|
|
#if 0
|
|
|
|
/* This is very wrong - like this a zero length file will be always created
|
|
|
|
and usually preclude uploading anything more desirable */
|
|
|
|
#if defined(UPLOAD_ZERO_LENGTH_FILE)
|
1998-09-18 15:02:04 +04:00
|
|
|
if (!store_file(ent)) goto error;
|
1998-09-13 14:40:43 +04:00
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
if (!linklist_insert(file_list, ent)) {
|
|
|
|
my_errno = ENOMEM;
|
1998-09-18 15:02:04 +04:00
|
|
|
goto error;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
ent->freshly_created = 1;
|
|
|
|
return ent;
|
|
|
|
}
|
1998-09-18 15:02:04 +04:00
|
|
|
else ERRNOR (ENOENT, NULL);
|
|
|
|
error:
|
|
|
|
direntry_destructor(ent);
|
|
|
|
return NULL;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* this just free's the local temp file. I don't know if the
|
|
|
|
remote file can be used after this without crashing - paul
|
|
|
|
psheer@obsidian.co.za psheer@icon.co.za */
|
1998-09-21 13:52:07 +04:00
|
|
|
static int
|
|
|
|
remove_temp_file (char *file_name)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
char *p, q;
|
|
|
|
struct connection *bucket;
|
|
|
|
struct direntry *ent;
|
|
|
|
struct linklist *file_list, *lptr;
|
|
|
|
struct dir *dcache;
|
|
|
|
|
|
|
|
if (!(file_name = get_path (&bucket, file_name)))
|
|
|
|
return -1;
|
|
|
|
p = strrchr (file_name, '/');
|
|
|
|
q = *p;
|
|
|
|
*p = '\0';
|
1998-09-15 23:41:22 +04:00
|
|
|
dcache = retrieve_dir (bucket, *file_name ? file_name : "/", 0);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (dcache == NULL)
|
|
|
|
return -1;
|
|
|
|
file_list = dcache->file_list;
|
|
|
|
*p++ = q;
|
|
|
|
if (!*p)
|
|
|
|
p = ".";
|
|
|
|
for (lptr = file_list->next; lptr != file_list; lptr = lptr->next) {
|
|
|
|
ent = lptr->data;
|
|
|
|
if (strcmp (p, ent->name) == 0) {
|
|
|
|
if (ent->local_filename) {
|
|
|
|
unlink (ent->local_filename);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free (ent->local_filename);
|
1998-09-29 20:01:16 +04:00
|
|
|
ent->local_filename = NULL;
|
1998-09-13 14:40:43 +04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static struct direntry *
|
|
|
|
get_file_entry(char *path, int op, int flags)
|
|
|
|
{
|
|
|
|
struct connection *bucket;
|
|
|
|
struct direntry *fe;
|
|
|
|
char *remote_path;
|
|
|
|
|
|
|
|
if (!(remote_path = get_path (&bucket, path)))
|
|
|
|
return NULL;
|
|
|
|
fe = _get_file_entry(bucket, remote_path, op,
|
|
|
|
flags);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(remote_path);
|
1998-09-13 14:40:43 +04:00
|
|
|
#if 0
|
|
|
|
if (op & DO_FREE_RESOURCE)
|
1998-10-13 02:07:53 +04:00
|
|
|
vfs_add_noncurrent_stamps (&vfs_X_ops, (vfsid) bucket, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
#endif
|
|
|
|
return fe;
|
|
|
|
}
|
|
|
|
|
|
|
|
#define OPT_FLUSH 1
|
|
|
|
#define OPT_IGNORE_ERROR 2
|
|
|
|
|
|
|
|
static int normal_flush = 1;
|
|
|
|
|
|
|
|
void X_hint_reread(int reread)
|
|
|
|
{
|
|
|
|
if (reread)
|
|
|
|
normal_flush++;
|
|
|
|
else
|
|
|
|
normal_flush--;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* The callbacks */
|
|
|
|
|
|
|
|
struct filp {
|
|
|
|
unsigned int has_changed:1;
|
|
|
|
struct direntry *fe;
|
|
|
|
int local_handle;
|
|
|
|
};
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static void *s_open (vfs *me, char *file, int flags, int mode)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct filp *fp;
|
|
|
|
struct direntry *fe;
|
|
|
|
|
1999-01-21 01:01:11 +03:00
|
|
|
fp = g_new (struct filp, 1);
|
1998-09-18 15:02:04 +04:00
|
|
|
if (fp == NULL) ERRNOR (ENOMEM, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
fe = get_file_entry(file, DO_OPEN | DO_RESOLVE_SYMLINK, flags);
|
1998-09-21 13:52:07 +04:00
|
|
|
if (!fe) {
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fp);
|
1998-09-13 14:40:43 +04:00
|
|
|
return NULL;
|
|
|
|
}
|
1998-10-13 02:07:53 +04:00
|
|
|
fe->linear_state = IS_LINEAR(flags);
|
|
|
|
if (!fe->linear_state) {
|
1998-09-13 14:40:43 +04:00
|
|
|
fp->local_handle = open(fe->local_filename, flags, mode);
|
|
|
|
if (fp->local_handle < 0) {
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fp);
|
1998-09-21 13:52:07 +04:00
|
|
|
ERRNOR (errno, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
1998-09-21 13:52:07 +04:00
|
|
|
} else fp->local_handle = -1;
|
1998-09-13 14:40:43 +04:00
|
|
|
#ifdef UPLOAD_ZERO_LENGTH_FILE
|
|
|
|
fp->has_changed = fe->freshly_created;
|
|
|
|
#else
|
|
|
|
fp->has_changed = 0;
|
1998-09-21 13:52:07 +04:00
|
|
|
#endif
|
1998-09-13 14:40:43 +04:00
|
|
|
fp->fe = fe;
|
|
|
|
qlock(fe->bucket)++;
|
|
|
|
fe->count++;
|
|
|
|
return fp;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_read (void *data, char *buffer, int count)
|
|
|
|
{
|
|
|
|
struct filp *fp;
|
|
|
|
int n;
|
|
|
|
|
|
|
|
fp = data;
|
1998-10-13 02:07:53 +04:00
|
|
|
if (fp->fe->linear_state == LS_LINEAR_CLOSED) {
|
1999-08-16 09:31:23 +04:00
|
|
|
print_vfs_message (_("Starting linear transfer..."));
|
1998-10-13 02:07:53 +04:00
|
|
|
if (!linear_start (fp->fe, 0))
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fp->fe->linear_state == LS_LINEAR_CLOSED)
|
|
|
|
vfs_die ("linear_start() did not set linear_state!");
|
|
|
|
|
|
|
|
if (fp->fe->linear_state == LS_LINEAR_OPEN)
|
1998-09-21 13:52:07 +04:00
|
|
|
return linear_read (fp->fe, buffer, count);
|
|
|
|
|
|
|
|
n = read (fp->local_handle, buffer, count);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (n < 0)
|
1998-09-21 13:52:07 +04:00
|
|
|
my_errno = errno;
|
1998-09-13 14:40:43 +04:00
|
|
|
return n;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_write (void *data, char *buf, int nbyte)
|
|
|
|
{
|
1998-10-15 19:18:12 +04:00
|
|
|
struct filp *fp = data;
|
1998-09-13 14:40:43 +04:00
|
|
|
int n;
|
1998-10-13 02:07:53 +04:00
|
|
|
|
|
|
|
if (fp->fe->linear_state)
|
|
|
|
vfs_die ("You may not write to linear file");
|
1998-09-21 13:52:07 +04:00
|
|
|
n = write (fp->local_handle, buf, nbyte);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (n < 0)
|
1998-09-21 13:52:07 +04:00
|
|
|
my_errno = errno;
|
1998-09-13 14:40:43 +04:00
|
|
|
fp->has_changed = 1;
|
|
|
|
return n;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_close (void *data)
|
|
|
|
{
|
|
|
|
struct filp *fp = data;
|
|
|
|
int result = 0;
|
|
|
|
|
|
|
|
if (fp->has_changed) {
|
|
|
|
if (!store_file(fp->fe))
|
|
|
|
result = -1;
|
|
|
|
if (normal_flush)
|
|
|
|
flush_all_directory(fp->fe->bucket);
|
|
|
|
}
|
1998-10-13 02:07:53 +04:00
|
|
|
if (fp->fe->linear_state == LS_LINEAR_OPEN)
|
1998-09-21 13:52:07 +04:00
|
|
|
linear_close(fp->fe);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (fp->local_handle >= 0)
|
|
|
|
close(fp->local_handle);
|
|
|
|
qlock(fp->fe->bucket)--;
|
|
|
|
direntry_destructor(fp->fe);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fp);
|
1998-09-13 14:40:43 +04:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static int s_errno (vfs *me)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
return my_errno;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Explanation:
|
|
|
|
* On some operating systems (Slowaris 2 for example)
|
|
|
|
* the d_name member is just a char long (nice trick that break everything),
|
|
|
|
* so we need to set up some space for the filename.
|
|
|
|
*/
|
|
|
|
struct my_dirent {
|
|
|
|
struct dirent dent;
|
|
|
|
#ifdef NEED_EXTRA_DIRENT_BUFFER
|
|
|
|
char extra_buffer [MC_MAXPATHLEN];
|
|
|
|
#endif
|
|
|
|
struct linklist *pos;
|
|
|
|
struct dir *dcache;
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Possible FIXME: what happens if one directory is opened twice ? */
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static void *s_opendir (vfs *me, char *dirname)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct connection *bucket;
|
|
|
|
char *remote_path;
|
|
|
|
struct my_dirent *dirp;
|
|
|
|
|
|
|
|
if (!(remote_path = get_path (&bucket, dirname)))
|
|
|
|
return NULL;
|
1999-01-21 01:01:11 +03:00
|
|
|
dirp = g_new (struct my_dirent, 1);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (dirp == NULL) {
|
|
|
|
my_errno = ENOMEM;
|
|
|
|
goto error_return;
|
|
|
|
}
|
1998-09-15 23:41:22 +04:00
|
|
|
dirp->dcache = retrieve_dir(bucket, remote_path, 1);
|
1998-09-13 14:40:43 +04:00
|
|
|
if (dirp->dcache == NULL)
|
|
|
|
goto error_return;
|
|
|
|
dirp->pos = dirp->dcache->file_list->next;
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(remote_path);
|
1998-09-13 14:40:43 +04:00
|
|
|
dirp->dcache->count++;
|
|
|
|
return (void *)dirp;
|
|
|
|
error_return:
|
1998-10-13 02:07:53 +04:00
|
|
|
vfs_add_noncurrent_stamps (&vfs_X_ops, (vfsid) bucket, NULL);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(remote_path);
|
|
|
|
g_free(dirp);
|
1998-09-13 14:40:43 +04:00
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void *s_readdir (void *data)
|
|
|
|
{
|
|
|
|
struct direntry *fe;
|
|
|
|
struct my_dirent *dirp = data;
|
|
|
|
|
|
|
|
if (dirp->pos == dirp->dcache->file_list)
|
|
|
|
return NULL;
|
|
|
|
fe = dirp->pos->data;
|
|
|
|
strcpy (&(dirp->dent.d_name [0]), fe->name);
|
|
|
|
#ifndef DIRENT_LENGTH_COMPUTED
|
|
|
|
dirp->d_namlen = strlen (dirp->d_name);
|
|
|
|
#endif
|
|
|
|
dirp->pos = dirp->pos->next;
|
|
|
|
return (void *) &dirp->dent;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_telldir (void *data)
|
|
|
|
{
|
|
|
|
struct my_dirent *dirp = data;
|
|
|
|
struct linklist *pos;
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
pos = dirp->dcache->file_list->next;
|
|
|
|
while( pos!=dirp->dcache->file_list) {
|
|
|
|
if (pos == dirp->pos)
|
|
|
|
return i;
|
|
|
|
pos = pos->next;
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void s_seekdir (void *data, int pos)
|
|
|
|
{
|
|
|
|
struct my_dirent *dirp = data;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
dirp->pos = dirp->dcache->file_list->next;
|
|
|
|
for (i=0; i<pos; i++)
|
|
|
|
s_readdir(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_closedir (void *info)
|
|
|
|
{
|
|
|
|
struct my_dirent *dirp = info;
|
|
|
|
dir_destructor(dirp->dcache);
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(dirp);
|
1998-09-13 14:40:43 +04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static int s_lstat (vfs *me, char *path, struct stat *buf)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct direntry *fe;
|
|
|
|
|
|
|
|
fe = get_file_entry(path, DO_FREE_RESOURCE, 0);
|
|
|
|
if (fe) {
|
|
|
|
*buf = fe->s;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static int s_stat (vfs *me, char *path, struct stat *buf)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct direntry *fe;
|
|
|
|
|
|
|
|
fe = get_file_entry(path, DO_RESOLVE_SYMLINK | DO_FREE_RESOURCE, 0);
|
|
|
|
if (fe) {
|
|
|
|
if (!S_ISLNK(fe->s.st_mode))
|
|
|
|
*buf = fe->s;
|
|
|
|
else
|
|
|
|
*buf = *fe->l_stat;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_fstat (void *data, struct stat *buf)
|
|
|
|
{
|
|
|
|
struct filp *fp = data;
|
|
|
|
|
|
|
|
if (!S_ISLNK(fp->fe->s.st_mode))
|
|
|
|
*buf = fp->fe->s;
|
|
|
|
else
|
|
|
|
*buf = *fp->fe->l_stat;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static int s_readlink (vfs *me, char *path, char *buf, int size)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct direntry *fe;
|
|
|
|
|
|
|
|
fe = get_file_entry(path, DO_FREE_RESOURCE, 0);
|
|
|
|
if (!fe)
|
|
|
|
return -1;
|
1998-09-18 15:02:04 +04:00
|
|
|
if (!S_ISLNK(fe->s.st_mode)) ERRNOR (EINVAL, -1);
|
|
|
|
if (fe->linkname == NULL) ERRNOR (EACCES, -1);
|
|
|
|
if (strlen(fe->linkname) >= size) ERRNOR (ERANGE, -1);
|
1998-09-13 14:40:43 +04:00
|
|
|
strncpy(buf, fe->linkname, size);
|
|
|
|
return strlen(fe->linkname);
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static int s_chdir (vfs *me, char *path)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
char *remote_path;
|
|
|
|
struct connection *bucket;
|
|
|
|
|
|
|
|
if (!(remote_path = get_path(&bucket, path)))
|
|
|
|
return -1;
|
|
|
|
if (qcdir(bucket))
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(qcdir(bucket));
|
1998-09-13 14:40:43 +04:00
|
|
|
qcdir(bucket) = remote_path;
|
|
|
|
bucket->cwd_defered = 1;
|
|
|
|
|
1998-10-13 02:07:53 +04:00
|
|
|
vfs_add_noncurrent_stamps (&vfs_X_ops, (vfsid) bucket, NULL);
|
1998-09-13 14:40:43 +04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_lseek (void *data, off_t offset, int whence)
|
|
|
|
{
|
|
|
|
struct filp *fp = data;
|
|
|
|
|
1998-10-13 02:07:53 +04:00
|
|
|
if (fp->fe->linear_state == LS_LINEAR_OPEN)
|
|
|
|
vfs_die ("You promissed not to seek!");
|
|
|
|
if (fp->fe->linear_state == LS_LINEAR_CLOSED) {
|
1999-08-16 09:31:23 +04:00
|
|
|
print_vfs_message (_("Preparing reget..."));
|
1998-10-13 02:07:53 +04:00
|
|
|
if (whence != SEEK_SET)
|
|
|
|
vfs_die ("You may not do such seek on linear file");
|
|
|
|
if (!linear_start (fp->fe, offset))
|
|
|
|
return -1;
|
|
|
|
return offset;
|
|
|
|
}
|
1998-09-13 14:40:43 +04:00
|
|
|
return lseek(fp->local_handle, offset, whence);
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static vfsid s_getid (vfs *me, char *p, struct vfs_stamping **parent)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
struct connection *bucket;
|
|
|
|
char *remote_path;
|
|
|
|
|
|
|
|
*parent = NULL; /* We are not enclosed in any other fs */
|
|
|
|
|
|
|
|
if (!(remote_path = get_path (&bucket, p)))
|
|
|
|
return (vfsid) -1;
|
|
|
|
else {
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(remote_path);
|
1998-09-13 14:40:43 +04:00
|
|
|
return (vfsid) bucket;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static int s_nothingisopen (vfsid id)
|
|
|
|
{
|
|
|
|
return qlock((struct connection *)id) == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void s_free (vfsid id)
|
|
|
|
{
|
|
|
|
struct connection *bucket = (struct connection *) id;
|
|
|
|
|
|
|
|
connection_destructor(bucket);
|
|
|
|
linklist_delete(connections_list, bucket);
|
|
|
|
}
|
|
|
|
|
1998-09-27 23:27:58 +04:00
|
|
|
static char *s_getlocalcopy (vfs *me, char *path)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
1998-09-27 23:27:58 +04:00
|
|
|
struct filp *fp = (struct filp *) s_open (me, path, O_RDONLY, 0);
|
1998-09-13 14:40:43 +04:00
|
|
|
char *p;
|
|
|
|
|
|
|
|
if (fp == NULL)
|
|
|
|
return NULL;
|
|
|
|
if (fp->fe->local_filename == NULL) {
|
|
|
|
s_close ((void *) fp);
|
|
|
|
return NULL;
|
|
|
|
}
|
1999-01-21 01:01:11 +03:00
|
|
|
p = g_strdup (fp->fe->local_filename);
|
1998-09-13 14:40:43 +04:00
|
|
|
qlock(fp->fe->bucket)++;
|
|
|
|
fp->fe->count++;
|
|
|
|
s_close ((void *) fp);
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
1999-11-11 17:50:50 +03:00
|
|
|
static int s_ungetlocalcopy (vfs *me, char *path, char *local, int has_changed)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
1998-09-27 23:27:58 +04:00
|
|
|
struct filp *fp = (struct filp *) s_open (me, path, O_WRONLY, 0);
|
1998-09-13 14:40:43 +04:00
|
|
|
|
|
|
|
if (fp == NULL)
|
1999-11-11 17:50:50 +03:00
|
|
|
return 0;
|
1998-09-13 14:40:43 +04:00
|
|
|
if (!strcmp (fp->fe->local_filename, local)) {
|
|
|
|
fp->has_changed = has_changed;
|
|
|
|
qlock(fp->fe->bucket)--;
|
|
|
|
direntry_destructor(fp->fe);
|
|
|
|
s_close ((void *) fp);
|
|
|
|
} else {
|
|
|
|
/* Should not happen */
|
|
|
|
s_close ((void *) fp);
|
1998-09-27 23:27:58 +04:00
|
|
|
mc_def_ungetlocalcopy (me, path, local, has_changed);
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
1999-11-11 17:50:50 +03:00
|
|
|
return 0;
|
1998-09-13 14:40:43 +04:00
|
|
|
}
|
|
|
|
|
1998-10-13 02:07:53 +04:00
|
|
|
static void
|
1998-09-27 23:27:58 +04:00
|
|
|
X_done(vfs *me)
|
1998-09-13 14:40:43 +04:00
|
|
|
{
|
|
|
|
linklist_destroy(connections_list, connection_destructor);
|
|
|
|
connections_list = NULL;
|
|
|
|
if (logfile)
|
|
|
|
fclose (logfile);
|
|
|
|
logfile = NULL;
|
|
|
|
}
|
1998-09-21 13:52:07 +04:00
|
|
|
|
|
|
|
static int retrieve_file(struct direntry *fe)
|
|
|
|
{
|
1998-10-13 02:07:53 +04:00
|
|
|
/* If you want reget, you'll have to open file with O_LINEAR */
|
|
|
|
int total = 0;
|
1998-09-21 13:52:07 +04:00
|
|
|
char buffer[8192];
|
|
|
|
int local_handle, n;
|
|
|
|
int stat_size = fe->s.st_size;
|
|
|
|
|
|
|
|
if (fe->local_filename)
|
|
|
|
return 1;
|
|
|
|
if (!(fe->local_filename = tempnam (NULL, X))) ERRNOR (ENOMEM, 0);
|
|
|
|
fe->local_is_temp = 1;
|
|
|
|
|
|
|
|
local_handle = open(fe->local_filename, O_RDWR | O_CREAT | O_TRUNC | O_EXCL, 0600);
|
|
|
|
if (local_handle == -1) {
|
|
|
|
my_errno = EIO;
|
|
|
|
goto error_4;
|
|
|
|
}
|
|
|
|
|
1998-10-13 02:07:53 +04:00
|
|
|
if (!linear_start (fe, 0))
|
1998-09-21 13:52:07 +04:00
|
|
|
goto error_3;
|
|
|
|
|
|
|
|
/* Clear the interrupt status */
|
|
|
|
enable_interrupt_key ();
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
if ((n = linear_read(fe, buffer, sizeof(buffer))) < 0)
|
|
|
|
goto error_1;
|
|
|
|
if (!n)
|
|
|
|
break;
|
|
|
|
|
|
|
|
total += n;
|
1999-12-16 15:55:16 +03:00
|
|
|
vfs_print_stats (X, _("Getting file"), fe->remote_filename, total, stat_size);
|
1998-09-21 13:52:07 +04:00
|
|
|
|
|
|
|
while (write(local_handle, buffer, n) < 0) {
|
|
|
|
if (errno == EINTR) {
|
|
|
|
if (got_interrupt()) {
|
|
|
|
my_errno = EINTR;
|
|
|
|
goto error_2;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
my_errno = errno;
|
|
|
|
goto error_1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
linear_close(fe);
|
|
|
|
disable_interrupt_key();
|
|
|
|
close(local_handle);
|
|
|
|
|
|
|
|
if (stat (fe->local_filename, &fe->local_stat) < 0)
|
|
|
|
fe->local_stat.st_mtime = 0;
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
error_1:
|
|
|
|
error_2:
|
|
|
|
linear_close(fe);
|
|
|
|
error_3:
|
|
|
|
disable_interrupt_key();
|
|
|
|
close(local_handle);
|
|
|
|
unlink(fe->local_filename);
|
|
|
|
error_4:
|
1999-01-21 01:01:11 +03:00
|
|
|
g_free(fe->local_filename);
|
1998-09-21 13:52:07 +04:00
|
|
|
fe->local_filename = NULL;
|
|
|
|
return 0;
|
|
|
|
}
|