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 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
+
@@ -0,0 +1,4 @@
1
+ module D3RubyClient
2
+ VERSION = "1.0.0"
3
+ end
4
+
@@ -0,0 +1,8 @@
1
+ require_relative "dragdropdo_sdk/version"
2
+ require_relative "dragdropdo_sdk/client"
3
+ require_relative "dragdropdo_sdk/errors"
4
+
5
+ module D3RubyClient
6
+ # Main module for DragDropDo SDK
7
+ end
8
+
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: []