class EL_FTP_PROTOCOL

(source code)

Client examples: FAUX_FTP_PROTOCOLLOCALIZATION_COMMAND_SHELL_APPNETWORK_AUTOTEST_APP

description

File transfer protocol (FTP)

note
	description: "File transfer protocol (FTP)"

	author: "Finnian Reilly"
	copyright: "Copyright (c) 2001-2022 Finnian Reilly"
	contact: "finnian at eiffel hyphen loop dot com"

	license: "MIT license (See: en.wikipedia.org/wiki/MIT_License)"
	date: "2024-09-25 11:15:59 GMT (Wednesday 25th September 2024)"
	revision: "47"

class
	EL_FTP_PROTOCOL

inherit
	EL_FTP_IMPLEMENTATION

create
	make_write, make_read

feature {NONE} -- Initialization

	initialize
		do
			set_read_buffer_size (Default_buffer_size)
			create current_directory
			set_binary_mode
			create created_directory_set.make_equal (10)
			create reply_parser.make
		end

	make (a_config: EL_FTP_CONFIGURATION; set_mode: PROCEDURE)
		require
			authenticated: a_config.is_authenticated
		do
			make_ftp (a_config.url)
			config := a_config
			passive_mode := a_config.passive_mode
			set_mode.apply
		end

	make_read (a_config: EL_FTP_CONFIGURATION)
		do
			make (a_config, agent set_read_mode)
		end

	make_write (a_config: EL_FTP_CONFIGURATION)
		do
			make (a_config, agent set_write_mode)
		end

feature -- Access

	config: EL_FTP_CONFIGURATION

	current_directory: DIR_PATH

	entry_list (dir_path: DIR_PATH): EL_FILE_PATH_LIST
		-- list of file and directory entries in remote directory `dir_path'
		local
			line_list: EL_SPLIT_IMMUTABLE_UTF_8_LIST
		do
			create Result.make_empty
			initiate_file_listing (dir_path)

			if transfer_initiated and then attached data_socket as socket then
				from until socket.is_closed loop
					read
					create line_list.make_shared_by_string (last_packet, Carriage_return_new_line)
					if attached line_list as list then
						Result.grow (Result.count + list.count)
						from list.start until list.after loop
							if list.utf_8_item_count = 0 then
								socket.close

							elseif attached list.item as name then
								if name.count <= 2 implies name.occurrences ('.') /= name.count then
									Result.extend (dir_path + name)
								end
							end
							list.forth
						end
					end
					receive_entry_list_count (Result.count)
				end
			end
			reset_file_listing
		ensure
			valid_count: Result.count = last_entry_count
			address_path_unchanged: old address.path ~ address.path
			detached_data_socket: not attached data_socket
		end

	user_home_dir: DIR_PATH
		do
			Result := config.user_home_dir
		end

feature -- Element change

	set_current_directory (a_current_directory: DIR_PATH)
		do
			send_absolute (
				Command.change_working_directory, a_current_directory, Reply.valid_file_action
			)
			if last_succeeded then
				if a_current_directory.is_absolute then
					current_directory.copy (a_current_directory)
				else
					current_directory.append_dir_path (a_current_directory)
				end
			end
		ensure
			changed: get_current_directory ~ current_directory
		end

feature -- Remote operations

	change_home_dir
		do
			set_current_directory (user_home_dir)
		end

	delete_file (file_path: FILE_PATH)
		do
			send_path (Command.delete_file, file_path, << Reply.file_action_ok >>)
		end

	make_directory (dir_path: DIR_PATH)
		-- create directory relative to current directory
		require
			dir_path_is_relative: not dir_path.is_absolute
		do
			if directory_exists (dir_path) then
				do_nothing
			else
				if attached dir_path.parent as parent and then not directory_exists (parent) then
					make_directory (parent) -- recurse
				end
				make_directory_step (dir_path)
			end
		ensure
			exists: directory_exists (dir_path)
		end

	remove_directory (dir_path: DIR_PATH)
		do
			send_absolute (Command.remove_directory, dir_path, Reply.valid_file_action)
			if last_succeeded then
				created_directory_set.prune (dir_path)
			end
		end

	remove_until_empty (dir_path: DIR_PATH)
		local
			done: BOOLEAN; l_path: DIR_PATH
		do
			from l_path := dir_path.twin until done or l_path.is_empty loop
				read_entry_count (l_path)
				inspect last_entry_count
					when 0 then
						remove_directory (l_path)
				else
					done := True
				end
				l_path := l_path.parent
			end
		end

feature -- Basic operations

	read_entry_count (dir_path: DIR_PATH)
		do
			initiate_file_listing (dir_path)
			if transfer_initiated and then attached data_socket as socket then
				if passive_mode then
					socket.get_reply (last_reply_utf_8)
				else
					socket.close
					receive_entry_list_count (0)
				end
			end
			reset_file_listing
		ensure
			address_path_unchanged: old address.path ~ address.path
			detached_data_socket: not attached data_socket
		end

	reset
		do
			close; reset_error
			execution.sleep (500)
			login; change_home_dir
		end

	send_size (dir_path: DIR_PATH)
		-- `True' if remote directory `dir_path' exists relative to `current_directory'
		do
			send_absolute (Command.size, dir_path, << Reply.action_not_taken >>)
		end

	upload (item: EL_FTP_UPLOAD_ITEM)
		-- upload file to destination directory relative to home directory
		require
			binary_mode_set: is_binary_mode
			file_to_upload_exists: item.source_path.exists
		do
			make_directory (item.destination_dir)
			transfer_file (item.source_path, item.destination_file_path)
		end

feature -- Status query

	directory_exists (dir_path: DIR_PATH): BOOLEAN
		-- `True' if remote directory `dir_path' exists relative to `current_directory'
		do
			if dir_path.is_empty or else created_directory_set.has (dir_path) then
				Result := True
			else
				send_absolute (Command.size, dir_path, << Reply.action_not_taken >>)
				Result := last_succeeded and then reply_contains (Not_regular_file)
			end
		end

	file_exists (file_path: FILE_PATH): BOOLEAN
		-- `True' if remote `file_path' exists relative to `current_directory'
		do
			if file_path.is_empty then
				Result := True
			else
				send_absolute (Command.size, file_path, << Reply.file_status >>)
				Result := last_succeeded
			end
		end

	is_default_state: BOOLEAN
		do
			Result := config.url.host.is_empty
		end

feature -- Status change

	close
			--
		do
			if is_logged_in then
				quit
			end
			close_sockets
			created_directory_set.wipe_out
		end

	login
		do
			if not is_open then
				open
			end
			attempt (agent try_login, Max_login_attempts)

			if not is_logged_in then
				close
			end
		end

	try_login (done: BOOLEAN_REF)
		do
			reset_error
			if is_open then
				authenticate
				if is_logged_in then
					done.set_item (True)
					if passive_mode implies send_passive_mode_command then
						if send_transfer_mode_command then
							bytes_transferred := 0
							transfer_initiated := False
							is_count_valid := False
						else
							display_error (Error.cannot_set_transfer_mode)
						end
					else
						display_error (Error.cannot_enter_passive_mode)
					end
				else
					display_error (Error.invalid_login)
				end
			end
		end

	open
			-- Open resource.
		do
			if not is_open then
				open_connection
				if not is_open then
					error_code := Connection_refused

				elseif attached main_socket as socket then
					socket.get_reply (last_reply_utf_8)
				else
					check socket_attached: False end
				end
			end
		rescue
			error_code := Connection_refused
		end

	quit
			--
		do
			send (Command.quit, Void, << Reply.closing_control_connection >>)
		end

	set_default_state
		do
			initialize
		end

feature {NONE} -- Implementation

	authenticate
		-- Log in to server.
		require
			opened: is_open
		do
			is_logged_in := send_username and then send_password
		end

	get_current_directory: DIR_PATH
		do
			send (Command.print_working_directory, Void, << Reply.PATHNAME_created >>)
			-- 257 %"/htdocs%" is your current location%R%N
			if last_succeeded then
				Result := last_reply.cropped ('"', '"')
			else
				create Result
			end
		end

	make_directory_step (dir_path: DIR_PATH)
		require
			parent_exists: directory_exists (dir_path.parent)
		do
			send_path (Command.make_directory, dir_path, << Reply.PATHNAME_created >>)
			if last_succeeded then
				created_directory_set.put (dir_path)
			end
		end

	receive_entry_list_count (list_count: INTEGER)
		-- Parse `last_reply_utf_8' from acknowledgement to NLST data transfer
		-- Eg. "226 4 matches total%R%N"
		local
			split_list: EL_SPLIT_IMMUTABLE_UTF_8_LIST
		do
			if attached main_socket as socket then
				socket.get_reply (last_reply_utf_8)
				if not has_error then
					create split_list.make_shared_adjusted (last_reply_utf_8, ' ', 0)
					if split_list.count = 0 then
						error_code := Transmission_error

					elseif attached split_list as list then
						from list.start until list.after loop
							inspect list.index
								when 1 then
									if split_list.natural_16_item /= Reply.closing_data_connection then
										error_code := Transmission_error
									end
								when 2 then
									last_entry_count := split_list.integer_item
							else
							end
							list.forth
						end
					end
				end
			else
				error_code := No_socket_to_connect
			end
		end

	send_absolute (cmd: IMMUTABLE_STRING_8; a_path: EL_PATH; codes: ARRAY [NATURAL_16])
		do
			if a_path.is_absolute then
				send_path (cmd, a_path, codes)

			elseif attached {FILE_PATH} a_path as file_path then
				send_path (cmd, current_directory.plus_file (file_path), codes)

			elseif attached {DIR_PATH} a_path as dir_path then
				send_path (cmd, current_directory.plus_dir (dir_path), codes)
			end
		end

end