Class: Scanii::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/scanii/client.rb

Overview

Synchronous client for the Scanii REST API v2.2.

Construct with either key: + secret: (HTTP Basic Auth) or token: (auth-token authentication). Mixing the two raises ArgumentError.

Per SDK Principle 3 the client is integration-only: it does not retry, batch, or paginate. Each public method maps to exactly one HTTP request.

Examples:

Scan a file from disk

client = Scanii::Client.new(key: "your-key", secret: "your-secret")
result = client.process_file("./file.pdf")
puts result.findings  # [] when clean

Scan content already in memory

result = client.process(StringIO.new(bytes), filename: "upload.bin")

See Also:

Constant Summary collapse

DEFAULT_ENDPOINT =
"https://api.scanii.com".freeze
DEFAULT_TIMEOUT =
60
API_VERSION_PATH =
"/v2.2".freeze
USER_AGENT =
"scanii-ruby/#{Scanii::VERSION}".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key: nil, secret: nil, token: nil, endpoint: DEFAULT_ENDPOINT, timeout: DEFAULT_TIMEOUT, user_agent: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • key (String, nil) (defaults to: nil)

    API key (mutually exclusive with token)

  • secret (String, nil) (defaults to: nil)

    API secret (required when key is set)

  • token (String, nil) (defaults to: nil)

    auth-token id (mutually exclusive with key/secret)

  • endpoint (String) (defaults to: DEFAULT_ENDPOINT)

    base URL; defaults to api.scanii.com

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    open + read timeout in seconds; default 60

  • user_agent (String, nil) (defaults to: nil)

    optional fragment prepended to the SDK’s default User-Agent

Raises:

  • (ArgumentError)


37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/scanii/client.rb', line 37

def initialize(key: nil, secret: nil, token: nil, endpoint: DEFAULT_ENDPOINT,
               timeout: DEFAULT_TIMEOUT, user_agent: nil)
  @auth_header = build_auth_header(key, secret, token)
  @endpoint    = endpoint.to_s.sub(%r{/+\z}, "")
  raise ArgumentError, "endpoint must not be empty" if @endpoint.empty?

  @base_uri = URI.parse("#{@endpoint}#{API_VERSION_PATH}")
  raise ArgumentError, "endpoint must be http(s)" unless %w[http https].include?(@base_uri.scheme)

  @timeout    = Integer(timeout)
  @user_agent = user_agent && !user_agent.empty? ? "#{user_agent} #{USER_AGENT}" : USER_AGENT
end

Instance Attribute Details

#endpointObject (readonly)

Returns the value of attribute endpoint.



29
30
31
# File 'lib/scanii/client.rb', line 29

def endpoint
  @endpoint
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



29
30
31
# File 'lib/scanii/client.rb', line 29

def timeout
  @timeout
end

#user_agentObject (readonly)

Returns the value of attribute user_agent.



29
30
31
# File 'lib/scanii/client.rb', line 29

def user_agent
  @user_agent
end

Instance Method Details

#create_auth_token(timeout_seconds) ⇒ Scanii::AuthToken

Mint a short-lived auth token. timeout_seconds must be positive.

Returns:

Raises:

  • (ArgumentError)

See Also:



202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/scanii/client.rb', line 202

def create_auth_token(timeout_seconds)
  ts = Integer(timeout_seconds)
  raise ArgumentError, "timeout_seconds must be positive" if ts <= 0

  status, resp_body, headers = post(
    "/auth/tokens",
    body: URI.encode_www_form("timeout" => ts),
    content_type: "application/x-www-form-urlencoded"
  )
  raise_for_status(status, resp_body, headers) unless [200, 201].include?(status)
  AuthToken.from_response(resp_body, headers)
end

#delete_auth_token(id) ⇒ Boolean

Revoke an auth token.

Returns:

  • (Boolean)

    true on 204

Raises:

  • (ArgumentError)

See Also:



231
232
233
234
235
236
237
# File 'lib/scanii/client.rb', line 231

def delete_auth_token(id)
  raise ArgumentError, "id must not be empty" if id.nil? || id.empty?

  status, resp_body, headers = request("DELETE", "/auth/tokens/#{url_encode(id)}")
  raise_for_status(status, resp_body, headers) unless status == 204
  true
end

#fetch(url, metadata: nil, callback: nil) ⇒ Scanii::PendingResult

Ask Scanii to download a remote URL and scan it asynchronously.

Returns:

Raises:

  • (ArgumentError)

See Also:



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/scanii/client.rb', line 159

def fetch(url, metadata: nil, callback: nil)
  raise ArgumentError, "url must not be empty" if url.nil? || url.empty?

  form = { "location" => url }
  form["callback"] = callback if callback && !callback.empty?
  ( || {}).each { |k, v| form["metadata[#{k}]"] = v.to_s }

  status, resp_body, headers = post(
    "/files/fetch",
    body: URI.encode_www_form(form),
    content_type: "application/x-www-form-urlencoded"
  )
  raise_for_status(status, resp_body, headers) unless status == 202
  PendingResult.from_response(resp_body, headers)
end

#pingBoolean

Verify that the configured credentials reach the API.

Returns:

  • (Boolean)

    true when the API responds 200

See Also:



191
192
193
194
195
196
# File 'lib/scanii/client.rb', line 191

def ping
  status, resp_body, headers = request("GET", "/ping")
  return true if status == 200

  raise_for_status(status, resp_body, headers)
end

#process(io, filename:, content_type: nil, metadata: nil, callback: nil) ⇒ Scanii::ProcessingResult

Submit an IO-like object for synchronous scanning.

io is duck-typed: anything responding to read(n) returning a String. Both File (opened with File.open(path, “rb”)) and StringIO work. The body is streamed to the socket; file content is never fully buffered.

Passing a String path is deprecated — use #process_file instead.

Parameters:

  • io (#read)

    IO-like object

  • filename (String)

    filename sent in the multipart part

  • content_type (String, nil) (defaults to: nil)

    content-type of the file part; guessed from filename when nil

  • metadata (Hash{String=>String}, nil) (defaults to: nil)

    arbitrary key/value pairs attached to the result

  • callback (String, nil) (defaults to: nil)

    URL to POST the result to on completion

Returns:

Raises:

  • (ArgumentError)

See Also:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/scanii/client.rb', line 67

def process(first_arg, filename: nil, content_type: nil, metadata: nil, callback: nil)
  if first_arg.is_a?(String)
    # @deprecated Use {#process_file} instead. Will be removed in a future major version.
    warn "[DEPRECATION] `Scanii::Client#process(path)` is deprecated; " \
         "use `process_file(path)` instead. Will be removed in a future major version."
    return process_file(first_arg, metadata: , callback: callback)
  end

  raise ArgumentError, "io must respond to read" unless first_arg.respond_to?(:read)
  raise ArgumentError, "filename: is required" if filename.nil? || filename.to_s.empty?

  fields = build_text_fields(, callback)
  stream, ct, length = Multipart.stream_encode(fields, first_arg, filename.to_s, content_type)
  status, resp_body, headers = post("/files", body_stream: stream, content_type: ct,
                                              content_length: length)
  raise_for_status(status, resp_body, headers) unless status == 201
  ProcessingResult.from_response(resp_body, headers)
end

#process_async(io, filename:, content_type: nil, metadata: nil, callback: nil) ⇒ Scanii::PendingResult

Submit an IO-like object for server-side asynchronous scanning.

Returns a pending id; the final result is delivered to callback (when supplied) or fetched via #retrieve.

Passing a String path is deprecated — use #process_async_file instead.

Parameters:

  • io (#read)

    IO-like object

  • filename (String)

    filename sent in the multipart part

  • content_type (String, nil) (defaults to: nil)
  • metadata (Hash{String=>String}, nil) (defaults to: nil)
  • callback (String, nil) (defaults to: nil)

Returns:

Raises:

  • (ArgumentError)

See Also:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/scanii/client.rb', line 119

def process_async(first_arg, filename: nil, content_type: nil, metadata: nil, callback: nil)
  if first_arg.is_a?(String)
    # @deprecated Use {#process_async_file} instead. Will be removed in a future major version.
    warn "[DEPRECATION] `Scanii::Client#process_async(path)` is deprecated; " \
         "use `process_async_file(path)` instead. Will be removed in a future major version."
    return process_async_file(first_arg, metadata: , callback: callback)
  end

  raise ArgumentError, "io must respond to read" unless first_arg.respond_to?(:read)
  raise ArgumentError, "filename: is required" if filename.nil? || filename.to_s.empty?

  fields = build_text_fields(, callback)
  stream, ct, length = Multipart.stream_encode(fields, first_arg, filename.to_s, content_type)
  status, resp_body, headers = post("/files/async", body_stream: stream, content_type: ct,
                                                    content_length: length)
  raise_for_status(status, resp_body, headers) unless status == 202
  PendingResult.from_response(resp_body, headers)
end

#process_async_file(file_path, metadata: nil, callback: nil) ⇒ Scanii::PendingResult

Submit a file path for server-side asynchronous scanning.

Opens the file in binary mode and delegates to #process_async.

Parameters:

  • file_path (String)

    path to the file to upload

  • metadata (Hash{String=>String}, nil) (defaults to: nil)
  • callback (String, nil) (defaults to: nil)

Returns:

See Also:



147
148
149
150
151
152
153
# File 'lib/scanii/client.rb', line 147

def process_async_file(file_path, metadata: nil, callback: nil)
  assert_readable(file_path)
  File.open(file_path.to_s, "rb") do |f|
    process_async(f, filename: File.basename(file_path.to_s), metadata: ,
                     callback: callback)
  end
end

#process_file(file_path, metadata: nil, callback: nil) ⇒ Scanii::ProcessingResult

Submit a file path for synchronous scanning.

Opens the file in binary mode, streams it to Scanii, and closes it. Delegates to #process with filename set to the basename.

Parameters:

  • file_path (String)

    path to the file to upload

  • metadata (Hash{String=>String}, nil) (defaults to: nil)
  • callback (String, nil) (defaults to: nil)

Returns:

See Also:



96
97
98
99
100
101
# File 'lib/scanii/client.rb', line 96

def process_file(file_path, metadata: nil, callback: nil)
  assert_readable(file_path)
  File.open(file_path.to_s, "rb") do |f|
    process(f, filename: File.basename(file_path.to_s), metadata: , callback: callback)
  end
end

#retrieve(id) ⇒ Scanii::ProcessingResult

Retrieve a previously submitted scan result by id.

Returns:

Raises:

  • (ArgumentError)

See Also:



179
180
181
182
183
184
185
# File 'lib/scanii/client.rb', line 179

def retrieve(id)
  raise ArgumentError, "id must not be empty" if id.nil? || id.empty?

  status, resp_body, headers = request("GET", "/files/#{url_encode(id)}")
  raise_for_status(status, resp_body, headers) unless status == 200
  ProcessingResult.from_response(resp_body, headers)
end

#retrieve_auth_token(id) ⇒ Scanii::AuthToken

Inspect a previously created auth token.

Returns:

Raises:

  • (ArgumentError)

See Also:



219
220
221
222
223
224
225
# File 'lib/scanii/client.rb', line 219

def retrieve_auth_token(id)
  raise ArgumentError, "id must not be empty" if id.nil? || id.empty?

  status, resp_body, headers = request("GET", "/auth/tokens/#{url_encode(id)}")
  raise_for_status(status, resp_body, headers) unless status == 200
  AuthToken.from_response(resp_body, headers)
end