daytona 0.200.0 → 0.201.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.
@@ -5,6 +5,7 @@
5
5
 
6
6
  require 'time'
7
7
  require 'timeout'
8
+ require_relative 'utils/file_url_signing'
8
9
 
9
10
  module Daytona
10
11
  # A single point-in-time sample of historical Sandbox resource usage.
@@ -196,6 +197,8 @@ module Daytona
196
197
  @analytics_api_url_provider = analytics_api_url_provider
197
198
  @subscription_manager = subscription_manager
198
199
  @sub_id = nil
200
+ @signing_key = nil
201
+ @signing_key_fetched_at = 0
199
202
 
200
203
  # Create toolbox API clients with dynamic configuration
201
204
  @toolbox_api_config = build_toolbox_api_config
@@ -435,8 +438,8 @@ module Daytona
435
438
  # puts "Sandbox user home: #{user_home_dir}"
436
439
  def get_user_home_dir
437
440
  @info_api.get_user_home_dir.dir
438
- rescue StandardError => e
439
- raise Sdk::Error, "Failed to get user home directory: #{e.message}"
441
+ rescue *Sdk::API_ERROR_CLASSES => e
442
+ raise Sdk.wrap_error(e, 'Failed to get user home directory')
440
443
  end
441
444
 
442
445
  # Gets the working directory path inside the Sandbox.
@@ -449,8 +452,8 @@ module Daytona
449
452
  # puts "Sandbox working directory: #{work_dir}"
450
453
  def get_work_dir
451
454
  @info_api.get_work_dir.dir
452
- rescue StandardError => e
453
- raise Sdk::Error, "Failed to get working directory path: #{e.message}"
455
+ rescue *Sdk::API_ERROR_CLASSES => e
456
+ raise Sdk.wrap_error(e, 'Failed to get working directory path')
454
457
  end
455
458
 
456
459
  # Updates the Sandbox daemon's process environment. Newly spawned processes, sessions
@@ -524,6 +527,76 @@ module Daytona
524
527
  sandbox_api.get_port_preview_url(id, port)
525
528
  end
526
529
 
530
+ # Creates a pre-signed URL for downloading a file from the Sandbox.
531
+ #
532
+ # The URL works with any HTTP client without auth headers and stays valid across
533
+ # sandbox restarts (downloads succeed only while the sandbox is running). The signing
534
+ # key is cached locally for up to 15 seconds; if the key was rotated from another
535
+ # client, URLs may be rejected until the cache refreshes.
536
+ #
537
+ # @param path [String] Path to the file in the Sandbox.
538
+ # @param ttl_seconds [Integer, nil] How long the URL stays valid, in seconds.
539
+ # Defaults to 3600. Zero or negative means the URL never expires.
540
+ # @return [String] Pre-signed download URL.
541
+ # @raise [Daytona::Sdk::Error] if the signing key cannot be fetched.
542
+ #
543
+ # @example
544
+ # url = sandbox.download_url('/home/user/report.pdf')
545
+ # # curl "$url" -o report.pdf
546
+ def download_url(path, ttl_seconds: nil)
547
+ Utils::FileUrlSigning.build_signed_file_url(
548
+ @toolbox_proxy_url,
549
+ id,
550
+ '/files/download',
551
+ 'GET',
552
+ path,
553
+ ensure_signing_key,
554
+ ttl_seconds
555
+ )
556
+ end
557
+
558
+ # Creates a pre-signed URL for uploading a file to the Sandbox.
559
+ #
560
+ # Send a POST request with the file as multipart/form-data. The URL works with any
561
+ # HTTP client without auth headers. The signing key is cached locally for up to
562
+ # 15 seconds; if the key was rotated from another client, URLs may be rejected
563
+ # until the cache refreshes.
564
+ #
565
+ # @param path [String] Destination path for the uploaded file in the Sandbox.
566
+ # @param ttl_seconds [Integer, nil] How long the URL stays valid, in seconds.
567
+ # Defaults to 3600. Zero or negative means the URL never expires.
568
+ # @return [String] Pre-signed upload URL.
569
+ # @raise [Daytona::Sdk::Error] if the signing key cannot be fetched.
570
+ #
571
+ # @example
572
+ # url = sandbox.upload_url('/home/user/data.bin')
573
+ # # curl -X POST -F "file=@local.bin" "$url"
574
+ def upload_url(path, ttl_seconds: nil)
575
+ Utils::FileUrlSigning.build_signed_file_url(
576
+ @toolbox_proxy_url,
577
+ id,
578
+ '/files/upload-v2',
579
+ 'POST',
580
+ path,
581
+ ensure_signing_key,
582
+ ttl_seconds
583
+ )
584
+ end
585
+
586
+ # Rotates the sandbox signing key, invalidating all previously signed URLs.
587
+ #
588
+ # @return [void]
589
+ # @raise [DaytonaApiClient::ApiError] if the signing key rotation request fails.
590
+ #
591
+ # @example
592
+ # sandbox.rotate_signing_key
593
+ # puts 'All previously signed URLs are now invalid.'
594
+ def rotate_signing_key
595
+ @signing_key = @sandbox_api.rotate_signing_key(id)
596
+ @signing_key_fetched_at = Time.now.to_f
597
+ nil
598
+ end
599
+
527
600
  # Creates a signed preview URL for the sandbox at the specified port.
528
601
  #
529
602
  # @param port [Integer] The port to open the preview link on
@@ -570,8 +643,8 @@ module Daytona
570
643
  def refresh_activity
571
644
  sandbox_api.update_last_activity(id)
572
645
  nil
573
- rescue StandardError => e
574
- raise Sdk::Error, "Failed to refresh sandbox activity: #{e.message}"
646
+ rescue *Sdk::API_ERROR_CLASSES => e
647
+ raise Sdk.wrap_error(e, 'Failed to refresh sandbox activity')
575
648
  end
576
649
 
577
650
  # Revokes an SSH access token for the sandbox.
@@ -619,8 +692,8 @@ module Daytona
619
692
  error_states: [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED]
620
693
  )
621
694
  end
622
- rescue StandardError => e
623
- raise Sdk::Error, "Failed to recover sandbox: #{e.message}"
695
+ rescue *Sdk::API_ERROR_CLASSES => e
696
+ raise Sdk.wrap_error(e, 'Failed to recover sandbox')
624
697
  end
625
698
 
626
699
  # Stops the Sandbox and waits for it to be stopped.
@@ -841,7 +914,8 @@ module Daytona
841
914
  :update_network_settings, :update_secrets, :update_env,
842
915
  :create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :get_metrics, :get_metrics_latest,
843
916
  :labels=,
844
- :preview_url, :create_signed_preview_url, :expire_signed_preview_url,
917
+ :preview_url, :download_url, :upload_url, :rotate_signing_key,
918
+ :create_signed_preview_url, :expire_signed_preview_url,
845
919
  :refresh, :refresh_activity, :revoke_ssh_access, :start, :recover, :stop,
846
920
  :create_lsp_server, :validate_ssh_access, :wait_for_sandbox_start,
847
921
  :wait_for_sandbox_stop, :resize, :wait_for_resize_complete,
@@ -964,6 +1038,16 @@ module Daytona
964
1038
  @toolbox_api_config.base_path = uri.path.empty? ? '/' : uri.path
965
1039
  end
966
1040
 
1041
+ def ensure_signing_key
1042
+ key = @signing_key
1043
+ if key.nil? || (Time.now.to_f - @signing_key_fetched_at) > 15
1044
+ key = @sandbox_api.get_sandbox_signing_key(id)
1045
+ @signing_key = key
1046
+ @signing_key_fetched_at = Time.now.to_f
1047
+ end
1048
+ key
1049
+ end
1050
+
967
1051
  # @params sandbox_dto [DaytonaApiClient::Sandbox, DaytonaApiClient::SandboxListItem]
968
1052
  # @return [void]
969
1053
  def process_response(sandbox_dto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -0,0 +1,18 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ # =============================================================================
7
+ # Backward-compatibility aliases. New functionality belongs in errors.rb, not
8
+ # here. Every name below exists solely so pre-typed-error-model user code
9
+ # keeps working, and this file can be removed as a whole in a future major
10
+ # release.
11
+ # =============================================================================
12
+ module Daytona
13
+ module Sdk
14
+ # @deprecated Use {BadRequestError} instead. Kept as an alias so existing
15
+ # `rescue Daytona::Sdk::ValidationError` blocks keep working.
16
+ ValidationError = BadRequestError
17
+ end
18
+ end
@@ -0,0 +1,279 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ require 'json'
7
+
8
+ module Daytona
9
+ module Sdk # rubocop:disable Metrics/ModuleLength
10
+ # ApiError classes raised by the generated OpenAPI clients. Kept here so
11
+ # the error module can resolve them without depending on `sdk.rb`.
12
+ API_ERROR_CLASSES = [DaytonaApiClient::ApiError, DaytonaToolboxApiClient::ApiError].freeze
13
+
14
+ # Wire-format `source` values set by the translation layer when a
15
+ # Daytona service stamps them on the wire envelope. `nil` source means
16
+ # the response did not carry a structured envelope (treat as opaque).
17
+ SOURCE_API = 'DAYTONA_API'
18
+ SOURCE_DAEMON = 'DAYTONA_DAEMON'
19
+ SOURCE_PROXY = 'DAYTONA_PROXY'
20
+
21
+ # ---------------------------------------------------------------------
22
+ # Base
23
+ # ---------------------------------------------------------------------
24
+
25
+ # Base class for every error raised by the Daytona SDK.
26
+ #
27
+ # @example Catching any SDK error
28
+ # begin
29
+ # daytona.create(params)
30
+ # rescue Daytona::Sdk::Error => e
31
+ # puts e.status_code, e.code, e.source
32
+ # end
33
+ class Error < StandardError
34
+ def initialize(message = nil, status_code: nil, code: nil, source: nil, headers: nil)
35
+ super(message)
36
+ @status_code = status_code
37
+ @code = code
38
+ @source = source
39
+ @headers = headers
40
+ end
41
+
42
+ def status_code = @status_code || metadata_from_cause[:status_code]
43
+
44
+ def code = @code || metadata_from_cause[:code]
45
+
46
+ def headers = @headers || metadata_from_cause[:headers] || {}
47
+
48
+ # Returns the originating service, or `nil` if unknown. Falls back to
49
+ # metadata carried by `cause` so a re-raised ApiError keeps its
50
+ # server-stamped source.
51
+ def source = @source || metadata_from_cause[:source]
52
+
53
+ private
54
+
55
+ def metadata_from_cause
56
+ @metadata_from_cause ||= Sdk.api_error_details(cause)
57
+ end
58
+ end
59
+
60
+ # ---------------------------------------------------------------------
61
+ # HTTP status-class errors
62
+ # ---------------------------------------------------------------------
63
+
64
+ # HTTP 400 — request was rejected as malformed or invalid.
65
+ class BadRequestError < Error; end
66
+
67
+ # HTTP 401 — authentication failed or credentials missing.
68
+ class AuthenticationError < Error; end
69
+
70
+ # HTTP 403 — caller is authenticated but not allowed.
71
+ class ForbiddenError < Error; end
72
+
73
+ # HTTP 404 — target resource does not exist.
74
+ class NotFoundError < Error; end
75
+
76
+ # HTTP 408 / 504 / client-side timeouts.
77
+ class TimeoutError < Error; end
78
+
79
+ # HTTP 409 — request conflicts with current resource state.
80
+ class ConflictError < Error; end
81
+
82
+ # HTTP 410 — resource is permanently gone.
83
+ class GoneError < Error; end
84
+
85
+ # HTTP 422 — request is well-formed but semantically invalid.
86
+ class UnprocessableEntityError < Error; end
87
+
88
+ # HTTP 429 — rate limit was exceeded.
89
+ class RateLimitError < Error; end
90
+
91
+ # Generic 5xx fallback when a more specific class doesn't apply.
92
+ class ServerError < Error; end
93
+
94
+ # HTTP 500 — server-side bug or unhandled condition.
95
+ class InternalServerError < ServerError; end
96
+
97
+ # HTTP 502 — an upstream dependency rejected or dropped the request.
98
+ class BadGatewayError < ServerError; end
99
+
100
+ # HTTP 503 — the service is temporarily refusing traffic.
101
+ class ServiceUnavailableError < ServerError; end
102
+
103
+ # Transport-level failure (no HTTP response received).
104
+ class ConnectionError < Error; end
105
+
106
+ # Transport-level timeout. Subclass of ConnectionError so callers that
107
+ # catch the broader connection-failure category also match.
108
+ class ConnectionTimeoutError < ConnectionError; end
109
+
110
+ # ---------------------------------------------------------------------
111
+ # Domain subclasses — each inherits from the HTTP-status parent that
112
+ # matches its server-side status code.
113
+ # ---------------------------------------------------------------------
114
+
115
+ # Daemon: git
116
+ class GitAuthFailedError < AuthenticationError; end
117
+ class GitRepoNotFoundError < NotFoundError; end
118
+ class GitBranchNotFoundError < NotFoundError; end
119
+ class GitBranchExistsError < ConflictError; end
120
+ class GitPushRejectedError < ConflictError; end
121
+ class GitDirtyWorktreeError < ConflictError; end
122
+ class GitMergeConflictError < ConflictError; end
123
+
124
+ # Daemon: filesystem
125
+ class FileNotFoundError < NotFoundError; end
126
+ class FileAccessDeniedError < ForbiddenError; end
127
+
128
+ # Daemon: LSP
129
+ class LspServerNotInitializedError < BadRequestError; end
130
+
131
+ # Daemon: process / session
132
+ class ProcessExecutionTimeoutError < TimeoutError; end
133
+ class ProcessNotFoundError < NotFoundError; end
134
+ class SessionEndedError < GoneError; end
135
+ class CommandAlreadyCompletedError < GoneError; end
136
+
137
+ # Daemon: computer-use
138
+ class A11yUnavailableError < ServiceUnavailableError; end
139
+ class RecordingStillActiveError < ConflictError; end
140
+ class RecordingFfmpegNotFoundError < ServiceUnavailableError; end
141
+
142
+ # ---------------------------------------------------------------------
143
+ # Routing tables
144
+ # ---------------------------------------------------------------------
145
+
146
+ STATUS_CODE_TO_ERROR = {
147
+ 400 => BadRequestError,
148
+ 401 => AuthenticationError,
149
+ 403 => ForbiddenError,
150
+ 404 => NotFoundError,
151
+ 408 => TimeoutError,
152
+ 409 => ConflictError,
153
+ 410 => GoneError,
154
+ 422 => UnprocessableEntityError,
155
+ 429 => RateLimitError,
156
+ 500 => InternalServerError,
157
+ 502 => BadGatewayError,
158
+ 503 => ServiceUnavailableError,
159
+ 504 => TimeoutError
160
+ }.freeze
161
+
162
+ # (source, code) tuple → exception class. Resolved BEFORE the status
163
+ # code, so a server-stamped domain code always wins over the generic
164
+ # status class.
165
+ CODE_TO_ERROR = {
166
+ # Daemon: git
167
+ [SOURCE_DAEMON, 'GIT_AUTH_FAILED'] => GitAuthFailedError,
168
+ [SOURCE_DAEMON, 'GIT_REPO_NOT_FOUND'] => GitRepoNotFoundError,
169
+ [SOURCE_DAEMON, 'GIT_BRANCH_NOT_FOUND'] => GitBranchNotFoundError,
170
+ [SOURCE_DAEMON, 'GIT_BRANCH_EXISTS'] => GitBranchExistsError,
171
+ [SOURCE_DAEMON, 'GIT_PUSH_REJECTED'] => GitPushRejectedError,
172
+ [SOURCE_DAEMON, 'GIT_DIRTY_WORKTREE'] => GitDirtyWorktreeError,
173
+ [SOURCE_DAEMON, 'GIT_MERGE_CONFLICT'] => GitMergeConflictError,
174
+
175
+ # Daemon: filesystem
176
+ [SOURCE_DAEMON, 'FILE_NOT_FOUND'] => FileNotFoundError,
177
+ [SOURCE_DAEMON, 'FILE_ACCESS_DENIED'] => FileAccessDeniedError,
178
+
179
+ # Daemon: LSP
180
+ [SOURCE_DAEMON, 'LSP_SERVER_NOT_INITIALIZED'] => LspServerNotInitializedError,
181
+
182
+ # Daemon: process / session
183
+ [SOURCE_DAEMON, 'PROCESS_EXECUTION_TIMEOUT'] => ProcessExecutionTimeoutError,
184
+ [SOURCE_DAEMON, 'PROCESS_NOT_FOUND'] => ProcessNotFoundError,
185
+ [SOURCE_DAEMON, 'SESSION_ENDED'] => SessionEndedError,
186
+ [SOURCE_DAEMON, 'COMMAND_ALREADY_COMPLETED'] => CommandAlreadyCompletedError,
187
+
188
+ # Daemon: computer-use
189
+ [SOURCE_DAEMON, 'A11Y_UNAVAILABLE'] => A11yUnavailableError,
190
+ [SOURCE_DAEMON, 'RECORDING_STILL_ACTIVE'] => RecordingStillActiveError,
191
+ [SOURCE_DAEMON, 'RECORDING_FFMPEG_NOT_FOUND'] => RecordingFfmpegNotFoundError
192
+ }.freeze
193
+
194
+ # ---------------------------------------------------------------------
195
+ # Public translation helpers (module-level functions on Daytona::Sdk)
196
+ # ---------------------------------------------------------------------
197
+
198
+ # Translate an OpenAPI-client error into the most specific Daytona SDK
199
+ # exception. Accepts an optional `prefix` that's prepended to the
200
+ # message for context (e.g. "Failed to create sandbox"). When `error`
201
+ # is already an `Sdk::Error` (e.g. raised by the streaming transfer
202
+ # helpers for cancel/timeout), its class is preserved and only the
203
+ # message is prefixed.
204
+ def self.wrap_error(error, prefix = nil)
205
+ if error.is_a?(Error)
206
+ message = prefix ? "#{prefix}: #{error.message}" : error.message
207
+ return error.class.new(message, status_code: error.status_code, code: error.code,
208
+ source: error.source, headers: error.headers)
209
+ end
210
+
211
+ details = api_error_details(error)
212
+ base_message = parsed_message(error) || error.message
213
+ message = prefix ? "#{prefix}: #{base_message}" : base_message
214
+ error_class_for(details).new(message, **details.slice(:status_code, :code, :source, :headers))
215
+ end
216
+
217
+ # Extract status code, code, source and headers from a raised OpenAPI
218
+ # error. Returns an empty hash when the error is not one of the
219
+ # generated client types.
220
+ def self.api_error_details(error)
221
+ return {} unless API_ERROR_CLASSES.any? { |c| error.is_a?(c) }
222
+
223
+ data = parse_error_body(error.respond_to?(:response_body) ? error.response_body : nil)
224
+ {
225
+ status_code: error.respond_to?(:code) ? error.code : nil,
226
+ code: data[:code],
227
+ source: data[:source],
228
+ headers: error.respond_to?(:response_headers) ? error.response_headers : nil
229
+ }
230
+ end
231
+
232
+ # Choose the exception class for a parsed error: (source, code) match
233
+ # wins, then HTTP status code, then the base Error.
234
+ def self.error_class_for(details)
235
+ code = details[:code]
236
+ source = details[:source]
237
+ if code && source
238
+ cls = CODE_TO_ERROR[[source, code]]
239
+ return cls if cls
240
+ end
241
+ status_code = details[:status_code]
242
+ exact_status_error = STATUS_CODE_TO_ERROR[status_code]
243
+ return exact_status_error if exact_status_error
244
+
245
+ return ServerError if status_code.is_a?(Integer) && status_code >= 500
246
+
247
+ Error
248
+ end
249
+
250
+ def self.parse_error_body(response_body)
251
+ return {} if response_body.nil? || response_body.empty?
252
+
253
+ data = JSON.parse(response_body)
254
+ return {} unless data.is_a?(Hash)
255
+
256
+ {
257
+ message: string_or_nil(data['message']) || string_or_nil(data['error']),
258
+ code: string_or_nil(data['code'] || data['error_code']),
259
+ source: string_or_nil(data['source'])
260
+ }
261
+ rescue JSON::ParserError
262
+ {}
263
+ end
264
+
265
+ # @api private
266
+ def self.parsed_message(error)
267
+ return nil unless API_ERROR_CLASSES.any? { |c| error.is_a?(c) }
268
+ return nil unless error.respond_to?(:response_body)
269
+
270
+ parse_error_body(error.response_body)[:message]
271
+ end
272
+
273
+ def self.string_or_nil(value)
274
+ value.is_a?(String) && !value.empty? ? value : nil
275
+ end
276
+
277
+ private_class_method :error_class_for, :parse_error_body, :parsed_message, :string_or_nil
278
+ end
279
+ end
@@ -0,0 +1,131 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # frozen_string_literal: true
5
+
6
+ # Patches the OpenAPI-generated `download_file` and `call_api` helpers in both
7
+ # API clients so that streaming responses (return_type == 'File') do not lose
8
+ # the response body when the server returns an error status.
9
+ #
10
+ # Background:
11
+ # For File downloads the generated `download_file` attaches Typhoeus
12
+ # `on_body` callbacks to stream the response into a Tempfile. The presence
13
+ # of `on_body` prevents Typhoeus from populating `response.body`, which
14
+ # means the daemon's structured error envelope (code/source/message) is
15
+ # discarded when the response is an error. The generated `call_api` then
16
+ # raises `ApiError` with an empty `response_body`.
17
+ #
18
+ # Fix:
19
+ # * In `download_file`, defer tempfile creation until `on_headers` and
20
+ # inspect the status code. For 2xx responses, behave exactly like the
21
+ # upstream code. For non-2xx responses, accumulate the body into a
22
+ # buffer stored as an instance variable on the ApiClient.
23
+ # * In `call_api`, rescue the `ApiError`, and if our buffer is populated,
24
+ # re-raise with `response_body` set to the buffered envelope.
25
+
26
+ require 'tempfile'
27
+
28
+ module Daytona
29
+ module Sdk
30
+ # rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
31
+ module FileDownloadPatch
32
+ def self.apply!(api_client_class, api_error_class)
33
+ return if api_client_class.method_defined?(:_daytona_orig_call_api)
34
+
35
+ api_client_class.class_eval do
36
+ define_method(:_daytona_error_body_key) do
37
+ @daytona_error_body_key ||= :"daytona_file_download_error_body_#{object_id}"
38
+ end
39
+
40
+ define_method(:download_file) do |request, &block|
41
+ tempfile = nil
42
+ encoding = nil
43
+ stream_to_tempfile = false
44
+ error_body = String.new.b
45
+ Thread.current.thread_variable_set(_daytona_error_body_key, nil)
46
+
47
+ request.on_headers do |response|
48
+ stream_to_tempfile = response.code && response.code >= 200 && response.code < 300
49
+ next unless stream_to_tempfile
50
+
51
+ content_disposition = response.headers['Content-Disposition']
52
+ if content_disposition && content_disposition =~ /filename=/i
53
+ filename = content_disposition[/filename=['"]?([^'"\s]+)['"]?/, 1]
54
+ prefix = sanitize_filename(filename)
55
+ else
56
+ prefix = 'download-'
57
+ end
58
+ prefix += '-' unless prefix.end_with?('-')
59
+ encoding = response.body.encoding
60
+ tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding)
61
+ end
62
+
63
+ request.on_body do |chunk|
64
+ if stream_to_tempfile
65
+ chunk.force_encoding(encoding)
66
+ tempfile.write(chunk)
67
+ else
68
+ error_body << chunk.b
69
+ end
70
+ end
71
+
72
+ request.on_complete do
73
+ if stream_to_tempfile
74
+ if tempfile.nil?
75
+ raise api_error_class,
76
+ "Failed to create the tempfile based on the HTTP response from the server: #{request.inspect}"
77
+ end
78
+ tempfile.close
79
+ @config.logger.info(
80
+ "Temp file written to #{tempfile.path}, please copy the file to a proper folder " \
81
+ "with e.g. `FileUtils.cp(tempfile.path, '/new/file/path')` otherwise the temp file " \
82
+ 'will be deleted automatically with GC. It\'s also recommended to delete the temp file ' \
83
+ 'explicitly with `tempfile.delete`'
84
+ )
85
+ block&.call(tempfile)
86
+ else
87
+ Thread.current.thread_variable_set(_daytona_error_body_key, error_body) unless error_body.empty?
88
+ end
89
+ end
90
+ end
91
+
92
+ alias_method :_daytona_orig_call_api, :call_api
93
+
94
+ define_method(:call_api) do |http_method, path, opts = {}|
95
+ _daytona_orig_call_api(http_method, path, opts)
96
+ rescue api_error_class => e
97
+ error_body = Thread.current.thread_variable_get(_daytona_error_body_key)
98
+ if opts[:return_type] == 'File' && error_body && !error_body.empty?
99
+ new_err = api_error_class.new(
100
+ code: e.code,
101
+ response_headers: e.response_headers,
102
+ response_body: error_body
103
+ )
104
+ raise new_err, e.message
105
+ end
106
+ raise
107
+ ensure
108
+ Thread.current.thread_variable_set(_daytona_error_body_key, nil)
109
+ end
110
+
111
+ private :_daytona_error_body_key
112
+ end
113
+ end
114
+ end
115
+ # rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
116
+ end
117
+ end
118
+
119
+ if Object.const_defined?(:DaytonaApiClient) &&
120
+ DaytonaApiClient.const_defined?(:ApiClient) &&
121
+ DaytonaApiClient.const_defined?(:ApiError)
122
+ Daytona::Sdk::FileDownloadPatch.apply!(DaytonaApiClient::ApiClient, DaytonaApiClient::ApiError)
123
+ end
124
+
125
+ if Object.const_defined?(:DaytonaToolboxApiClient) &&
126
+ DaytonaToolboxApiClient.const_defined?(:ApiClient) &&
127
+ DaytonaToolboxApiClient.const_defined?(:ApiError)
128
+ Daytona::Sdk::FileDownloadPatch.apply!(
129
+ DaytonaToolboxApiClient::ApiClient, DaytonaToolboxApiClient::ApiError
130
+ )
131
+ end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.200.0'
8
+ VERSION = '0.201.0'
9
9
  end
10
10
  end
data/lib/daytona/sdk.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # Copyright Daytona Platforms Inc.
4
4
  # SPDX-License-Identifier: Apache-2.0
5
5
 
6
+ require 'json'
6
7
  require 'logger'
7
8
 
8
9
  require 'daytona_api_client'
@@ -12,6 +13,9 @@ require 'toml'
12
13
  require 'websocket-client-simple'
13
14
 
14
15
  require_relative 'sdk/version'
16
+ require_relative 'sdk/errors'
17
+ require_relative 'sdk/deprecated'
18
+ require_relative 'sdk/file_download_patch'
15
19
  require_relative 'config'
16
20
  require_relative 'otel'
17
21
  require_relative 'common/charts'
@@ -47,15 +51,9 @@ require_relative 'process'
47
51
 
48
52
  module Daytona
49
53
  module Sdk
50
- class Error < StandardError; end
51
- class TimeoutError < Error; end
52
- class AuthenticationError < Error; end
53
- class ForbiddenError < Error; end
54
- class NotFoundError < Error; end
55
- class ConflictError < Error; end
56
- class ValidationError < Error; end
57
- class RateLimitError < Error; end
58
- class ServerError < Error; end
54
+ # The error hierarchy and translation helpers live in `sdk/errors.rb`.
55
+ # This file just provides cross-cutting bits that need to be loaded
56
+ # alongside them.
59
57
 
60
58
  def self.logger = @logger ||= Logger.new($stdout, level: Logger::INFO)
61
59
  end
@@ -147,7 +147,8 @@ module Daytona
147
147
  aws_access_key_id: push_access_creds.access_key,
148
148
  aws_secret_access_key: push_access_creds.secret,
149
149
  aws_session_token: push_access_creds.session_token,
150
- bucket_name: push_access_creds.bucket
150
+ bucket_name: push_access_creds.bucket,
151
+ region: push_access_creds.region
151
152
  )
152
153
 
153
154
  image.context_list.map do |context|