class EL_FILE_ROUTINES_I

(source code)

description

File related routines accessible via EL_MODULE_FILE

note
	description: "File related routines accessible via ${EL_MODULE_FILE}"

	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-13 16:51:07 GMT (Friday 13th September 2024)"
	revision: "23"

deferred class
	EL_FILE_ROUTINES_I

inherit
	EL_OS_DEPENDENT

	EL_MODULE_FILE_SYSTEM

	NATIVE_STRING_HANDLER; EL_STRING_HANDLER

	EL_FILE_OPEN_ROUTINES

	EL_STRING_8_CONSTANTS

	EL_MODULE_CHECKSUM
		rename
			Checksum as Checksum_
		end

feature {NONE} -- Initialization

	make
		do
			create internal_info_file.make
		end

feature -- Access

	info (a_path: EL_PATH; keep_ref: BOOLEAN): EL_INFO_RAW_FILE
		do
			if keep_ref then
				create Result.make
			else
				Result := internal_info_file
			end
			Result.set_path (a_path)
		end

	new_plain_text (file_path: detachable FILE_PATH): PLAIN_TEXT_FILE
		do
			if attached file_path as path then
				create Result.make_with_name (path)
			else
				create Result.make_with_name ("None.txt")
			end
		end

feature -- Measurement

	access_time (file_path: FILE_PATH): INTEGER
		do
			Result := info (file_path, False).access_date
		end

	average_line_count (file_path: FILE_PATH): INTEGER
		require
			path_exists: file_path.exists
		do
			if file_path.exists and then attached open (file_path, Read) as file then
				if file.count > 0 then
					Result := file.average_line_count
				end
				file.close
			end
		end

	average_line_count_of (file: PLAIN_TEXT_FILE): INTEGER
		-- average characters per line based on leading 1K block
		require
			file_readable: file.readable
		local
			position, char_count, newline_count: INTEGER; pending_break, break: BOOLEAN
		do
			if attached {EL_PLAIN_TEXT_FILE} file as f then
			-- optimized to not use `last_character' and correctly counts UTF encoded characters
				Result := f.average_line_count
			else
			-- not accurate for UTF encoded files, but OK for Latin-1
				position := file.position
				file.go (0)
				from until break loop
					file.read_character
					if file.end_of_file then
						break := True
					else
						inspect file.last_character when '%N' then
							newline_count := newline_count + 1
							if pending_break then
								break := True
							end
						else
						end
						char_count := char_count + 1
						if char_count > 1024 then
							pending_break := True
						end
					end
				end
				Result := (char_count / newline_count.max (1)).rounded
				file.go (position)
			end
		ensure
			position_unchanged: file.position = old file.position
		end

	byte_count (file_path: FILE_PATH): INTEGER
			--
		do
			Result := info (file_path, False).count
		end

	checksum (file_path: FILE_PATH): NATURAL
		-- CRC-32 checksum
		do
			Result := Checksum_.file_content (file_path)
		end

	megabyte_count (file_path: FILE_PATH): DOUBLE
			--
		do
			Result := byte_count (file_path) / 1000000
		end

	modification_time (file_path: FILE_PATH): INTEGER
		do
			if attached info (file_path, False) as l_info and then l_info.exists then
				Result := l_info.date
			end
		end

feature -- File content

	data (file_path: FILE_PATH): MANAGED_POINTER
		require
			file_exists: file_path.exists
		do
			if attached open_raw (file_path, Read) as file then
				create Result.make (file.count)
				file.read_to_managed_pointer (Result, 0, file.count)
				file.close
			end
		end

	line_one (file_path: FILE_PATH): STRING
		-- First line of file
		require
			file_exists: file_path.exists
		do
			if attached open (file_path, Read) as file then
				if file.is_empty then
					create Result.make_empty
				else
					file.read_line_8
					Result := file.last_string_8
					if {PLATFORM}.is_unix then
						Result.prune_all_trailing ('%R')
					end
				end
				file.close
			end
		end

	plain_text (file_path: FILE_PATH): STRING
		-- plain text excluding any Windows carriage return characters '%R'
		do
			Result := raw_plain_text (file_path)
			if {PLATFORM}.is_unix and then has_windows_line_break (Result) then
				Result.prune_all ('%R')
			end
		end

	plain_text_bomless (file_path: FILE_PATH): READABLE_STRING_8
		-- file text without any byte-order mark
		local
			utf: EL_UTF_CONVERTER
		do
			Result := plain_text (file_path)
			if utf.is_utf_8_file (Result) then
				Result := utf.bomless_utf_8 (Result)

			elseif utf.is_utf_16_le_file (Result) then
				Result := utf.bomless_utf_16_le (Result)

			end
		end

	plain_text_lines (file_path: FILE_PATH): EL_ITERABLE_SPLIT [STRING, ANY]
		require
			file_exists: file_path.exists
		do
			if attached raw_plain_text (file_path) as content then
				if {PLATFORM}.is_unix and then has_windows_line_break (content) then
					-- Check if content has Windows carriage return
					create {EL_SPLIT_ON_STRING [STRING]} Result.make (content, "%R%N")

				else
					create {EL_SPLIT_ON_CHARACTER [STRING]} Result.make (content, '%N')
				end
			else
				create {EL_SPLIT_ON_CHARACTER [STRING]} Result.make (Empty_string_8, '%N')
			end
		end

	raw_plain_text (file_path: FILE_PATH): STRING
		-- plain text possibly containing '%R' on Unix platforms
		require
			file_exists: file_path.exists
		local
			file: PLAIN_TEXT_FILE; read_count, count: INTEGER
		do
			create file.make_open_read (file_path)
			count := file.count
			create Result.make (count)
			Result.set_count (count)
			read_count := file.read_to_string (Result, 1, count)
			Result.set_count (read_count)

			if {PLATFORM}.is_windows then
				-- which condition applies probably depends on whether the file has Unix or Windows line endings
				check
					complete_file_read: count = read_count or else (count - (read_count + Result.occurrences ('%N'))) <= 2
				end
			else
				check
					complete_file_read: read_count = count
				end
			end
			file.close
		end

feature -- Status report

	exists (a_path: EL_PATH): BOOLEAN
		do
			Result := info (a_path, False).exists
		end

	has_content (file_path: FILE_PATH): BOOLEAN
			-- True if file not empty
		do
			if attached open_raw (file_path, Read) as file then
				Result := not file.is_empty
				file.close
			end
		end

	has_utf_8_bom (file_path: FILE_PATH): BOOLEAN
		do
			if attached new_plain_text (file_path) as text then
				text.open_read
				Result := has_utf_8_bom_marker (text)
				text.close
			end
		end

	has_utf_8_bom_marker (file: PLAIN_TEXT_FILE): BOOLEAN
		require
			file_open: file.is_open_read
			at_start_position: file.position = 0
		local
			bom: STRING
		do
			bom := {UTF_CONVERTER}.Utf_8_bom_to_string_8
			if file.count >= bom.count then
				file.read_stream (bom.count)
				Result := file.last_string ~ bom
			end
		end

	has_windows_line_break (content: STRING): BOOLEAN
		-- True if `content' contains `%R%N'
		local
			index: INTEGER
		do
			index := content.index_of ('%N', 1)
			Result := index > 1 and then content [index - 1] = '%R'
		end

	is_access_owner (a_path: EL_PATH): BOOLEAN
		do
			Result := info (a_path, False).is_access_owner
		end

	is_access_writable (a_path: FILE_PATH): BOOLEAN
		do
			Result := info (a_path, False).is_access_writable
		end

	is_newer_than (path_1, path_2: FILE_PATH): BOOLEAN
		-- `True' if either A or B is true
		-- A. `path_1' modification time is greater than `path_2' modification time
		-- B. `path_2' does not exist
		require
			path_1_exists: path_1.exists
		do
			Result := not path_2.exists or else modification_time (path_1) > modification_time (path_2)
		end

	is_owner (a_path: FILE_PATH): BOOLEAN
		do
			Result := info (a_path, False).is_owner
		end

	is_readable (a_path: FILE_PATH): BOOLEAN
		deferred
		end

	is_symlink (path: EL_PATH): BOOLEAN
		-- `True' if file is a symbolic link
		-- (Does not seem to work for "C:\Users\Default User" on 16.05)
		do
			Result := info (path, False).is_symlink
		end

	is_writable (a_path: FILE_PATH): BOOLEAN
		deferred
		end

feature -- Property change

	add_permission (path: FILE_PATH; who, what: STRING)
			-- Add read, write, execute or setuid permission
			-- for `who' ('u', 'g' or 'o') to `what'.
		require
			file_exists: path.exists
			valid_who: valid_who (who)
			valid_what: valid_what (what)
		do
			change_permission (path, who, what, agent {FILE}.add_permission)
		end

	remove_permission (path: FILE_PATH; who, what: STRING)
			-- remove read, write, execute or setuid permission
			-- for `who' ('u', 'g' or 'o') to `what'.
		require
			file_exists: path.exists
			valid_who: valid_who (who)
			valid_what: valid_what (what)
		do
			change_permission (path, who, what, agent {FILE}.remove_permission)
		end

	set_modification_time (file_path: FILE_PATH; date_time: INTEGER)
			-- set modification time with date_time as secs since Unix epoch
		deferred
		ensure
			modification_time_set: modification_time (file_path) = date_time
		end

	set_stamp (file_path: FILE_PATH; date_time: INTEGER)
			-- Stamp file with `time' (for both access and modification).
		deferred
		ensure
			file_access_time_set: access_time (file_path) = date_time
			modification_time_set: modification_time (file_path) = date_time
		end

feature -- Basic operations

	copy_contents (source_file: FILE; destination_path: FILE_PATH)
		require
			exists_and_closed: source_file.is_closed and source_file.exists
		local
			destination_file: FILE; file_data: MANAGED_POINTER
			l_byte_count: INTEGER
		do
			File_system.make_directory (destination_path.parent)
			destination_file := source_file.twin
			source_file.open_read
			l_byte_count := source_file.count
			-- Read
			create file_data.make (l_byte_count)
			source_file.read_to_managed_pointer (file_data, 0, l_byte_count)
			notify_progress (source_file, False)
			source_file.close
			-- Write
			destination_file.make_open_write (destination_path)
			destination_file.put_managed_pointer (file_data, 0, l_byte_count)
			notify_progress (destination_file, True)
			destination_file.close
		end

	copy_contents_to_dir (source_file: FILE; destination_dir: DIR_PATH)
		local
			destination_path: FILE_PATH
		do
			destination_path := source_file.path
			destination_path.set_parent (destination_dir)
			copy_contents (source_file, destination_path)
		end

	do_with_all_blocks (file_path: FILE_PATH; action: PROCEDURE [MANAGED_POINTER]; block_size: INTEGER)
		-- call `action' with consective data blocks of maximum `block_size' bytes for entire file at `file_path'
		do
			do_with_blocks (file_path, action, 0, block_size)
		end

	do_with_blocks (file_path: FILE_PATH; action: PROCEDURE [MANAGED_POINTER]; max_byte_count, block_size: INTEGER)
		-- call `action' with consective data blocks of maximum `block_size' bytes for entire file at `file_path'
		-- but if `max_byte_count > 0' then limit to first `max_byte_count' bytes

		-- NOTE: clone each `MANAGED_POINTER' block if keeping a reference
		require
			path_exists: file_path.exists
		local
			maximum_count, file_count, block_count, remainder_count, i: INTEGER
			block: MANAGED_POINTER; file: RAW_FILE
		do
			file_count := byte_count (file_path)

			if max_byte_count.to_boolean then
				maximum_count := max_byte_count.min (file_count)
			else
				maximum_count := file_count
			end

			block_count := maximum_count // block_size
			remainder_count := maximum_count \\ block_size
			create block.make (block_size)
			create file.make_open_read (file_path)
			from i := 1 until i > block_count loop
				file.read_to_managed_pointer (block, 0, block.count)
				action (block)
				i := i + 1
			end
			if remainder_count.to_boolean then
				block.resize (remainder_count)
				file.read_to_managed_pointer (block, 0, remainder_count)
				check
					byte_read_agrees: file.bytes_read = remainder_count
				end
				action (block)
			end
			file.close
		end

	write_text (file_path: FILE_PATH; text: STRING)
		-- write plain text
		do
			write_marked_text (file_path, text, False)
		end

	write_marked_text (file_path: FILE_PATH; text: STRING; utf_bom: BOOLEAN)
		-- write plain text with optional byte order mark
		local
			file: PLAIN_TEXT_FILE
		do
			create file.make_open_write (file_path)
			if utf_bom then
				file.put_string ({UTF_CONVERTER}.Utf_8_bom_to_string_8)
			end
			file.put_string (text)
			file.close
		end

feature -- Contract Support

	valid_what (what: STRING): BOOLEAN
		do
			Result := across what as c all ("rwxs").has (c.item) end
		end

	valid_who (who: STRING): BOOLEAN
		do
			Result := across who as c all ("uog").has (c.item) end
		end

feature {NONE} -- Implementation

	change_permission (path: EL_PATH; who, what: STRING; change: PROCEDURE [FILE, STRING, STRING])
			-- Add/remove permissions to file or directory specified by `path' using `change' action
			-- Add read, write, execute or setuid permission
			-- for `who' ('u', 'g' or 'o') to `what'.
		local
			file: FILE; l_who: STRING
		do
			file := info (path, False)
			create l_who.make (1)
			across who as c loop
				l_who.wipe_out
				l_who.append_character (c.item)
				change (file, l_who, what)
			end
		end

	notify_progress (a_file: FILE; final: BOOLEAN)
		do
			if attached {EL_NOTIFYING_FILE} a_file as file then
				if final then
					file.notify_final
				else
					file.notify
				end
			end
		end

feature {NONE} -- Internal attributes

	internal_info_file: EL_INFO_RAW_FILE

end