Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Base.readdir() #18

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/src/sftp.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ Depth = 3

## SftpSession
```@docs
SftpError
SftpSession
SftpSession(::Session)
SftpSession(::Function)
Expand All @@ -39,6 +38,7 @@ Base.isopen(::SftpSession)
Base.lock(::SftpSession)
Base.unlock(::SftpSession)
Base.stat(::String, ::SftpSession)
Base.readdir(::AbstractString, ::SftpSession)
get_extensions(::SftpSession)
get_limits(::SftpSession)
get_error(::SftpSession)
Expand All @@ -65,3 +65,9 @@ Base.seek(::SftpFile, ::Integer)
Base.seekstart(::SftpFile)
Base.seekend(::SftpFile)
```

## Other types
```@docs
SftpError
SftpAttributes
```
3 changes: 2 additions & 1 deletion gen/gen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ ssh_ok_functions = [:ssh_message_auth_reply_success, :ssh_message_auth_set_metho
# call them with @threadcall.
threadcall_functions = [:sftp_new, :sftp_init, :sftp_open, :sftp_close,
:sftp_home_directory, :sftp_stat,
:sftp_aio_wait_read, :sftp_aio_wait_write]
:sftp_aio_wait_read, :sftp_aio_wait_write,
:sftp_opendir, :sftp_readdir, :sftp_closedir]
all_rewritable_functions = vcat(string_functions, bool_functions, ssh_ok_functions, threadcall_functions)

"""
Expand Down
54 changes: 45 additions & 9 deletions src/bindings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3051,8 +3051,19 @@ function sftp_extension_supported(sftp, name, data)
@ccall libssh.sftp_extension_supported(sftp::sftp_session, name::Ptr{Cchar}, data::Ptr{Cchar})::Cint
end

function _threadcall_sftp_opendir(session::sftp_session, path::Ptr{Cchar})
gc_state = @ccall(jl_gc_safe_enter()::Int8)
ret = @ccall(libssh.sftp_opendir(session::sftp_session, path::Ptr{Cchar})::sftp_dir)
@ccall jl_gc_safe_leave(gc_state::Int8)::Cvoid
return ret
end

"""
sftp_opendir(session, path)
sftp_opendir(session::sftp_session, path::Ptr{Cchar})

Auto-generated wrapper around `sftp_opendir()`. Original upstream documentation is below.

---

Open a directory used to obtain directory entries.

Expand All @@ -3064,12 +3075,24 @@ A sftp directory handle or NULL on error with ssh and sftp error set.
# See also
[`sftp_readdir`](@ref), [`sftp_closedir`](@ref)
"""
function sftp_opendir(session, path)
@ccall libssh.sftp_opendir(session::sftp_session, path::Ptr{Cchar})::sftp_dir
function sftp_opendir(session::sftp_session, path::Ptr{Cchar})
cfunc = @cfunction(_threadcall_sftp_opendir, sftp_dir, (sftp_session, Ptr{Cchar}))
return @threadcall(cfunc, sftp_dir, (sftp_session, Ptr{Cchar}), session, path)
end

function _threadcall_sftp_readdir(session::sftp_session, dir::sftp_dir)
gc_state = @ccall(jl_gc_safe_enter()::Int8)
ret = @ccall(libssh.sftp_readdir(session::sftp_session, dir::sftp_dir)::sftp_attributes)
@ccall jl_gc_safe_leave(gc_state::Int8)::Cvoid
return ret
end

"""
sftp_readdir(session, dir)
sftp_readdir(session::sftp_session, dir::sftp_dir)

Auto-generated wrapper around `sftp_readdir()`. Original upstream documentation is below.

---

Get a single file attributes structure of a directory.

Expand All @@ -3081,8 +3104,9 @@ A file attribute structure or NULL at the end of the directory.
# See also
[`sftp_opendir`](@ref)(), sftp\\_attribute\\_free(), [`sftp_closedir`](@ref)()
"""
function sftp_readdir(session, dir)
@ccall libssh.sftp_readdir(session::sftp_session, dir::sftp_dir)::sftp_attributes
function sftp_readdir(session::sftp_session, dir::sftp_dir)
cfunc = @cfunction(_threadcall_sftp_readdir, sftp_attributes, (sftp_session, sftp_dir))
return @threadcall(cfunc, sftp_attributes, (sftp_session, sftp_dir), session, dir)
end

"""
Expand Down Expand Up @@ -3177,8 +3201,19 @@ function sftp_attributes_free(file)
@ccall libssh.sftp_attributes_free(file::sftp_attributes)::Cvoid
end

function _threadcall_sftp_closedir(dir::sftp_dir)
gc_state = @ccall(jl_gc_safe_enter()::Int8)
ret = @ccall(libssh.sftp_closedir(dir::sftp_dir)::Cint)
@ccall jl_gc_safe_leave(gc_state::Int8)::Cvoid
return ret
end

"""
sftp_closedir(dir)
sftp_closedir(dir::sftp_dir)

Auto-generated wrapper around `sftp_closedir()`. Original upstream documentation is below.

---

Close a directory handle opened by [`sftp_opendir`](@ref)().

Expand All @@ -3187,8 +3222,9 @@ Close a directory handle opened by [`sftp_opendir`](@ref)().
# Returns
Returns SSH\\_NO\\_ERROR or [`SSH_ERROR`](@ref) if an error occurred.
"""
function sftp_closedir(dir)
@ccall libssh.sftp_closedir(dir::sftp_dir)::Cint
function sftp_closedir(dir::sftp_dir)
cfunc = @cfunction(_threadcall_sftp_closedir, Cint, (sftp_dir,))
return @threadcall(cfunc, Cint, (sftp_dir,), dir)
end

function _threadcall_sftp_close(file::sftp_file)
Expand Down
127 changes: 102 additions & 25 deletions src/sftp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@
end
end

function Base.show(io::IO, sftp::SftpSession)
if isopen(sftp)
print(io, SftpSession, "(session=$(sftp.session))")
else
print(io, SftpSession, "([closed])")
end
end

"""
$(TYPEDSIGNATURES)

Expand Down Expand Up @@ -265,30 +273,73 @@
"""
$(TYPEDSIGNATURES)

Get information about the file object at `path`. This will return an object with
the following properties:
- `name::String`
- `longname::String`
- `flags::UInt32`
- `type::UInt8`
- `size::UInt64`
- `uid::UInt32`
- `gid::UInt32`
- `owner::String`
- `group::String`
- `permissions::UInt32`
- `atime64::UInt64`
- `atime::UInt32`
- `atime_nseconds::UInt32`
- `createtime::UInt64`
- `createtime_nseconds::UInt32`
- `mtime64::UInt64`
- `mtime::UInt32`
- `mtime_nseconds::UInt32`
- `acl::String`
- `extended_count::UInt32`
- `extended_type::String`
- `extended_data::String`
Read the contents of a remote directory. By default this will behave the same as
`Base.readdir()` and return a list of names, but if `only_names=false` it will
return a list of [`SftpAttributes`](@ref). The `join` and `sort` arguments
are the same as in `Base.readdir()` but only apply when `only_names=true`.

# Throws
- `ArgumentError`: If `sftp` is closed.
- [`LibSSHException`](@ref): If retrieving the directory contents failed.
"""
function Base.readdir(dir::AbstractString, sftp::SftpSession;
only_names=true, join::Bool=false, sort::Bool=true)
if !isopen(sftp)
throw(ArgumentError("$(sftp) is closed, cannot call readdir() on it"))
end

entries = SftpAttributes[]

# Open directory
dir_ptr = GC.@preserve dir begin
cstr = Base.unsafe_convert(Ptr{Cchar}, dir)
@lockandblock sftp.session lib.sftp_opendir(sftp.ptr, cstr)
end
if dir_ptr == C_NULL
error_code = get_error(sftp)
throw(LibSSHException("Couldn't open path $(dir) on $(sftp): $(error_code)"))
end

# Read contents
while isopen(sftp)
attr_ptr = @lockandblock sftp.session lib.sftp_readdir(sftp.ptr, dir_ptr)
if attr_ptr != C_NULL
attr = SftpAttributes(attr_ptr)

# Skip the current and parent entries to be compatible with Base.readdir()
if attr.name != "." && attr.name != ".."
push!(entries, attr)
end
else
break
end
end

# Close directory
ret = @lockandblock sftp.session lib.sftp_closedir(dir_ptr)
if ret == SSH_ERROR
throw(LibSSHException("Closing remote directory failed: $(ret)"))

Check warning on line 321 in src/sftp.jl

View check run for this annotation

Codecov / codecov/patch

src/sftp.jl#L321

Added line #L321 was not covered by tests
end

if only_names
entry_names = [x.name for x in entries]
if join
map!(x -> joinpath(dir, x), entry_names, entry_names)
end
if sort
sort!(entry_names)
end

return entry_names
else
return entries
end
end

"""
$(TYPEDSIGNATURES)

Get information about the file object at `path`.

Note: the [`Demo.DemoServer`](@ref) does not support setting all of these
properties.
Expand Down Expand Up @@ -319,7 +370,33 @@

## SftpAttributes

"""
$(TYPEDEF)

Attributes of remote file objects. This has the following (read-only) properties:
- `name::String`
- `longname::String`
- `flags::UInt32`
- `type::UInt8`
- `size::UInt64`
- `uid::UInt32`
- `gid::UInt32`
- `owner::String`
- `group::String`
- `permissions::UInt32`
- `atime64::UInt64`
- `atime::UInt32`
- `atime_nseconds::UInt32`
- `createtime::UInt64`
- `createtime_nseconds::UInt32`
- `mtime64::UInt64`
- `mtime::UInt32`
- `mtime_nseconds::UInt32`
- `acl::String`
- `extended_count::UInt32`
- `extended_type::String`
- `extended_data::String`
"""
mutable struct SftpAttributes
ptr::Union{lib.sftp_attributes, Nothing}

Expand All @@ -340,7 +417,7 @@

function _show_attrs(io::IO, attrs::SftpAttributes)
mode = string(attrs.permissions, base=8, pad=6)
print(io, SftpAttributes, "(size=$(attrs.size) bytes, owner=$(attrs.owner), uid=$(attrs.uid), gid=$(attrs.gid), permissions=0o$(mode))")
print(io, SftpAttributes, "(name='$(attrs.name)', size=$(attrs.size) bytes, owner=$(attrs.owner), permissions=0o$(mode))")
end

Base.show(io::IO, attrs::SftpAttributes) = _show_attrs(io, attrs)
Expand Down
18 changes: 18 additions & 0 deletions test/LibSSHTests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -801,12 +801,30 @@ end
@test !isassigned(attrs)
end

# Test readdir()
mktempdir() do tmpdir
# Test reading an empty directory
@test isempty(readdir(tmpdir, sftp))

# And a non-empty directory
write(joinpath(tmpdir, "foo"), "foo")
write(joinpath(tmpdir, "bar"), "bar")

@test readdir(tmpdir, sftp) == ["bar", "foo"]
@test readdir(tmpdir, sftp; join=true) == [joinpath(tmpdir, "bar"), joinpath(tmpdir, "foo")]
@test readdir(tmpdir, sftp; only_names=false) isa Vector{ssh.SftpAttributes}

# And a non-existent directory
@test_throws ssh.LibSSHException readdir(tmpdir * "_bad", sftp)
end

close(sftp)

@test_throws ArgumentError stat("/tmp", sftp)
@test_throws ArgumentError ssh.get_extensions(sftp)
@test_throws ArgumentError ssh.get_limits(sftp)
@test_throws ArgumentError homedir(sftp)
@test_throws ArgumentError readdir("/tmp", sftp)
end
end
end
Expand Down