daytona 0.171.0.rc.1 → 0.172.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/common/daytona.rb +2 -12
- data/lib/daytona/config.rb +8 -0
- data/lib/daytona/daytona.rb +15 -5
- data/lib/daytona/file_system.rb +35 -3
- data/lib/daytona/file_transfer.rb +184 -0
- data/lib/daytona/sdk/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd543d5d029c078ff63374de891fb89abee7c0c798fb1bcf92287c445962834b
|
|
4
|
+
data.tar.gz: 5879e8070ff90cd8cb11845415dbebd7fdfed93ee7e4ec3d2f2ff2b38d43a5b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dec60e70de4a100442929fe6099cec5e748ef2db651d6a34e010771a125578eb62dc91a29ca772ab3a3671de7a2a1e91f42a1b6287e1bbc3cf9945cacb576769
|
|
7
|
+
data.tar.gz: c456a41e40e2cc956ac50e2cdb4737e1287c8acd01fb22b321018ba9c09cb7b0766eac623fbaf73f5ed57b64d16ebf24a14f2b3a73fa328bb7f0e55af0469688
|
|
@@ -46,12 +46,6 @@ module Daytona
|
|
|
46
46
|
# @return [Boolean, nil] Whether the Sandbox should be ephemeral
|
|
47
47
|
attr_accessor :ephemeral
|
|
48
48
|
|
|
49
|
-
# @return [String, nil] ID or name of an existing Sandbox to link the new Sandbox to. The new
|
|
50
|
-
# Sandbox will be scheduled on the same runner as the linked Sandbox so a local network can be
|
|
51
|
-
# established between them. Only supported for android-class snapshots. Linked Sandboxes must be
|
|
52
|
-
# ephemeral (auto_delete_interval=0) and cannot themselves be linked to another Sandbox.
|
|
53
|
-
attr_accessor :linked_sandbox
|
|
54
|
-
|
|
55
49
|
# Initialize CreateSandboxBaseParams
|
|
56
50
|
#
|
|
57
51
|
# @param language [Symbol, nil] Programming language for the Sandbox
|
|
@@ -67,7 +61,6 @@ module Daytona
|
|
|
67
61
|
# @param network_block_all [Boolean, nil] Whether to block all network access for the Sandbox
|
|
68
62
|
# @param network_allow_list [String, nil] Comma-separated list of allowed CIDR network addresses for the Sandbox
|
|
69
63
|
# @param ephemeral [Boolean, nil] Whether the Sandbox should be ephemeral
|
|
70
|
-
# @param linked_sandbox [String, nil] ID or name of an existing Sandbox to link the new Sandbox to
|
|
71
64
|
def initialize( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
72
65
|
language: nil,
|
|
73
66
|
os_user: nil,
|
|
@@ -81,8 +74,7 @@ module Daytona
|
|
|
81
74
|
volumes: nil,
|
|
82
75
|
network_block_all: nil,
|
|
83
76
|
network_allow_list: nil,
|
|
84
|
-
ephemeral: nil
|
|
85
|
-
linked_sandbox: nil
|
|
77
|
+
ephemeral: nil
|
|
86
78
|
)
|
|
87
79
|
@language = language
|
|
88
80
|
@os_user = os_user
|
|
@@ -97,7 +89,6 @@ module Daytona
|
|
|
97
89
|
@network_block_all = network_block_all
|
|
98
90
|
@network_allow_list = network_allow_list
|
|
99
91
|
@ephemeral = ephemeral
|
|
100
|
-
@linked_sandbox = linked_sandbox
|
|
101
92
|
|
|
102
93
|
# Handle ephemeral and auto_delete_interval conflict
|
|
103
94
|
handle_ephemeral_auto_delete_conflict
|
|
@@ -120,8 +111,7 @@ module Daytona
|
|
|
120
111
|
volumes:,
|
|
121
112
|
network_block_all:,
|
|
122
113
|
network_allow_list:,
|
|
123
|
-
ephemeral
|
|
124
|
-
linked_sandbox:
|
|
114
|
+
ephemeral:
|
|
125
115
|
}.compact
|
|
126
116
|
end
|
|
127
117
|
|
data/lib/daytona/config.rb
CHANGED
|
@@ -34,6 +34,11 @@ module Daytona
|
|
|
34
34
|
# @return [String, nil] Daytona target
|
|
35
35
|
attr_accessor :target
|
|
36
36
|
|
|
37
|
+
# Enable OpenTelemetry tracing for SDK operations.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean, nil]
|
|
40
|
+
attr_accessor :otel_enabled
|
|
41
|
+
|
|
37
42
|
# Experimental configuration options
|
|
38
43
|
#
|
|
39
44
|
# @return [Hash, nil] Experimental configuration hash
|
|
@@ -46,6 +51,7 @@ module Daytona
|
|
|
46
51
|
# @param api_url [String, nil] Daytona API URL. Defaults to ENV['DAYTONA_API_URL'] or Daytona::Config::API_URL.
|
|
47
52
|
# @param organization_id [String, nil] Daytona organization ID. Defaults to ENV['DAYTONA_ORGANIZATION_ID'].
|
|
48
53
|
# @param target [String, nil] Daytona target. Defaults to ENV['DAYTONA_TARGET'].
|
|
54
|
+
# @param otel_enabled [Boolean, nil] Enable OpenTelemetry tracing for SDK operations.
|
|
49
55
|
# @param _experimental [Hash, nil] Experimental configuration options.
|
|
50
56
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
51
57
|
api_key: nil,
|
|
@@ -53,6 +59,7 @@ module Daytona
|
|
|
53
59
|
api_url: nil,
|
|
54
60
|
organization_id: nil,
|
|
55
61
|
target: nil,
|
|
62
|
+
otel_enabled: nil,
|
|
56
63
|
_experimental: nil
|
|
57
64
|
)
|
|
58
65
|
@env_reader = daytona_env_reader
|
|
@@ -62,6 +69,7 @@ module Daytona
|
|
|
62
69
|
@api_url = api_url || @env_reader.call('DAYTONA_API_URL') || API_URL
|
|
63
70
|
@target = target || @env_reader.call('DAYTONA_TARGET')
|
|
64
71
|
@organization_id = organization_id || @env_reader.call('DAYTONA_ORGANIZATION_ID')
|
|
72
|
+
@otel_enabled = otel_enabled
|
|
65
73
|
@_experimental = _experimental
|
|
66
74
|
end
|
|
67
75
|
|
data/lib/daytona/daytona.rb
CHANGED
|
@@ -36,7 +36,10 @@ module Daytona
|
|
|
36
36
|
@config = config
|
|
37
37
|
ensure_access_token_defined
|
|
38
38
|
|
|
39
|
-
otel_enabled = config.
|
|
39
|
+
otel_enabled = config.otel_enabled ||
|
|
40
|
+
config._experimental&.dig('otel_enabled') ||
|
|
41
|
+
config.read_env('DAYTONA_OTEL_ENABLED') == 'true' ||
|
|
42
|
+
config.read_env('DAYTONA_EXPERIMENTAL_OTEL_ENABLED') == 'true'
|
|
40
43
|
@otel_state = (::Daytona.init_otel(Sdk::VERSION) if otel_enabled)
|
|
41
44
|
|
|
42
45
|
@api_client = build_api_client
|
|
@@ -169,8 +172,7 @@ module Daytona
|
|
|
169
172
|
auto_delete_interval: params.auto_delete_interval,
|
|
170
173
|
volumes: params.volumes,
|
|
171
174
|
network_block_all: params.network_block_all,
|
|
172
|
-
network_allow_list: params.network_allow_list
|
|
173
|
-
linked_sandbox: params.linked_sandbox
|
|
175
|
+
network_allow_list: params.network_allow_list
|
|
174
176
|
)
|
|
175
177
|
|
|
176
178
|
create_sandbox.snapshot = params.snapshot if params.respond_to?(:snapshot)
|
|
@@ -225,8 +227,16 @@ module Daytona
|
|
|
225
227
|
def ensure_access_token_defined
|
|
226
228
|
return if config.api_key
|
|
227
229
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
unless config.jwt_token
|
|
231
|
+
raise Sdk::Error,
|
|
232
|
+
'Authentication credentials not found. Set DAYTONA_API_KEY, or both DAYTONA_JWT_TOKEN and ' \
|
|
233
|
+
'DAYTONA_ORGANIZATION_ID. These can also be provided via Daytona::Config.'
|
|
234
|
+
end
|
|
235
|
+
return if config.organization_id
|
|
236
|
+
|
|
237
|
+
raise Sdk::Error,
|
|
238
|
+
'DAYTONA_ORGANIZATION_ID is required when authenticating with DAYTONA_JWT_TOKEN. ' \
|
|
239
|
+
'It can also be provided via Daytona::Config.'
|
|
230
240
|
end
|
|
231
241
|
|
|
232
242
|
# @return [DaytonaApiClient::ApiClient]
|
data/lib/daytona/file_system.rb
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
require 'tempfile'
|
|
7
7
|
require 'fileutils'
|
|
8
|
+
require_relative 'file_transfer'
|
|
8
9
|
|
|
9
10
|
module Daytona
|
|
10
|
-
class FileSystem
|
|
11
|
+
class FileSystem # rubocop:disable Metrics/ClassLength
|
|
11
12
|
include Instrumentation
|
|
12
13
|
|
|
13
14
|
# @return [String] The Sandbox ID
|
|
@@ -153,6 +154,37 @@ module Daytona
|
|
|
153
154
|
raise Sdk::Error, "Failed to download file: #{e.message}"
|
|
154
155
|
end
|
|
155
156
|
|
|
157
|
+
# Downloads a single file from the Sandbox as a stream without buffering the entire
|
|
158
|
+
# file into memory. Yields file content in chunks to the given block, or returns an
|
|
159
|
+
# Enumerator if no block is given.
|
|
160
|
+
#
|
|
161
|
+
# @param remote_path [String] Path to the file in the Sandbox. Relative paths are resolved
|
|
162
|
+
# based on the sandbox working directory.
|
|
163
|
+
# @param timeout [Integer] Timeout for the download operation in seconds. 0 means no timeout.
|
|
164
|
+
# Default is 30 minutes.
|
|
165
|
+
# @yield [chunk] Yields each chunk of file content as it arrives
|
|
166
|
+
# @yieldparam chunk [String] A binary string chunk of file content
|
|
167
|
+
# @return [Enumerator, nil] An Enumerator yielding chunks if no block given, nil otherwise
|
|
168
|
+
# @raise [Daytona::Sdk::Error] If the file does not exist or the operation fails
|
|
169
|
+
#
|
|
170
|
+
# @example Stream to a local file without loading into memory
|
|
171
|
+
# File.open("local_copy.bin", "wb") do |f|
|
|
172
|
+
# sandbox.fs.download_file_stream("workspace/large-file.bin") { |chunk| f.write(chunk) }
|
|
173
|
+
# end
|
|
174
|
+
#
|
|
175
|
+
# @example Collect chunks with an Enumerator
|
|
176
|
+
# content = sandbox.fs.download_file_stream("workspace/data.json").reduce(:+)
|
|
177
|
+
# puts content
|
|
178
|
+
def download_file_stream(remote_path, timeout: 30 * 60, &)
|
|
179
|
+
return enum_for(__method__, remote_path, timeout:) unless block_given?
|
|
180
|
+
|
|
181
|
+
FileTransfer.stream_download(api_client: toolbox_api.api_client, remote_path: remote_path,
|
|
182
|
+
timeout: timeout, &)
|
|
183
|
+
nil
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
raise Sdk::Error, "Failed to download file: #{e.message}"
|
|
186
|
+
end
|
|
187
|
+
|
|
156
188
|
# Uploads a file to the specified path in the Sandbox. If a file already exists at
|
|
157
189
|
# the destination path, it will be overwritten.
|
|
158
190
|
#
|
|
@@ -364,8 +396,8 @@ module Daytona
|
|
|
364
396
|
end
|
|
365
397
|
|
|
366
398
|
instrument :create_folder, :delete_file, :get_file_info, :list_files, :download_file,
|
|
367
|
-
:upload_file, :upload_files, :find_files,
|
|
368
|
-
:replace_in_files, :set_file_permissions,
|
|
399
|
+
:download_file_stream, :upload_file, :upload_files, :find_files,
|
|
400
|
+
:search_files, :move_files, :replace_in_files, :set_file_permissions,
|
|
369
401
|
component: 'FileSystem'
|
|
370
402
|
|
|
371
403
|
private
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright Daytona Platforms Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'typhoeus'
|
|
8
|
+
|
|
9
|
+
module Daytona
|
|
10
|
+
class MultipartDownloadStreamParser
|
|
11
|
+
attr_reader :error_message
|
|
12
|
+
attr_writer :boundary_token
|
|
13
|
+
|
|
14
|
+
def initialize(&on_file_chunk)
|
|
15
|
+
@on_file_chunk = on_file_chunk
|
|
16
|
+
@boundary_token = nil
|
|
17
|
+
@buffer = String.new.b
|
|
18
|
+
@state = :preamble
|
|
19
|
+
@part_name = nil
|
|
20
|
+
@error_buffer = String.new.b
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def <<(chunk)
|
|
24
|
+
@buffer << chunk.b
|
|
25
|
+
process!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finish!
|
|
29
|
+
process!
|
|
30
|
+
|
|
31
|
+
return if @state == :done || @buffer.empty?
|
|
32
|
+
|
|
33
|
+
emit(@buffer)
|
|
34
|
+
finalize_part!
|
|
35
|
+
@buffer = String.new.b
|
|
36
|
+
@state = :done
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def process!
|
|
42
|
+
loop do
|
|
43
|
+
advanced = case @state
|
|
44
|
+
when :preamble then consume_preamble?
|
|
45
|
+
when :headers then consume_headers?
|
|
46
|
+
when :body then consume_body?
|
|
47
|
+
else false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
break unless advanced
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def consume_preamble?
|
|
55
|
+
start_marker = "#{boundary}\r\n".b
|
|
56
|
+
index = @buffer.index(start_marker)
|
|
57
|
+
return retain_tail?(start_marker.bytesize - 1) unless index
|
|
58
|
+
|
|
59
|
+
@buffer = remaining_bytes(index + start_marker.bytesize)
|
|
60
|
+
@state = :headers
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def consume_headers?
|
|
65
|
+
separator = "\r\n\r\n".b
|
|
66
|
+
index = @buffer.index(separator)
|
|
67
|
+
return false unless index
|
|
68
|
+
|
|
69
|
+
headers = @buffer.byteslice(0, index)
|
|
70
|
+
@buffer = remaining_bytes(index + separator.bytesize)
|
|
71
|
+
@part_name = headers[/Content-Disposition:\s*[^\r\n]*\bname="([^"]+)"/i, 1]
|
|
72
|
+
raise Sdk::Error, 'Invalid multipart response' if @part_name.nil?
|
|
73
|
+
|
|
74
|
+
@state = :body
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def consume_body? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
79
|
+
marker = "\r\n#{boundary}".b
|
|
80
|
+
index = @buffer.index(marker)
|
|
81
|
+
|
|
82
|
+
if index
|
|
83
|
+
emit(@buffer.byteslice(0, index))
|
|
84
|
+
@buffer = remaining_bytes(index + marker.bytesize)
|
|
85
|
+
finalize_part!
|
|
86
|
+
@state = :done
|
|
87
|
+
return true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
flushable = @buffer.bytesize - marker.bytesize + 1
|
|
91
|
+
return false if flushable <= 0
|
|
92
|
+
|
|
93
|
+
emit(@buffer.byteslice(0, flushable))
|
|
94
|
+
@buffer = remaining_bytes(flushable)
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def emit(data)
|
|
99
|
+
return if data.nil? || data.empty?
|
|
100
|
+
|
|
101
|
+
case @part_name
|
|
102
|
+
when 'file'
|
|
103
|
+
@on_file_chunk.call(data)
|
|
104
|
+
when 'error'
|
|
105
|
+
@error_buffer << data
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def finalize_part!
|
|
110
|
+
return unless @part_name == 'error'
|
|
111
|
+
|
|
112
|
+
@error_message = extract_error_message(@error_buffer)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def extract_error_message(payload)
|
|
116
|
+
parsed = JSON.parse(payload)
|
|
117
|
+
parsed['message'] || parsed['error'] || payload
|
|
118
|
+
rescue JSON::ParserError
|
|
119
|
+
payload
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def retain_tail?(size)
|
|
123
|
+
@buffer = @buffer.byteslice(-size, size) || String.new.b if size.positive? && @buffer.bytesize > size
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def remaining_bytes(offset)
|
|
128
|
+
@buffer.byteslice(offset, @buffer.bytesize - offset) || String.new.b
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def boundary
|
|
132
|
+
"--#{@boundary_token}".b
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
module FileTransfer
|
|
137
|
+
def self.extract_multipart_boundary(content_type)
|
|
138
|
+
match = content_type&.match(/boundary=(?:"([^"]+)"|([^;]+))/i)
|
|
139
|
+
return unless match
|
|
140
|
+
|
|
141
|
+
match.captures.compact.first
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.stream_download(api_client:, remote_path:, timeout:, &) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
145
|
+
config = api_client.config
|
|
146
|
+
parser = MultipartDownloadStreamParser.new(&)
|
|
147
|
+
response = nil
|
|
148
|
+
|
|
149
|
+
request = Typhoeus::Request.new(
|
|
150
|
+
"#{config.base_url}/files/bulk-download",
|
|
151
|
+
method: :post,
|
|
152
|
+
headers: api_client.default_headers.dup.merge(
|
|
153
|
+
'Accept' => 'multipart/form-data',
|
|
154
|
+
'Content-Type' => 'application/json'
|
|
155
|
+
),
|
|
156
|
+
body: JSON.generate(paths: [remote_path]),
|
|
157
|
+
timeout: timeout,
|
|
158
|
+
ssl_verifypeer: config.verify_ssl,
|
|
159
|
+
ssl_verifyhost: config.verify_ssl_host ? 2 : 0
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
request.on_headers do |stream_response|
|
|
163
|
+
boundary = extract_multipart_boundary(stream_response.headers['Content-Type'])
|
|
164
|
+
raise Sdk::Error, 'Missing multipart boundary in download response' unless boundary
|
|
165
|
+
|
|
166
|
+
parser.boundary_token = boundary
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
request.on_body do |chunk|
|
|
170
|
+
parser << chunk
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
request.on_complete do |completed_response|
|
|
174
|
+
response = completed_response
|
|
175
|
+
parser.finish!
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
request.run
|
|
179
|
+
|
|
180
|
+
raise Sdk::Error, parser.error_message if parser.error_message
|
|
181
|
+
raise Sdk::Error, "HTTP #{response.code}" if response && !response.success?
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/daytona/sdk/version.rb
CHANGED
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.172.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daytona Platforms Inc.
|
|
@@ -85,28 +85,28 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - '='
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.
|
|
88
|
+
version: 0.172.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.172.0
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: daytona_toolbox_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.172.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.172.0
|
|
110
110
|
- !ruby/object:Gem::Dependency
|
|
111
111
|
name: dotenv
|
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -196,6 +196,7 @@ files:
|
|
|
196
196
|
- lib/daytona/config.rb
|
|
197
197
|
- lib/daytona/daytona.rb
|
|
198
198
|
- lib/daytona/file_system.rb
|
|
199
|
+
- lib/daytona/file_transfer.rb
|
|
199
200
|
- lib/daytona/git.rb
|
|
200
201
|
- lib/daytona/lsp_server.rb
|
|
201
202
|
- lib/daytona/object_storage.rb
|