s3_io.f90 Source File


Source Code

!> High-level Fortran I/O interface for S3 operations.
!>
!> This module provides a familiar Fortran-style I/O interface for working with S3 objects.
!> It supports operations similar to standard Fortran file I/O: open, read, write, close, and rewind.
!> The module internally buffers content for efficient line-by-line operations.
!>
!> ## Features
!>
!> - Familiar Fortran I/O patterns (open/read/write/close)
!> - Line-based text file operations
!> - Internal buffering for efficient I/O
!> - Support for up to 100 concurrent file handles
!> - Automatic upload on close for write operations
!>
!> ## Usage
!>
!> ```fortran
!> use s3_http
!> use s3_io
!> type(s3_config) :: config
!> integer :: unit, iostat
!> character(len=1024) :: line
!>
!> ! Initialize S3
!> config%bucket = 'my-bucket'
!> call s3_init(config)
!>
!> ! Open and read
!> call s3_open(unit, 'data/input.txt', 'read', iostat)
!> call s3_read_line(unit, line, iostat)
!> call s3_close(unit, iostat)
!> ```
!>
!> @note This module depends on the s3_http module for underlying S3 operations.
module s3_io
    use s3_http
    implicit none
    private

    integer, parameter :: MAX_FILES = 100  !< Maximum number of concurrent open files

    !> Internal file handle type for managing S3 objects as file-like entities.
    !>
    !> This type maintains the state of an open S3 object, including its content buffer,
    !> read/write position, and mode. Used internally by the module.
    type :: s3_file
        logical :: is_open = .false.                !< Whether this file handle is in use
        character(len=256) :: key = ''              !< S3 object key
        character(len=:), allocatable :: buffer     !< Content buffer
        integer :: position = 1                     !< Current read/write position
        logical :: is_write = .false.               !< Write mode flag
    end type s3_file

    type(s3_file), save :: files(MAX_FILES)

    public :: s3_open
    public :: s3_close
    public :: s3_read_line
    public :: s3_write_line
    public :: s3_rewind

contains

    !> Open an S3 object for reading or writing.
    !>
    !> Opens an S3 object and returns a unit number for subsequent I/O operations.
    !> For read mode, the object is downloaded immediately. For write mode, content
    !> is buffered in memory until s3_close() is called.
    !>
    !> @param[out] unit The allocated unit number (set to -1 on error)
    !> @param[in] key The S3 object key to open
    !> @param[in] mode Open mode: 'read'/'r' for reading, 'write'/'w' for writing
    !> @param[out] iostat Status code: 0 on success, -1 on error
    !>
    !> ## Example
    !>
    !> ```fortran
    !> integer :: unit, iostat
    !>
    !> ! Open for reading
    !> call s3_open(unit, 'data/input.txt', 'read', iostat)
    !> if (iostat == 0) then
    !>     ! Read operations...
    !>     call s3_close(unit, iostat)
    !> end if
    !> ```
    subroutine s3_open(unit, key, mode, iostat)
        integer, intent(out) :: unit
        character(len=*), intent(in) :: key
        character(len=*), intent(in) :: mode
        integer, intent(out) :: iostat
        integer :: i
        character(len=:), allocatable :: content

        iostat = 0
        unit = -1

        ! Find available unit
        do i = 1, MAX_FILES
            if (.not. files(i)%is_open) then
                unit = i
                exit
            end if
        end do

        if (unit < 0) then
            iostat = -1
            return
        end if

        files(unit)%key = key
        files(unit)%is_open = .true.
        files(unit)%position = 1

        select case(mode)
        case('read', 'r')
            files(unit)%is_write = .false.
            ! Download the file content
            if (s3_get_object(key, content)) then
                files(unit)%buffer = content
            else
                iostat = -1
                files(unit)%is_open = .false.
            end if

        case('write', 'w')
            files(unit)%is_write = .true.
            files(unit)%buffer = ''

        case default
            iostat = -1
            files(unit)%is_open = .false.
        end select
    end subroutine s3_open

    !> Close an S3 file handle.
    !>
    !> Closes an open S3 file handle. For write mode, this uploads the buffered
    !> content to S3. The file handle is released and can be reused.
    !>
    !> @param[in] unit The unit number to close
    !> @param[out] iostat Status code: 0 on success, -1 on error
    !>
    !> @warning For write mode, upload errors will be reflected in iostat.
    !>
    !> ## Example
    !>
    !> ```fortran
    !> call s3_close(unit, iostat)
    !> if (iostat /= 0) then
    !>     print *, 'Error closing file'
    !> end if
    !> ```
    subroutine s3_close(unit, iostat)
        integer, intent(in) :: unit
        integer, intent(out) :: iostat
        logical :: success

        iostat = 0

        if (unit < 1 .or. unit > MAX_FILES) then
            iostat = -1
            return
        end if

        if (.not. files(unit)%is_open) then
            iostat = -1
            return
        end if

        ! If writing, upload the buffer
        if (files(unit)%is_write .and. allocated(files(unit)%buffer)) then
            success = s3_put_object(files(unit)%key, files(unit)%buffer)
            if (.not. success) iostat = -1
        end if

        ! Clean up
        files(unit)%is_open = .false.
        files(unit)%key = ''
        files(unit)%position = 1
        files(unit)%is_write = .false.
        if (allocated(files(unit)%buffer)) deallocate(files(unit)%buffer)
    end subroutine s3_close

    !> Read a line from an open S3 file.
    !>
    !> Reads the next line from the file buffer. Lines are delimited by newline characters.
    !> The file must be opened in read mode.
    !>
    !> @param[in] unit The unit number to read from
    !> @param[out] line The line content (truncated if longer than buffer)
    !> @param[out] iostat Status code: 0 on success, -1 on EOF or error
    !>
    !> ## Example
    !>
    !> ```fortran
    !> character(len=1024) :: line
    !> integer :: iostat
    !>
    !> do
    !>     call s3_read_line(unit, line, iostat)
    !>     if (iostat /= 0) exit
    !>     print *, trim(line)
    !> end do
    !> ```
    subroutine s3_read_line(unit, line, iostat)
        integer, intent(in) :: unit
        character(len=*), intent(out) :: line
        integer, intent(out) :: iostat
        integer :: i, line_end, buffer_len

        iostat = 0
        line = ''

        if (unit < 1 .or. unit > MAX_FILES) then
            iostat = -1
            return
        end if

        if (.not. files(unit)%is_open .or. files(unit)%is_write) then
            iostat = -1
            return
        end if

        if (.not. allocated(files(unit)%buffer)) then
            iostat = -1
            return
        end if

        buffer_len = len(files(unit)%buffer)

        ! Check if at end of file
        if (files(unit)%position > buffer_len) then
            iostat = -1  ! EOF
            return
        end if

        ! Find next newline
        line_end = 0
        do i = files(unit)%position, buffer_len
            if (files(unit)%buffer(i:i) == new_line('')) then
                line_end = i - 1
                exit
            end if
        end do

        ! If no newline found, read to end
        if (line_end == 0) then
            line_end = buffer_len
        end if

        ! Extract line
        if (line_end >= files(unit)%position) then
            line = files(unit)%buffer(files(unit)%position:line_end)
            files(unit)%position = line_end + 2  ! Skip newline
        else
            iostat = -1
        end if
    end subroutine s3_read_line

    !> Write a line to an open S3 file.
    !>
    !> Appends a line to the file buffer. A newline character is automatically added.
    !> The file must be opened in write mode. Content is uploaded when s3_close() is called.
    !>
    !> @param[in] unit The unit number to write to
    !> @param[in] line The line content to write
    !> @param[out] iostat Status code: 0 on success, -1 on error
    !>
    !> ## Example
    !>
    !> ```fortran
    !> call s3_write_line(unit, 'temperature,pressure', iostat)
    !> call s3_write_line(unit, '25.3,1013.2', iostat)
    !> ```
    subroutine s3_write_line(unit, line, iostat)
        integer, intent(in) :: unit
        character(len=*), intent(in) :: line
        integer, intent(out) :: iostat
        character(len=:), allocatable :: new_buffer

        iostat = 0

        if (unit < 1 .or. unit > MAX_FILES) then
            iostat = -1
            return
        end if

        if (.not. files(unit)%is_open .or. .not. files(unit)%is_write) then
            iostat = -1
            return
        end if

        ! Append line to buffer
        if (.not. allocated(files(unit)%buffer)) then
            files(unit)%buffer = trim(line) // new_line('')
        else
            new_buffer = files(unit)%buffer // trim(line) // new_line('')
            files(unit)%buffer = new_buffer
        end if
    end subroutine s3_write_line

    !> Rewind an S3 file to the beginning.
    !>
    !> Resets the read position to the start of the file buffer. Only valid for read mode.
    !>
    !> @param[in] unit The unit number to rewind
    !> @param[out] iostat Status code: 0 on success, -1 on error
    !>
    !> ## Example
    !>
    !> ```fortran
    !> ! Read file twice
    !> call s3_open(unit, 'data/file.txt', 'read', iostat)
    !> ! ... read operations ...
    !> call s3_rewind(unit, iostat)
    !> ! ... read again from start ...
    !> call s3_close(unit, iostat)
    !> ```
    subroutine s3_rewind(unit, iostat)
        integer, intent(in) :: unit
        integer, intent(out) :: iostat

        iostat = 0

        if (unit < 1 .or. unit > MAX_FILES) then
            iostat = -1
            return
        end if

        if (.not. files(unit)%is_open) then
            iostat = -1
            return
        end if

        files(unit)%position = 1
    end subroutine s3_rewind

end module s3_io