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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5471eba0ff200757c6372f92f80f5369c48a17cddb2dcf46f1c75f802a5e4519
4
- data.tar.gz: d3ce842087c3ce1a84ee083d510650e2cd64ff3c516ab0240a967e9a59047810
3
+ metadata.gz: fd543d5d029c078ff63374de891fb89abee7c0c798fb1bcf92287c445962834b
4
+ data.tar.gz: 5879e8070ff90cd8cb11845415dbebd7fdfed93ee7e4ec3d2f2ff2b38d43a5b3
5
5
  SHA512:
6
- metadata.gz: 15fa69f937b6c13f5ae96509ae0ab2e31be713075f7247fd3d3a71c1078771aa775f762a00ab9c6e84cd78688ac7cda9973ca0c12bced64894d758100758f5bc
7
- data.tar.gz: b5cccda24d664d4ecc74a01fe891e12310a51d20152dfbae8be15255da5ed4244c23cd7e77250cbb8405ba98871e25e40125a0f1538615124d2e3f9a3e599710
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
 
@@ -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
 
@@ -36,7 +36,10 @@ module Daytona
36
36
  @config = config
37
37
  ensure_access_token_defined
38
38
 
39
- otel_enabled = config._experimental&.dig('otel_enabled') || config.read_env('DAYTONA_EXPERIMENTAL_OTEL_ENABLED') == 'true'
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
- raise Sdk::Error, 'API key or JWT token is required' unless config.jwt_token
229
- raise Sdk::Error, 'Organization ID is required when using JWT token' unless config.organization_id
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]
@@ -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, :search_files, :move_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
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.171.0.rc.1'
8
+ VERSION = '0.172.0'
9
9
  end
10
10
  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.171.0.rc.1
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.171.0.rc.1
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.171.0.rc.1
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.171.0.rc.1
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.171.0.rc.1
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