class EL_FTP_PROTOCOL

(source code)

Client examples: AUTOTEST_APP

description

Ftp protocol

note
	description: "Ftp protocol"

	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-01-11 14:49:08 GMT (Thursday 11th January 2024)"
	revision: "39"

class
	EL_FTP_PROTOCOL

inherit
	EL_FTP_IMPLEMENTATION
		redefine
			open, initialize
		end

create
	make_write, make_read

feature {NONE} -- Initialization

	initialize
		do
			Precursor
			create current_directory
			set_binary_mode
			create created_directory_set.make (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
			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 then
				from until data_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
								data_socket.close
							else
								Result.extend (dir_path + list.item)
							end
							list.forth
						end
					end
					receive_entry_list_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

	last_reply: ZSTRING
		do
			create Result.make_from_utf_8 (last_reply_utf_8)
			Result.right_adjust
		end

	user_home_dir: DIR_PATH
		do
			Result := config.user_home_dir
		end

feature -- Measurement

	file_size (file_path: FILE_PATH): INTEGER
		do
			send_path (Command.size, file_path, << Reply.file_status >>)
			if last_succeeded then
				Result := String_8.substring_to_reversed (last_reply_utf_8, ' ').to_integer
			end
		end

	last_entry_count: INTEGER
		-- directory entry count set by `read_entry_count' or

feature -- Element change

	set_current_directory (a_current_directory: DIR_PATH)
		do
			send_absolute (
				Command.change_working_directory, a_current_directory, << Reply.success, Reply.file_action_ok >>
			)
			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 dir_path.is_empty or 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.file_action_ok >>)
			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 loop
				read_entry_count (l_path)
				if last_entry_count = 0 then
					remove_directory (l_path)
					l_path := l_path.parent
					done := l_path.is_empty
				else
					done := True
				end
			end
		end

feature -- Basic operations

	read_entry_count (dir_path: DIR_PATH)
		do
			initiate_file_listing (dir_path)
			if transfer_initiated then
				data_socket.close
				receive_entry_list_count
			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

	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 last_reply_utf_8.has_substring (Error.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

	last_succeeded: BOOLEAN
		do
			Result := error_code = 0
		end

feature -- Status change

	close
			--
		do
			if is_logged_in then
				quit
			end
			close_sockets
		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 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.invalid_login)
				end
			end
		end

	open
			-- Open resource.
		local
			l_socket: like main_socket
		do
			if not is_open then
				open_connection
				if not is_open then
					error_code := Connection_refused
				else
					l_socket := main_socket
					check l_socket_attached: l_socket /= Void end
					receive (l_socket)
				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

	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

	set_last_entry_count (a_count: INTEGER)
		do
			last_entry_count := a_count
		end

end