daytona-sdk 0.125.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 +7 -0
- data/.rubocop.yml +16 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +39 -0
- data/Rakefile +12 -0
- data/lib/daytona/code_toolbox/sandbox_python_code_toolbox.rb +439 -0
- data/lib/daytona/code_toolbox/sandbox_ts_code_toolbox.rb +23 -0
- data/lib/daytona/common/charts.rb +298 -0
- data/lib/daytona/common/code_language.rb +11 -0
- data/lib/daytona/common/daytona.rb +206 -0
- data/lib/daytona/common/file_system.rb +23 -0
- data/lib/daytona/common/git.rb +16 -0
- data/lib/daytona/common/image.rb +493 -0
- data/lib/daytona/common/process.rb +141 -0
- data/lib/daytona/common/pty.rb +306 -0
- data/lib/daytona/common/resources.rb +31 -0
- data/lib/daytona/common/response.rb +28 -0
- data/lib/daytona/common/snapshot.rb +110 -0
- data/lib/daytona/computer_use.rb +549 -0
- data/lib/daytona/config.rb +53 -0
- data/lib/daytona/daytona.rb +278 -0
- data/lib/daytona/file_system.rb +359 -0
- data/lib/daytona/git.rb +287 -0
- data/lib/daytona/lsp_server.rb +130 -0
- data/lib/daytona/object_storage.rb +169 -0
- data/lib/daytona/process.rb +484 -0
- data/lib/daytona/sandbox.rb +376 -0
- data/lib/daytona/sdk/version.rb +7 -0
- data/lib/daytona/sdk.rb +45 -0
- data/lib/daytona/snapshot_service.rb +198 -0
- data/lib/daytona/util.rb +56 -0
- data/lib/daytona/volume.rb +43 -0
- data/lib/daytona/volume_service.rb +49 -0
- data/sig/daytona/sdk.rbs +6 -0
- metadata +149 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Daytona
|
|
7
|
+
class Daytona # rubocop:disable Metrics/ClassLength
|
|
8
|
+
# @return [Daytona::Config]
|
|
9
|
+
attr_reader :config
|
|
10
|
+
|
|
11
|
+
# @return [DaytonaApiClient]
|
|
12
|
+
attr_reader :api_client
|
|
13
|
+
|
|
14
|
+
# @return [DaytonaApiClient::SandboxApi]
|
|
15
|
+
attr_reader :sandbox_api
|
|
16
|
+
|
|
17
|
+
# @return [DaytonaApiClient::ToolboxApi]
|
|
18
|
+
attr_reader :toolbox_api
|
|
19
|
+
|
|
20
|
+
# @return [Daytona::VolumeService]
|
|
21
|
+
attr_reader :volume
|
|
22
|
+
|
|
23
|
+
# @return [DaytonaApiClient::ObjectStorageApi]
|
|
24
|
+
attr_reader :object_storage_api
|
|
25
|
+
|
|
26
|
+
# @return [DaytonaApiClient::SnapshotsApi]
|
|
27
|
+
attr_reader :snapshots_api
|
|
28
|
+
|
|
29
|
+
# @return [Daytona::SnapshotService]
|
|
30
|
+
attr_reader :snapshot
|
|
31
|
+
|
|
32
|
+
# @param config [Daytona::Config] Configuration options. Defaults to Daytona::Config.new
|
|
33
|
+
def initialize(config = Config.new) # rubocop:disable Metrics/AbcSize
|
|
34
|
+
@config = config
|
|
35
|
+
ensure_access_token_defined
|
|
36
|
+
@api_client = build_api_client
|
|
37
|
+
@sandbox_api = DaytonaApiClient::SandboxApi.new(api_client)
|
|
38
|
+
@toolbox_api = DaytonaApiClient::ToolboxApi.new(api_client)
|
|
39
|
+
@volume = VolumeService.new(DaytonaApiClient::VolumesApi.new(api_client))
|
|
40
|
+
@object_storage_api = DaytonaApiClient::ObjectStorageApi.new(api_client)
|
|
41
|
+
@snapshots_api = DaytonaApiClient::SnapshotsApi.new(api_client)
|
|
42
|
+
@snapshot = SnapshotService.new(snapshots_api:, object_storage_api:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Creates a sandbox with the specified parameters
|
|
46
|
+
#
|
|
47
|
+
# @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
|
|
48
|
+
# @return [Daytona::Sandbox] The created sandbox
|
|
49
|
+
# @raise [Daytona::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
|
|
50
|
+
def create(params = nil, on_snapshot_create_logs: nil)
|
|
51
|
+
if params.nil?
|
|
52
|
+
params = CreateSandboxFromSnapshotParams.new(language: CodeLanguage::PYTHON)
|
|
53
|
+
elsif params.language.nil?
|
|
54
|
+
params.language = CodeLanguage::PYTHON
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
_create(params, on_snapshot_create_logs:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Deletes a Sandbox.
|
|
61
|
+
#
|
|
62
|
+
# @param sandbox [Daytona::Sandbox]
|
|
63
|
+
# @return [void]
|
|
64
|
+
def delete(sandbox) = sandbox.delete
|
|
65
|
+
|
|
66
|
+
# Gets a Sandbox by its ID.
|
|
67
|
+
#
|
|
68
|
+
# @param id [String]
|
|
69
|
+
# @return [Daytona::Sandbox]
|
|
70
|
+
def get(id)
|
|
71
|
+
sandbox_dto = sandbox_api.get_sandbox(id)
|
|
72
|
+
to_sandbox(sandbox_dto:, code_toolbox: code_toolbox_from_labels(sandbox_dto.labels))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Finds a Sandbox by its ID or labels.
|
|
76
|
+
#
|
|
77
|
+
# @param id [String, Nil]
|
|
78
|
+
# @param labels [Hash<String, String>]
|
|
79
|
+
# @return [Daytona::Sandbox]
|
|
80
|
+
# @raise [Daytona::Sdk::Error]
|
|
81
|
+
def find_one(id: nil, labels: nil)
|
|
82
|
+
return get(id) if id
|
|
83
|
+
|
|
84
|
+
response = list(labels)
|
|
85
|
+
raise Sdk::Error, "No sandbox found with labels #{labels}" if response.items.empty?
|
|
86
|
+
|
|
87
|
+
response.items.first
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Lists Sandboxes filtered by labels.
|
|
91
|
+
#
|
|
92
|
+
# @param labels [Hash<String, String>]
|
|
93
|
+
# @param page [Integer, Nil]
|
|
94
|
+
# @param limit [Integer, Nil]
|
|
95
|
+
# @return [Daytona::PaginatedResource]
|
|
96
|
+
# @raise [Daytona::Sdk::Error]
|
|
97
|
+
def list(labels = {}, page: nil, limit: nil) # rubocop:disable Metrics/MethodLength
|
|
98
|
+
raise Sdk::Error, 'page must be positive integer' if page && page < 1
|
|
99
|
+
|
|
100
|
+
raise Sdk::Error, 'limit must be positive integer' if limit && limit < 1
|
|
101
|
+
|
|
102
|
+
response = sandbox_api.list_sandboxes_paginated(labels: JSON.dump(labels), page:, limit:)
|
|
103
|
+
|
|
104
|
+
PaginatedResource.new(
|
|
105
|
+
total: response.total,
|
|
106
|
+
page: response.page,
|
|
107
|
+
total_pages: response.total_pages,
|
|
108
|
+
items: response
|
|
109
|
+
.items
|
|
110
|
+
.map { |sandbox_dto| to_sandbox(sandbox_dto:, code_toolbox: code_toolbox_from_labels(sandbox_dto.labels)) }
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Starts a Sandbox and waits for it to be ready.
|
|
115
|
+
#
|
|
116
|
+
# @param sandbox [Daytona::Sandbox]
|
|
117
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
118
|
+
# @return [void]
|
|
119
|
+
def start(sandbox, timeout = Sandbox::DEFAULT_TIMEOUT) = sandbox.start(timeout)
|
|
120
|
+
|
|
121
|
+
# Stops a Sandbox and waits for it to be stopped.
|
|
122
|
+
#
|
|
123
|
+
# @param sandbox [Daytona::Sandbox]
|
|
124
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
125
|
+
# @return [void]
|
|
126
|
+
def stop(sandbox, timeout = Sandbox::DEFAULT_TIMEOUT) = sandbox.stop(timeout)
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# Creates a sandbox with the specified parameters
|
|
131
|
+
#
|
|
132
|
+
# @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams] Sandbox creation parameters
|
|
133
|
+
# @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
|
|
134
|
+
# @param on_snapshot_create_logs [Proc]
|
|
135
|
+
# @return [Daytona::Sandbox] The created sandbox
|
|
136
|
+
# @raise [Daytona::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
|
|
137
|
+
def _create(params, timeout: 60, on_snapshot_create_logs: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
138
|
+
raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
|
|
139
|
+
|
|
140
|
+
start_time = Time.now
|
|
141
|
+
|
|
142
|
+
raise Sdk::Error, 'auto_stop_interval must be a non-negative integer' if params.auto_stop_interval&.negative?
|
|
143
|
+
|
|
144
|
+
if params.auto_archive_interval&.negative?
|
|
145
|
+
raise Sdk::Error, 'auto_archive_interval must be a non-negative integer'
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
create_sandbox = DaytonaApiClient::CreateSandbox.new(
|
|
149
|
+
user: params.os_user,
|
|
150
|
+
env: params.env_vars || {},
|
|
151
|
+
labels: params.labels,
|
|
152
|
+
public: params.public,
|
|
153
|
+
target: config.target,
|
|
154
|
+
auto_stop_interval: params.auto_stop_interval,
|
|
155
|
+
auto_archive_interval: params.auto_archive_interval,
|
|
156
|
+
auto_delete_interval: params.auto_delete_interval,
|
|
157
|
+
volumes: params.volumes,
|
|
158
|
+
network_block_all: params.network_block_all,
|
|
159
|
+
network_allow_list: params.network_allow_list
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
create_sandbox.snapshot = params.snapshot if params.respond_to?(:snapshot)
|
|
163
|
+
|
|
164
|
+
if params.respond_to?(:image) && params.image.is_a?(String)
|
|
165
|
+
create_sandbox.build_info = DaytonaApiClient::CreateBuildInfo.new(
|
|
166
|
+
dockerfile_content: Image.base(params.image).dockerfile
|
|
167
|
+
)
|
|
168
|
+
elsif params.respond_to?(:image) && params.image.is_a?(Image)
|
|
169
|
+
create_sandbox.build_info = DaytonaApiClient::CreateBuildInfo.new(
|
|
170
|
+
context_hashes: SnapshotService.process_image_context(object_storage_api, params.image),
|
|
171
|
+
dockerfile_content: params.image.dockerfile
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if params.respond_to?(:resources)
|
|
176
|
+
create_sandbox.cpu = params.resources&.cpu
|
|
177
|
+
create_sandbox.memory = params.resources&.memory
|
|
178
|
+
create_sandbox.disk = params.resources&.disk
|
|
179
|
+
create_sandbox.gpu = params.resources&.gpu
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
response = sandbox_api.create_sandbox(create_sandbox)
|
|
183
|
+
|
|
184
|
+
if response.state == DaytonaApiClient::SandboxState::PENDING_BUILD && on_snapshot_create_logs
|
|
185
|
+
uri = URI.parse(sandbox_api.api_client.config.base_url)
|
|
186
|
+
uri.path = "/api/sandbox/#{response.id}/build-logs"
|
|
187
|
+
uri.query = 'follow=true'
|
|
188
|
+
|
|
189
|
+
headers = {}
|
|
190
|
+
sandbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
|
|
191
|
+
Util.stream_async(uri:, headers:, on_chunk: on_snapshot_create_logs)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
sandbox = to_sandbox(sandbox_dto: response, code_toolbox: code_toolbox_from_labels(response.labels))
|
|
195
|
+
|
|
196
|
+
if sandbox.state != DaytonaApiClient::SandboxState::STARTED
|
|
197
|
+
sandbox.wait_for_sandbox_start([0.001, timeout - (Time.now - start_time)].max)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
sandbox
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @return [void]
|
|
204
|
+
# @raise [Daytona::Sdk::Error]
|
|
205
|
+
def ensure_access_token_defined
|
|
206
|
+
return if config.api_key
|
|
207
|
+
|
|
208
|
+
raise Sdk::Error, 'API key or JWT token is required' unless config.jwt_token
|
|
209
|
+
raise Sdk::Error, 'Organization ID is required when using JWT token' unless config.organization_id
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @return [DaytonaApiClient::ApiClient]
|
|
213
|
+
def build_api_client
|
|
214
|
+
DaytonaApiClient::ApiClient.new(api_client_config).tap do |client|
|
|
215
|
+
client.default_headers[HEADER_SOURCE] = SOURCE_RUBY
|
|
216
|
+
client.default_headers[HEADER_SDK_VERSION] = Sdk::VERSION
|
|
217
|
+
client.default_headers[HEADER_ORGANIZATION_ID] = config.organization_id if config.jwt_token
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @return [DaytonaApiClient::Configuration]
|
|
222
|
+
def api_client_config
|
|
223
|
+
DaytonaApiClient::Configuration.new.configure do |api_config|
|
|
224
|
+
uri = URI(config.api_url)
|
|
225
|
+
api_config.scheme = uri.scheme
|
|
226
|
+
api_config.host = uri.host
|
|
227
|
+
api_config.base_path = uri.path
|
|
228
|
+
|
|
229
|
+
api_config.access_token_getter = proc { config.api_key || config.jwt_token }
|
|
230
|
+
api_config
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @param sandbox_dto [DaytonaApiClient::Sandbox]
|
|
235
|
+
# @param code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
|
|
236
|
+
# @return [Daytona::Sandbox]
|
|
237
|
+
def to_sandbox(sandbox_dto:, code_toolbox:)
|
|
238
|
+
Sandbox.new(sandbox_dto:, config:, sandbox_api:, toolbox_api:, code_toolbox:)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Converts a language to a code toolbox
|
|
242
|
+
#
|
|
243
|
+
# @param language [Symbol]
|
|
244
|
+
# @return [Daytona::CodeToolbox]
|
|
245
|
+
# @raise [Daytona::Sdk::Error] If the language is not supported
|
|
246
|
+
def code_toolbox_from_language(language)
|
|
247
|
+
case language
|
|
248
|
+
when CodeLanguage::PYTHON, nil
|
|
249
|
+
SandboxPythonCodeToolbox.new
|
|
250
|
+
when SandboxTsCodeToolbox, CodeLanguage::TYPESCRIPT
|
|
251
|
+
SandboxTsCodeToolbox.new
|
|
252
|
+
else
|
|
253
|
+
raise Sdk::Error, "Unsupported language: #{language}"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Get code toolbox from Sandbox labels
|
|
258
|
+
#
|
|
259
|
+
# @param labels [Hash<String, String>]
|
|
260
|
+
# @return [Daytona::CodeToolbox]
|
|
261
|
+
def code_toolbox_from_labels(labels) = code_toolbox_from_language(labels[LABEL_CODE_TOOLBOX_LANGUAGE]&.to_sym)
|
|
262
|
+
|
|
263
|
+
SOURCE_RUBY = 'ruby-sdk'
|
|
264
|
+
private_constant :SOURCE_RUBY
|
|
265
|
+
|
|
266
|
+
HEADER_SOURCE = 'X-Daytona-Source'
|
|
267
|
+
private_constant :HEADER_SOURCE
|
|
268
|
+
|
|
269
|
+
HEADER_SDK_VERSION = 'X-Daytona-SDK-Version'
|
|
270
|
+
private_constant :HEADER_SDK_VERSION
|
|
271
|
+
|
|
272
|
+
HEADER_ORGANIZATION_ID = 'X-Daytona-Organization-ID'
|
|
273
|
+
private_constant :HEADER_ORGANIZATION_ID
|
|
274
|
+
|
|
275
|
+
LABEL_CODE_TOOLBOX_LANGUAGE = 'code-toolbox-language'
|
|
276
|
+
private_constant :LABEL_CODE_TOOLBOX_LANGUAGE
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Daytona
|
|
7
|
+
class FileSystem
|
|
8
|
+
# @return [String] The Sandbox ID
|
|
9
|
+
attr_reader :sandbox_id
|
|
10
|
+
|
|
11
|
+
# @return [DaytonaApiClient::ToolboxApi] API client for Sandbox operations
|
|
12
|
+
attr_reader :toolbox_api
|
|
13
|
+
|
|
14
|
+
# Initializes a new FileSystem instance.
|
|
15
|
+
#
|
|
16
|
+
# @param sandbox_id [String] The Sandbox ID
|
|
17
|
+
# @param toolbox_api [DaytonaApiClient::ToolboxApi] API client for Sandbox operations
|
|
18
|
+
def initialize(sandbox_id:, toolbox_api:)
|
|
19
|
+
@sandbox_id = sandbox_id
|
|
20
|
+
@toolbox_api = toolbox_api
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Creates a new directory in the Sandbox at the specified path with the given
|
|
24
|
+
# permissions.
|
|
25
|
+
#
|
|
26
|
+
# @param path [String] Path where the folder should be created. Relative paths are resolved based
|
|
27
|
+
# on the sandbox working directory.
|
|
28
|
+
# @param mode [String] Folder permissions in octal format (e.g., "755" for rwxr-xr-x).
|
|
29
|
+
# @return [void]
|
|
30
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# # Create a directory with standard permissions
|
|
34
|
+
# sandbox.fs.create_folder("workspace/data", "755")
|
|
35
|
+
#
|
|
36
|
+
# # Create a private directory
|
|
37
|
+
# sandbox.fs.create_folder("workspace/secrets", "700")
|
|
38
|
+
def create_folder(path, mode)
|
|
39
|
+
Sdk.logger.debug("Creating folder #{path} with mode #{mode}")
|
|
40
|
+
toolbox_api.create_folder(sandbox_id, path, mode)
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
raise Sdk::Error, "Failed to create folder: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Deletes a file from the Sandbox.
|
|
46
|
+
#
|
|
47
|
+
# @param path [String] Path to the file to delete. Relative paths are resolved based on the sandbox working directory.
|
|
48
|
+
# @param recursive [Boolean] If the file is a directory, this must be true to delete it.
|
|
49
|
+
# @return [void]
|
|
50
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# # Delete a file
|
|
54
|
+
# sandbox.fs.delete_file("workspace/data/old_file.txt")
|
|
55
|
+
#
|
|
56
|
+
# # Delete a directory recursively
|
|
57
|
+
# sandbox.fs.delete_file("workspace/old_dir", recursive: true)
|
|
58
|
+
def delete_file(path, recursive: false)
|
|
59
|
+
toolbox_api.delete_file(sandbox_id, path, { recursive: })
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
raise Sdk::Error, "Failed to delete file: #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Gets detailed information about a file or directory, including its
|
|
65
|
+
# size, permissions, and timestamps.
|
|
66
|
+
#
|
|
67
|
+
# @param path [String] Path to the file or directory. Relative paths are resolved based
|
|
68
|
+
# on the sandbox working directory.
|
|
69
|
+
# @return [DaytonaApiClient::FileInfo] Detailed file information
|
|
70
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# # Get file metadata
|
|
74
|
+
# info = sandbox.fs.get_file_info("workspace/data/file.txt")
|
|
75
|
+
# puts "Size: #{info.size} bytes"
|
|
76
|
+
# puts "Modified: #{info.mod_time}"
|
|
77
|
+
# puts "Mode: #{info.mode}"
|
|
78
|
+
#
|
|
79
|
+
# # Check if path is a directory
|
|
80
|
+
# info = sandbox.fs.get_file_info("workspace/data")
|
|
81
|
+
# puts "Path is a directory" if info.is_dir
|
|
82
|
+
def get_file_info(path)
|
|
83
|
+
toolbox_api.get_file_info(sandbox_id, path)
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
raise Sdk::Error, "Failed to get file info: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Lists files and directories in a given path and returns their information, similar to the ls -l command.
|
|
89
|
+
#
|
|
90
|
+
# @param path [String] Path to the directory to list contents from. Relative paths are resolved
|
|
91
|
+
# based on the sandbox working directory.
|
|
92
|
+
# @return [Array<DaytonaApiClient::FileInfo>] List of file and directory information
|
|
93
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# # List directory contents
|
|
97
|
+
# files = sandbox.fs.list_files("workspace/data")
|
|
98
|
+
#
|
|
99
|
+
# # Print files and their sizes
|
|
100
|
+
# files.each do |file|
|
|
101
|
+
# puts "#{file.name}: #{file.size} bytes" unless file.is_dir
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# # List only directories
|
|
105
|
+
# dirs = files.select(&:is_dir)
|
|
106
|
+
# puts "Subdirectories: #{dirs.map(&:name).join(', ')}"
|
|
107
|
+
def list_files(path)
|
|
108
|
+
toolbox_api.list_files(sandbox_id, { path: })
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
raise Sdk::Error, "Failed to list files: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Downloads a file from the Sandbox. Returns the file contents as a string.
|
|
114
|
+
# This method is useful when you want to load the file into memory without saving it to disk.
|
|
115
|
+
# It can only be used for smaller files.
|
|
116
|
+
#
|
|
117
|
+
# @param remote_path [String] Path to the file in the Sandbox. Relative paths are resolved based
|
|
118
|
+
# on the sandbox working directory.
|
|
119
|
+
# @param local_path [String, nil] Optional path to save the file locally. If provided, the file will be saved to disk.
|
|
120
|
+
# @return [File, nil] The file if local_path is nil, otherwise nil
|
|
121
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# # Download and get file content
|
|
125
|
+
# content = sandbox.fs.download_file("workspace/data/file.txt")
|
|
126
|
+
# puts content
|
|
127
|
+
#
|
|
128
|
+
# # Download and save a file locally
|
|
129
|
+
# sandbox.fs.download_file("workspace/data/file.txt", "local_copy.txt")
|
|
130
|
+
# size_mb = File.size("local_copy.txt") / 1024.0 / 1024.0
|
|
131
|
+
# puts "Size of the downloaded file: #{size_mb} MB"
|
|
132
|
+
def download_file(remote_path, local_path = nil) # rubocop:disable Metrics/MethodLength
|
|
133
|
+
file = toolbox_api.download_file(sandbox_id, remote_path)
|
|
134
|
+
|
|
135
|
+
if local_path
|
|
136
|
+
|
|
137
|
+
parent_dir = File.dirname(local_path)
|
|
138
|
+
FileUtils.mkdir_p(parent_dir) unless parent_dir == '.'
|
|
139
|
+
|
|
140
|
+
File.binwrite(local_path, file.open.read)
|
|
141
|
+
nil
|
|
142
|
+
else
|
|
143
|
+
file
|
|
144
|
+
end
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
raise Sdk::Error, "Failed to download file: #{e.message}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Uploads a file to the specified path in the Sandbox. If a file already exists at
|
|
150
|
+
# the destination path, it will be overwritten.
|
|
151
|
+
#
|
|
152
|
+
# @param source [String, IO] File contents as a string/bytes or a local file path or IO object.
|
|
153
|
+
# @param remote_path [String] Path to the destination file. Relative paths are resolved based on
|
|
154
|
+
# the sandbox working directory.
|
|
155
|
+
# @return [void]
|
|
156
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
157
|
+
#
|
|
158
|
+
# @example
|
|
159
|
+
# # Upload a text file from string content
|
|
160
|
+
# content = "Hello, World!"
|
|
161
|
+
# sandbox.fs.upload_file(content, "tmp/hello.txt")
|
|
162
|
+
#
|
|
163
|
+
# # Upload a local file
|
|
164
|
+
# sandbox.fs.upload_file("local_file.txt", "tmp/file.txt")
|
|
165
|
+
#
|
|
166
|
+
# # Upload binary data
|
|
167
|
+
# data = { key: "value" }.to_json
|
|
168
|
+
# sandbox.fs.upload_file(data, "tmp/config.json")
|
|
169
|
+
def upload_file(source, remote_path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
170
|
+
if source.is_a?(String) && File.exist?(source)
|
|
171
|
+
# Source is a file path
|
|
172
|
+
File.open(source, 'rb') { |file| toolbox_api.upload_file(sandbox_id, remote_path, { file: }) }
|
|
173
|
+
elsif source.respond_to?(:read)
|
|
174
|
+
# Source is an IO object
|
|
175
|
+
toolbox_api.upload_file(sandbox_id, remote_path, { file: source })
|
|
176
|
+
else
|
|
177
|
+
# Source is string content - create a temporary file
|
|
178
|
+
Tempfile.create('daytona_upload') do |file|
|
|
179
|
+
file.binmode
|
|
180
|
+
file.write(source)
|
|
181
|
+
file.rewind
|
|
182
|
+
toolbox_api.upload_file(sandbox_id, remote_path, { file: })
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
raise Sdk::Error, "Failed to upload file: #{e.message}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Uploads multiple files to the Sandbox. If files already exist at the destination paths,
|
|
190
|
+
# they will be overwritten.
|
|
191
|
+
#
|
|
192
|
+
# @param files [Array<FileUpload>] List of files to upload.
|
|
193
|
+
# @return [void]
|
|
194
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
195
|
+
#
|
|
196
|
+
# @example
|
|
197
|
+
# # Upload multiple files
|
|
198
|
+
# files = [
|
|
199
|
+
# FileUpload.new("Content of file 1", "/tmp/file1.txt"),
|
|
200
|
+
# FileUpload.new("workspace/data/file2.txt", "/tmp/file2.txt"),
|
|
201
|
+
# FileUpload.new('{"key": "value"}', "/tmp/config.json")
|
|
202
|
+
# ]
|
|
203
|
+
# sandbox.fs.upload_files(files)
|
|
204
|
+
def upload_files(files)
|
|
205
|
+
files.each { |file_upload| upload_file(file_upload.source, file_upload.destination) }
|
|
206
|
+
rescue StandardError => e
|
|
207
|
+
raise Sdk::Error, "Failed to upload files: #{e.message}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Searches for files containing a pattern, similar to the grep command.
|
|
211
|
+
#
|
|
212
|
+
# @param path [String] Path to the file or directory to search. If the path is a directory,
|
|
213
|
+
# the search will be performed recursively. Relative paths are resolved based
|
|
214
|
+
# on the sandbox working directory.
|
|
215
|
+
# @param pattern [String] Search pattern to match against file contents.
|
|
216
|
+
# @return [Array<DaytonaApiClient::Match>] List of matches found in files
|
|
217
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# # Search for TODOs in Ruby files
|
|
221
|
+
# matches = sandbox.fs.find_files("workspace/src", "TODO:")
|
|
222
|
+
# matches.each do |match|
|
|
223
|
+
# puts "#{match.file}:#{match.line}: #{match.content.strip}"
|
|
224
|
+
# end
|
|
225
|
+
def find_files(path, pattern)
|
|
226
|
+
toolbox_api.find_in_files(sandbox_id, path, pattern)
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
raise Sdk::Error, "Failed to find files: #{e.message}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Searches for files and directories whose names match the specified pattern.
|
|
232
|
+
# The pattern can be a simple string or a glob pattern.
|
|
233
|
+
#
|
|
234
|
+
# @param path [String] Path to the root directory to start search from. Relative paths are resolved
|
|
235
|
+
# based on the sandbox working directory.
|
|
236
|
+
# @param pattern [String] Pattern to match against file names. Supports glob
|
|
237
|
+
# patterns (e.g., "*.rb" for Ruby files).
|
|
238
|
+
# @return [DaytonaApiClient::SearchFilesResponse]
|
|
239
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
# # Find all Ruby files
|
|
243
|
+
# result = sandbox.fs.search_files("workspace", "*.rb")
|
|
244
|
+
# result.files.each { |file| puts file }
|
|
245
|
+
#
|
|
246
|
+
# # Find files with specific prefix
|
|
247
|
+
# result = sandbox.fs.search_files("workspace/data", "test_*")
|
|
248
|
+
# puts "Found #{result.files.length} test files"
|
|
249
|
+
def search_files(path, pattern)
|
|
250
|
+
toolbox_api.search_files(sandbox_id, path, pattern)
|
|
251
|
+
rescue StandardError => e
|
|
252
|
+
raise Sdk::Error, "Failed to search files: #{e.message}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Moves or renames a file or directory. The parent directory of the destination must exist.
|
|
256
|
+
#
|
|
257
|
+
# @param source [String] Path to the source file or directory. Relative paths are resolved
|
|
258
|
+
# based on the sandbox working directory.
|
|
259
|
+
# @param destination [String] Path to the destination. Relative paths are resolved based on
|
|
260
|
+
# the sandbox working directory.
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
263
|
+
#
|
|
264
|
+
# @example
|
|
265
|
+
# # Rename a file
|
|
266
|
+
# sandbox.fs.move_files(
|
|
267
|
+
# "workspace/data/old_name.txt",
|
|
268
|
+
# "workspace/data/new_name.txt"
|
|
269
|
+
# )
|
|
270
|
+
#
|
|
271
|
+
# # Move a file to a different directory
|
|
272
|
+
# sandbox.fs.move_files(
|
|
273
|
+
# "workspace/data/file.txt",
|
|
274
|
+
# "workspace/archive/file.txt"
|
|
275
|
+
# )
|
|
276
|
+
#
|
|
277
|
+
# # Move a directory
|
|
278
|
+
# sandbox.fs.move_files(
|
|
279
|
+
# "workspace/old_dir",
|
|
280
|
+
# "workspace/new_dir"
|
|
281
|
+
# )
|
|
282
|
+
def move_files(source, destination)
|
|
283
|
+
toolbox_api.move_file(sandbox_id, source, destination)
|
|
284
|
+
rescue StandardError => e
|
|
285
|
+
raise Sdk::Error, "Failed to move files: #{e.message}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Performs search and replace operations across multiple files.
|
|
289
|
+
#
|
|
290
|
+
# @param files [Array<String>] List of file paths to perform replacements in. Relative paths are
|
|
291
|
+
# resolved based on the sandbox working directory.
|
|
292
|
+
# @param pattern [String] Pattern to search for.
|
|
293
|
+
# @param new_value [String] Text to replace matches with.
|
|
294
|
+
# @return [Array<DaytonaApiClient::ReplaceResult>] List of results indicating replacements made in each file
|
|
295
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
296
|
+
#
|
|
297
|
+
# @example
|
|
298
|
+
# # Replace in specific files
|
|
299
|
+
# results = sandbox.fs.replace_in_files(
|
|
300
|
+
# files: ["workspace/src/file1.rb", "workspace/src/file2.rb"],
|
|
301
|
+
# pattern: "old_function",
|
|
302
|
+
# new_value: "new_function"
|
|
303
|
+
# )
|
|
304
|
+
#
|
|
305
|
+
# # Print results
|
|
306
|
+
# results.each do |result|
|
|
307
|
+
# if result.success
|
|
308
|
+
# puts "#{result.file}: #{result.success}"
|
|
309
|
+
# else
|
|
310
|
+
# puts "#{result.file}: #{result.error}"
|
|
311
|
+
# end
|
|
312
|
+
# end
|
|
313
|
+
def replace_in_files(files:, pattern:, new_value:)
|
|
314
|
+
replace_request = DaytonaApiClient::ReplaceRequest.new(
|
|
315
|
+
files: files,
|
|
316
|
+
pattern: pattern,
|
|
317
|
+
new_value: new_value
|
|
318
|
+
)
|
|
319
|
+
toolbox_api.replace_in_files(sandbox_id, replace_request)
|
|
320
|
+
rescue StandardError => e
|
|
321
|
+
raise Sdk::Error, "Failed to replace in files: #{e.message}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Sets permissions and ownership for a file or directory. Any of the parameters can be nil
|
|
325
|
+
# to leave that attribute unchanged.
|
|
326
|
+
#
|
|
327
|
+
# @param path [String] Path to the file or directory. Relative paths are resolved based on
|
|
328
|
+
# the sandbox working directory.
|
|
329
|
+
# @param mode [String, nil] File mode/permissions in octal format (e.g., "644" for rw-r--r--).
|
|
330
|
+
# @param owner [String, nil] User owner of the file.
|
|
331
|
+
# @param group [String, nil] Group owner of the file.
|
|
332
|
+
# @return [void]
|
|
333
|
+
# @raise [Daytona::Sdk::Error] If the operation fails
|
|
334
|
+
#
|
|
335
|
+
# @example
|
|
336
|
+
# # Make a file executable
|
|
337
|
+
# sandbox.fs.set_file_permissions(
|
|
338
|
+
# path: "workspace/scripts/run.sh",
|
|
339
|
+
# mode: "755" # rwxr-xr-x
|
|
340
|
+
# )
|
|
341
|
+
#
|
|
342
|
+
# # Change file owner
|
|
343
|
+
# sandbox.fs.set_file_permissions(
|
|
344
|
+
# path: "workspace/data/file.txt",
|
|
345
|
+
# owner: "daytona",
|
|
346
|
+
# group: "daytona"
|
|
347
|
+
# )
|
|
348
|
+
def set_file_permissions(path:, mode: nil, owner: nil, group: nil)
|
|
349
|
+
opts = {}
|
|
350
|
+
opts[:mode] = mode if mode
|
|
351
|
+
opts[:owner] = owner if owner
|
|
352
|
+
opts[:group] = group if group
|
|
353
|
+
|
|
354
|
+
toolbox_api.set_file_permissions(sandbox_id, path, opts)
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
raise Sdk::Error, "Failed to set file permissions: #{e.message}"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|