wowsql-sdk 1.3.0 → 3.0.1
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 +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +313 -164
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +229 -120
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +327 -176
- data/lib/wowsql/table.rb +111 -10
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +2 -2
- metadata +20 -5
data/lib/wowsql/storage.rb
CHANGED
|
@@ -3,20 +3,136 @@ require 'faraday/multipart'
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'stringio'
|
|
5
5
|
require 'uri'
|
|
6
|
+
require 'fileutils'
|
|
6
7
|
require_relative 'exceptions'
|
|
7
8
|
|
|
8
9
|
module WOWSQL
|
|
9
|
-
#
|
|
10
|
+
# Bucket information.
|
|
11
|
+
class StorageBucket
|
|
12
|
+
attr_reader :id, :name, :public, :file_size_limit,
|
|
13
|
+
:allowed_mime_types, :created_at, :object_count, :total_size
|
|
14
|
+
|
|
15
|
+
def initialize(data)
|
|
16
|
+
@id = data['id'] || data[:id] || ''
|
|
17
|
+
@name = data['name'] || data[:name] || ''
|
|
18
|
+
@public = !!(data['public'] || data[:public])
|
|
19
|
+
@file_size_limit = data['file_size_limit'] || data[:file_size_limit]
|
|
20
|
+
@allowed_mime_types = data['allowed_mime_types'] || data[:allowed_mime_types]
|
|
21
|
+
@created_at = data['created_at'] || data[:created_at]
|
|
22
|
+
@object_count = data['object_count'] || data[:object_count] || 0
|
|
23
|
+
@total_size = data['total_size'] || data[:total_size] || 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
"StorageBucket(name=#{@name.inspect}, public=#{@public})"
|
|
28
|
+
end
|
|
29
|
+
alias inspect to_s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Storage file/object information.
|
|
33
|
+
class StorageFile
|
|
34
|
+
attr_reader :id, :bucket_id, :name, :path, :mime_type,
|
|
35
|
+
:size, :metadata, :created_at, :public_url
|
|
36
|
+
|
|
37
|
+
def initialize(data)
|
|
38
|
+
@id = data['id'] || data[:id] || ''
|
|
39
|
+
@bucket_id = data['bucket_id'] || data[:bucket_id] || ''
|
|
40
|
+
@name = data['name'] || data[:name] || ''
|
|
41
|
+
@path = data['path'] || data[:path] || ''
|
|
42
|
+
@mime_type = data['mime_type'] || data[:mime_type]
|
|
43
|
+
@size = data['size'] || data[:size] || 0
|
|
44
|
+
@metadata = data['metadata'] || data[:metadata] || {}
|
|
45
|
+
@created_at = data['created_at'] || data[:created_at]
|
|
46
|
+
@public_url = data['public_url'] || data[:public_url]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def size_mb
|
|
50
|
+
@size / (1024.0 * 1024.0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def size_gb
|
|
54
|
+
@size / (1024.0 ** 3)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_s
|
|
58
|
+
"StorageFile(path=#{@path.inspect}, size=#{'%.2f' % size_mb}MB)"
|
|
59
|
+
end
|
|
60
|
+
alias inspect to_s
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Storage quota / statistics information.
|
|
64
|
+
class StorageQuota
|
|
65
|
+
attr_reader :total_files, :total_size_bytes, :total_size_gb, :file_types
|
|
66
|
+
|
|
67
|
+
def initialize(data)
|
|
68
|
+
@total_files = data['total_files'] || data[:total_files] || 0
|
|
69
|
+
@total_size_bytes = data['total_size_bytes'] || data[:total_size_bytes] || 0
|
|
70
|
+
@total_size_gb = data['total_size_gb'] || data[:total_size_gb] || 0
|
|
71
|
+
@file_types = data['file_types'] || data[:file_types] || {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_s
|
|
75
|
+
"StorageQuota(files=#{@total_files}, size=#{'%.4f' % @total_size_gb}GB)"
|
|
76
|
+
end
|
|
77
|
+
alias inspect to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# WowSQL Storage Client — PostgreSQL-native file storage.
|
|
81
|
+
#
|
|
82
|
+
# Files are stored as BYTEA inside each project's storage schema.
|
|
83
|
+
# No external S3 dependency — everything lives in PostgreSQL.
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# storage = WOWSQL::WOWSQLStorage.new(
|
|
87
|
+
# "https://myproject.wowsql.com",
|
|
88
|
+
# "wowsql_anon_..."
|
|
89
|
+
# )
|
|
90
|
+
# bucket = storage.create_bucket("avatars", public: true)
|
|
91
|
+
# File.open("photo.jpg", "rb") { |f| storage.upload("avatars", f, path: "users/profile.jpg") }
|
|
92
|
+
# files = storage.list_files("avatars", prefix: "users/")
|
|
93
|
+
# url = storage.get_public_url("avatars", "users/profile.jpg")
|
|
10
94
|
class WOWSQLStorage
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
95
|
+
attr_reader :base_url, :project_slug
|
|
96
|
+
|
|
97
|
+
# @param project_url [String] Project subdomain or full URL
|
|
98
|
+
# @param api_key [String] API key for authentication
|
|
99
|
+
# @param project_slug [String] Explicit slug (used with base_url)
|
|
100
|
+
# @param base_url [String] Explicit base URL (used with project_slug)
|
|
101
|
+
# @param base_domain [String] Base domain (default: "wowsql.com")
|
|
102
|
+
# @param secure [Boolean] Use HTTPS (default: true)
|
|
103
|
+
# @param timeout [Integer] Request timeout in seconds (default: 60)
|
|
104
|
+
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
105
|
+
def initialize(project_url = '', api_key = '', project_slug: '', base_url: '',
|
|
106
|
+
base_domain: 'wowsql.com', secure: true, timeout: 60, verify_ssl: true)
|
|
107
|
+
if !project_slug.empty? && !base_url.empty?
|
|
108
|
+
@base_url = base_url.chomp('/')
|
|
109
|
+
@project_slug = project_slug
|
|
110
|
+
elsif !project_url.empty?
|
|
111
|
+
url = project_url.strip
|
|
112
|
+
if url.start_with?('http://') || url.start_with?('https://')
|
|
113
|
+
@base_url = url.chomp('/')
|
|
114
|
+
@base_url = @base_url.split('/api').first if @base_url.include?('/api')
|
|
115
|
+
else
|
|
116
|
+
protocol = secure ? 'https' : 'http'
|
|
117
|
+
if url.include?(".#{base_domain}") || url.end_with?(base_domain)
|
|
118
|
+
@base_url = "#{protocol}://#{url}"
|
|
119
|
+
else
|
|
120
|
+
@base_url = "#{protocol}://#{url}.#{base_domain}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
@project_slug = url.split('.').first
|
|
124
|
+
.sub('https://', '')
|
|
125
|
+
.sub('http://', '')
|
|
126
|
+
else
|
|
127
|
+
raise ArgumentError, 'Either project_url or (project_slug + base_url) must be provided'
|
|
128
|
+
end
|
|
129
|
+
|
|
15
130
|
@timeout = timeout
|
|
16
|
-
@
|
|
17
|
-
@quota_cache = nil
|
|
131
|
+
@verify_ssl = verify_ssl
|
|
18
132
|
|
|
19
|
-
|
|
133
|
+
ssl_options = verify_ssl ? {} : { verify: false }
|
|
134
|
+
|
|
135
|
+
@conn = Faraday.new(url: @base_url, ssl: ssl_options) do |f|
|
|
20
136
|
f.request :multipart
|
|
21
137
|
f.request :json
|
|
22
138
|
f.response :json
|
|
@@ -27,203 +143,238 @@ module WOWSQL
|
|
|
27
143
|
@conn.headers['Authorization'] = "Bearer #{api_key}"
|
|
28
144
|
end
|
|
29
145
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
146
|
+
# ── Buckets ──────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
# Create a new storage bucket.
|
|
149
|
+
#
|
|
150
|
+
# @param name [String] Bucket name
|
|
151
|
+
# @param public [Boolean] Whether the bucket is public
|
|
152
|
+
# @param file_size_limit [Integer, nil] Max file size in bytes
|
|
153
|
+
# @param allowed_mime_types [Array<String>, nil] Allowed MIME types
|
|
154
|
+
# @return [StorageBucket]
|
|
155
|
+
def create_bucket(name, public: false, file_size_limit: nil, allowed_mime_types: nil)
|
|
156
|
+
data = request('POST', "/api/v1/storage/projects/#{@project_slug}/buckets", nil, {
|
|
157
|
+
name: name,
|
|
158
|
+
public: public,
|
|
159
|
+
file_size_limit: file_size_limit,
|
|
160
|
+
allowed_mime_types: allowed_mime_types
|
|
161
|
+
})
|
|
162
|
+
StorageBucket.new(data)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# List all buckets in the project.
|
|
166
|
+
#
|
|
167
|
+
# @return [Array<StorageBucket>]
|
|
168
|
+
def list_buckets
|
|
169
|
+
data = request('GET', "/api/v1/storage/projects/#{@project_slug}/buckets", nil, nil)
|
|
170
|
+
Array(data).map { |b| StorageBucket.new(b) }
|
|
171
|
+
end
|
|
36
172
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
173
|
+
# Get a specific bucket by name.
|
|
174
|
+
#
|
|
175
|
+
# @param name [String] Bucket name
|
|
176
|
+
# @return [StorageBucket]
|
|
177
|
+
def get_bucket(name)
|
|
178
|
+
data = request('GET', "/api/v1/storage/projects/#{@project_slug}/buckets/#{name}", nil, nil)
|
|
179
|
+
StorageBucket.new(data)
|
|
40
180
|
end
|
|
41
181
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# @param
|
|
45
|
-
# @param
|
|
46
|
-
# @
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
182
|
+
# Update bucket settings.
|
|
183
|
+
#
|
|
184
|
+
# @param name [String] Bucket name
|
|
185
|
+
# @param options [Hash] Settings to update
|
|
186
|
+
# @return [StorageBucket]
|
|
187
|
+
def update_bucket(name, **options)
|
|
188
|
+
data = request('PATCH', "/api/v1/storage/projects/#{@project_slug}/buckets/#{name}", nil, options)
|
|
189
|
+
StorageBucket.new(data)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Delete a bucket and all its files.
|
|
193
|
+
#
|
|
194
|
+
# @param name [String] Bucket name
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def delete_bucket(name)
|
|
197
|
+
request('DELETE', "/api/v1/storage/projects/#{@project_slug}/buckets/#{name}", nil, nil)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# ── Files ────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
# Upload a file to a bucket.
|
|
203
|
+
#
|
|
204
|
+
# @param bucket_name [String] Target bucket
|
|
205
|
+
# @param file_data [IO] File-like object (opened in binary mode)
|
|
206
|
+
# @param path [String, nil] File path within bucket
|
|
207
|
+
# @param file_name [String, nil] Override filename
|
|
208
|
+
# @return [StorageFile]
|
|
209
|
+
def upload(bucket_name, file_data, path: nil, file_name: nil)
|
|
210
|
+
content = file_data.respond_to?(:read) ? file_data.read : file_data
|
|
211
|
+
name = file_name || (file_data.respond_to?(:path) ? File.basename(file_data.path) : nil) || path || 'file'
|
|
212
|
+
name = name.split('/').last if name.include?('/')
|
|
213
|
+
|
|
214
|
+
folder = ''
|
|
215
|
+
if path && path.include?('/')
|
|
216
|
+
folder = path.rpartition('/').first
|
|
53
217
|
end
|
|
54
218
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def upload_file(file_data, file_key, folder = nil, content_type = nil, check_quota = nil)
|
|
68
|
-
should_check = check_quota.nil? ? @auto_check_quota : check_quota
|
|
69
|
-
|
|
70
|
-
# Read file data
|
|
71
|
-
file_bytes = file_data.respond_to?(:read) ? file_data.read : File.read(file_data)
|
|
72
|
-
file_size = file_bytes.bytesize
|
|
73
|
-
|
|
74
|
-
# Check quota if enabled
|
|
75
|
-
if should_check
|
|
76
|
-
quota = get_quota(true)
|
|
77
|
-
file_size_gb = file_size / (1024.0 * 1024.0 * 1024.0)
|
|
78
|
-
if file_size_gb > quota['storage_available_gb']
|
|
79
|
-
raise StorageLimitExceededException.new(
|
|
80
|
-
"Storage limit exceeded! File size: #{format('%.4f', file_size_gb)} GB, " \
|
|
81
|
-
"Available: #{format('%.4f', quota['storage_available_gb'])} GB."
|
|
82
|
-
)
|
|
83
|
-
end
|
|
219
|
+
params = {}
|
|
220
|
+
params['folder'] = folder unless folder.empty?
|
|
221
|
+
|
|
222
|
+
upload_io = Faraday::Multipart::FilePart.new(
|
|
223
|
+
StringIO.new(content.is_a?(String) ? content : content.to_s),
|
|
224
|
+
'application/octet-stream',
|
|
225
|
+
name
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
response = @conn.post("/api/v1/storage/projects/#{@project_slug}/buckets/#{bucket_name}/files") do |req|
|
|
229
|
+
req.params = params unless params.empty?
|
|
230
|
+
req.body = { file: upload_io }
|
|
84
231
|
end
|
|
85
232
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
begin
|
|
91
|
-
# Create multipart form
|
|
92
|
-
payload = {
|
|
93
|
-
file: Faraday::Multipart::FilePart.new(
|
|
94
|
-
StringIO.new(file_bytes),
|
|
95
|
-
content_type || 'application/octet-stream',
|
|
96
|
-
file_key
|
|
97
|
-
),
|
|
98
|
-
key: file_key
|
|
99
|
-
}
|
|
100
|
-
payload[:content_type] = content_type if content_type
|
|
101
|
-
|
|
102
|
-
response = @conn.post(url) do |req|
|
|
103
|
-
req.body = payload
|
|
104
|
-
end
|
|
233
|
+
if response.status >= 400
|
|
234
|
+
handle_error(response)
|
|
235
|
+
end
|
|
105
236
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
237
|
+
StorageFile.new(response.body)
|
|
238
|
+
rescue Faraday::Error => e
|
|
239
|
+
raise StorageError.new("Upload failed: #{e.message}")
|
|
240
|
+
end
|
|
109
241
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
242
|
+
# Upload a file from a local filesystem path.
|
|
243
|
+
#
|
|
244
|
+
# @param file_path [String] Path to local file
|
|
245
|
+
# @param bucket_name [String] Target bucket
|
|
246
|
+
# @param path [String, nil] File path within bucket
|
|
247
|
+
# @return [StorageFile]
|
|
248
|
+
def upload_from_path(file_path, bucket_name: 'default', path: nil)
|
|
249
|
+
raise StorageError.new("File not found: #{file_path}") unless File.exist?(file_path)
|
|
250
|
+
|
|
251
|
+
file_name = File.basename(file_path)
|
|
252
|
+
File.open(file_path, 'rb') do |f|
|
|
253
|
+
upload(bucket_name, f, path: path || file_name, file_name: file_name)
|
|
114
254
|
end
|
|
115
255
|
end
|
|
116
256
|
|
|
117
|
-
# List files in
|
|
118
|
-
#
|
|
257
|
+
# List files in a bucket.
|
|
258
|
+
#
|
|
259
|
+
# @param bucket_name [String] Bucket name
|
|
119
260
|
# @param prefix [String, nil] Filter by prefix/folder
|
|
120
|
-
# @param
|
|
121
|
-
# @
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
params
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
# @param
|
|
153
|
-
# @param
|
|
154
|
-
# @
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
# @param
|
|
178
|
-
# @return [
|
|
179
|
-
def
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
261
|
+
# @param limit [Integer] Maximum files to return
|
|
262
|
+
# @param offset [Integer] Offset for pagination
|
|
263
|
+
# @return [Array<StorageFile>]
|
|
264
|
+
def list_files(bucket_name, prefix: nil, limit: 100, offset: 0)
|
|
265
|
+
params = { 'limit' => limit, 'offset' => offset }
|
|
266
|
+
params['prefix'] = prefix if prefix
|
|
267
|
+
|
|
268
|
+
data = request('GET', "/api/v1/storage/projects/#{@project_slug}/buckets/#{bucket_name}/files", params, nil)
|
|
269
|
+
items = data.is_a?(Array) ? data : (data['files'] || data['data'] || [])
|
|
270
|
+
items.map { |f| StorageFile.new(f) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Download a file and return its binary contents.
|
|
274
|
+
#
|
|
275
|
+
# @param bucket_name [String] Bucket name
|
|
276
|
+
# @param file_path [String] File path within bucket
|
|
277
|
+
# @return [String] Binary file contents
|
|
278
|
+
def download(bucket_name, file_path)
|
|
279
|
+
url = "/api/v1/storage/projects/#{@project_slug}/files/#{bucket_name}/#{file_path}"
|
|
280
|
+
|
|
281
|
+
response = @conn.get(url)
|
|
282
|
+
if response.status >= 400
|
|
283
|
+
handle_error(response)
|
|
284
|
+
end
|
|
285
|
+
response.body
|
|
286
|
+
rescue Faraday::Error => e
|
|
287
|
+
raise StorageError.new("Download failed: #{e.message}")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Download a file and save it to a local path.
|
|
291
|
+
#
|
|
292
|
+
# @param bucket_name [String] Bucket name
|
|
293
|
+
# @param file_path [String] File path within bucket
|
|
294
|
+
# @param local_path [String] Local destination path
|
|
295
|
+
# @return [String] The local path
|
|
296
|
+
def download_to_file(bucket_name, file_path, local_path)
|
|
297
|
+
content = download(bucket_name, file_path)
|
|
298
|
+
dir = File.dirname(local_path)
|
|
299
|
+
FileUtils.mkdir_p(dir) unless dir == '.'
|
|
300
|
+
File.open(local_path, 'wb') { |f| f.write(content) }
|
|
301
|
+
local_path
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Delete a file from a bucket.
|
|
305
|
+
#
|
|
306
|
+
# @param bucket_name [String] Bucket name
|
|
307
|
+
# @param file_path [String] File path within bucket
|
|
308
|
+
# @return [Hash]
|
|
309
|
+
def delete_file(bucket_name, file_path)
|
|
310
|
+
request('DELETE', "/api/v1/storage/projects/#{@project_slug}/files/#{bucket_name}/#{file_path}", nil, nil)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# ── Utilities ────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
# Get the public URL for a file in a public bucket (no auth required).
|
|
316
|
+
#
|
|
317
|
+
# @param bucket_name [String] Bucket name
|
|
318
|
+
# @param file_path [String] File path within bucket
|
|
319
|
+
# @return [String]
|
|
320
|
+
def get_public_url(bucket_name, file_path)
|
|
321
|
+
"#{@base_url}/api/v1/storage/projects/#{@project_slug}/files/#{bucket_name}/#{file_path}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Get storage statistics for the project.
|
|
325
|
+
#
|
|
326
|
+
# @return [StorageQuota]
|
|
327
|
+
def get_stats
|
|
328
|
+
data = request('GET', "/api/v1/storage/projects/#{@project_slug}/stats", nil, nil)
|
|
329
|
+
StorageQuota.new(data)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Get storage quota (alias for get_stats).
|
|
333
|
+
#
|
|
334
|
+
# @return [StorageQuota]
|
|
335
|
+
def get_quota(force_refresh: false)
|
|
336
|
+
get_stats
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# ── Lifecycle ────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
# Close the HTTP connection.
|
|
342
|
+
def close
|
|
343
|
+
@conn.close if @conn.respond_to?(:close)
|
|
194
344
|
end
|
|
195
345
|
|
|
346
|
+
def to_s
|
|
347
|
+
"WOWSQLStorage(project=#{@project_slug.inspect})"
|
|
348
|
+
end
|
|
349
|
+
alias inspect to_s
|
|
350
|
+
|
|
196
351
|
private
|
|
197
352
|
|
|
198
|
-
# Make HTTP request to Storage API.
|
|
199
353
|
def request(method, path, params = nil, json = nil)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if response.status >= 400
|
|
207
|
-
handle_error(response)
|
|
208
|
-
end
|
|
354
|
+
response = @conn.public_send(method.downcase, path) do |req|
|
|
355
|
+
req.params = params if params
|
|
356
|
+
req.body = json if json
|
|
357
|
+
req.headers['Content-Type'] = 'application/json' if json
|
|
358
|
+
end
|
|
209
359
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
raise StorageException.new("Request failed: #{e.message}")
|
|
360
|
+
if response.status >= 400
|
|
361
|
+
handle_error(response)
|
|
213
362
|
end
|
|
363
|
+
|
|
364
|
+
response.body
|
|
365
|
+
rescue Faraday::Error => e
|
|
366
|
+
raise StorageError.new("Request failed: #{e.message}")
|
|
214
367
|
end
|
|
215
368
|
|
|
216
|
-
# Handle HTTP errors
|
|
217
369
|
def handle_error(response)
|
|
218
370
|
error_data = response.body.is_a?(Hash) ? response.body : {}
|
|
219
371
|
error_msg = error_data['detail'] || error_data['message'] || "Request failed with status #{response.status}"
|
|
220
372
|
|
|
221
373
|
if response.status == 413
|
|
222
|
-
raise
|
|
374
|
+
raise StorageLimitExceededError.new(error_msg, 413, error_data)
|
|
223
375
|
end
|
|
224
376
|
|
|
225
|
-
raise
|
|
377
|
+
raise StorageError.new(error_msg, response.status, error_data)
|
|
226
378
|
end
|
|
227
379
|
end
|
|
228
380
|
end
|
|
229
|
-
|