class EL_FTP_IMPLEMENTATION

(source code)

description

Constants base on list of raw ftp commands

note
	description: "[
		Constants base on [http://www.nsftools.com/tips/RawFTP.htm list of raw ftp commands]
	]"

	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: "14"

deferred class
	EL_FTP_IMPLEMENTATION

inherit
	FTP_PROTOCOL
		rename
			close as close_sockets,
			error as has_error,
			exception as exception_code,
			make as make_ftp,
			send as send_to_socket,
			login as ftp_login,
			last_reply as last_reply_utf_8,
			reply_code_ok as integer_32_reply_code_ok
		redefine
			close_sockets, send_transfer_command
		end

	EL_ITERATION_ROUTINES

	EL_FILE_OPEN_ROUTINES
		rename
			Open as File_open,
			Read as Read_from
		end

	EL_MODULE_EXCEPTION; EL_MODULE_EXECUTION_ENVIRONMENT; EL_MODULE_FILE; EL_MODULE_FILE_SYSTEM

	EL_MODULE_LIO; EL_MODULE_TUPLE; EL_MODULE_STRING_8

	EL_MODULE_USER_INPUT

	EL_STRING_8_CONSTANTS

	EL_SHARED_STRING_8_BUFFER_SCOPES

feature {NONE} -- Implementation

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

	close_sockets
		do
			Precursor
			initiating_listing := False
		end

	display_command_error (cmd: STRING; message: READABLE_STRING_GENERAL)
		local
			upper_command: STRING
		do
			if is_lio_enabled then
				upper_command := String_8.substring_to (cmd, ' ')
				upper_command.to_upper
				lio.put_labeled_string (upper_command + " error", message)
				lio.put_new_line
			end
		end

	display_error (message: READABLE_STRING_GENERAL)
		do
			if is_lio_enabled then
				lio.put_labeled_string (Error.label, message)
				lio.put_new_line
			end
		end

	display_reply_error
		do
			if is_lio_enabled then
				lio.put_labeled_string (Error.label + " server reply", last_reply)
				lio.put_new_line
			end
		end

	initiate_file_listing (dir_path: DIR_PATH)
		do
			across String_8_scope as scope loop
				push_address_path (unix_utf_8_path (scope, dir_path))
				set_passive_mode
				initiating_listing := True
				initiate_transfer
				pop_address_path
			end
		end

	pop_address_path
		do
			address.path.share (Stored_path)
		end

	push_address_path (new_path: STRING)
		do
			Stored_path.share (address.path)
			address.path.share (new_path)
		end

	receive_entry_list_count
		-- 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
			receive (main_socket)
			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
							set_last_entry_count (split_list.integer_item)
					else
					end
					list.forth
				end
			end
		end

	reply_code_ok (a_reply: STRING; codes: ARRAY [NATURAL_16]): BOOLEAN
		do
			if attached String_8.substring_to (a_reply, ' ') as part then
				Result := codes.has (part.to_natural_16)
			end
		end

	reset_file_listing
		do
			transfer_initiated := False; is_packet_pending := False
			initiating_listing := False
			data_socket := Void
		end

	send (cmd: IMMUTABLE_STRING_8; utf_8_path: detachable STRING; codes: ARRAY [NATURAL_16])
		require
			valid_path: cmd [cmd.count] = '%S' implies attached utf_8_path
		local
			utf_8_cmd: STRING; substitute_index: INTEGER
		do
			substitute_index := cmd.index_of ('%S', 1)
			across String_8_scope as scope loop
				if substitute_index > 0 then
					if attached utf_8_path as path then
						utf_8_cmd := scope.item
						utf_8_cmd.append_substring (cmd, 1, substitute_index - 1)
						utf_8_cmd.append (path)
					else
						utf_8_cmd := Empty_string_8
						error_code := Wrong_command
					end
				else
					utf_8_cmd := cmd
				end
				if error_code = Wrong_command then
					display_command_error (cmd, Error.missing_argument)
				else
					attempt (agent try_send (utf_8_cmd, codes, ?), 3)
				end
			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 + file_path, codes)

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

	send_path (cmd: IMMUTABLE_STRING_8; a_path: EL_PATH; codes: ARRAY [NATURAL_16])
		-- send command `cmd' with `path' argument and possible success `codes'
		do
			across String_8_scope as scope loop
				send (cmd, unix_utf_8_path (scope, a_path), codes)
			end
		end

	send_transfer_command: BOOLEAN
		do
			if initiating_listing then
				if passive_mode then
					Result := send_passive_mode_command
				else
					Result := send_port_command
				end
				if Result then
					send (Command.name_list, address.path, << Reply.about_to_open_data_connection >>)
					if has_error then
						error_code := Permission_denied
					end
				end
			else
				Result := Precursor
			end
		end

	transfer_file (source_path, destination_path: FILE_PATH)
		do
			attempt (agent try_transfer_file (source_path, destination_path, ?), 3)
--			else
--				lio.put_labeled_string (Error.socket_error, data_socket.error)
--				lio.put_new_line
--				lio.put_labeled_string ("Description", Exception.last_exception.description)
--				lio.put_new_line
--				if User_input.approved_action_y_n ("Retry transfer") then
--					reset
--					transfer_file (source_path, destination_path)
--				end
--			end
		end

	transfer_file_data (a_file_path: FILE_PATH)
			--
		local
			packet: PACKET; bytes_read: INTEGER
		do
			create packet.make (Default_packet_size)
			if attached open_raw (a_file_path, Read_from) as file_in then
				from until file_in.after loop
					file_in.read_to_managed_pointer (packet.data, 0, packet.count)
					bytes_read := file_in.bytes_read
					if bytes_read > 0 then
						if bytes_read /= packet.count then
							packet.data.resize (bytes_read)
						end
						data_socket.write (packet)
					end
				end
				data_socket.close
				data_socket := Void
				is_packet_pending := false
				file_in.close
			end
			receive (main_socket)
			if has_error then
				display_reply_error
			end
		end

	try_send (utf_8_command: STRING; codes: ARRAY [NATURAL_16]; done: BOOLEAN_REF)
		do
			reset_error
			send_to_socket (main_socket, utf_8_command)
			if last_succeeded then
				last_reply_utf_8.adjust
				last_reply_utf_8.to_lower
				if reply_code_ok (last_reply_utf_8, codes) then
					done.set_item (True)
				else
					error_code := Wrong_command
				end
			end
			if has_error then
				display_command_error (utf_8_command, error_text (error_code))
			end
		rescue
			close_sockets
			login
			retry
		end

	try_transfer_file (source_path, destination_path: FILE_PATH; done: BOOLEAN_REF)
		do
			across String_8_scope as scope loop
				push_address_path (unix_utf_8_path (scope, destination_path))
				set_passive_mode
				initiate_transfer
				pop_address_path
			end
			if transfer_initiated then
				transfer_file_data (source_path)
				transfer_initiated := False
			end
			if has_error then
				done.set_item (file_size (destination_path) = File.byte_count (source_path))
			else
				done.set_item (True)
			end
		ensure
			address_path_unchanged: old address.path ~ address.path
			unattached_data_socket: not attached data_socket
		end

	unix_utf_8_path (cursor: EL_BORROWED_STRING_8_CURSOR; a_path: EL_PATH): STRING
		do
			Result := cursor.item
			a_path.append_to_utf_8 (Result)
			if {PLATFORM}.is_windows then
				String_8.replace_character (Result, '\', '/')
			end
		end

feature {NONE} -- Deferred

	current_directory: DIR_PATH
		deferred
		end

	file_size (file_path: FILE_PATH): INTEGER
		deferred
		end

	last_reply: ZSTRING
		deferred
		end

	last_succeeded: BOOLEAN
		deferred
		end

	login
		deferred
		end

	reset
		deferred
		end

	set_last_entry_count (a_count: INTEGER)
		deferred
		end

feature {NONE} -- Internal attributes

	created_directory_set: EL_HASH_SET [DIR_PATH]

	initiating_listing: BOOLEAN
		-- `True' if initiating download of directory entry listing

	reply_parser: EL_FTP_REPLY_PARSER

feature {NONE} -- Numeric constants

	Default_packet_size: INTEGER
		once
			Result := 2048
		end

	Max_login_attempts: INTEGER
		once
			Result := 2
		end

feature {NONE} -- Constants

	Carriage_return_new_line: STRING = "%R%N"

	Command: TUPLE [
		change_working_directory, delete_file, make_directory, name_list,
		print_working_directory, quit, remove_directory, size: IMMUTABLE_STRING_8
	]
		once
			create Result
			Tuple.fill_immutable (Result, "CWD %S, DELE %S, MKD %S, NLST %S, PWD, QUIT, RMD %S, SIZE %S")
		end

	Error: TUPLE [
		cannot_set_transfer_mode, invalid_login, label, missing_argument,
		not_regular_file, socket_error: ZSTRING
	]
		once
			create Result
			Tuple.fill (Result,
				"cannot set transfer mode, Invalid username or password, ERROR, missing argument,%
				%not a regular file, Socket error"
			)
		end

	Reply: EL_FTP_SERVER_REPLY_ENUM
		once
			create Result.make
		end

	Stored_path: STRING
		once
			create Result.make_empty
		end

end