/*
    cssh - cluster secure shell
    Copyright (C) 2015-2026  Andreas Baumann

    This program 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 3 of the License, or
    (at your option) any later version.

    This program 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, see <http://www.gnu.org/licenses/>.
*/

#define _FILE_OFFSET_BITS 64

#include "sftp_engine.h"
#include "copy_common.h"
#include "progressbar.h"

#include <libssh/sftp.h>

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define BUFSIZE 4096

// iterative walk over a *remote* filesystem tree, sourced via sftp_opendir/
// sftp_readdir instead of the local opendir/readdir that copy_common.c's
// walk_local_tree_* uses -- structurally identical (same walk_event_t shape,
// same symlink-skip policy), used by the recursive download side.
typedef struct remote_walk_dir_frame_t {
	sftp_dir dh;
	char *path;
	struct remote_walk_dir_frame_t *next;
} remote_walk_dir_frame_t;

typedef struct remote_walk_t {
	sftp_session sftp;
	ssh_session session; // only for ssh_get_error() on failures
	bool recursive;
	bool root_pending;
	bool finished;
	char *root_path;
	char *root_name;
	remote_walk_dir_frame_t *stack;
} remote_walk_t;

static remote_walk_t *remote_walk_open( sftp_session sftp, ssh_session session, const char *root_path, bool recursive )
{
	remote_walk_t *w = (remote_walk_t *)calloc( 1, sizeof( remote_walk_t ) );
	if( w == NULL ) {
		return NULL;
	}
	w->sftp = sftp;
	w->session = session;
	w->root_path = strdup( root_path );
	w->root_name = path_basename( root_path );
	if( w->root_path == NULL || w->root_name == NULL ) {
		free( w->root_path );
		free( w->root_name );
		free( w );
		return NULL;
	}
	w->recursive = recursive;
	w->root_pending = true;
	return w;
}

static void remote_push_frame( remote_walk_t *w, sftp_dir dh, char *path )
{
	remote_walk_dir_frame_t *f = (remote_walk_dir_frame_t *)malloc( sizeof( remote_walk_dir_frame_t ) );
	if( f == NULL ) {
		sftp_closedir( dh );
		free( path );
		return;
	}
	f->dh = dh;
	f->path = path;
	f->next = w->stack;
	w->stack = f;
}

static void remote_pop_frame( remote_walk_t *w )
{
	remote_walk_dir_frame_t *top = w->stack;
	w->stack = top->next;
	sftp_closedir( top->dh );
	free( top->path );
	free( top );
}

static int remote_walk_next( remote_walk_t *w, walk_event_t *ev )
{
	memset( ev, 0, sizeof( *ev ) );

	if( w->finished ) {
		return 0;
	}

	if( w->root_pending ) {
		w->root_pending = false;

		sftp_attributes st = sftp_stat( w->sftp, w->root_path );
		if( st == NULL ) {
			fprintf( stderr, "ERROR: failed to stat remote path '%s': %s\n",
				w->root_path, ssh_get_error( w->session ) );
			return -1;
		}

		uint32_t ft = st->permissions & SSH_S_IFMT;
		if( ft == SSH_S_IFLNK ) {
			fprintf( stderr, "WARNING: skipping remote symlink '%s'\n", w->root_path );
			sftp_attributes_free( st );
			w->finished = true;
			return 0;
		} else if( ft == SSH_S_IFDIR ) {
			if( !w->recursive ) {
				fprintf( stderr, "ERROR: omitting remote directory '%s' (use -r for recursive copy)\n", w->root_path );
				sftp_attributes_free( st );
				return -1;
			}
			sftp_dir dh = sftp_opendir( w->sftp, w->root_path );
			if( dh == NULL ) {
				fprintf( stderr, "ERROR: failed to open remote directory '%s': %s\n",
					w->root_path, ssh_get_error( w->session ) );
				sftp_attributes_free( st );
				return -1;
			}
			ev->type = CSSH_WALK_ENTER_DIR;
			ev->name = strdup( w->root_name );
			ev->full_path = strdup( w->root_path );
			ev->mode = st->permissions & 07777;
			remote_push_frame( w, dh, strdup( w->root_path ) );
			sftp_attributes_free( st );
			return 1;
		} else if( ft == SSH_S_IFREG ) {
			ev->type = CSSH_WALK_FILE;
			ev->name = strdup( w->root_name );
			ev->full_path = strdup( w->root_path );
			ev->mode = st->permissions & 07777;
			ev->size = st->size;
			sftp_attributes_free( st );
			w->finished = true;
			return 1;
		} else {
			fprintf( stderr, "WARNING: skipping special remote file '%s'\n", w->root_path );
			sftp_attributes_free( st );
			w->finished = true;
			return 0;
		}
	}

	while( w->stack != NULL ) {
		sftp_attributes de = sftp_readdir( w->sftp, w->stack->dh );
		if( de == NULL ) {
			if( !sftp_dir_eof( w->stack->dh ) ) {
				fprintf( stderr, "ERROR: reading remote directory '%s' failed: %s\n",
					w->stack->path, ssh_get_error( w->session ) );
				return -1;
			}
			ev->type = CSSH_WALK_LEAVE_DIR;
			ev->full_path = strdup( w->stack->path );
			remote_pop_frame( w );
			if( w->stack == NULL ) {
				w->finished = true;
			}
			return 1;
		}

		if( strcmp( de->name, "." ) == 0 || strcmp( de->name, ".." ) == 0 ) {
			sftp_attributes_free( de );
			continue;
		}

		char *full = join_path( w->stack->path, de->name );
		if( full == NULL ) {
			fprintf( stderr, "ERROR: memory allocation failed for path of '%s'\n", de->name );
			sftp_attributes_free( de );
			return -1;
		}

		uint32_t ft = de->permissions & SSH_S_IFMT;
		if( ft == SSH_S_IFLNK ) {
			fprintf( stderr, "WARNING: skipping remote symlink '%s'\n", full );
			free( full );
			sftp_attributes_free( de );
			continue;
		} else if( ft == SSH_S_IFDIR ) {
			sftp_dir dh = sftp_opendir( w->sftp, full );
			if( dh == NULL ) {
				fprintf( stderr, "WARNING: failed to open remote directory '%s': %s\n",
					full, ssh_get_error( w->session ) );
				free( full );
				sftp_attributes_free( de );
				continue;
			}
			ev->type = CSSH_WALK_ENTER_DIR;
			ev->name = strdup( de->name );
			ev->mode = de->permissions & 07777;
			ev->full_path = full;
			remote_push_frame( w, dh, strdup( full ) );
			sftp_attributes_free( de );
			return 1;
		} else if( ft == SSH_S_IFREG ) {
			ev->type = CSSH_WALK_FILE;
			ev->name = strdup( de->name );
			ev->mode = de->permissions & 07777;
			ev->size = de->size;
			ev->full_path = full;
			sftp_attributes_free( de );
			return 1;
		} else {
			fprintf( stderr, "WARNING: skipping special remote file '%s'\n", full );
			free( full );
			sftp_attributes_free( de );
			continue;
		}
	}

	w->finished = true;
	return 0;
}

static void remote_walk_close( remote_walk_t *w )
{
	if( w == NULL ) {
		return;
	}
	while( w->stack != NULL ) {
		remote_pop_frame( w );
	}
	free( w->root_path );
	free( w->root_name );
	free( w );
}

static int ensure_remote_dir( sftp_session sftp, ssh_session session, const char *path, mode_t mode )
{
	if( sftp_mkdir( sftp, path, mode ) == SSH_OK ) {
		return 0;
	}
	sftp_attributes st = sftp_stat( sftp, path );
	if( st != NULL ) {
		bool ok = ( ( st->permissions & SSH_S_IFMT ) == SSH_S_IFDIR );
		sftp_attributes_free( st );
		if( ok ) {
			return 0;
		}
	}
	fprintf( stderr, "ERROR: failed to create remote directory '%s': %s\n", path, ssh_get_error( session ) );
	return -1;
}

typedef enum sftp_state_e {
	CSSH_SFTP_STATE_IDLE,
	CSSH_SFTP_STATE_TRANSFERRING
} sftp_state_e;

// one file transfer in flight (or idle, waiting for the next one); a host
// runs up to 'nof_jobs' of these concurrently, each with its own progress
// bar, interleaved chunk by chunk in the same round-robin fashion already
// used to interleave hosts against each other
typedef struct sftp_slot_t {
	sftp_state_e state;
	sftp_file file;
	int fd;
	char *filename;
	uint64_t size;
	uint64_t bytesTransferred;
	cssh_progressbar_t progressbar;
} sftp_slot_t;

typedef struct sftp_data_t {
	sftp_session sftp;
	dir_stack_t dir_stack;      // download: local landing dir / upload: remote target dir
	remote_walk_t *remote_walk; // download only
	local_walk_t *local_walk;   // upload only
	bool dest_is_dir;           // upload only: does remote_directory already exist as a dir?
	bool first_entry;           // upload only: is the next walk event the root entry?
	bool walk_finished;         // the tree walk for this host produced its last event
	unsigned int nof_jobs;
	unsigned int active_jobs;   // number of slots currently TRANSFERRING
	sftp_slot_t *slots;
} sftp_data_t;

static void free_sftp_host_data( sftp_data_t *sftp_data, unsigned int nof_sessions )
{
	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		if( sftp_data[i].slots != NULL ) {
			for( unsigned int j = 0; j < sftp_data[i].nof_jobs; j++ ) {
				sftp_slot_t *slot = &sftp_data[i].slots[j];
				if( slot->file != NULL ) {
					sftp_close( slot->file );
					slot->file = NULL;
				}
				if( slot->fd > 0 ) {
					close( slot->fd );
					slot->fd = 0;
				}
				free( slot->filename );
				slot->filename = NULL;
			}
			free( sftp_data[i].slots );
			sftp_data[i].slots = NULL;
		}
		free_dir_stack( &sftp_data[i].dir_stack );
		remote_walk_close( sftp_data[i].remote_walk );
		sftp_data[i].remote_walk = NULL;
		walk_local_tree_close( sftp_data[i].local_walk );
		sftp_data[i].local_walk = NULL;
		if( sftp_data[i].sftp != NULL ) {
			sftp_free( sftp_data[i].sftp );
			sftp_data[i].sftp = NULL;
		}
	}
}

static void sftp_fatal( ssh_session *session, sftp_data_t *sftp_data, unsigned int nof_sessions,
	char **host, unsigned int *port, bool verbose, cssh_progressbar_pool_t *pool )
{
	if( sftp_data != NULL ) {
		free_sftp_host_data( sftp_data, nof_sessions );
		free( sftp_data );
	}
	cssh_finalize( &session, NULL, host, port, nof_sessions, verbose, pool );
	exit( EXIT_FAILURE );
}

static sftp_data_t *setup_sftp_sessions( ssh_session *session, char **host, unsigned int *port,
	unsigned int nof_sessions, bool verbose )
{
	sftp_data_t *sftp_data = (sftp_data_t *)calloc( nof_sessions, sizeof( sftp_data_t ) );
	if( sftp_data == NULL ) {
		fprintf( stderr, "ERROR: Memory allocation failed for sftp_data array\n" );
		sftp_fatal( session, NULL, nof_sessions, host, port, verbose, NULL );
	}

	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		sftp_data[i].sftp = sftp_new( session[i] );
		if( sftp_data[i].sftp == NULL ) {
			fprintf( stderr, "ERROR: Unable to create SFTP session for host '%s': %s\n",
				host[i], ssh_get_error( session[i] ) );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, NULL );
		}
		if( sftp_init( sftp_data[i].sftp ) != SSH_OK ) {
			fprintf( stderr, "ERROR: Unable to initialize SFTP session for host '%s': %s\n",
				host[i], ssh_get_error( session[i] ) );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, NULL );
		}
	}

	return sftp_data;
}

// pulls walk events for one host until either a free slot is filled with
// a file transfer or the walk runs dry; ENTER_DIR/LEAVE_DIR events are
// applied directly to the host's dir_stack and don't consume a slot
static void fill_download_slot( ssh_session *session, char **host, unsigned int i,
	sftp_data_t *sftp_data, unsigned int nof_sessions, unsigned int *port, bool verbose,
	cssh_progressbar_pool_t *progressbars, sftp_slot_t *slot )
{
	sftp_data_t *hd = &sftp_data[i];

	for( ;; ) {
		walk_event_t ev;
		int wrc = remote_walk_next( hd->remote_walk, &ev );
		if( wrc < 0 ) {
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
		} else if( wrc == 0 ) {
			hd->walk_finished = true;
			return;
		}

		switch( ev.type ) {
			case CSSH_WALK_ENTER_DIR: {
				char *local_dir = join_path( top_dir_stack( &hd->dir_stack ), ev.name );
				if( verbose ) {
					fprintf( stderr, "Receiving directory '%s' with permissions '0%o'\n",
						local_dir, ev.mode );
				}
				if( mkdir( local_dir, ev.mode ) < 0 && errno != EEXIST ) {
					fprintf( stderr, "ERROR: failed to create directory '%s': %s\n",
						local_dir, strerror( errno ) );
					free( local_dir );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}
				push_dir_stack( &hd->dir_stack, local_dir );
				free( local_dir );
				free_walk_event( &ev );
				} continue;

			case CSSH_WALK_LEAVE_DIR:
				pop_dir_stack( &hd->dir_stack );
				free_walk_event( &ev );
				continue;

			case CSSH_WALK_FILE: {
				char *local_path = join_path( top_dir_stack( &hd->dir_stack ), ev.name );
				if( verbose ) {
					fprintf( stderr, "Receiving file '%s' with permissions '0%o' of size '%"PRIu64"'\n",
						local_path, ev.mode, ev.size );
				}

				int fd = open( local_path, O_WRONLY | O_CREAT | O_TRUNC, ev.mode ? ev.mode : 0644 );
				if( fd < 0 ) {
					fprintf( stderr, "ERROR: Unable to open file '%s': %s\n",
						local_path, strerror( errno ) );
					free( local_path );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}

				sftp_file rf = sftp_open( hd->sftp, ev.full_path, O_RDONLY, 0 );
				if( rf == NULL ) {
					fprintf( stderr, "ERROR: Unable to open remote file '%s': %s\n",
						ev.full_path, ssh_get_error( session[i] ) );
					close( fd );
					free( local_path );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}

				slot->fd = fd;
				slot->file = rf;
				slot->size = ev.size;
				slot->bytesTransferred = 0;
				slot->filename = local_path;
				slot->state = CSSH_SFTP_STATE_TRANSFERRING;
				hd->active_jobs++;

				size_t len = strlen( host[i] ) + strlen( local_path ) + 5;
				if( create_progressbar( &slot->progressbar, 0, ev.size, len,
					"[%s:%s]", host[i], local_path ) < 0 ) {
					fprintf( stderr, "ERROR: Unable to create progress bar for host '%s' and file '%s'\n",
						host[i], local_path );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}
				append_progressbar_to_pool( progressbars, &slot->progressbar );
				free_walk_event( &ev );
				} return;
		}
	}
}

void run_sftp_download( ssh_session *session, char **host, unsigned int *port, unsigned int nof_sessions,
	const char *remote_directory, struct gengetopt_args_info *args_info )
{
	bool verbose = args_info->verbose_given > 0;
	bool recursive = args_info->recursive_given > 0;
	unsigned int nof_jobs = ( args_info->parallel_arg > 0 ) ? (unsigned int)args_info->parallel_arg : 1;

	sftp_data_t *sftp_data = setup_sftp_sessions( session, host, port, nof_sessions, verbose );

	cssh_progressbar_pool_t progressbars;
	if( create_progressbar_pool( &progressbars, nof_sessions * nof_jobs ) < 0 ) {
		fprintf( stderr, "ERROR: Creation of a pool of progress bars failed\n" );
		sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, NULL );
	}

	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		create_dir_stack( &sftp_data[i].dir_stack );

		char *pwd = getcwd( NULL, 0 );
		if( pwd == NULL ) {
			fprintf( stderr, "ERROR: failed to determine working directory for host '%s': %s\n",
				host[i], strerror( errno ) );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
		}
		push_dir_stack( &sftp_data[i].dir_stack, pwd );

		if( nof_sessions > 1 ) {
			char *full_path = join_path( pwd, host[i] );
			if( full_path == NULL || mkdir( full_path, 0750 ) < 0 ) {
				if( full_path == NULL || errno != EEXIST ) {
					fprintf( stderr, "ERROR: failed to create base directory for host '%s': %s\n",
						host[i], strerror( errno ) );
					free( pwd );
					free( full_path );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
				}
			}
			push_dir_stack( &sftp_data[i].dir_stack, full_path );
			free( full_path );
		}
		free( pwd );

		sftp_data[i].remote_walk = remote_walk_open( sftp_data[i].sftp, session[i], remote_directory, recursive );
		if( sftp_data[i].remote_walk == NULL ) {
			fprintf( stderr, "ERROR: Memory allocation failed for remote tree walk of '%s'\n", remote_directory );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
		}

		sftp_data[i].nof_jobs = nof_jobs;
		sftp_data[i].slots = (sftp_slot_t *)calloc( nof_jobs, sizeof( sftp_slot_t ) );
		if( sftp_data[i].slots == NULL ) {
			fprintf( stderr, "ERROR: Memory allocation failed for transfer slots for host '%s'\n", host[i] );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
		}
	}

	bool all_done = false;
	while( !all_done ) {
		for( unsigned int i = 0; i < nof_sessions; i++ ) {
			sftp_data_t *hd = &sftp_data[i];

			for( unsigned int j = 0; j < hd->nof_jobs; j++ ) {
				sftp_slot_t *slot = &hd->slots[j];
				if( slot->state != CSSH_SFTP_STATE_TRANSFERRING ) {
					continue;
				}

				char buf[BUFSIZE];
				if( slot->bytesTransferred < slot->size ) {
					uint64_t remaining = slot->size - slot->bytesTransferred;
					size_t want = ( remaining < BUFSIZE ) ? (size_t)remaining : BUFSIZE;
					ssize_t r = sftp_read( slot->file, buf, want );
					if( r < 0 ) {
						fprintf( stderr, "ERROR: reading remote data for file '%s' failed: %s\n",
							slot->filename, ssh_get_error( session[i] ) );
						sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
					} else if( r == 0 ) {
						// server delivered fewer bytes than initially stat()'d; stop here
						slot->bytesTransferred = slot->size;
					} else {
						ssize_t w = write( slot->fd, buf, (size_t)r );
						if( w < 0 ) {
							fprintf( stderr, "ERROR: writing data to file '%s' failed: %s\n",
								slot->filename, strerror( errno ) );
							sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
						}
						slot->bytesTransferred += (uint64_t)r;
						if( args_info->progress_bar_given > 0 ) {
							set_value_of_progressbar( &slot->progressbar, slot->bytesTransferred );
						}
					}
				} else {
					sftp_close( slot->file );
					slot->file = NULL;
					if( close( slot->fd ) < 0 ) {
						fprintf( stderr, "ERROR: Unable to close file '%s': %s\n",
							slot->filename, strerror( errno ) );
						sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
					}
					slot->fd = 0;
					if( args_info->progress_bar_given > 0 ) {
						print_transfer_done( host[i], slot->filename, slot->size );
					}
					free( slot->filename );
					slot->filename = NULL;
					remove_progressbar_from_pool( &progressbars, &slot->progressbar );
					free_progressbar( &slot->progressbar );
					slot->state = CSSH_SFTP_STATE_IDLE;
					hd->active_jobs--;
				}
			}

			if( !hd->walk_finished ) {
				for( unsigned int j = 0; j < hd->nof_jobs; j++ ) {
					sftp_slot_t *slot = &hd->slots[j];
					if( slot->state != CSSH_SFTP_STATE_IDLE ) {
						continue;
					}
					fill_download_slot( session, host, i, sftp_data, nof_sessions, port, verbose, &progressbars, slot );
					if( hd->walk_finished ) {
						break;
					}
				}
			}
		}

		all_done = true;
		for( unsigned int i = 0; i < nof_sessions; i++ ) {
			if( !sftp_data[i].walk_finished || sftp_data[i].active_jobs > 0 ) {
				all_done = false;
				break;
			}
		}

		if( args_info->progress_bar_given ) {
			redraw_progressbars( &progressbars );
		}
	}

	free_sftp_host_data( sftp_data, nof_sessions );
	free( sftp_data );
	cssh_finalize( &session, NULL, host, port, nof_sessions, verbose, &progressbars );
}

static void fill_upload_slot( ssh_session *session, char **host, unsigned int i,
	sftp_data_t *sftp_data, unsigned int nof_sessions, unsigned int *port, bool verbose,
	const char *remote_directory, cssh_progressbar_pool_t *progressbars, sftp_slot_t *slot )
{
	sftp_data_t *hd = &sftp_data[i];

	for( ;; ) {
		walk_event_t ev;
		int wrc = walk_local_tree_next( hd->local_walk, &ev );
		if( wrc < 0 ) {
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
		} else if( wrc == 0 ) {
			hd->walk_finished = true;
			return;
		}

		switch( ev.type ) {
			case CSSH_WALK_ENTER_DIR: {
				bool is_root_entry = hd->first_entry;
				hd->first_entry = false;
				char *remote_dir = ( is_root_entry && !hd->dest_is_dir )
					? strdup( remote_directory )
					: join_path( top_dir_stack( &hd->dir_stack ), ev.name );
				if( verbose ) {
					fprintf( stderr, "Sending directory '%s' with permissions '0%o'\n",
						remote_dir, ev.mode );
				}
				if( ensure_remote_dir( hd->sftp, session[i], remote_dir, ev.mode ) < 0 ) {
					free( remote_dir );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}
				push_dir_stack( &hd->dir_stack, remote_dir );
				free( remote_dir );
				free_walk_event( &ev );
				} continue;

			case CSSH_WALK_LEAVE_DIR:
				pop_dir_stack( &hd->dir_stack );
				free_walk_event( &ev );
				continue;

			case CSSH_WALK_FILE: {
				bool is_root_entry = hd->first_entry;
				hd->first_entry = false;
				char *remote_path = ( is_root_entry && !hd->dest_is_dir )
					? strdup( remote_directory )
					: join_path( top_dir_stack( &hd->dir_stack ), ev.name );
				if( verbose ) {
					fprintf( stderr, "Sending file '%s' with permissions '0%o' of size '%"PRIu64"'\n",
						remote_path, ev.mode, ev.size );
				}

				int fd = open( ev.full_path, O_RDONLY );
				if( fd < 0 ) {
					fprintf( stderr, "ERROR: Unable to open file '%s': %s\n",
						ev.full_path, strerror( errno ) );
					free( remote_path );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}

				sftp_file wf = sftp_open( hd->sftp, remote_path,
					O_WRONLY | O_CREAT | O_TRUNC, ev.mode ? ev.mode : 0644 );
				if( wf == NULL ) {
					fprintf( stderr, "ERROR: Unable to open remote file '%s': %s\n",
						remote_path, ssh_get_error( session[i] ) );
					close( fd );
					free( remote_path );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}

				slot->fd = fd;
				slot->file = wf;
				slot->size = ev.size;
				slot->bytesTransferred = 0;
				slot->filename = remote_path;
				slot->state = CSSH_SFTP_STATE_TRANSFERRING;
				hd->active_jobs++;

				size_t len = strlen( host[i] ) + strlen( remote_path ) + 5;
				if( create_progressbar( &slot->progressbar, 0, ev.size, len,
					"[%s:%s]", host[i], remote_path ) < 0 ) {
					fprintf( stderr, "ERROR: Unable to create progress bar for host '%s' and file '%s'\n",
						host[i], remote_path );
					free_walk_event( &ev );
					sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, progressbars );
				}
				append_progressbar_to_pool( progressbars, &slot->progressbar );
				free_walk_event( &ev );
				} return;
		}
	}
}

void run_sftp_upload( ssh_session *session, char **host, unsigned int *port, unsigned int nof_sessions,
	const char *local_directory, const char *remote_directory, struct gengetopt_args_info *args_info )
{
	bool verbose = args_info->verbose_given > 0;
	bool recursive = args_info->recursive_given > 0;
	unsigned int nof_jobs = ( args_info->parallel_arg > 0 ) ? (unsigned int)args_info->parallel_arg : 1;

	sftp_data_t *sftp_data = setup_sftp_sessions( session, host, port, nof_sessions, verbose );

	cssh_progressbar_pool_t progressbars;
	if( create_progressbar_pool( &progressbars, nof_sessions * nof_jobs ) < 0 ) {
		fprintf( stderr, "ERROR: Creation of a pool of progress bars failed\n" );
		sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, NULL );
	}

	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		create_dir_stack( &sftp_data[i].dir_stack );

		/* real scp/sftp semantics: an existing directory is a container
		 * entries land inside (named after their own basename); otherwise
		 * remote_directory itself is the exact target for the single root
		 * entry (see the matching 'rename_first' logic in the scp engine
		 * and tests/mock_sshd.c's handle_scp_sink) */
		sftp_attributes dest_st = sftp_stat( sftp_data[i].sftp, remote_directory );
		sftp_data[i].dest_is_dir = ( dest_st != NULL && ( dest_st->permissions & SSH_S_IFMT ) == SSH_S_IFDIR );
		if( dest_st != NULL ) {
			sftp_attributes_free( dest_st );
		}
		sftp_data[i].first_entry = true;
		if( sftp_data[i].dest_is_dir ) {
			push_dir_stack( &sftp_data[i].dir_stack, remote_directory );
		}

		sftp_data[i].local_walk = walk_local_tree_open( local_directory, recursive );
		if( sftp_data[i].local_walk == NULL ) {
			fprintf( stderr, "ERROR: Memory allocation failed for local tree walk of '%s'\n", local_directory );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
		}

		sftp_data[i].nof_jobs = nof_jobs;
		sftp_data[i].slots = (sftp_slot_t *)calloc( nof_jobs, sizeof( sftp_slot_t ) );
		if( sftp_data[i].slots == NULL ) {
			fprintf( stderr, "ERROR: Memory allocation failed for transfer slots for host '%s'\n", host[i] );
			sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
		}
	}

	bool all_done = false;
	while( !all_done ) {
		for( unsigned int i = 0; i < nof_sessions; i++ ) {
			sftp_data_t *hd = &sftp_data[i];

			for( unsigned int j = 0; j < hd->nof_jobs; j++ ) {
				sftp_slot_t *slot = &hd->slots[j];
				if( slot->state != CSSH_SFTP_STATE_TRANSFERRING ) {
					continue;
				}

				char buf[BUFSIZE];
				if( slot->bytesTransferred < slot->size ) {
					uint64_t remaining = slot->size - slot->bytesTransferred;
					size_t want = ( remaining < BUFSIZE ) ? (size_t)remaining : BUFSIZE;
					ssize_t r = read( slot->fd, buf, want );
					if( r < 0 ) {
						fprintf( stderr, "ERROR: reading data from file '%s' failed: %s\n",
							slot->filename, strerror( errno ) );
						sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
					} else if( r == 0 ) {
						slot->bytesTransferred = slot->size;
					} else {
						ssize_t w = sftp_write( slot->file, buf, (size_t)r );
						if( w < 0 ) {
							fprintf( stderr, "ERROR: writing remote data for file '%s' failed: %s\n",
								slot->filename, ssh_get_error( session[i] ) );
							sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
						}
						slot->bytesTransferred += (uint64_t)r;
						if( args_info->progress_bar_given > 0 ) {
							set_value_of_progressbar( &slot->progressbar, slot->bytesTransferred );
						}
					}
				} else {
					sftp_close( slot->file );
					slot->file = NULL;
					if( close( slot->fd ) < 0 ) {
						fprintf( stderr, "ERROR: Unable to close file '%s': %s\n",
							slot->filename, strerror( errno ) );
						sftp_fatal( session, sftp_data, nof_sessions, host, port, verbose, &progressbars );
					}
					slot->fd = 0;
					if( args_info->progress_bar_given > 0 ) {
						print_transfer_done( host[i], slot->filename, slot->size );
					}
					free( slot->filename );
					slot->filename = NULL;
					remove_progressbar_from_pool( &progressbars, &slot->progressbar );
					free_progressbar( &slot->progressbar );
					slot->state = CSSH_SFTP_STATE_IDLE;
					hd->active_jobs--;
				}
			}

			if( !hd->walk_finished ) {
				for( unsigned int j = 0; j < hd->nof_jobs; j++ ) {
					sftp_slot_t *slot = &hd->slots[j];
					if( slot->state != CSSH_SFTP_STATE_IDLE ) {
						continue;
					}
					fill_upload_slot( session, host, i, sftp_data, nof_sessions, port, verbose,
						remote_directory, &progressbars, slot );
					if( hd->walk_finished ) {
						break;
					}
				}
			}
		}

		all_done = true;
		for( unsigned int i = 0; i < nof_sessions; i++ ) {
			if( !sftp_data[i].walk_finished || sftp_data[i].active_jobs > 0 ) {
				all_done = false;
				break;
			}
		}

		if( args_info->progress_bar_given ) {
			redraw_progressbars( &progressbars );
		}
	}

	free_sftp_host_data( sftp_data, nof_sessions );
	free( sftp_data );
	cssh_finalize( &session, NULL, host, port, nof_sessions, verbose, &progressbars );
}
