dragdropdo-sdk 1.0.0
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/README.md +439 -0
- data/lib/dragdropdo_sdk/client.rb +482 -0
- data/lib/dragdropdo_sdk/errors.rb +42 -0
- data/lib/dragdropdo_sdk/version.rb +4 -0
- data/lib/dragdropdo_sdk.rb +8 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a315b1cadc079e0df05955911e1fa00858d61edfb057fca54a651ba40a207324
|
|
4
|
+
data.tar.gz: 4df36717b6f1cee5b32e07e7bb3f5d3c7b2bb07c708eaf04f9684a6a310c5509
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 181d6075876141b5a881e72afd5fcc76a446b3de3ad3a8f26de7721228dc063df1ffdca50d79f53ac08a710e74f323f227a01895ac18cdb0b17213f68cd9478e
|
|
7
|
+
data.tar.gz: d18f2289c07414cf650eea2404ed1760289ffdd60482275c1de3e2ac32bda65e72ad555c2f00fc28fbdc5059389d0418069efe86586a1f15fd2a0888db85c478
|
data/README.md
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# DragDropDo Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby client library for the D3 Business API. This library provides a simple and elegant interface for developers to interact with D3's file processing services.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **File Upload** - Upload files with automatic multipart handling
|
|
8
|
+
- ✅ **Operation Support** - Check which operations are available for file types
|
|
9
|
+
- ✅ **File Operations** - Convert, compress, merge, zip, and more
|
|
10
|
+
- ✅ **Status Polling** - Built-in polling for operation status
|
|
11
|
+
- ✅ **Error Handling** - Comprehensive error types and messages
|
|
12
|
+
- ✅ **Progress Tracking** - Upload progress callbacks
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'dragdropdo-sdk'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it yourself as:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install dragdropdo-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require 'dragdropdo_sdk'
|
|
38
|
+
|
|
39
|
+
# Initialize the client
|
|
40
|
+
client = D3RubyClient::Dragdropdo.new(
|
|
41
|
+
api_key: 'your-api-key-here',
|
|
42
|
+
base_url: 'https://api.d3.com', # Optional, defaults to https://api.d3.com
|
|
43
|
+
timeout: 30000 # Optional, defaults to 30000ms
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Upload a file
|
|
47
|
+
upload_result = client.upload_file(
|
|
48
|
+
file: '/path/to/document.pdf',
|
|
49
|
+
file_name: 'document.pdf',
|
|
50
|
+
mime_type: 'application/pdf'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
puts "File key: #{upload_result[:file_key]}"
|
|
54
|
+
|
|
55
|
+
# Check if convert to PNG is supported
|
|
56
|
+
supported = client.check_supported_operation(
|
|
57
|
+
ext: 'pdf',
|
|
58
|
+
action: 'convert',
|
|
59
|
+
parameters: { convert_to: 'png' }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if supported[:supported]
|
|
63
|
+
# Convert PDF to PNG
|
|
64
|
+
operation = client.convert(
|
|
65
|
+
file_keys: [upload_result[:file_key]],
|
|
66
|
+
convert_to: 'png'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Poll for completion
|
|
70
|
+
status = client.poll_status(
|
|
71
|
+
main_task_id: operation[:main_task_id],
|
|
72
|
+
interval: 2000, # Check every 2 seconds
|
|
73
|
+
on_update: ->(status) { puts "Status: #{status[:operation_status]}" }
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if status[:operation_status] == 'completed'
|
|
77
|
+
puts "Download links:"
|
|
78
|
+
status[:files_data].each do |file|
|
|
79
|
+
puts " #{file[:download_link]}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### Initialization
|
|
88
|
+
|
|
89
|
+
#### `D3RubyClient::Dragdropdo.new(api_key:, base_url: nil, timeout: 30000, headers: {})`
|
|
90
|
+
|
|
91
|
+
Create a new D3 client instance.
|
|
92
|
+
|
|
93
|
+
**Parameters:**
|
|
94
|
+
|
|
95
|
+
- `api_key` (required) - Your D3 API key
|
|
96
|
+
- `base_url` (optional) - Base URL of the D3 API (default: `'https://api.d3.com'`)
|
|
97
|
+
- `timeout` (optional) - Request timeout in milliseconds (default: `30000`)
|
|
98
|
+
- `headers` (optional) - Custom headers to include in all requests
|
|
99
|
+
|
|
100
|
+
**Example:**
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
client = D3RubyClient::Dragdropdo.new(
|
|
104
|
+
api_key: 'your-api-key',
|
|
105
|
+
base_url: 'https://api.d3.com',
|
|
106
|
+
timeout: 30000
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### File Upload
|
|
113
|
+
|
|
114
|
+
#### `upload_file(file:, file_name:, mime_type: nil, parts: nil, on_progress: nil)`
|
|
115
|
+
|
|
116
|
+
Upload a file to D3 storage. This method handles the complete upload flow including multipart uploads.
|
|
117
|
+
|
|
118
|
+
**Parameters:**
|
|
119
|
+
|
|
120
|
+
- `file` (required) - File path (string)
|
|
121
|
+
- `file_name` (required) - Original file name
|
|
122
|
+
- `mime_type` (optional) - MIME type (auto-detected if not provided)
|
|
123
|
+
- `parts` (optional) - Number of parts for multipart upload (auto-calculated if not provided)
|
|
124
|
+
- `on_progress` (optional) - Progress callback (Proc)
|
|
125
|
+
|
|
126
|
+
**Returns:** Hash with `file_key` and `presigned_urls`
|
|
127
|
+
|
|
128
|
+
**Example:**
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
result = client.upload_file(
|
|
132
|
+
file: '/path/to/file.pdf',
|
|
133
|
+
file_name: 'document.pdf',
|
|
134
|
+
mime_type: 'application/pdf',
|
|
135
|
+
on_progress: ->(progress) { puts "Upload: #{progress[:percentage]}%" }
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Check Supported Operations
|
|
142
|
+
|
|
143
|
+
#### `check_supported_operation(ext:, action: nil, parameters: nil)`
|
|
144
|
+
|
|
145
|
+
Check which operations are supported for a file extension.
|
|
146
|
+
|
|
147
|
+
**Parameters:**
|
|
148
|
+
|
|
149
|
+
- `ext` (required) - File extension (e.g., `'pdf'`, `'jpg'`)
|
|
150
|
+
- `action` (optional) - Specific action to check (e.g., `'convert'`, `'compress'`)
|
|
151
|
+
- `parameters` (optional) - Parameters for validation (e.g., `{ convert_to: 'png' }`)
|
|
152
|
+
|
|
153
|
+
**Returns:** Hash with support information
|
|
154
|
+
|
|
155
|
+
**Example:**
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# Get all available actions for PDF
|
|
159
|
+
result = client.check_supported_operation(ext: 'pdf')
|
|
160
|
+
puts "Available actions: #{result[:available_actions]}"
|
|
161
|
+
|
|
162
|
+
# Check if convert to PNG is supported
|
|
163
|
+
result = client.check_supported_operation(
|
|
164
|
+
ext: 'pdf',
|
|
165
|
+
action: 'convert',
|
|
166
|
+
parameters: { convert_to: 'png' }
|
|
167
|
+
)
|
|
168
|
+
puts "Supported: #{result[:supported]}"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Create Operations
|
|
174
|
+
|
|
175
|
+
#### `create_operation(action:, file_keys:, parameters: nil, notes: nil)`
|
|
176
|
+
|
|
177
|
+
Create a file operation (convert, compress, merge, zip, etc.).
|
|
178
|
+
|
|
179
|
+
**Parameters:**
|
|
180
|
+
|
|
181
|
+
- `action` (required) - Action to perform: `'convert'`, `'compress'`, `'merge'`, `'zip'`, `'lock'`, `'unlock'`, `'reset_password'`
|
|
182
|
+
- `file_keys` (required) - Array of file keys from upload
|
|
183
|
+
- `parameters` (optional) - Action-specific parameters
|
|
184
|
+
- `notes` (optional) - User metadata
|
|
185
|
+
|
|
186
|
+
**Returns:** Hash with `main_task_id`
|
|
187
|
+
|
|
188
|
+
**Example:**
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# Convert PDF to PNG
|
|
192
|
+
result = client.create_operation(
|
|
193
|
+
action: 'convert',
|
|
194
|
+
file_keys: ['file-key-123'],
|
|
195
|
+
parameters: { convert_to: 'png' },
|
|
196
|
+
notes: { userId: 'user-123' }
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Convenience Methods
|
|
201
|
+
|
|
202
|
+
The client also provides convenience methods for common operations:
|
|
203
|
+
|
|
204
|
+
**Convert:**
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
client.convert(file_keys: ['file-key-123'], convert_to: 'png')
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Compress:**
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
client.compress(file_keys: ['file-key-123'], compression_value: 'recommended')
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Merge:**
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
client.merge(file_keys: ['file-key-1', 'file-key-2'])
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Zip:**
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
client.zip(file_keys: ['file-key-1', 'file-key-2'])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Lock PDF:**
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
client.lock_pdf(file_keys: ['file-key-123'], password: 'secure-password')
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Unlock PDF:**
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
client.unlock_pdf(file_keys: ['file-key-123'], password: 'password')
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Reset PDF Password:**
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
client.reset_pdf_password(
|
|
244
|
+
file_keys: ['file-key-123'],
|
|
245
|
+
old_password: 'old',
|
|
246
|
+
new_password: 'new'
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Get Status
|
|
253
|
+
|
|
254
|
+
#### `get_status(main_task_id:, file_key: nil)`
|
|
255
|
+
|
|
256
|
+
Get the current status of an operation.
|
|
257
|
+
|
|
258
|
+
**Parameters:**
|
|
259
|
+
|
|
260
|
+
- `main_task_id` (required) - Main task ID from operation creation
|
|
261
|
+
- `file_key` (optional) - Input file key for specific file status
|
|
262
|
+
|
|
263
|
+
**Returns:** Hash with operation and file statuses
|
|
264
|
+
|
|
265
|
+
**Example:**
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# Get main task status
|
|
269
|
+
status = client.get_status(main_task_id: 'task-123')
|
|
270
|
+
|
|
271
|
+
# Get specific file status by file key
|
|
272
|
+
status = client.get_status(
|
|
273
|
+
main_task_id: 'task-123',
|
|
274
|
+
file_key: 'file-key-456'
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
puts "Operation status: #{status[:operation_status]}"
|
|
278
|
+
# Possible values: 'queued', 'running', 'completed', 'failed'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### `poll_status(main_task_id:, file_key: nil, interval: 2000, timeout: 300000, on_update: nil)`
|
|
282
|
+
|
|
283
|
+
Poll operation status until completion or failure.
|
|
284
|
+
|
|
285
|
+
**Parameters:**
|
|
286
|
+
|
|
287
|
+
- `main_task_id` (required) - Main task ID
|
|
288
|
+
- `file_key` (optional) - Input file key for specific file status
|
|
289
|
+
- `interval` (optional) - Polling interval in milliseconds (default: `2000`)
|
|
290
|
+
- `timeout` (optional) - Maximum polling duration in milliseconds (default: `300000` = 5 minutes)
|
|
291
|
+
- `on_update` (optional) - Callback for each status update
|
|
292
|
+
|
|
293
|
+
**Returns:** Hash with final status
|
|
294
|
+
|
|
295
|
+
**Example:**
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
status = client.poll_status(
|
|
299
|
+
main_task_id: 'task-123',
|
|
300
|
+
interval: 2000,
|
|
301
|
+
timeout: 300000,
|
|
302
|
+
on_update: ->(status) { puts "Status: #{status[:operation_status]}" }
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if status[:operation_status] == 'completed'
|
|
306
|
+
puts "All files processed successfully!"
|
|
307
|
+
status[:files_data].each do |file|
|
|
308
|
+
puts "Download: #{file[:download_link]}"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Complete Workflow Example
|
|
316
|
+
|
|
317
|
+
Here's a complete example showing the typical workflow:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
require 'dragdropdo_sdk'
|
|
321
|
+
|
|
322
|
+
def process_file
|
|
323
|
+
# Initialize client
|
|
324
|
+
client = D3RubyClient::Dragdropdo.new(
|
|
325
|
+
api_key: ENV['D3_API_KEY'],
|
|
326
|
+
base_url: 'https://api.d3.com'
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
begin
|
|
330
|
+
# Step 1: Upload file
|
|
331
|
+
puts "Uploading file..."
|
|
332
|
+
upload_result = client.upload_file(
|
|
333
|
+
file: './document.pdf',
|
|
334
|
+
file_name: 'document.pdf',
|
|
335
|
+
on_progress: ->(progress) { puts "Upload progress: #{progress[:percentage]}%" }
|
|
336
|
+
)
|
|
337
|
+
puts "Upload complete. File key: #{upload_result[:file_key]}"
|
|
338
|
+
|
|
339
|
+
# Step 2: Check if operation is supported
|
|
340
|
+
puts "Checking supported operations..."
|
|
341
|
+
supported = client.check_supported_operation(
|
|
342
|
+
ext: 'pdf',
|
|
343
|
+
action: 'convert',
|
|
344
|
+
parameters: { convert_to: 'png' }
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
unless supported[:supported]
|
|
348
|
+
raise "Convert to PNG is not supported for PDF"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Step 3: Create operation
|
|
352
|
+
puts "Creating convert operation..."
|
|
353
|
+
operation = client.convert(
|
|
354
|
+
file_keys: [upload_result[:file_key]],
|
|
355
|
+
convert_to: 'png',
|
|
356
|
+
notes: { userId: 'user-123', source: 'api' }
|
|
357
|
+
)
|
|
358
|
+
puts "Operation created. Task ID: #{operation[:main_task_id]}"
|
|
359
|
+
|
|
360
|
+
# Step 4: Poll for completion
|
|
361
|
+
puts "Waiting for operation to complete..."
|
|
362
|
+
status = client.poll_status(
|
|
363
|
+
main_task_id: operation[:main_task_id],
|
|
364
|
+
interval: 2000,
|
|
365
|
+
on_update: ->(status) { puts "Status: #{status[:operation_status]}" }
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Step 5: Handle result
|
|
369
|
+
if status[:operation_status] == 'completed'
|
|
370
|
+
puts "Operation completed successfully!"
|
|
371
|
+
status[:files_data].each_with_index do |file, index|
|
|
372
|
+
puts "File #{index + 1}:"
|
|
373
|
+
puts " Status: #{file[:status]}"
|
|
374
|
+
puts " Download: #{file[:download_link]}"
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
puts "Operation failed"
|
|
378
|
+
status[:files_data].each do |file|
|
|
379
|
+
puts "Error: #{file[:error_message]}" if file[:error_message]
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
rescue D3RubyClient::D3APIError => e
|
|
383
|
+
puts "API Error (#{e.status_code}): #{e.message}"
|
|
384
|
+
rescue D3RubyClient::D3ValidationError => e
|
|
385
|
+
puts "Validation Error: #{e.message}"
|
|
386
|
+
rescue StandardError => e
|
|
387
|
+
puts "Error: #{e.message}"
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
process_file
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Error Handling
|
|
397
|
+
|
|
398
|
+
The client provides several error types for better error handling:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
begin
|
|
402
|
+
client.upload_file(...)
|
|
403
|
+
rescue D3RubyClient::D3APIError => e
|
|
404
|
+
# API returned an error
|
|
405
|
+
puts "API Error (#{e.status_code}): #{e.message}"
|
|
406
|
+
puts "Error code: #{e.code}"
|
|
407
|
+
puts "Details: #{e.details}"
|
|
408
|
+
rescue D3RubyClient::D3ValidationError => e
|
|
409
|
+
# Validation error (missing required fields, etc.)
|
|
410
|
+
puts "Validation Error: #{e.message}"
|
|
411
|
+
rescue D3RubyClient::D3UploadError => e
|
|
412
|
+
# Upload-specific error
|
|
413
|
+
puts "Upload Error: #{e.message}"
|
|
414
|
+
rescue D3RubyClient::D3TimeoutError => e
|
|
415
|
+
# Timeout error (from polling)
|
|
416
|
+
puts "Timeout: #{e.message}"
|
|
417
|
+
rescue StandardError => e
|
|
418
|
+
# Other errors
|
|
419
|
+
puts "Error: #{e.message}"
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Requirements
|
|
426
|
+
|
|
427
|
+
- Ruby 2.7.0 or higher
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
ISC
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Support
|
|
438
|
+
|
|
439
|
+
For API documentation and support, visit [D3 Developer Portal](https://developer.d3.com).
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/multipart"
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module D3RubyClient
|
|
8
|
+
# Dragdropdo Business API Client
|
|
9
|
+
#
|
|
10
|
+
# A Ruby client library for interacting with the Dragdropdo Business API.
|
|
11
|
+
# Provides methods for file uploads, operations, and status checking.
|
|
12
|
+
class Dragdropdo
|
|
13
|
+
attr_reader :api_key, :base_url, :timeout
|
|
14
|
+
|
|
15
|
+
# Create a new Dragdropdo Client instance
|
|
16
|
+
#
|
|
17
|
+
# @param api_key [String] API key for authentication
|
|
18
|
+
# @param base_url [String] Base URL of the D3 API (default: 'https://api-dev.dragdropdo.com')
|
|
19
|
+
# @param timeout [Integer] Request timeout in milliseconds (default: 30000)
|
|
20
|
+
# @param headers [Hash] Custom headers to include in all requests
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# client = D3RubyClient::Dragdropdo.new(
|
|
24
|
+
# api_key: 'your-api-key',
|
|
25
|
+
# base_url: 'https://api-dev.dragdropdo.com',
|
|
26
|
+
# timeout: 30000
|
|
27
|
+
# )
|
|
28
|
+
def initialize(api_key:, base_url: nil, timeout: 30000, headers: {})
|
|
29
|
+
raise D3ValidationError, "API key is required" if api_key.nil? || api_key.empty?
|
|
30
|
+
|
|
31
|
+
@api_key = api_key
|
|
32
|
+
@base_url = (base_url || "https://api-dev.dragdropdo.com").chomp("/")
|
|
33
|
+
@timeout = timeout
|
|
34
|
+
@headers = {
|
|
35
|
+
"Content-Type" => "application/json",
|
|
36
|
+
"Authorization" => "Bearer #{@api_key}",
|
|
37
|
+
}.merge(headers)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Upload a file to D3 storage
|
|
41
|
+
#
|
|
42
|
+
# This method handles the complete upload flow:
|
|
43
|
+
# 1. Request presigned URLs from the API
|
|
44
|
+
# 2. Upload file parts to presigned URLs
|
|
45
|
+
# 3. Return the file key for use in operations
|
|
46
|
+
#
|
|
47
|
+
# @param file [String] File path
|
|
48
|
+
# @param file_name [String] Original file name
|
|
49
|
+
# @param mime_type [String] MIME type (auto-detected if not provided)
|
|
50
|
+
# @param parts [Integer] Number of parts for multipart upload (auto-calculated if not provided)
|
|
51
|
+
# @param on_progress [Proc] Optional progress callback
|
|
52
|
+
# @return [Hash] Upload response with file key
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# result = client.upload_file(
|
|
56
|
+
# file: '/path/to/file.pdf',
|
|
57
|
+
# file_name: 'document.pdf',
|
|
58
|
+
# mime_type: 'application/pdf',
|
|
59
|
+
# on_progress: ->(progress) { puts "Upload: #{progress[:percentage]}%" }
|
|
60
|
+
# )
|
|
61
|
+
# puts "File key: #{result[:file_key]}"
|
|
62
|
+
def upload_file(file:, file_name:, mime_type: nil, parts: nil, on_progress: nil)
|
|
63
|
+
raise D3ValidationError, "file_name is required" if file_name.nil? || file_name.empty?
|
|
64
|
+
raise D3ValidationError, "file must be a file path string" unless file.is_a?(String)
|
|
65
|
+
raise D3ValidationError, "File not found: #{file}" unless File.exist?(file)
|
|
66
|
+
|
|
67
|
+
file_size = File.size(file)
|
|
68
|
+
|
|
69
|
+
# Calculate parts if not provided
|
|
70
|
+
chunk_size = 5 * 1024 * 1024 # 5MB per part
|
|
71
|
+
calculated_parts = parts || (file_size.to_f / chunk_size).ceil
|
|
72
|
+
actual_parts = [1, [calculated_parts, 100].min].max # Limit to 100 parts
|
|
73
|
+
|
|
74
|
+
# Detect MIME type if not provided
|
|
75
|
+
detected_mime_type = mime_type || get_mime_type(file_name)
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
# Step 1: Request presigned URLs
|
|
79
|
+
upload_response = request(:post, "/api/v1/initiate-upload", {
|
|
80
|
+
file_name: file_name,
|
|
81
|
+
size: file_size,
|
|
82
|
+
mime_type: detected_mime_type,
|
|
83
|
+
parts: actual_parts,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
upload_data = if upload_response.is_a?(Hash)
|
|
87
|
+
upload_response[:data] || upload_response["data"] || upload_response
|
|
88
|
+
else
|
|
89
|
+
parsed = JSON.parse(upload_response) rescue {}
|
|
90
|
+
parsed["data"] || parsed[:data] || parsed
|
|
91
|
+
end
|
|
92
|
+
upload_data ||= {}
|
|
93
|
+
file_key = upload_data[:file_key] || upload_data["file_key"]
|
|
94
|
+
upload_id = upload_data[:upload_id] || upload_data["upload_id"]
|
|
95
|
+
presigned_urls = upload_data[:presigned_urls] || upload_data["presigned_urls"] || []
|
|
96
|
+
object_name = upload_data[:object_name] || upload_data["object_name"]
|
|
97
|
+
|
|
98
|
+
if presigned_urls.length != actual_parts
|
|
99
|
+
raise D3UploadError, "Mismatch: requested #{actual_parts} parts but received #{presigned_urls.length} presigned URLs"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise D3UploadError, "Upload ID not received from server" if upload_id.nil? || upload_id.empty?
|
|
103
|
+
|
|
104
|
+
# Step 2: Upload file parts and capture ETags
|
|
105
|
+
chunk_size_per_part = (file_size.to_f / actual_parts).ceil
|
|
106
|
+
bytes_uploaded = 0
|
|
107
|
+
upload_parts = []
|
|
108
|
+
|
|
109
|
+
File.open(file, "rb") do |file_handle|
|
|
110
|
+
(0...actual_parts).each do |i|
|
|
111
|
+
start = i * chunk_size_per_part
|
|
112
|
+
ending = [start + chunk_size_per_part, file_size].min
|
|
113
|
+
part_size = ending - start
|
|
114
|
+
|
|
115
|
+
# Read chunk
|
|
116
|
+
file_handle.seek(start)
|
|
117
|
+
chunk = file_handle.read(part_size)
|
|
118
|
+
|
|
119
|
+
# Upload chunk and capture ETag from response headers
|
|
120
|
+
put_response = Faraday.put(presigned_urls[i]) do |req|
|
|
121
|
+
req.body = chunk
|
|
122
|
+
req.headers["Content-Type"] = detected_mime_type
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
raise D3UploadError, "Failed to upload part #{i + 1}" unless put_response.success?
|
|
126
|
+
|
|
127
|
+
# Extract ETag from response
|
|
128
|
+
etag = put_response.headers["etag"] || put_response.headers["ETag"] || ""
|
|
129
|
+
raise D3UploadError, "Failed to get ETag for part #{i + 1}" if etag.empty?
|
|
130
|
+
|
|
131
|
+
upload_parts << {
|
|
132
|
+
etag: etag.gsub(/^"|"$/, ""), # Remove quotes if present
|
|
133
|
+
part_number: i + 1,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
bytes_uploaded += part_size
|
|
137
|
+
|
|
138
|
+
# Report progress
|
|
139
|
+
if on_progress
|
|
140
|
+
progress = {
|
|
141
|
+
current_part: i + 1,
|
|
142
|
+
total_parts: actual_parts,
|
|
143
|
+
bytes_uploaded: bytes_uploaded,
|
|
144
|
+
total_bytes: file_size,
|
|
145
|
+
percentage: ((bytes_uploaded.to_f / file_size) * 100).round,
|
|
146
|
+
}
|
|
147
|
+
on_progress.call(progress)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Step 3: Complete the multipart upload
|
|
153
|
+
begin
|
|
154
|
+
request(:post, "/api/v1/complete-upload", {
|
|
155
|
+
file_key: file_key,
|
|
156
|
+
upload_id: upload_id,
|
|
157
|
+
object_name: object_name,
|
|
158
|
+
parts: upload_parts,
|
|
159
|
+
})
|
|
160
|
+
rescue D3ClientError => e
|
|
161
|
+
raise D3UploadError, "Failed to complete upload: #{e.message}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
file_key: file_key,
|
|
166
|
+
upload_id: upload_id,
|
|
167
|
+
presigned_urls: presigned_urls,
|
|
168
|
+
object_name: object_name,
|
|
169
|
+
# CamelCase aliases for compatibility
|
|
170
|
+
fileKey: file_key,
|
|
171
|
+
uploadId: upload_id,
|
|
172
|
+
presignedUrls: presigned_urls,
|
|
173
|
+
objectName: object_name,
|
|
174
|
+
}
|
|
175
|
+
rescue D3ClientError, D3APIError => e
|
|
176
|
+
raise e
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
raise D3UploadError, "Upload failed: #{e.message}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check if an operation is supported for a file extension
|
|
183
|
+
#
|
|
184
|
+
# @param ext [String] File extension (e.g., 'pdf', 'jpg')
|
|
185
|
+
# @param action [String] Optional specific action to check
|
|
186
|
+
# @param parameters [Hash] Optional parameters for validation
|
|
187
|
+
# @return [Hash] Supported operation response
|
|
188
|
+
def check_supported_operation(ext:, action: nil, parameters: nil)
|
|
189
|
+
raise D3ValidationError, "Extension (ext) is required" if ext.nil? || ext.empty?
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
response = request(:post, "/api/v1/supported-operation", {
|
|
193
|
+
ext: ext,
|
|
194
|
+
action: action,
|
|
195
|
+
parameters: parameters,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
# Handle both symbol and string keys
|
|
199
|
+
data = if response.is_a?(Hash)
|
|
200
|
+
response[:data] || response["data"] || response
|
|
201
|
+
else
|
|
202
|
+
{}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Ensure we return a hash with symbol keys
|
|
206
|
+
if data.is_a?(Hash)
|
|
207
|
+
data.transform_keys(&:to_sym) rescue data
|
|
208
|
+
else
|
|
209
|
+
data
|
|
210
|
+
end
|
|
211
|
+
rescue D3ClientError, D3APIError => e
|
|
212
|
+
raise e
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
raise D3ClientError, "Failed to check supported operation: #{e.message}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Create a file operation (convert, compress, merge, zip, etc.)
|
|
219
|
+
#
|
|
220
|
+
# @param action [String] Action to perform
|
|
221
|
+
# @param file_keys [Array<String>] Array of file keys from upload
|
|
222
|
+
# @param parameters [Hash] Optional action-specific parameters
|
|
223
|
+
# @param notes [Hash] Optional user metadata
|
|
224
|
+
# @return [Hash] Operation response with main task ID
|
|
225
|
+
def create_operation(action:, file_keys:, parameters: nil, notes: nil)
|
|
226
|
+
raise D3ValidationError, "Action is required" if action.nil? || action.empty?
|
|
227
|
+
raise D3ValidationError, "At least one file key is required" if file_keys.nil? || file_keys.empty?
|
|
228
|
+
|
|
229
|
+
begin
|
|
230
|
+
response = request(:post, "/api/v1/do", {
|
|
231
|
+
action: action,
|
|
232
|
+
file_keys: file_keys,
|
|
233
|
+
parameters: parameters,
|
|
234
|
+
notes: notes,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
data = if response.is_a?(Hash)
|
|
238
|
+
response[:data] || response["data"] || response
|
|
239
|
+
else
|
|
240
|
+
parsed = JSON.parse(response) rescue {}
|
|
241
|
+
parsed["data"] || parsed[:data] || parsed
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
main_task_id = data.is_a?(Hash) ? (data[:main_task_id] || data["main_task_id"]) : nil
|
|
245
|
+
|
|
246
|
+
{
|
|
247
|
+
main_task_id: main_task_id,
|
|
248
|
+
mainTaskId: main_task_id, # CamelCase alias
|
|
249
|
+
}
|
|
250
|
+
rescue D3ClientError, D3APIError => e
|
|
251
|
+
raise e
|
|
252
|
+
rescue StandardError => e
|
|
253
|
+
raise D3ClientError, "Failed to create operation: #{e.message}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Convenience methods
|
|
258
|
+
|
|
259
|
+
# Convert files to a different format
|
|
260
|
+
def convert(file_keys:, convert_to:, notes: nil)
|
|
261
|
+
create_operation(
|
|
262
|
+
action: "convert",
|
|
263
|
+
file_keys: file_keys,
|
|
264
|
+
parameters: { convert_to: convert_to },
|
|
265
|
+
notes: notes
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Compress files
|
|
270
|
+
def compress(file_keys:, compression_value: "recommended", notes: nil)
|
|
271
|
+
create_operation(
|
|
272
|
+
action: "compress",
|
|
273
|
+
file_keys: file_keys,
|
|
274
|
+
parameters: { compression_value: compression_value },
|
|
275
|
+
notes: notes
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Merge multiple files
|
|
280
|
+
def merge(file_keys:, notes: nil)
|
|
281
|
+
create_operation(action: "merge", file_keys: file_keys, notes: notes)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Create a ZIP archive from files
|
|
285
|
+
def zip(file_keys:, notes: nil)
|
|
286
|
+
create_operation(action: "zip", file_keys: file_keys, notes: notes)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Lock PDF with password
|
|
290
|
+
def lock_pdf(file_keys:, password:, notes: nil)
|
|
291
|
+
create_operation(
|
|
292
|
+
action: "lock",
|
|
293
|
+
file_keys: file_keys,
|
|
294
|
+
parameters: { password: password },
|
|
295
|
+
notes: notes
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Unlock PDF with password
|
|
300
|
+
def unlock_pdf(file_keys:, password:, notes: nil)
|
|
301
|
+
create_operation(
|
|
302
|
+
action: "unlock",
|
|
303
|
+
file_keys: file_keys,
|
|
304
|
+
parameters: { password: password },
|
|
305
|
+
notes: notes
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Reset PDF password
|
|
310
|
+
def reset_pdf_password(file_keys:, old_password:, new_password:, notes: nil)
|
|
311
|
+
create_operation(
|
|
312
|
+
action: "reset_password",
|
|
313
|
+
file_keys: file_keys,
|
|
314
|
+
parameters: {
|
|
315
|
+
old_password: old_password,
|
|
316
|
+
new_password: new_password,
|
|
317
|
+
},
|
|
318
|
+
notes: notes
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Get operation status
|
|
323
|
+
#
|
|
324
|
+
# @param main_task_id [String] Main task ID
|
|
325
|
+
# @param file_key [String] Optional input file key for specific file status
|
|
326
|
+
# @return [Hash] Status response
|
|
327
|
+
def get_status(main_task_id:, file_key: nil)
|
|
328
|
+
raise D3ValidationError, "main_task_id is required" if main_task_id.nil? || main_task_id.empty?
|
|
329
|
+
|
|
330
|
+
begin
|
|
331
|
+
url = "/api/v1/status/#{main_task_id}"
|
|
332
|
+
url += "/#{file_key}" if file_key
|
|
333
|
+
|
|
334
|
+
response = request(:get, url)
|
|
335
|
+
data = if response.is_a?(Hash)
|
|
336
|
+
response[:data] || response["data"] || response
|
|
337
|
+
else
|
|
338
|
+
{}
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Ensure we return a hash with symbol keys, and convert nested arrays
|
|
342
|
+
if data.is_a?(Hash)
|
|
343
|
+
result = data.transform_keys(&:to_sym) rescue data
|
|
344
|
+
# Convert files_data array elements to symbol keys
|
|
345
|
+
if result.is_a?(Hash) && result[:files_data].is_a?(Array)
|
|
346
|
+
result[:files_data] = result[:files_data].map do |file|
|
|
347
|
+
file.is_a?(Hash) ? file.transform_keys(&:to_sym) : file
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
# Normalize status to lowercase
|
|
351
|
+
result[:operation_status] = result[:operation_status].to_s.downcase if result[:operation_status]
|
|
352
|
+
# Add camelCase aliases
|
|
353
|
+
result[:operationStatus] = result[:operation_status] if result[:operation_status]
|
|
354
|
+
result[:filesData] = result[:files_data] if result[:files_data]
|
|
355
|
+
if result[:files_data].is_a?(Array)
|
|
356
|
+
result[:files_data] = result[:files_data].map do |file|
|
|
357
|
+
file[:status] = file[:status].to_s.downcase if file[:status]
|
|
358
|
+
file[:downloadLink] = file[:download_link] if file[:download_link]
|
|
359
|
+
file[:errorCode] = file[:error_code] if file[:error_code]
|
|
360
|
+
file[:errorMessage] = file[:error_message] if file[:error_message]
|
|
361
|
+
file[:fileKey] = file[:file_key] if file[:file_key]
|
|
362
|
+
file
|
|
363
|
+
end
|
|
364
|
+
result[:filesData] = result[:files_data]
|
|
365
|
+
end
|
|
366
|
+
result
|
|
367
|
+
else
|
|
368
|
+
data
|
|
369
|
+
end
|
|
370
|
+
rescue D3ClientError, D3APIError => e
|
|
371
|
+
raise e
|
|
372
|
+
rescue StandardError => e
|
|
373
|
+
raise D3ClientError, "Failed to get status: #{e.message}"
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Poll operation status until completion or failure
|
|
378
|
+
#
|
|
379
|
+
# @param main_task_id [String] Main task ID
|
|
380
|
+
# @param file_key [String] Optional input file key for specific file status
|
|
381
|
+
# @param interval [Integer] Polling interval in milliseconds (default: 2000)
|
|
382
|
+
# @param timeout [Integer] Maximum polling duration in milliseconds (default: 300000)
|
|
383
|
+
# @param on_update [Proc] Optional callback for each status update
|
|
384
|
+
# @return [Hash] Status response with final status
|
|
385
|
+
def poll_status(main_task_id:, file_key: nil, interval: 2000, timeout: 300_000, on_update: nil)
|
|
386
|
+
start_time = Time.now.to_f * 1000
|
|
387
|
+
|
|
388
|
+
loop do
|
|
389
|
+
# Check timeout
|
|
390
|
+
if (Time.now.to_f * 1000) - start_time > timeout
|
|
391
|
+
raise D3TimeoutError, "Polling timed out after #{timeout}ms"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Get status
|
|
395
|
+
status = get_status(main_task_id: main_task_id, file_key: file_key)
|
|
396
|
+
|
|
397
|
+
# Call update callback
|
|
398
|
+
on_update&.call(status)
|
|
399
|
+
|
|
400
|
+
# Check if completed or failed (support both snake_case and camelCase)
|
|
401
|
+
op_status = status[:operation_status] || status[:operationStatus]
|
|
402
|
+
return status if %w[completed failed].include?(op_status)
|
|
403
|
+
|
|
404
|
+
# Wait before next poll
|
|
405
|
+
sleep(interval / 1000.0) # Convert ms to seconds
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
private
|
|
410
|
+
|
|
411
|
+
# Make an HTTP request to the API
|
|
412
|
+
def request(method, endpoint, data = nil)
|
|
413
|
+
url = "#{@base_url}#{endpoint}"
|
|
414
|
+
|
|
415
|
+
connection = Faraday.new(url: url, headers: @headers) do |f|
|
|
416
|
+
f.request :json
|
|
417
|
+
f.response :json
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
response = connection.public_send(method) do |req|
|
|
421
|
+
req.body = data.to_json if data
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
unless response.success?
|
|
425
|
+
body = response.body.is_a?(Hash) ? response.body : (JSON.parse(response.body) rescue {})
|
|
426
|
+
raise D3APIError.new(
|
|
427
|
+
body[:message] || body["message"] || body[:error] || body["error"] || "API request failed",
|
|
428
|
+
response.status,
|
|
429
|
+
body[:code] || body["code"],
|
|
430
|
+
body
|
|
431
|
+
)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Parse JSON if needed (WebMock might return string)
|
|
435
|
+
body = response.body
|
|
436
|
+
if body.is_a?(String)
|
|
437
|
+
body = JSON.parse(body, symbolize_names: true)
|
|
438
|
+
elsif body.is_a?(Hash)
|
|
439
|
+
# Ensure keys are symbols for consistency
|
|
440
|
+
body = body.transform_keys(&:to_sym) rescue body
|
|
441
|
+
end
|
|
442
|
+
body
|
|
443
|
+
rescue Faraday::Error => e
|
|
444
|
+
if e.response
|
|
445
|
+
body = JSON.parse(e.response[:body]) rescue {}
|
|
446
|
+
raise D3APIError.new(
|
|
447
|
+
body[:message] || body[:error] || e.message || "API request failed",
|
|
448
|
+
e.response[:status],
|
|
449
|
+
body[:code],
|
|
450
|
+
body
|
|
451
|
+
)
|
|
452
|
+
end
|
|
453
|
+
raise D3ClientError, "Network error: #{e.message}"
|
|
454
|
+
rescue StandardError => e
|
|
455
|
+
raise D3ClientError, "Request error: #{e.message}"
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Get MIME type from file extension
|
|
459
|
+
def get_mime_type(file_name)
|
|
460
|
+
mime_types = {
|
|
461
|
+
".pdf" => "application/pdf",
|
|
462
|
+
".jpg" => "image/jpeg",
|
|
463
|
+
".jpeg" => "image/jpeg",
|
|
464
|
+
".png" => "image/png",
|
|
465
|
+
".gif" => "image/gif",
|
|
466
|
+
".webp" => "image/webp",
|
|
467
|
+
".doc" => "application/msword",
|
|
468
|
+
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
469
|
+
".xls" => "application/vnd.ms-excel",
|
|
470
|
+
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
471
|
+
".zip" => "application/zip",
|
|
472
|
+
".txt" => "text/plain",
|
|
473
|
+
".mp4" => "video/mp4",
|
|
474
|
+
".mp3" => "audio/mpeg",
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
ext = File.extname(file_name).downcase
|
|
478
|
+
mime_types[ext] || "application/octet-stream"
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module D3RubyClient
|
|
2
|
+
# Base error class for D3 Client errors
|
|
3
|
+
class D3ClientError < StandardError
|
|
4
|
+
attr_reader :status_code, :code, :details
|
|
5
|
+
|
|
6
|
+
def initialize(message, status_code = nil, code = nil, details = nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@status_code = status_code
|
|
9
|
+
@code = code
|
|
10
|
+
@details = details
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Error returned by the API
|
|
15
|
+
class D3APIError < D3ClientError
|
|
16
|
+
def initialize(message, status_code, code = nil, details = nil)
|
|
17
|
+
super(message, status_code, code, details)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Client-side validation error
|
|
22
|
+
class D3ValidationError < D3ClientError
|
|
23
|
+
def initialize(message, details = nil)
|
|
24
|
+
super(message, 400, nil, details)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Upload-specific error
|
|
29
|
+
class D3UploadError < D3ClientError
|
|
30
|
+
def initialize(message, details = nil)
|
|
31
|
+
super(message, nil, nil, details)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Timeout error (from polling)
|
|
36
|
+
class D3TimeoutError < D3ClientError
|
|
37
|
+
def initialize(message = "Operation timed out")
|
|
38
|
+
super(message)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dragdropdo-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- dragdropdo
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday-multipart
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: webmock
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
description: Official Ruby client library for the Dragdropdo Business API. Provides
|
|
70
|
+
a simple and elegant interface for developers to interact with Dragdropdo's file
|
|
71
|
+
processing services.
|
|
72
|
+
email:
|
|
73
|
+
- sunil@dragdropdo.com
|
|
74
|
+
executables: []
|
|
75
|
+
extensions: []
|
|
76
|
+
extra_rdoc_files: []
|
|
77
|
+
files:
|
|
78
|
+
- README.md
|
|
79
|
+
- lib/dragdropdo_sdk.rb
|
|
80
|
+
- lib/dragdropdo_sdk/client.rb
|
|
81
|
+
- lib/dragdropdo_sdk/errors.rb
|
|
82
|
+
- lib/dragdropdo_sdk/version.rb
|
|
83
|
+
homepage: https://github.com/dragdropdo/dragdropdo-sdk-ruby
|
|
84
|
+
licenses:
|
|
85
|
+
- ISC
|
|
86
|
+
metadata:
|
|
87
|
+
homepage_uri: https://dragdropdo.com
|
|
88
|
+
source_code_uri: https://github.com/dragdropdo/dragdropdo-sdk-ruby
|
|
89
|
+
bug_tracker_uri: https://github.com/dragdropdo/d3-ruby-client/issues
|
|
90
|
+
changelog_uri: https://github.com/dragdropdo/dragdropdo-sdk-ruby/blob/main/CHANGELOG.md
|
|
91
|
+
documentation_uri: https://docs.dragdropdo.com
|
|
92
|
+
wiki_uri: https://github.com/dragdropdo/dragdropdo-sdk-ruby/wiki
|
|
93
|
+
mailing_list_uri: https://dragdropdo.com/company
|
|
94
|
+
funding_uri: https://dragdropdo.com/about-us
|
|
95
|
+
post_install_message:
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 2.7.0
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 3.3.5
|
|
111
|
+
signing_key:
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: Official Ruby client library for the Dragdropdo Business API
|
|
114
|
+
test_files: []
|