From 907850958a73658c80239293c96924a1a026eb84 Mon Sep 17 00:00:00 2001 From: JamesWrigley Date: Thu, 10 Oct 2024 18:12:01 +0200 Subject: [PATCH] Add support for Base.readdir() --- docs/src/sftp.md | 8 ++- gen/gen.jl | 3 +- src/bindings.jl | 54 +++++++++++++++---- src/sftp.jl | 127 +++++++++++++++++++++++++++++++++++--------- test/LibSSHTests.jl | 18 +++++++ 5 files changed, 174 insertions(+), 36 deletions(-) diff --git a/docs/src/sftp.md b/docs/src/sftp.md index 41da05e..e951203 100644 --- a/docs/src/sftp.md +++ b/docs/src/sftp.md @@ -30,7 +30,6 @@ Depth = 3 ## SftpSession ```@docs -SftpError SftpSession SftpSession(::Session) SftpSession(::Function) @@ -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) @@ -65,3 +65,9 @@ Base.seek(::SftpFile, ::Integer) Base.seekstart(::SftpFile) Base.seekend(::SftpFile) ``` + +## Other types +```@docs +SftpError +SftpAttributes +``` diff --git a/gen/gen.jl b/gen/gen.jl index f04522e..4836bcc 100644 --- a/gen/gen.jl +++ b/gen/gen.jl @@ -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) """ diff --git a/src/bindings.jl b/src/bindings.jl index cbf59d9..ddab294 100644 --- a/src/bindings.jl +++ b/src/bindings.jl @@ -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. @@ -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. @@ -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 """ @@ -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)(). @@ -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) diff --git a/src/sftp.jl b/src/sftp.jl index 84179ed..a2eedda 100644 --- a/src/sftp.jl +++ b/src/sftp.jl @@ -111,6 +111,14 @@ function SftpSession(f::Function, args...; kwargs...) 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) @@ -265,30 +273,73 @@ end """ $(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)")) + 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. @@ -319,7 +370,33 @@ end ## 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} @@ -340,7 +417,7 @@ end 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) diff --git a/test/LibSSHTests.jl b/test/LibSSHTests.jl index 0a0624c..d940190 100644 --- a/test/LibSSHTests.jl +++ b/test/LibSSHTests.jl @@ -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