daytona 0.200.1 → 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.
- checksums.yaml +4 -4
- data/lib/daytona/code_interpreter.rb +6 -6
- data/lib/daytona/common/response.rb +2 -0
- data/lib/daytona/computer_use.rb +62 -62
- data/lib/daytona/daytona.rb +9 -0
- data/lib/daytona/file_system.rb +36 -28
- data/lib/daytona/git.rb +21 -31
- data/lib/daytona/lsp_server.rb +20 -2
- data/lib/daytona/object_storage.rb +2 -5
- data/lib/daytona/process.rb +44 -4
- data/lib/daytona/sandbox.rb +93 -9
- data/lib/daytona/sdk/deprecated.rb +18 -0
- data/lib/daytona/sdk/errors.rb +279 -0
- data/lib/daytona/sdk/file_download_patch.rb +131 -0
- data/lib/daytona/sdk/version.rb +1 -1
- data/lib/daytona/sdk.rb +7 -9
- data/lib/daytona/snapshot_service.rb +2 -1
- data/lib/daytona/utils/file_url_signing.rb +56 -0
- metadata +19 -9
|
@@ -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
|
data/lib/daytona/sdk/version.rb
CHANGED
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Copyright Daytona Platforms Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'base64'
|
|
7
|
+
require 'openssl'
|
|
8
|
+
require 'uri'
|
|
9
|
+
|
|
10
|
+
module Daytona
|
|
11
|
+
module Utils
|
|
12
|
+
module FileUrlSigning
|
|
13
|
+
SIGNATURE_V1_PREFIX = 'v1_'
|
|
14
|
+
private_constant :SIGNATURE_V1_PREFIX
|
|
15
|
+
|
|
16
|
+
DEFAULT_TTL_SECONDS = 3600
|
|
17
|
+
private_constant :DEFAULT_TTL_SECONDS
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def compute_file_url_signature(signing_key, method, path, expires)
|
|
22
|
+
canonical = "v1:files:#{method}:#{path}:#{expires}"
|
|
23
|
+
digest = OpenSSL::HMAC.digest('sha256', signing_key, canonical)
|
|
24
|
+
|
|
25
|
+
"#{SIGNATURE_V1_PREFIX}#{Base64.urlsafe_encode64(digest, padding: false)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve_expires(ttl_seconds)
|
|
29
|
+
return Time.now.to_i + DEFAULT_TTL_SECONDS if ttl_seconds.nil?
|
|
30
|
+
|
|
31
|
+
return 0 if ttl_seconds <= 0
|
|
32
|
+
|
|
33
|
+
Time.now.to_i + ttl_seconds
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_signed_file_url(toolbox_proxy_url, sandbox_id, operation_path, method, file_path, signing_key,
|
|
37
|
+
ttl_seconds)
|
|
38
|
+
if signing_key.nil? || signing_key.empty?
|
|
39
|
+
raise Daytona::Sdk::Error,
|
|
40
|
+
'Sandbox signing key is not available. Call refresh or fetch the sandbox by ID to load it.'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if toolbox_proxy_url.nil? || toolbox_proxy_url.empty?
|
|
44
|
+
raise Daytona::Sdk::Error,
|
|
45
|
+
'Sandbox toolbox proxy URL is not available. Call refresh or fetch the sandbox by ID to load it.'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
expires = resolve_expires(ttl_seconds)
|
|
49
|
+
signature = compute_file_url_signature(signing_key, method, file_path, expires)
|
|
50
|
+
query = URI.encode_www_form(path: file_path, expires: expires.to_s, signature:)
|
|
51
|
+
|
|
52
|
+
"#{toolbox_proxy_url.sub(%r{/+\z}, '')}/#{sandbox_id}#{operation_path}?#{query}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: daytona
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.201.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daytona Platforms Inc.
|
|
@@ -85,56 +85,62 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - '='
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.
|
|
88
|
+
version: 0.201.0
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - '='
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: 0.
|
|
95
|
+
version: 0.201.0
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: daytona_api_client
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
99
99
|
requirements:
|
|
100
100
|
- - '='
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: 0.
|
|
102
|
+
version: 0.201.0
|
|
103
103
|
type: :runtime
|
|
104
104
|
prerelease: false
|
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - '='
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: 0.
|
|
109
|
+
version: 0.201.0
|
|
110
110
|
- !ruby/object:Gem::Dependency
|
|
111
111
|
name: daytona_toolbox_api_client
|
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|
|
113
113
|
requirements:
|
|
114
114
|
- - '='
|
|
115
115
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: 0.
|
|
116
|
+
version: 0.201.0
|
|
117
117
|
type: :runtime
|
|
118
118
|
prerelease: false
|
|
119
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
120
|
requirements:
|
|
121
121
|
- - '='
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: 0.
|
|
123
|
+
version: 0.201.0
|
|
124
124
|
- !ruby/object:Gem::Dependency
|
|
125
125
|
name: dotenv
|
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
|
127
127
|
requirements:
|
|
128
|
-
- - "
|
|
128
|
+
- - ">="
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
130
|
version: '2.0'
|
|
131
|
+
- - "<"
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '4'
|
|
131
134
|
type: :runtime
|
|
132
135
|
prerelease: false
|
|
133
136
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
137
|
requirements:
|
|
135
|
-
- - "
|
|
138
|
+
- - ">="
|
|
136
139
|
- !ruby/object:Gem::Version
|
|
137
140
|
version: '2.0'
|
|
141
|
+
- - "<"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '4'
|
|
138
144
|
- !ruby/object:Gem::Dependency
|
|
139
145
|
name: observer
|
|
140
146
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -221,11 +227,15 @@ files:
|
|
|
221
227
|
- lib/daytona/process.rb
|
|
222
228
|
- lib/daytona/sandbox.rb
|
|
223
229
|
- lib/daytona/sdk.rb
|
|
230
|
+
- lib/daytona/sdk/deprecated.rb
|
|
231
|
+
- lib/daytona/sdk/errors.rb
|
|
232
|
+
- lib/daytona/sdk/file_download_patch.rb
|
|
224
233
|
- lib/daytona/sdk/version.rb
|
|
225
234
|
- lib/daytona/secret.rb
|
|
226
235
|
- lib/daytona/secret_service.rb
|
|
227
236
|
- lib/daytona/snapshot_service.rb
|
|
228
237
|
- lib/daytona/util.rb
|
|
238
|
+
- lib/daytona/utils/file_url_signing.rb
|
|
229
239
|
- lib/daytona/volume.rb
|
|
230
240
|
- lib/daytona/volume_service.rb
|
|
231
241
|
- project.json
|