synor/sdk/ruby/lib/synor_storage/client.rb
Gulshan Yadav 74b82d2bb2 Add Synor Storage and Wallet SDKs for Swift
- Implement SynorStorage class for decentralized storage operations including upload, download, pinning, and CAR file management.
- Create supporting types and models for storage operations such as UploadOptions, Pin, and StorageConfig.
- Implement SynorWallet class for wallet operations including wallet creation, address generation, transaction signing, and balance queries.
- Create supporting types and models for wallet operations such as Wallet, Address, and Transaction.
- Introduce error handling for both storage and wallet operations.
2026-01-27 01:56:45 +05:30

317 lines
7.8 KiB
Ruby

# frozen_string_literal: true
require "faraday"
require "faraday/multipart"
require "json"
require "base64"
module SynorStorage
# Synor Storage SDK Client
#
# @example
# client = SynorStorage::Client.new(api_key: 'your-api-key')
#
# # Upload file
# result = client.upload(data, name: 'file.txt')
#
# # Download file
# data = client.download(cid)
#
class Client
attr_reader :config
def initialize(api_key: nil, **options)
@config = Config.new(api_key: api_key, **options)
raise ArgumentError, "API key is required" unless @config.api_key
@conn = Faraday.new(url: @config.base_url) do |f|
f.request :multipart
f.request :url_encoded
f.response :json
f.options.timeout = @config.timeout
f.headers["Authorization"] = "Bearer #{@config.api_key}"
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
end
@closed = false
end
# ==================== Upload Operations ====================
# Upload data to storage
#
# @param data [String, IO] Data to upload
# @param options [UploadOptions, Hash] Upload options
# @return [UploadResponse]
def upload(data, name: nil, content_type: nil, pin: true, metadata: nil, wrap_with_directory: false)
check_closed!
payload = {
file: Faraday::Multipart::FilePart.new(
StringIO.new(data.is_a?(String) ? data : data.read),
content_type || "application/octet-stream",
name || "file"
),
pin: pin.to_s,
wrap_with_directory: wrap_with_directory.to_s
}
if metadata
metadata.each do |key, value|
payload["metadata[#{key}]"] = value
end
end
response = with_retry { @conn.post("/upload", payload) }
UploadResponse.from_hash(response.body)
end
# Upload a directory of files
#
# @param files [Array<FileEntry>] Files to upload
# @param directory_name [String, nil] Optional directory name
# @return [UploadResponse]
def upload_directory(files, directory_name: nil)
check_closed!
payload = {}
files.each_with_index do |file, i|
payload["files[#{i}]"] = Faraday::Multipart::FilePart.new(
StringIO.new(file.content),
file.content_type || "application/octet-stream",
file.path
)
end
payload["directory_name"] = directory_name if directory_name
response = with_retry { @conn.post("/upload/directory", payload) }
UploadResponse.from_hash(response.body)
end
# ==================== Download Operations ====================
# Download content by CID
#
# @param cid [String]
# @return [String] Binary data
def download(cid)
check_closed!
response = with_retry do
@conn.get("/download/#{cid}") do |req|
req.headers["Accept"] = "*/*"
end
end
response.body
end
# Download content with streaming
#
# @param cid [String]
# @yield [String] Chunks of data
def download_stream(cid, &block)
check_closed!
raise ArgumentError, "Block required for streaming" unless block_given?
@conn.get("/download/#{cid}") do |req|
req.headers["Accept"] = "*/*"
req.options.on_data = proc do |chunk, _|
block.call(chunk)
end
end
end
# Get gateway URL for a CID
#
# @param cid [String]
# @param path [String, nil] Optional path within the CID
# @return [String]
def get_gateway_url(cid, path: nil)
url = "#{@config.gateway}/ipfs/#{cid}"
url += "/#{path.gsub(%r{^/}, '')}" if path
url
end
# ==================== Pinning Operations ====================
# Pin content by CID
#
# @param request [PinRequest, Hash] Pin request
# @return [Pin]
def pin(request)
check_closed!
body = request.is_a?(PinRequest) ? request.to_h : request
response = with_retry { post_json("/pins", body) }
Pin.from_hash(response.body)
end
# Unpin content
#
# @param cid [String]
def unpin(cid)
check_closed!
with_retry { @conn.delete("/pins/#{cid}") }
nil
end
# Get pin status
#
# @param cid [String]
# @return [Pin]
def get_pin_status(cid)
check_closed!
response = with_retry { @conn.get("/pins/#{cid}") }
Pin.from_hash(response.body)
end
# List pins
#
# @param status [String, nil] Filter by status
# @param limit [Integer]
# @param offset [Integer]
# @return [Array<Pin>]
def list_pins(status: nil, limit: 20, offset: 0)
check_closed!
params = { limit: limit, offset: offset }
params[:status] = status if status
response = with_retry { @conn.get("/pins", params) }
response.body.map { |p| Pin.from_hash(p) }
end
# ==================== CAR File Operations ====================
# Create a CAR file from entries
#
# @param entries [Array<FileEntry>]
# @return [CarFile]
def create_car(entries)
check_closed!
body = entries.map do |e|
{
path: e.path,
content: Base64.strict_encode64(e.content),
content_type: e.content_type
}
end
response = with_retry { post_json("/car/create", body) }
CarFile.from_hash(response.body)
end
# Import a CAR file
#
# @param car_data [String] Binary CAR data
# @return [Array<String>] CIDs
def import_car(car_data)
check_closed!
response = with_retry do
@conn.post("/car/import") do |req|
req.headers["Content-Type"] = "application/vnd.ipld.car"
req.body = car_data
end
end
response.body["cids"]
end
# Export content as a CAR file
#
# @param cid [String]
# @return [String] Binary CAR data
def export_car(cid)
check_closed!
response = with_retry do
@conn.get("/car/export/#{cid}") do |req|
req.headers["Accept"] = "application/vnd.ipld.car"
end
end
response.body
end
# ==================== Directory Operations ====================
# List directory contents
#
# @param cid [String]
# @return [Array<DirectoryEntry>]
def list_directory(cid)
check_closed!
response = with_retry { @conn.get("/directory/#{cid}") }
response.body.map { |e| DirectoryEntry.from_hash(e) }
end
# ==================== Statistics ====================
# Get storage statistics
#
# @return [StorageStats]
def get_stats
check_closed!
response = with_retry { @conn.get("/stats") }
StorageStats.from_hash(response.body)
end
# ==================== Lifecycle ====================
def close
@closed = true
@conn.close if @conn.respond_to?(:close)
end
def closed?
@closed
end
private
def check_closed!
raise ClientClosedError, "Client has been closed" if @closed
end
def post_json(path, body)
@conn.post(path) do |req|
req.headers["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
end
def with_retry
attempts = 0
begin
attempts += 1
response = yield
handle_response(response)
response
rescue Faraday::Error, ApiError => e
if attempts < @config.retries
sleep(attempts)
retry
end
raise
end
end
def handle_response(response)
return if response.success?
error_message = if response.body.is_a?(Hash)
response.body["error"] || response.body["message"] || "Unknown error"
else
"HTTP #{response.status}"
end
raise ApiError.new(error_message, status_code: response.status)
end
end
end