edgebase_core 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +77 -0
- data/lib/edgebase_core/context_manager.rb +23 -0
- data/lib/edgebase_core/errors.rb +41 -0
- data/lib/edgebase_core/field_ops.rb +21 -0
- data/lib/edgebase_core/generated/api_core.rb +915 -0
- data/lib/edgebase_core/generated/client_wrappers.rb +268 -0
- data/lib/edgebase_core/http_client.rb +219 -0
- data/lib/edgebase_core/storage.rb +161 -0
- data/lib/edgebase_core/table_ref.rb +472 -0
- data/lib/edgebase_core.rb +10 -0
- data/llms.txt +88 -0
- metadata +60 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Auto-generated client wrapper methods — DO NOT EDIT.
|
|
4
|
+
#
|
|
5
|
+
# Regenerate: npx tsx tools/sdk-codegen/generate.ts
|
|
6
|
+
# Source: wrapper-config.json + openapi.json (0.1.0)
|
|
7
|
+
|
|
8
|
+
module EdgebaseCore
|
|
9
|
+
class GeneratedAuthMethods
|
|
10
|
+
# Authentication wrapper methods
|
|
11
|
+
|
|
12
|
+
def initialize(core)
|
|
13
|
+
@core = core
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Sign up with email and password
|
|
17
|
+
def sign_up(body = nil)
|
|
18
|
+
@core.auth_signup(body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Sign in with email and password
|
|
22
|
+
def sign_in(body = nil)
|
|
23
|
+
@core.auth_signin(body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Sign out and revoke refresh token
|
|
27
|
+
def sign_out(body = nil)
|
|
28
|
+
@core.auth_signout(body)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sign in anonymously
|
|
32
|
+
def sign_in_anonymously(body = nil)
|
|
33
|
+
@core.auth_signin_anonymous(body)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Send magic link to email
|
|
37
|
+
def sign_in_with_magic_link(body = nil)
|
|
38
|
+
@core.auth_signin_magic_link(body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Verify magic link token
|
|
42
|
+
def verify_magic_link(body = nil)
|
|
43
|
+
@core.auth_verify_magic_link(body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Send OTP SMS to phone number
|
|
47
|
+
def sign_in_with_phone(body = nil)
|
|
48
|
+
@core.auth_signin_phone(body)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Verify phone OTP and create session
|
|
52
|
+
def verify_phone(body = nil)
|
|
53
|
+
@core.auth_verify_phone(body)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Send OTP code to email
|
|
57
|
+
def sign_in_with_email_otp(body = nil)
|
|
58
|
+
@core.auth_signin_email_otp(body)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Verify email OTP and create session
|
|
62
|
+
def verify_email_otp(body = nil)
|
|
63
|
+
@core.auth_verify_email_otp(body)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Link phone number to existing account
|
|
67
|
+
def link_with_phone(body = nil)
|
|
68
|
+
@core.auth_link_phone(body)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Verify OTP and link phone to account
|
|
72
|
+
def verify_link_phone(body = nil)
|
|
73
|
+
@core.auth_verify_link_phone(body)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Link email and password to existing account
|
|
77
|
+
def link_with_email(body = nil)
|
|
78
|
+
@core.auth_link_email(body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Request email change with password confirmation
|
|
82
|
+
def change_email(body = nil)
|
|
83
|
+
@core.auth_change_email(body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Verify email change token
|
|
87
|
+
def verify_email_change(body = nil)
|
|
88
|
+
@core.auth_verify_email_change(body)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Verify email address with token
|
|
92
|
+
def verify_email(body = nil)
|
|
93
|
+
@core.auth_verify_email(body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Request password reset email
|
|
97
|
+
def request_password_reset(body = nil)
|
|
98
|
+
@core.auth_request_password_reset(body)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Reset password with token
|
|
102
|
+
def reset_password(body = nil)
|
|
103
|
+
@core.auth_reset_password(body)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Change password for authenticated user
|
|
107
|
+
def change_password(body = nil)
|
|
108
|
+
@core.auth_change_password(body)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get current authenticated user info
|
|
112
|
+
def get_me()
|
|
113
|
+
@core.auth_get_me()
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Update user profile
|
|
117
|
+
def update_profile(body = nil)
|
|
118
|
+
@core.auth_update_profile(body)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# List active sessions
|
|
122
|
+
def list_sessions()
|
|
123
|
+
@core.auth_get_sessions()
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Delete a session
|
|
127
|
+
def revoke_session(id)
|
|
128
|
+
@core.auth_delete_session(id)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Enroll new TOTP factor
|
|
132
|
+
def enroll_totp()
|
|
133
|
+
@core.auth_mfa_totp_enroll()
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Confirm TOTP enrollment with code
|
|
137
|
+
def verify_totp_enrollment(body = nil)
|
|
138
|
+
@core.auth_mfa_totp_verify(body)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Verify MFA code during signin
|
|
142
|
+
def verify_totp(body = nil)
|
|
143
|
+
@core.auth_mfa_verify(body)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Use recovery code during MFA signin
|
|
147
|
+
def use_recovery_code(body = nil)
|
|
148
|
+
@core.auth_mfa_recovery(body)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Disable TOTP factor
|
|
152
|
+
def disable_totp(body = nil)
|
|
153
|
+
@core.auth_mfa_totp_delete(body)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# List MFA factors for authenticated user
|
|
157
|
+
def list_factors()
|
|
158
|
+
@core.auth_mfa_factors()
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Generate passkey registration options
|
|
162
|
+
def passkeys_register_options()
|
|
163
|
+
@core.auth_passkeys_register_options()
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Verify and store passkey registration
|
|
167
|
+
def passkeys_register(body = nil)
|
|
168
|
+
@core.auth_passkeys_register(body)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Generate passkey authentication options
|
|
172
|
+
def passkeys_auth_options(body = nil)
|
|
173
|
+
@core.auth_passkeys_auth_options(body)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Authenticate with passkey
|
|
177
|
+
def passkeys_authenticate(body = nil)
|
|
178
|
+
@core.auth_passkeys_authenticate(body)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# List passkeys for authenticated user
|
|
182
|
+
def passkeys_list()
|
|
183
|
+
@core.auth_passkeys_list()
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Delete a passkey
|
|
187
|
+
def passkeys_delete(credential_id)
|
|
188
|
+
@core.auth_passkeys_delete(credential_id)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
class GeneratedStorageMethods
|
|
193
|
+
# Storage wrapper methods (bucket-scoped)
|
|
194
|
+
|
|
195
|
+
def initialize(core)
|
|
196
|
+
@core = core
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Delete file
|
|
200
|
+
def delete(bucket, key)
|
|
201
|
+
@core.delete_file(bucket, key)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Batch delete files
|
|
205
|
+
def delete_many(bucket, body = nil)
|
|
206
|
+
@core.delete_batch(bucket, body)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Check if file exists
|
|
210
|
+
def exists(bucket, key)
|
|
211
|
+
@core.check_file_exists(bucket, key)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get file metadata
|
|
215
|
+
def get_metadata(bucket, key)
|
|
216
|
+
@core.get_file_metadata(bucket, key)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Update file metadata
|
|
220
|
+
def update_metadata(bucket, key, body = nil)
|
|
221
|
+
@core.update_file_metadata(bucket, key, body)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Create signed download URL
|
|
225
|
+
def create_signed_url(bucket, body = nil)
|
|
226
|
+
@core.create_signed_download_url(bucket, body)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Batch create signed download URLs
|
|
230
|
+
def create_signed_urls(bucket, body = nil)
|
|
231
|
+
@core.create_signed_download_urls(bucket, body)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Create signed upload URL
|
|
235
|
+
def create_signed_upload_url(bucket, body = nil)
|
|
236
|
+
@core.create_signed_upload_url(bucket, body)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Start multipart upload
|
|
240
|
+
def create_multipart_upload(bucket, body = nil)
|
|
241
|
+
@core.create_multipart_upload(bucket, body)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Complete multipart upload
|
|
245
|
+
def complete_multipart_upload(bucket, body = nil)
|
|
246
|
+
@core.complete_multipart_upload(bucket, body)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Abort multipart upload
|
|
250
|
+
def abort_multipart_upload(bucket, body = nil)
|
|
251
|
+
@core.abort_multipart_upload(bucket, body)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
class GeneratedAnalyticsMethods
|
|
256
|
+
# Analytics wrapper methods
|
|
257
|
+
|
|
258
|
+
def initialize(core)
|
|
259
|
+
@core = core
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Track custom events
|
|
263
|
+
def track(body = nil)
|
|
264
|
+
@core.track_events(body)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
require_relative "context_manager"
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module EdgebaseCore
|
|
11
|
+
DEFAULT_OPEN_TIMEOUT = 30
|
|
12
|
+
DEFAULT_READ_TIMEOUT = 120
|
|
13
|
+
|
|
14
|
+
# Synchronous HTTP client for server-side use.
|
|
15
|
+
#
|
|
16
|
+
# Features:
|
|
17
|
+
# - Service Key header injection (X-EdgeBase-Service-Key)
|
|
18
|
+
# - Optional Bearer token injection (for impersonation)
|
|
19
|
+
# - Legacy context state for compatibility (not serialized into HTTP headers)
|
|
20
|
+
class HttpClient
|
|
21
|
+
attr_reader :base_url
|
|
22
|
+
|
|
23
|
+
def initialize(base_url, context_manager: nil, service_key: nil, bearer_token: nil)
|
|
24
|
+
@base_url = base_url.chomp("/")
|
|
25
|
+
@context_manager = context_manager || ContextManager.new
|
|
26
|
+
@service_key = service_key
|
|
27
|
+
@bearer_token = bearer_token
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(path, params: nil)
|
|
31
|
+
request("GET", path, params: params)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def post(path, body = nil, params: nil)
|
|
35
|
+
request("POST", path, params: params, json_body: body)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def patch(path, body = nil, params: nil)
|
|
39
|
+
request("PATCH", path, params: params, json_body: body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def put(path, body = nil, params: nil)
|
|
43
|
+
request("PUT", path, params: params, json_body: body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete(path, params: nil)
|
|
47
|
+
request("DELETE", path, params: params)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# HEAD request — returns true if resource exists (2xx).
|
|
51
|
+
def head(path)
|
|
52
|
+
uri = URI(build_url(path))
|
|
53
|
+
http = build_http(uri)
|
|
54
|
+
req = Net::HTTP::Head.new(uri)
|
|
55
|
+
auth_headers.each { |k, v| req[k] = v }
|
|
56
|
+
response = http.request(req)
|
|
57
|
+
response.code.to_i < 400
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# POST multipart form data (for file uploads).
|
|
61
|
+
def post_multipart(path, files:, data: nil)
|
|
62
|
+
uri = URI(build_url(path))
|
|
63
|
+
http = build_http(uri)
|
|
64
|
+
|
|
65
|
+
boundary = "EdgeBase#{rand(10**16)}"
|
|
66
|
+
body = build_multipart_body(files, data, boundary)
|
|
67
|
+
|
|
68
|
+
req = Net::HTTP::Post.new(uri)
|
|
69
|
+
headers = auth_headers
|
|
70
|
+
headers.delete("Content-Type")
|
|
71
|
+
headers["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
72
|
+
headers.each { |k, v| req[k] = v }
|
|
73
|
+
req.body = body
|
|
74
|
+
|
|
75
|
+
parse_response(http.request(req))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# POST raw binary data (for multipart upload-part).
|
|
79
|
+
def post_raw(path, data:, content_type: "application/octet-stream")
|
|
80
|
+
uri = URI(build_url(path))
|
|
81
|
+
http = build_http(uri)
|
|
82
|
+
|
|
83
|
+
req = Net::HTTP::Post.new(uri)
|
|
84
|
+
headers = auth_headers
|
|
85
|
+
headers["Content-Type"] = content_type
|
|
86
|
+
headers.each { |k, v| req[k] = v }
|
|
87
|
+
req.body = data
|
|
88
|
+
|
|
89
|
+
parse_response(http.request(req))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# GET raw bytes (for file downloads).
|
|
93
|
+
def get_raw(path)
|
|
94
|
+
uri = URI(build_url(path))
|
|
95
|
+
http = build_http(uri)
|
|
96
|
+
req = Net::HTTP::Get.new(uri)
|
|
97
|
+
auth_headers.each { |k, v| req[k] = v }
|
|
98
|
+
response = http.request(req)
|
|
99
|
+
if response.code.to_i >= 400
|
|
100
|
+
raise EdgeBaseError.new(response.code.to_i, response.body)
|
|
101
|
+
end
|
|
102
|
+
response.body
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def request(method, path, params: nil, json_body: nil)
|
|
108
|
+
url = build_url(path)
|
|
109
|
+
if params && !params.empty?
|
|
110
|
+
query = URI.encode_www_form(params)
|
|
111
|
+
url = "#{url}?#{query}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
uri = URI(url)
|
|
115
|
+
http = build_http(uri)
|
|
116
|
+
|
|
117
|
+
req = case method
|
|
118
|
+
when "GET" then Net::HTTP::Get.new(uri)
|
|
119
|
+
when "POST" then Net::HTTP::Post.new(uri)
|
|
120
|
+
when "PATCH" then Net::HTTP::Patch.new(uri)
|
|
121
|
+
when "PUT" then Net::HTTP::Put.new(uri)
|
|
122
|
+
when "DELETE" then Net::HTTP::Delete.new(uri)
|
|
123
|
+
else Net::HTTP::Get.new(uri)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
auth_headers.each { |k, v| req[k] = v }
|
|
127
|
+
req.body = JSON.generate(json_body) if json_body
|
|
128
|
+
|
|
129
|
+
parse_response(http.request(req))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_url(path)
|
|
133
|
+
if path.start_with?("/api/")
|
|
134
|
+
"#{@base_url}#{path}"
|
|
135
|
+
else
|
|
136
|
+
"#{@base_url}/api#{path}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def auth_headers
|
|
141
|
+
headers = { "Content-Type" => "application/json", "Connection" => "close" }
|
|
142
|
+
begin
|
|
143
|
+
if @bearer_token
|
|
144
|
+
headers["Authorization"] = "Bearer #{@bearer_token}"
|
|
145
|
+
end
|
|
146
|
+
if @service_key
|
|
147
|
+
headers["X-EdgeBase-Service-Key"] = @service_key
|
|
148
|
+
headers["Authorization"] = "Bearer #{@service_key}"
|
|
149
|
+
end
|
|
150
|
+
rescue StandardError
|
|
151
|
+
# Token refresh failed — proceed as unauthenticated
|
|
152
|
+
end
|
|
153
|
+
headers
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def build_http(uri)
|
|
157
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
158
|
+
http.use_ssl = (uri.scheme == "https")
|
|
159
|
+
http.open_timeout = request_timeout_seconds(DEFAULT_OPEN_TIMEOUT)
|
|
160
|
+
http.read_timeout = request_timeout_seconds(DEFAULT_READ_TIMEOUT)
|
|
161
|
+
http.keep_alive_timeout = 0 if http.respond_to?(:keep_alive_timeout=)
|
|
162
|
+
http
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def request_timeout_seconds(default_seconds)
|
|
166
|
+
raw = ENV.fetch("EDGEBASE_HTTP_TIMEOUT_MS", "").strip
|
|
167
|
+
return default_seconds if raw.empty?
|
|
168
|
+
|
|
169
|
+
timeout_ms = Integer(raw, exception: false)
|
|
170
|
+
return default_seconds if timeout_ms.nil? || timeout_ms <= 0
|
|
171
|
+
|
|
172
|
+
timeout_ms / 1000.0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_response(response)
|
|
176
|
+
code = response.code.to_i
|
|
177
|
+
if code >= 400
|
|
178
|
+
begin
|
|
179
|
+
data = JSON.parse(response.body)
|
|
180
|
+
raise EdgeBaseError.from_json(data, code)
|
|
181
|
+
rescue EdgeBaseError
|
|
182
|
+
raise
|
|
183
|
+
rescue StandardError
|
|
184
|
+
raise EdgeBaseError.new(code, response.body || "Unknown error")
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
return nil if response.body.nil? || response.body.empty?
|
|
189
|
+
|
|
190
|
+
begin
|
|
191
|
+
JSON.parse(response.body)
|
|
192
|
+
rescue StandardError
|
|
193
|
+
raise EdgeBaseError.new(code, "Expected a JSON response but received malformed JSON.")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def build_multipart_body(files, data, boundary)
|
|
198
|
+
body = +""
|
|
199
|
+
# Data fields
|
|
200
|
+
if data
|
|
201
|
+
data.each do |key, value|
|
|
202
|
+
body << "--#{boundary}\r\n"
|
|
203
|
+
body << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
|
|
204
|
+
body << "#{value}\r\n"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
# File fields
|
|
208
|
+
files.each do |field_name, (filename, file_data, content_type)|
|
|
209
|
+
body << "--#{boundary}\r\n"
|
|
210
|
+
body << "Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{filename}\"\r\n"
|
|
211
|
+
body << "Content-Type: #{content_type}\r\n\r\n"
|
|
212
|
+
body << file_data
|
|
213
|
+
body << "\r\n"
|
|
214
|
+
end
|
|
215
|
+
body << "--#{boundary}--\r\n"
|
|
216
|
+
body
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module EdgebaseCore
|
|
7
|
+
# Signed URL result.
|
|
8
|
+
SignedUrlResult = Struct.new(:url, :expires_in, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# File metadata.
|
|
11
|
+
FileInfo = Struct.new(:key, :size, :content_type, :etag, :custom_metadata, keyword_init: true) do
|
|
12
|
+
def self.from_json(data)
|
|
13
|
+
new(
|
|
14
|
+
key: data["key"] || "",
|
|
15
|
+
size: data["size"] || 0,
|
|
16
|
+
content_type: data["contentType"],
|
|
17
|
+
etag: data["etag"],
|
|
18
|
+
custom_metadata: data["customMetadata"] || data["custom_metadata"] || {}
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Storage subsystem — bucket factory.
|
|
24
|
+
#
|
|
25
|
+
# bucket = client.storage.bucket("avatars")
|
|
26
|
+
# url = bucket.get_url("profile.png")
|
|
27
|
+
class StorageClient
|
|
28
|
+
def initialize(client)
|
|
29
|
+
@client = client
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def bucket(name)
|
|
33
|
+
StorageBucket.new(@client, name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Bucket-level storage operations.
|
|
38
|
+
class StorageBucket
|
|
39
|
+
attr_reader :name
|
|
40
|
+
|
|
41
|
+
def initialize(client, name)
|
|
42
|
+
@client = client
|
|
43
|
+
@core = EdgebaseCore::GeneratedDbApi.new(client)
|
|
44
|
+
@name = name
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get the public URL of a file.
|
|
48
|
+
def get_url(path)
|
|
49
|
+
"#{@client.base_url}/api/storage/#{@name}/#{URI.encode_www_form_component(path)}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ── Upload ─────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
def upload(path, data, content_type: "application/octet-stream")
|
|
55
|
+
@client.post_multipart(
|
|
56
|
+
"/storage/#{@name}/upload",
|
|
57
|
+
files: { "file" => [path, data, content_type] },
|
|
58
|
+
data: { "key" => path }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def upload_string(path, data, encoding: "raw", content_type: "text/plain")
|
|
63
|
+
raw_bytes = case encoding
|
|
64
|
+
when "raw"
|
|
65
|
+
data.encode("UTF-8")
|
|
66
|
+
when "base64"
|
|
67
|
+
Base64.decode64(data)
|
|
68
|
+
when "base64url"
|
|
69
|
+
Base64.urlsafe_decode64(data)
|
|
70
|
+
when "data_url"
|
|
71
|
+
_, encoded = data.split(",", 2)
|
|
72
|
+
Base64.decode64(encoded)
|
|
73
|
+
else
|
|
74
|
+
data.encode("UTF-8")
|
|
75
|
+
end
|
|
76
|
+
upload(path, raw_bytes, content_type: content_type)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ── Download ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def download(path)
|
|
82
|
+
@client.get_raw("/storage/#{@name}/#{URI.encode_www_form_component(path)}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ── Metadata ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def get_metadata(path)
|
|
88
|
+
data = @core.get_file_metadata(@name, path)
|
|
89
|
+
FileInfo.from_json(data)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def update_metadata(path, metadata)
|
|
93
|
+
@core.update_file_metadata(@name, path, metadata)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ── Signed URLs ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
def create_signed_url(path, expires_in: "1h")
|
|
99
|
+
data = @core.create_signed_download_url(
|
|
100
|
+
@name, { "key" => path, "expiresIn" => expires_in }
|
|
101
|
+
)
|
|
102
|
+
SignedUrlResult.new(url: data["url"] || "", expires_in: data["expiresIn"] || expires_in)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def create_signed_upload_url(path, expires_in: 3600)
|
|
106
|
+
data = @core.create_signed_upload_url(
|
|
107
|
+
@name, { "key" => path, "expiresIn" => "#{expires_in}s" }
|
|
108
|
+
)
|
|
109
|
+
SignedUrlResult.new(url: data["url"] || "", expires_in: data["expiresIn"] || expires_in)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ── Management ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
def delete_file(path)
|
|
115
|
+
@core.delete_file(@name, path)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
alias delete delete_file
|
|
119
|
+
|
|
120
|
+
def list_files(prefix: "", limit: 100, offset: 0)
|
|
121
|
+
params = { "limit" => limit.to_s, "offset" => offset.to_s }
|
|
122
|
+
params["prefix"] = prefix unless prefix.empty?
|
|
123
|
+
data = @client.get("/storage/#{@name}", params: params)
|
|
124
|
+
items = data.is_a?(Hash) ? (data["files"] || data["items"] || []) : []
|
|
125
|
+
items.map { |item| FileInfo.from_json(item) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
alias list list_files
|
|
129
|
+
|
|
130
|
+
# ── Resumable / Multipart Upload ───────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def initiate_resumable_upload(path, content_type: "application/octet-stream", total_size: nil)
|
|
133
|
+
body = { "key" => path, "contentType" => content_type }
|
|
134
|
+
body["totalSize"] = total_size if total_size
|
|
135
|
+
data = @core.create_multipart_upload(@name, body)
|
|
136
|
+
data["uploadId"] || ""
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def resume_upload(path, upload_id, chunk, part_number: 1, is_last_chunk: false)
|
|
140
|
+
encoded_path = URI.encode_www_form_component(path)
|
|
141
|
+
params = "uploadId=#{upload_id}&partNumber=#{part_number}&key=#{encoded_path}"
|
|
142
|
+
@client.post_raw(
|
|
143
|
+
"/storage/#{@name}/multipart/upload-part?#{params}",
|
|
144
|
+
data: chunk,
|
|
145
|
+
content_type: "application/octet-stream"
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def complete_resumable_upload(path, upload_id, parts)
|
|
150
|
+
@core.complete_multipart_upload(
|
|
151
|
+
@name, { "uploadId" => upload_id, "key" => path, "parts" => parts }
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def abort_resumable_upload(path, upload_id)
|
|
156
|
+
@core.abort_multipart_upload(
|
|
157
|
+
@name, { "uploadId" => upload_id, "key" => path }
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|