Class: NeocitiesRed::Client

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

Overview

HTTP client for the Neocities API.

Wraps all API interactions — listing, uploading, deleting, and querying site information. Supports both API-key (Bearer) and basic-auth (sitename/password) authentication.

Retries transient failures (429, 5xx) automatically via Faraday::Retry.

Examples:

API key authentication

client = NeocitiesRed::Client.new(api_key: "your-api-key")
client.list

Basic auth authentication

client = NeocitiesRed::Client.new(sitename: "my-site", password: "secret")
client.list

Constant Summary collapse

API_URI =

Returns Base URL for the Neocities REST API.

Returns:

  • (String)

    Base URL for the Neocities REST API.

"https://neocities.org/api/"

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Client

Creates a new API client.

Parameters:

  • opts (Hash) (defaults to: {})

    authentication options

Options Hash (opts):

  • :api_key (String)

    Bearer token for API-key authentication

  • :sitename (String)

    site name for basic-auth (requires :password)

  • :password (String)

    site password for basic-auth (requires :sitename)

Raises:

  • (ArgumentError)

    if neither :api_key nor (+:sitename+ and :password) are provided



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/neocities_red/client.rb', line 48

def initialize(opts = {})
  @uri = URI.parse API_URI
  @opts = opts
  @conn = Faraday.new(@uri) do |conn|
    conn.options.timeout = 30
    conn.options.open_timeout = 10
    conn.adapter :net_http

    conn.request :multipart
    conn.request :url_encoded

    conn.request :retry,
                 max: 3,
                 interval: 0.3,
                 backoff_factor: 2,
                 retry_statuses: [429, 500, 502, 503, 504],
                 exceptions: [
                   Faraday::TimeoutError,
                   Faraday::ConnectionFailed,
                   Faraday::SSLError
                 ]

    conn.response :follow_redirects
  end

  raise ArgumentError, "client requires a login (sitename/password) or an api_key" unless opts[:api_key] || (opts[:sitename] && opts[:password])

  if opts[:api_key]
    @conn.request(:authorization, "Bearer", opts[:api_key])
  else
    @conn.request(:authorization, :basic, opts[:sitename], opts[:password])
  end
end

Instance Method Details

#delete(*paths) ⇒ Hash

Deletes one or more files from the remote Neocities site.

Parameters:

  • paths (Array<String>)

    remote file paths to delete

Returns:

  • (Hash)

    parsed API response



160
161
162
# File 'lib/neocities_red/client.rb', line 160

def delete(*paths)
  post "delete", "filenames" => paths
end

#delete_wrapper_with_dry_run(paths, dry_run: false) ⇒ Hash

Deletes one or more remote files, with optional dry-run support.

Parameters:

  • paths (Array<String>)

    remote file paths to delete

  • dry_run (Boolean) (defaults to: false)

    when true, simulates the deletion

Returns:

  • (Hash)

    API response with :result key



150
151
152
153
154
# File 'lib/neocities_red/client.rb', line 150

def delete_wrapper_with_dry_run(paths, dry_run: false)
  return { result: "success" } if dry_run

  delete(paths)
end

#download(url) ⇒ Faraday::Response

Downloads a file from a URL.

Parameters:

  • url (String)

    full URL to download

Returns:

  • (Faraday::Response)

    raw Faraday response object



191
192
193
# File 'lib/neocities_red/client.rb', line 191

def download(url)
  @conn.get(url)
end

#get(path, params = {}) ⇒ Hash

Performs an HTTP GET request to the Neocities API.

Parameters:

  • path (String)

    API endpoint path (e.g. "list", "info")

  • params (Hash) (defaults to: {})

    query parameters

Returns:

  • (Hash)

    parsed JSON response with symbolized keys



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

def get(path, params = {})
  uri = @uri + path
  uri.query = URI.encode_www_form params
  resp = @conn.get(uri)

  JSON.parse resp.body, symbolize_names: true
end

#info(sitename) ⇒ Hash

Retrieves information and statistics for a Neocities site.

Parameters:

  • sitename (String)

    the site name to query

Returns:

  • (Hash)

    parsed API response containing :info hash with site metadata (domain, created_at, last_updated, bandwidth, etc.)

Raises:



170
171
172
# File 'lib/neocities_red/client.rb', line 170

def info(sitename)
  get "info", sitename: sitename
end

#keyHash

Retrieves the API key for the currently authenticated user.

Only meaningful when authenticated via basic-auth (sitename/password).

Returns:

  • (Hash)

    parsed API response containing :api_key



95
96
97
# File 'lib/neocities_red/client.rb', line 95

def key
  get "key"
end

#list(path = nil) ⇒ Hash

Lists files on the remote Neocities site.

Parameters:

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

    directory path to list (nil for root)

Returns:

  • (Hash)

    parsed API response containing :files array



86
87
88
# File 'lib/neocities_red/client.rb', line 86

def list(path = nil)
  get "list", path: path
end

#post(path, args = {}) ⇒ Hash

Performs an HTTP POST request to the Neocities API.

Parameters:

  • path (String)

    API endpoint path (e.g. "upload", "delete")

  • args (Hash) (defaults to: {})

    request body parameters

Returns:

  • (Hash)

    parsed JSON response with symbolized keys



200
201
202
203
204
205
# File 'lib/neocities_red/client.rb', line 200

def post(path, args = {})
  uri = @uri + path
  resp = @conn.post(uri, args)

  JSON.parse resp.body, symbolize_names: true
end

#upload(path, remote_path = nil, dry_run: false) ⇒ Hash

Uploads a single file to the Neocities site.

Computes the SHA1 hash of the local file and compares it with the remote version. If the file already exists remotely with the same hash, the upload is skipped and an "exists" response is returned.

Parameters:

  • path (String, Pathname)

    local file path to upload

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

    remote destination path; defaults to the basename of path

  • dry_run (Boolean) (defaults to: false)

    when true, simulates the upload without sending data

Returns:

  • (Hash)

    API response with :result key ("success", "error", or "file_exists")

Raises:

  • (ArgumentError)

    if the local file does not exist



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/neocities_red/client.rb', line 121

def upload(path, remote_path = nil, dry_run: false)
  path = Pathname path
  raise ArgumentError, "#{path} does not exist." unless path.exist?

  rpath = remote_path || path.basename
  res = upload_hash(rpath.to_s, Digest::SHA1.file(path.to_s).hexdigest)

  file_exists_remotely = res[:files] ? res[:files][rpath.to_s.to_sym] == true : false

  if file_exists_remotely
    {
      result: "error",
      error_type: "file_exists",
      message: "file already exists and matches local file, not uploading"
    }
  else
    return { result: "success" } if dry_run

    File.open(path.to_s) do |file|
      post "upload", rpath.to_s => Faraday::Multipart::FilePart.new(file, "text/html")
    end
  end
end

#upload_hash(remote_path, sha1_hash) ⇒ Hash

Checks whether the remote file matches the given SHA1 hash.

Used by #upload to skip uploading files that haven't changed.

Parameters:

  • remote_path (String)

    remote file path to check

  • sha1_hash (String)

    hex-encoded SHA1 hash of the local file

Returns:

  • (Hash)

    parsed API response with :files mapping paths to booleans



106
107
108
# File 'lib/neocities_red/client.rb', line 106

def upload_hash(remote_path, sha1_hash)
  post "upload_hash", remote_path => sha1_hash
end