daytona 0.126.0.pre.alpha.4

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.
@@ -0,0 +1,289 @@
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 [Daytona::VolumeService]
18
+ attr_reader :volume
19
+
20
+ # @return [DaytonaApiClient::ObjectStorageApi]
21
+ attr_reader :object_storage_api
22
+
23
+ # @return [DaytonaApiClient::SnapshotsApi]
24
+ attr_reader :snapshots_api
25
+
26
+ # @return [Daytona::SnapshotService]
27
+ attr_reader :snapshot
28
+
29
+ # @param config [Daytona::Config] Configuration options. Defaults to Daytona::Config.new
30
+ def initialize(config = Config.new) # rubocop:disable Metrics/AbcSize
31
+ @config = config
32
+ ensure_access_token_defined
33
+ @api_client = build_api_client
34
+ @sandbox_api = DaytonaApiClient::SandboxApi.new(api_client)
35
+ @config_api = DaytonaApiClient::ConfigApi.new(api_client)
36
+ @volume = VolumeService.new(DaytonaApiClient::VolumesApi.new(api_client))
37
+ @object_storage_api = DaytonaApiClient::ObjectStorageApi.new(api_client)
38
+ @snapshots_api = DaytonaApiClient::SnapshotsApi.new(api_client)
39
+ @snapshot = SnapshotService.new(snapshots_api:, object_storage_api:)
40
+ @proxy_toolbox_url = nil
41
+ end
42
+
43
+ # Creates a sandbox with the specified parameters
44
+ #
45
+ # @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
46
+ # @return [Daytona::Sandbox] The created sandbox
47
+ # @raise [Daytona::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
48
+ def create(params = nil, on_snapshot_create_logs: nil)
49
+ if params.nil?
50
+ params = CreateSandboxFromSnapshotParams.new(language: CodeLanguage::PYTHON)
51
+ elsif params.language.nil?
52
+ params.language = CodeLanguage::PYTHON
53
+ end
54
+
55
+ _create(params, on_snapshot_create_logs:)
56
+ end
57
+
58
+ # Deletes a Sandbox.
59
+ #
60
+ # @param sandbox [Daytona::Sandbox]
61
+ # @return [void]
62
+ def delete(sandbox) = sandbox.delete
63
+
64
+ # Gets a Sandbox by its ID.
65
+ #
66
+ # @param id [String]
67
+ # @return [Daytona::Sandbox]
68
+ def get(id)
69
+ sandbox_dto = sandbox_api.get_sandbox(id)
70
+ to_sandbox(sandbox_dto:, code_toolbox: code_toolbox_from_labels(sandbox_dto.labels))
71
+ end
72
+
73
+ # Finds a Sandbox by its ID or labels.
74
+ #
75
+ # @param id [String, Nil]
76
+ # @param labels [Hash<String, String>]
77
+ # @return [Daytona::Sandbox]
78
+ # @raise [Daytona::Sdk::Error]
79
+ def find_one(id: nil, labels: nil)
80
+ return get(id) if id
81
+
82
+ response = list(labels)
83
+ raise Sdk::Error, "No sandbox found with labels #{labels}" if response.items.empty?
84
+
85
+ response.items.first
86
+ end
87
+
88
+ # Lists Sandboxes filtered by labels.
89
+ #
90
+ # @param labels [Hash<String, String>]
91
+ # @param page [Integer, Nil]
92
+ # @param limit [Integer, Nil]
93
+ # @return [Daytona::PaginatedResource]
94
+ # @raise [Daytona::Sdk::Error]
95
+ def list(labels = {}, page: nil, limit: nil) # rubocop:disable Metrics/MethodLength
96
+ raise Sdk::Error, 'page must be positive integer' if page && page < 1
97
+
98
+ raise Sdk::Error, 'limit must be positive integer' if limit && limit < 1
99
+
100
+ response = sandbox_api.list_sandboxes_paginated(labels: JSON.dump(labels), page:, limit:)
101
+
102
+ PaginatedResource.new(
103
+ total: response.total,
104
+ page: response.page,
105
+ total_pages: response.total_pages,
106
+ items: response
107
+ .items
108
+ .map { |sandbox_dto| to_sandbox(sandbox_dto:, code_toolbox: code_toolbox_from_labels(sandbox_dto.labels)) }
109
+ )
110
+ end
111
+
112
+ # Starts a Sandbox and waits for it to be ready.
113
+ #
114
+ # @param sandbox [Daytona::Sandbox]
115
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
116
+ # @return [void]
117
+ def start(sandbox, timeout = Sandbox::DEFAULT_TIMEOUT) = sandbox.start(timeout)
118
+
119
+ # Stops a Sandbox and waits for it to be stopped.
120
+ #
121
+ # @param sandbox [Daytona::Sandbox]
122
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
123
+ # @return [void]
124
+ def stop(sandbox, timeout = Sandbox::DEFAULT_TIMEOUT) = sandbox.stop(timeout)
125
+
126
+ private
127
+
128
+ # Creates a sandbox with the specified parameters
129
+ #
130
+ # @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams] Sandbox creation parameters
131
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
132
+ # @param on_snapshot_create_logs [Proc]
133
+ # @return [Daytona::Sandbox] The created sandbox
134
+ # @raise [Daytona::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
135
+ def _create(params, timeout: 60, on_snapshot_create_logs: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
136
+ raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
137
+
138
+ start_time = Time.now
139
+
140
+ raise Sdk::Error, 'auto_stop_interval must be a non-negative integer' if params.auto_stop_interval&.negative?
141
+
142
+ if params.auto_archive_interval&.negative?
143
+ raise Sdk::Error, 'auto_archive_interval must be a non-negative integer'
144
+ end
145
+
146
+ create_sandbox = DaytonaApiClient::CreateSandbox.new(
147
+ user: params.os_user,
148
+ env: params.env_vars || {},
149
+ labels: params.labels,
150
+ public: params.public,
151
+ target: config.target,
152
+ auto_stop_interval: params.auto_stop_interval,
153
+ auto_archive_interval: params.auto_archive_interval,
154
+ auto_delete_interval: params.auto_delete_interval,
155
+ volumes: params.volumes,
156
+ network_block_all: params.network_block_all,
157
+ network_allow_list: params.network_allow_list
158
+ )
159
+
160
+ create_sandbox.snapshot = params.snapshot if params.respond_to?(:snapshot)
161
+
162
+ if params.respond_to?(:image) && params.image.is_a?(String)
163
+ create_sandbox.build_info = DaytonaApiClient::CreateBuildInfo.new(
164
+ dockerfile_content: Image.base(params.image).dockerfile
165
+ )
166
+ elsif params.respond_to?(:image) && params.image.is_a?(Image)
167
+ create_sandbox.build_info = DaytonaApiClient::CreateBuildInfo.new(
168
+ context_hashes: SnapshotService.process_image_context(object_storage_api, params.image),
169
+ dockerfile_content: params.image.dockerfile
170
+ )
171
+ end
172
+
173
+ if params.respond_to?(:resources)
174
+ create_sandbox.cpu = params.resources&.cpu
175
+ create_sandbox.memory = params.resources&.memory
176
+ create_sandbox.disk = params.resources&.disk
177
+ create_sandbox.gpu = params.resources&.gpu
178
+ end
179
+
180
+ response = sandbox_api.create_sandbox(create_sandbox)
181
+
182
+ if response.state == DaytonaApiClient::SandboxState::PENDING_BUILD && on_snapshot_create_logs
183
+ uri = URI.parse(sandbox_api.api_client.config.base_url)
184
+ uri.path = "/api/sandbox/#{response.id}/build-logs"
185
+ uri.query = 'follow=true'
186
+
187
+ headers = {}
188
+ sandbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
189
+ Util.stream_async(uri:, headers:, on_chunk: on_snapshot_create_logs)
190
+ end
191
+
192
+ sandbox = to_sandbox(sandbox_dto: response, code_toolbox: code_toolbox_from_labels(response.labels))
193
+
194
+ if sandbox.state != DaytonaApiClient::SandboxState::STARTED
195
+ sandbox.wait_for_sandbox_start([0.001, timeout - (Time.now - start_time)].max)
196
+ end
197
+
198
+ sandbox
199
+ end
200
+
201
+ # @return [void]
202
+ # @raise [Daytona::Sdk::Error]
203
+ def ensure_access_token_defined
204
+ return if config.api_key
205
+
206
+ raise Sdk::Error, 'API key or JWT token is required' unless config.jwt_token
207
+ raise Sdk::Error, 'Organization ID is required when using JWT token' unless config.organization_id
208
+ end
209
+
210
+ # @return [DaytonaApiClient::ApiClient]
211
+ def build_api_client
212
+ DaytonaApiClient::ApiClient.new(api_client_config).tap do |client|
213
+ client.default_headers[HEADER_SOURCE] = SOURCE_RUBY
214
+ client.default_headers[HEADER_SDK_VERSION] = Sdk::VERSION
215
+ client.default_headers[HEADER_ORGANIZATION_ID] = config.organization_id if config.jwt_token
216
+ end
217
+ end
218
+
219
+ # @return [DaytonaApiClient::Configuration]
220
+ def api_client_config
221
+ DaytonaApiClient::Configuration.new.configure do |api_config|
222
+ uri = URI(config.api_url)
223
+ api_config.scheme = uri.scheme
224
+ api_config.host = uri.host
225
+ api_config.base_path = uri.path
226
+
227
+ api_config.access_token_getter = proc { config.api_key || config.jwt_token }
228
+ api_config
229
+ end
230
+ end
231
+
232
+ # @param sandbox_dto [DaytonaApiClient::Sandbox]
233
+ # @param code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
234
+ # @return [Daytona::Sandbox]
235
+ def to_sandbox(sandbox_dto:, code_toolbox:)
236
+ Sandbox.new(
237
+ sandbox_dto:,
238
+ config:,
239
+ sandbox_api:,
240
+ code_toolbox:,
241
+ get_proxy_toolbox_url: method(:proxy_toolbox_url)
242
+ )
243
+ end
244
+
245
+ # Gets the proxy toolbox URL from the config API (lazy loaded)
246
+ #
247
+ # @return [String] The proxy toolbox URL
248
+ def proxy_toolbox_url
249
+ @proxy_toolbox_url ||= @config_api.config_controller_get_config.proxy_toolbox_url
250
+ end
251
+
252
+ # Converts a language to a code toolbox
253
+ #
254
+ # @param language [Symbol]
255
+ # @return [Daytona::CodeToolbox]
256
+ # @raise [Daytona::Sdk::Error] If the language is not supported
257
+ def code_toolbox_from_language(language)
258
+ case language
259
+ when CodeLanguage::PYTHON, nil
260
+ SandboxPythonCodeToolbox.new
261
+ when SandboxTsCodeToolbox, CodeLanguage::TYPESCRIPT
262
+ SandboxTsCodeToolbox.new
263
+ else
264
+ raise Sdk::Error, "Unsupported language: #{language}"
265
+ end
266
+ end
267
+
268
+ # Get code toolbox from Sandbox labels
269
+ #
270
+ # @param labels [Hash<String, String>]
271
+ # @return [Daytona::CodeToolbox]
272
+ def code_toolbox_from_labels(labels) = code_toolbox_from_language(labels[LABEL_CODE_TOOLBOX_LANGUAGE]&.to_sym)
273
+
274
+ SOURCE_RUBY = 'ruby-sdk'
275
+ private_constant :SOURCE_RUBY
276
+
277
+ HEADER_SOURCE = 'X-Daytona-Source'
278
+ private_constant :HEADER_SOURCE
279
+
280
+ HEADER_SDK_VERSION = 'X-Daytona-SDK-Version'
281
+ private_constant :HEADER_SDK_VERSION
282
+
283
+ HEADER_ORGANIZATION_ID = 'X-Daytona-Organization-ID'
284
+ private_constant :HEADER_ORGANIZATION_ID
285
+
286
+ LABEL_CODE_TOOLBOX_LANGUAGE = 'code-toolbox-language'
287
+ private_constant :LABEL_CODE_TOOLBOX_LANGUAGE
288
+ end
289
+ 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 [DaytonaToolboxApiClient::FileSystemApi] 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 [DaytonaToolboxApiClient::FileSystemApi] 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(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(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(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({ 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(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(remote_path, file) }
173
+ elsif source.respond_to?(:read)
174
+ # Source is an IO object
175
+ toolbox_api.upload_file(remote_path, 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(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(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(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(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(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(path, opts)
355
+ rescue StandardError => e
356
+ raise Sdk::Error, "Failed to set file permissions: #{e.message}"
357
+ end
358
+ end
359
+ end