nightona 0.191.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/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +190 -0
- data/README.md +184 -0
- data/Rakefile +12 -0
- data/lib/nightona/code_interpreter.rb +359 -0
- data/lib/nightona/common/charts.rb +124 -0
- data/lib/nightona/common/code_interpreter.rb +56 -0
- data/lib/nightona/common/code_language.rb +14 -0
- data/lib/nightona/common/file_system.rb +26 -0
- data/lib/nightona/common/git.rb +19 -0
- data/lib/nightona/common/image.rb +500 -0
- data/lib/nightona/common/nightona.rb +230 -0
- data/lib/nightona/common/process.rb +149 -0
- data/lib/nightona/common/pty.rb +309 -0
- data/lib/nightona/common/resources.rb +39 -0
- data/lib/nightona/common/response.rb +83 -0
- data/lib/nightona/common/snapshot.rb +124 -0
- data/lib/nightona/computer_use.rb +919 -0
- data/lib/nightona/config.rb +116 -0
- data/lib/nightona/file_system.rb +451 -0
- data/lib/nightona/file_transfer.rb +383 -0
- data/lib/nightona/git.rb +334 -0
- data/lib/nightona/lsp_server.rb +139 -0
- data/lib/nightona/nightona.rb +336 -0
- data/lib/nightona/object_storage.rb +172 -0
- data/lib/nightona/otel.rb +183 -0
- data/lib/nightona/process.rb +550 -0
- data/lib/nightona/sandbox.rb +751 -0
- data/lib/nightona/sdk/version.rb +10 -0
- data/lib/nightona/sdk.rb +56 -0
- data/lib/nightona/snapshot_service.rb +238 -0
- data/lib/nightona/util.rb +80 -0
- data/lib/nightona/volume.rb +46 -0
- data/lib/nightona/volume_service.rb +61 -0
- data/lib/nightona.rb +10 -0
- data/project.json +100 -0
- data/scripts/generate-docs.rb +402 -0
- data/sig/nightona/sdk.rbs +6 -0
- metadata +242 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Copyright Daytona Platforms Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'dotenv'
|
|
7
|
+
|
|
8
|
+
module Nightona
|
|
9
|
+
class Config
|
|
10
|
+
API_URL = 'http://localhost:3000/api'
|
|
11
|
+
|
|
12
|
+
# API key for authentication with the Nightona API
|
|
13
|
+
#
|
|
14
|
+
# @return [String, nil] Nightona API key
|
|
15
|
+
attr_accessor :api_key
|
|
16
|
+
|
|
17
|
+
# JWT token for authentication with the Nightona API
|
|
18
|
+
#
|
|
19
|
+
# @return [String, nil] Nightona JWT token
|
|
20
|
+
attr_accessor :jwt_token
|
|
21
|
+
|
|
22
|
+
# URL of the Nightona API
|
|
23
|
+
#
|
|
24
|
+
# @return [String, nil] Nightona API URL
|
|
25
|
+
attr_accessor :api_url
|
|
26
|
+
|
|
27
|
+
# Organization ID for authentication with the Nightona API
|
|
28
|
+
#
|
|
29
|
+
# @return [String, nil] Nightona API URL
|
|
30
|
+
attr_accessor :organization_id
|
|
31
|
+
|
|
32
|
+
# Target environment for sandboxes
|
|
33
|
+
#
|
|
34
|
+
# @return [String, nil] Nightona target
|
|
35
|
+
attr_accessor :target
|
|
36
|
+
|
|
37
|
+
# Enable OpenTelemetry tracing for SDK operations.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean, nil]
|
|
40
|
+
attr_accessor :otel_enabled
|
|
41
|
+
|
|
42
|
+
# Experimental configuration options
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash, nil] Experimental configuration hash
|
|
45
|
+
attr_accessor :_experimental
|
|
46
|
+
|
|
47
|
+
# Initializes a new Nightona::Config object.
|
|
48
|
+
#
|
|
49
|
+
# @param api_key [String, nil] Nightona API key. Defaults to ENV['NIGHTONA_API_KEY'].
|
|
50
|
+
# @param jwt_token [String, nil] Nightona JWT token. Defaults to ENV['NIGHTONA_JWT_TOKEN'].
|
|
51
|
+
# @param api_url [String, nil] Nightona API URL. Defaults to ENV['NIGHTONA_API_URL'] or Nightona::Config::API_URL.
|
|
52
|
+
# @param organization_id [String, nil] Nightona organization ID. Defaults to ENV['NIGHTONA_ORGANIZATION_ID'].
|
|
53
|
+
# @param target [String, nil] Nightona target. Defaults to ENV['NIGHTONA_TARGET'].
|
|
54
|
+
# @param otel_enabled [Boolean, nil] Enable OpenTelemetry tracing for SDK operations.
|
|
55
|
+
# @param _experimental [Hash, nil] Experimental configuration options.
|
|
56
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
57
|
+
api_key: nil,
|
|
58
|
+
jwt_token: nil,
|
|
59
|
+
api_url: nil,
|
|
60
|
+
organization_id: nil,
|
|
61
|
+
target: nil,
|
|
62
|
+
otel_enabled: nil,
|
|
63
|
+
_experimental: nil
|
|
64
|
+
)
|
|
65
|
+
@env_reader = nightona_env_reader
|
|
66
|
+
|
|
67
|
+
@api_key = api_key || @env_reader.call('NIGHTONA_API_KEY')
|
|
68
|
+
@jwt_token = jwt_token || @env_reader.call('NIGHTONA_JWT_TOKEN')
|
|
69
|
+
@api_url = api_url || @env_reader.call('NIGHTONA_API_URL') || API_URL
|
|
70
|
+
@target = target || @env_reader.call('NIGHTONA_TARGET')
|
|
71
|
+
@organization_id = organization_id || @env_reader.call('NIGHTONA_ORGANIZATION_ID')
|
|
72
|
+
@otel_enabled = otel_enabled
|
|
73
|
+
@_experimental = _experimental
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Reads a NIGHTONA_-prefixed environment variable using the same precedence
|
|
77
|
+
# as the Config initializer: runtime ENV first, then .env.local, then .env.
|
|
78
|
+
# For backwards compatibility, the legacy DAYTONA_-prefixed variable is used
|
|
79
|
+
# as a fallback at each level when the NIGHTONA_ one is not set.
|
|
80
|
+
# Only names starting with NIGHTONA_ are accepted.
|
|
81
|
+
#
|
|
82
|
+
# @param name [String] The environment variable name. Must start with NIGHTONA_.
|
|
83
|
+
# @return [String, nil] The value of the environment variable, or nil if not set.
|
|
84
|
+
# @raise [ArgumentError] If name does not start with NIGHTONA_.
|
|
85
|
+
def read_env(name)
|
|
86
|
+
@env_reader.call(name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Returns a lambda that looks up NIGHTONA_-prefixed env vars without writing to ENV.
|
|
92
|
+
# Files are parsed once; lookups check runtime env first, then .env.local, then .env.
|
|
93
|
+
# Legacy DAYTONA_-prefixed variables are honored as a fallback.
|
|
94
|
+
def nightona_env_reader
|
|
95
|
+
file_vars = {}
|
|
96
|
+
env_file = File.join(Dir.pwd, '.env')
|
|
97
|
+
file_vars.merge!(nightona_filter(Dotenv.parse(env_file))) if File.exist?(env_file)
|
|
98
|
+
env_local_file = File.join(Dir.pwd, '.env.local')
|
|
99
|
+
file_vars.merge!(nightona_filter(Dotenv.parse(env_local_file))) if File.exist?(env_local_file)
|
|
100
|
+
|
|
101
|
+
lambda do |name|
|
|
102
|
+
raise ArgumentError, "Variable must start with 'NIGHTONA_', got '#{name}'" unless name.start_with?('NIGHTONA_')
|
|
103
|
+
|
|
104
|
+
legacy_name = name.sub(/\ANIGHTONA_/, 'DAYTONA_')
|
|
105
|
+
return ENV[name] if ENV.key?(name)
|
|
106
|
+
return ENV[legacy_name] if ENV.key?(legacy_name)
|
|
107
|
+
|
|
108
|
+
file_vars.key?(name) ? file_vars[name] : file_vars[legacy_name]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def nightona_filter(env_hash)
|
|
113
|
+
env_hash.select { |k, _| k.start_with?('NIGHTONA_') || k.start_with?('DAYTONA_') }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# Copyright Daytona Platforms Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require_relative 'file_transfer'
|
|
9
|
+
|
|
10
|
+
module Nightona
|
|
11
|
+
class FileSystem # rubocop:disable Metrics/ClassLength
|
|
12
|
+
include Instrumentation
|
|
13
|
+
|
|
14
|
+
# @return [String] The Sandbox ID
|
|
15
|
+
attr_reader :sandbox_id
|
|
16
|
+
|
|
17
|
+
# @return [NightonaToolboxApiClient::FileSystemApi] API client for Sandbox operations
|
|
18
|
+
attr_reader :toolbox_api
|
|
19
|
+
|
|
20
|
+
# Initializes a new FileSystem instance.
|
|
21
|
+
#
|
|
22
|
+
# @param sandbox_id [String] The Sandbox ID
|
|
23
|
+
# @param toolbox_api [NightonaToolboxApiClient::FileSystemApi] API client for Sandbox operations
|
|
24
|
+
# @param otel_state [Nightona::OtelState, nil]
|
|
25
|
+
def initialize(sandbox_id:, toolbox_api:, otel_state: nil)
|
|
26
|
+
@sandbox_id = sandbox_id
|
|
27
|
+
@toolbox_api = toolbox_api
|
|
28
|
+
@otel_state = otel_state
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Creates a new directory in the Sandbox at the specified path with the given
|
|
32
|
+
# permissions.
|
|
33
|
+
#
|
|
34
|
+
# @param path [String] Path where the folder should be created. Relative paths are resolved based
|
|
35
|
+
# on the sandbox working directory.
|
|
36
|
+
# @param mode [String] Folder permissions in octal format (e.g., "755" for rwxr-xr-x).
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# # Create a directory with standard permissions
|
|
42
|
+
# sandbox.fs.create_folder("workspace/data", "755")
|
|
43
|
+
#
|
|
44
|
+
# # Create a private directory
|
|
45
|
+
# sandbox.fs.create_folder("workspace/secrets", "700")
|
|
46
|
+
def create_folder(path, mode)
|
|
47
|
+
Sdk.logger.debug("Creating folder #{path} with mode #{mode}")
|
|
48
|
+
toolbox_api.create_folder(path, mode)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
raise Sdk::Error, "Failed to create folder: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Deletes a file from the Sandbox.
|
|
54
|
+
#
|
|
55
|
+
# @param path [String] Path to the file to delete. Relative paths are resolved based on the sandbox working directory.
|
|
56
|
+
# @param recursive [Boolean] If the file is a directory, this must be true to delete it.
|
|
57
|
+
# @return [void]
|
|
58
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# # Delete a file
|
|
62
|
+
# sandbox.fs.delete_file("workspace/data/old_file.txt")
|
|
63
|
+
#
|
|
64
|
+
# # Delete a directory recursively
|
|
65
|
+
# sandbox.fs.delete_file("workspace/old_dir", recursive: true)
|
|
66
|
+
def delete_file(path, recursive: false)
|
|
67
|
+
toolbox_api.delete_file(path, { recursive: })
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
raise Sdk::Error, "Failed to delete file: #{e.message}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Gets detailed information about a file or directory, including its
|
|
73
|
+
# size, permissions, and timestamps.
|
|
74
|
+
#
|
|
75
|
+
# @param path [String] Path to the file or directory. Relative paths are resolved based
|
|
76
|
+
# on the sandbox working directory.
|
|
77
|
+
# @return [NightonaApiClient::FileInfo] Detailed file information
|
|
78
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# # Get file metadata
|
|
82
|
+
# info = sandbox.fs.get_file_info("workspace/data/file.txt")
|
|
83
|
+
# puts "Size: #{info.size} bytes"
|
|
84
|
+
# puts "Modified: #{info.mod_time}"
|
|
85
|
+
# puts "Mode: #{info.mode}"
|
|
86
|
+
#
|
|
87
|
+
# # Check if path is a directory
|
|
88
|
+
# info = sandbox.fs.get_file_info("workspace/data")
|
|
89
|
+
# puts "Path is a directory" if info.is_dir
|
|
90
|
+
def get_file_info(path)
|
|
91
|
+
toolbox_api.get_file_info(path)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
raise Sdk::Error, "Failed to get file info: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Lists files and directories in a given path and returns their information, similar to the ls -l command.
|
|
97
|
+
#
|
|
98
|
+
# @param path [String] Path to the directory to list contents from. Relative paths are resolved
|
|
99
|
+
# based on the sandbox working directory.
|
|
100
|
+
# @return [Array<NightonaApiClient::FileInfo>] List of file and directory information
|
|
101
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# # List directory contents
|
|
105
|
+
# files = sandbox.fs.list_files("workspace/data")
|
|
106
|
+
#
|
|
107
|
+
# # Print files and their sizes
|
|
108
|
+
# files.each do |file|
|
|
109
|
+
# puts "#{file.name}: #{file.size} bytes" unless file.is_dir
|
|
110
|
+
# end
|
|
111
|
+
#
|
|
112
|
+
# # List only directories
|
|
113
|
+
# dirs = files.select(&:is_dir)
|
|
114
|
+
# puts "Subdirectories: #{dirs.map(&:name).join(', ')}"
|
|
115
|
+
def list_files(path)
|
|
116
|
+
toolbox_api.list_files({ path: })
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
raise Sdk::Error, "Failed to list files: #{e.message}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Downloads a file from the Sandbox. Returns the file contents as a string.
|
|
122
|
+
# This method is useful when you want to load the file into memory without saving it to disk.
|
|
123
|
+
# It can only be used for smaller files.
|
|
124
|
+
#
|
|
125
|
+
# @param remote_path [String] Path to the file in the Sandbox. Relative paths are resolved based
|
|
126
|
+
# on the sandbox working directory.
|
|
127
|
+
# @param local_path [String, nil] Optional path to save the file locally. If provided, the file will be saved to disk.
|
|
128
|
+
# @return [File, nil] The file if local_path is nil, otherwise nil
|
|
129
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# # Download and get file content
|
|
133
|
+
# content = sandbox.fs.download_file("workspace/data/file.txt")
|
|
134
|
+
# puts content
|
|
135
|
+
#
|
|
136
|
+
# # Download and save a file locally
|
|
137
|
+
# sandbox.fs.download_file("workspace/data/file.txt", "local_copy.txt")
|
|
138
|
+
# size_mb = File.size("local_copy.txt") / 1024.0 / 1024.0
|
|
139
|
+
# puts "Size of the downloaded file: #{size_mb} MB"
|
|
140
|
+
def download_file(remote_path, local_path = nil) # rubocop:disable Metrics/MethodLength
|
|
141
|
+
file = toolbox_api.download_file(remote_path)
|
|
142
|
+
|
|
143
|
+
if local_path
|
|
144
|
+
|
|
145
|
+
parent_dir = File.dirname(local_path)
|
|
146
|
+
FileUtils.mkdir_p(parent_dir) unless parent_dir == '.'
|
|
147
|
+
|
|
148
|
+
File.binwrite(local_path, file.open.read)
|
|
149
|
+
nil
|
|
150
|
+
else
|
|
151
|
+
file
|
|
152
|
+
end
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
raise Sdk::Error, "Failed to download file: #{e.message}"
|
|
155
|
+
end
|
|
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
|
+
# @param on_progress [Proc, nil] Optional callback invoked with a Nightona::DownloadProgress
|
|
166
|
+
# struct containing bytes_received (Integer) and total_bytes (Integer or nil).
|
|
167
|
+
# @param cancel_event [#set?, nil] Optional cancellation token (anything responding to +set?+;
|
|
168
|
+
# the standard library's +Concurrent::Event+ or a small ad-hoc object both work). When set
|
|
169
|
+
# during streaming, the next chunk raises Nightona::Sdk::Error and the underlying HTTP
|
|
170
|
+
# connection is torn down.
|
|
171
|
+
# @yield [chunk] Yields each chunk of file content as it arrives
|
|
172
|
+
# @yieldparam chunk [String] A binary string chunk of file content
|
|
173
|
+
# @return [Enumerator, nil] An Enumerator yielding chunks if no block given, nil otherwise
|
|
174
|
+
# @raise [Nightona::Sdk::Error] If the file does not exist, the operation fails, or
|
|
175
|
+
# +cancel_event+ is set during streaming
|
|
176
|
+
#
|
|
177
|
+
# @example Stream to a local file without loading into memory
|
|
178
|
+
# File.open("local_copy.bin", "wb") do |f|
|
|
179
|
+
# sandbox.fs.download_file_stream("workspace/large-file.bin") { |chunk| f.write(chunk) }
|
|
180
|
+
# end
|
|
181
|
+
#
|
|
182
|
+
# @example Collect chunks with an Enumerator
|
|
183
|
+
# content = sandbox.fs.download_file_stream("workspace/data.json").reduce(:+)
|
|
184
|
+
# puts content
|
|
185
|
+
def download_file_stream(remote_path, timeout: 30 * 60, on_progress: nil, cancel_event: nil, &)
|
|
186
|
+
return enum_for(__method__, remote_path, timeout:, on_progress:, cancel_event:) unless block_given?
|
|
187
|
+
|
|
188
|
+
FileTransfer.stream_download(api_client: toolbox_api.api_client, remote_path: remote_path,
|
|
189
|
+
timeout: timeout, on_progress: on_progress,
|
|
190
|
+
cancel_event: cancel_event, &)
|
|
191
|
+
nil
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
raise Sdk::Error, "Failed to download file: #{e.message}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Uploads a file to the specified path in the Sandbox. If a file already exists at
|
|
197
|
+
# the destination path, it will be overwritten.
|
|
198
|
+
#
|
|
199
|
+
# @param source [String, IO] File contents as a string/bytes or a local file path or IO object.
|
|
200
|
+
# @param remote_path [String] Path to the destination file. Relative paths are resolved based on
|
|
201
|
+
# the sandbox working directory.
|
|
202
|
+
# @return [void]
|
|
203
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
204
|
+
#
|
|
205
|
+
# @example
|
|
206
|
+
# # Upload a text file from string content
|
|
207
|
+
# content = "Hello, World!"
|
|
208
|
+
# sandbox.fs.upload_file(content, "tmp/hello.txt")
|
|
209
|
+
#
|
|
210
|
+
# # Upload a local file
|
|
211
|
+
# sandbox.fs.upload_file("local_file.txt", "tmp/file.txt")
|
|
212
|
+
#
|
|
213
|
+
# # Upload binary data
|
|
214
|
+
# data = { key: "value" }.to_json
|
|
215
|
+
# sandbox.fs.upload_file(data, "tmp/config.json")
|
|
216
|
+
def upload_file(source, remote_path)
|
|
217
|
+
if source.is_a?(String) && File.exist?(source)
|
|
218
|
+
# Source is a file path
|
|
219
|
+
File.open(source, 'rb') { |file| toolbox_api.upload_file(remote_path, file) }
|
|
220
|
+
elsif source.respond_to?(:read)
|
|
221
|
+
# Source is an IO object
|
|
222
|
+
toolbox_api.upload_file(remote_path, source)
|
|
223
|
+
else
|
|
224
|
+
# Tempfile.create yields a ::File (works with Typhoeus) but deletes
|
|
225
|
+
# on block exit — too early if curl reads asynchronously. Write via
|
|
226
|
+
# Tempfile, close it, then reopen as ::File for the upload.
|
|
227
|
+
tmp = Tempfile.new('nightona_upload')
|
|
228
|
+
begin
|
|
229
|
+
tmp.binmode
|
|
230
|
+
tmp.write(source.to_s.b)
|
|
231
|
+
tmp.close
|
|
232
|
+
File.open(tmp.path, 'rb') { |file| toolbox_api.upload_file(remote_path, file) }
|
|
233
|
+
ensure
|
|
234
|
+
tmp.unlink
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
raise Sdk::Error, "Failed to upload file: #{e.message}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Streams +source+ to the Sandbox without buffering its contents in memory, with
|
|
242
|
+
# optional progress reporting.
|
|
243
|
+
#
|
|
244
|
+
# @param source [String, IO] A local file path or any IO-like object responding to
|
|
245
|
+
# +read(n)+. Strings that don't reference an existing file are uploaded as their
|
|
246
|
+
# raw bytes (still streamed, just from memory).
|
|
247
|
+
# @param remote_path [String] Destination path in the Sandbox.
|
|
248
|
+
# @param timeout [Integer] Timeout in seconds. 0 means no timeout. Default 30 minutes.
|
|
249
|
+
# @param on_progress [Proc, nil] Optional callback invoked with a
|
|
250
|
+
# +Nightona::UploadProgress+ struct as libcurl reports bytes actually uploaded.
|
|
251
|
+
# @param cancel_event [#set?, nil] Optional cancellation token. When set while
|
|
252
|
+
# staging a non-file source or during the libcurl upload, the operation raises
|
|
253
|
+
# Nightona::Sdk::Error and the in-progress upload is aborted (no destination file
|
|
254
|
+
# is left on the sandbox thanks to the daemon's atomic-rename behaviour).
|
|
255
|
+
# @return [void]
|
|
256
|
+
# @raise [Nightona::Sdk::Error] If the operation fails or +cancel_event+ is set.
|
|
257
|
+
#
|
|
258
|
+
# @example
|
|
259
|
+
# File.open("large.bin", "rb") do |f|
|
|
260
|
+
# sandbox.fs.upload_file_stream(f, "tmp/large.bin",
|
|
261
|
+
# on_progress: ->(p) { puts "#{p.bytes_sent} bytes sent" })
|
|
262
|
+
# end
|
|
263
|
+
def upload_file_stream(source, remote_path, timeout: 30 * 60, on_progress: nil, cancel_event: nil)
|
|
264
|
+
FileTransfer.stream_upload(api_client: toolbox_api.api_client, remote_path: remote_path,
|
|
265
|
+
source: source, timeout: timeout, on_progress: on_progress,
|
|
266
|
+
cancel_event: cancel_event)
|
|
267
|
+
rescue StandardError => e
|
|
268
|
+
raise Sdk::Error, "Failed to upload file: #{e.message}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Uploads multiple files to the Sandbox. If files already exist at the destination paths,
|
|
272
|
+
# they will be overwritten.
|
|
273
|
+
#
|
|
274
|
+
# @param files [Array<FileUpload>] List of files to upload.
|
|
275
|
+
# @return [void]
|
|
276
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
277
|
+
#
|
|
278
|
+
# @example
|
|
279
|
+
# # Upload multiple files
|
|
280
|
+
# files = [
|
|
281
|
+
# FileUpload.new("Content of file 1", "/tmp/file1.txt"),
|
|
282
|
+
# FileUpload.new("workspace/data/file2.txt", "/tmp/file2.txt"),
|
|
283
|
+
# FileUpload.new('{"key": "value"}', "/tmp/config.json")
|
|
284
|
+
# ]
|
|
285
|
+
# sandbox.fs.upload_files(files)
|
|
286
|
+
def upload_files(files)
|
|
287
|
+
files.each { |file_upload| upload_file(file_upload.source, file_upload.destination) }
|
|
288
|
+
rescue StandardError => e
|
|
289
|
+
raise Sdk::Error, "Failed to upload files: #{e.message}"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Searches for files containing a pattern, similar to the grep command.
|
|
293
|
+
#
|
|
294
|
+
# @param path [String] Path to the file or directory to search. If the path is a directory,
|
|
295
|
+
# the search will be performed recursively. Relative paths are resolved based
|
|
296
|
+
# on the sandbox working directory.
|
|
297
|
+
# @param pattern [String] Search pattern to match against file contents.
|
|
298
|
+
# @return [Array<NightonaApiClient::Match>] List of matches found in files
|
|
299
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
300
|
+
#
|
|
301
|
+
# @example
|
|
302
|
+
# # Search for TODOs in Ruby files
|
|
303
|
+
# matches = sandbox.fs.find_files("workspace/src", "TODO:")
|
|
304
|
+
# matches.each do |match|
|
|
305
|
+
# puts "#{match.file}:#{match.line}: #{match.content.strip}"
|
|
306
|
+
# end
|
|
307
|
+
def find_files(path, pattern)
|
|
308
|
+
toolbox_api.find_in_files(path, pattern)
|
|
309
|
+
rescue StandardError => e
|
|
310
|
+
raise Sdk::Error, "Failed to find files: #{e.message}"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Searches for files and directories whose names match the specified pattern.
|
|
314
|
+
# The pattern can be a simple string or a glob pattern.
|
|
315
|
+
#
|
|
316
|
+
# @param path [String] Path to the root directory to start search from. Relative paths are resolved
|
|
317
|
+
# based on the sandbox working directory.
|
|
318
|
+
# @param pattern [String] Pattern to match against file names. Supports glob
|
|
319
|
+
# patterns (e.g., "*.rb" for Ruby files).
|
|
320
|
+
# @return [NightonaApiClient::SearchFilesResponse]
|
|
321
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
322
|
+
#
|
|
323
|
+
# @example
|
|
324
|
+
# # Find all Ruby files
|
|
325
|
+
# result = sandbox.fs.search_files("workspace", "*.rb")
|
|
326
|
+
# result.files.each { |file| puts file }
|
|
327
|
+
#
|
|
328
|
+
# # Find files with specific prefix
|
|
329
|
+
# result = sandbox.fs.search_files("workspace/data", "test_*")
|
|
330
|
+
# puts "Found #{result.files.length} test files"
|
|
331
|
+
def search_files(path, pattern)
|
|
332
|
+
toolbox_api.search_files(path, pattern)
|
|
333
|
+
rescue StandardError => e
|
|
334
|
+
raise Sdk::Error, "Failed to search files: #{e.message}"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Moves or renames a file or directory. The parent directory of the destination must exist.
|
|
338
|
+
#
|
|
339
|
+
# @param source [String] Path to the source file or directory. Relative paths are resolved
|
|
340
|
+
# based on the sandbox working directory.
|
|
341
|
+
# @param destination [String] Path to the destination. Relative paths are resolved based on
|
|
342
|
+
# the sandbox working directory.
|
|
343
|
+
# @return [void]
|
|
344
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
345
|
+
#
|
|
346
|
+
# @example
|
|
347
|
+
# # Rename a file
|
|
348
|
+
# sandbox.fs.move_files(
|
|
349
|
+
# "workspace/data/old_name.txt",
|
|
350
|
+
# "workspace/data/new_name.txt"
|
|
351
|
+
# )
|
|
352
|
+
#
|
|
353
|
+
# # Move a file to a different directory
|
|
354
|
+
# sandbox.fs.move_files(
|
|
355
|
+
# "workspace/data/file.txt",
|
|
356
|
+
# "workspace/archive/file.txt"
|
|
357
|
+
# )
|
|
358
|
+
#
|
|
359
|
+
# # Move a directory
|
|
360
|
+
# sandbox.fs.move_files(
|
|
361
|
+
# "workspace/old_dir",
|
|
362
|
+
# "workspace/new_dir"
|
|
363
|
+
# )
|
|
364
|
+
def move_files(source, destination)
|
|
365
|
+
toolbox_api.move_file(source, destination)
|
|
366
|
+
rescue StandardError => e
|
|
367
|
+
raise Sdk::Error, "Failed to move files: #{e.message}"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Performs search and replace operations across multiple files.
|
|
371
|
+
#
|
|
372
|
+
# @param files [Array<String>] List of file paths to perform replacements in. Relative paths are
|
|
373
|
+
# resolved based on the sandbox working directory.
|
|
374
|
+
# @param pattern [String] Pattern to search for.
|
|
375
|
+
# @param new_value [String] Text to replace matches with.
|
|
376
|
+
# @return [Array<NightonaApiClient::ReplaceResult>] List of results indicating replacements made in each file
|
|
377
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
378
|
+
#
|
|
379
|
+
# @example
|
|
380
|
+
# # Replace in specific files
|
|
381
|
+
# results = sandbox.fs.replace_in_files(
|
|
382
|
+
# files: ["workspace/src/file1.rb", "workspace/src/file2.rb"],
|
|
383
|
+
# pattern: "old_function",
|
|
384
|
+
# new_value: "new_function"
|
|
385
|
+
# )
|
|
386
|
+
#
|
|
387
|
+
# # Print results
|
|
388
|
+
# results.each do |result|
|
|
389
|
+
# if result.success
|
|
390
|
+
# puts "#{result.file}: #{result.success}"
|
|
391
|
+
# else
|
|
392
|
+
# puts "#{result.file}: #{result.error}"
|
|
393
|
+
# end
|
|
394
|
+
# end
|
|
395
|
+
def replace_in_files(files:, pattern:, new_value:)
|
|
396
|
+
replace_request = NightonaApiClient::ReplaceRequest.new(
|
|
397
|
+
files: files,
|
|
398
|
+
pattern: pattern,
|
|
399
|
+
new_value: new_value
|
|
400
|
+
)
|
|
401
|
+
toolbox_api.replace_in_files(replace_request)
|
|
402
|
+
rescue StandardError => e
|
|
403
|
+
raise Sdk::Error, "Failed to replace in files: #{e.message}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Sets permissions and ownership for a file or directory. Any of the parameters can be nil
|
|
407
|
+
# to leave that attribute unchanged.
|
|
408
|
+
#
|
|
409
|
+
# @param path [String] Path to the file or directory. Relative paths are resolved based on
|
|
410
|
+
# the sandbox working directory.
|
|
411
|
+
# @param mode [String, nil] File mode/permissions in octal format (e.g., "644" for rw-r--r--).
|
|
412
|
+
# @param owner [String, nil] User owner of the file.
|
|
413
|
+
# @param group [String, nil] Group owner of the file.
|
|
414
|
+
# @return [void]
|
|
415
|
+
# @raise [Nightona::Sdk::Error] If the operation fails
|
|
416
|
+
#
|
|
417
|
+
# @example
|
|
418
|
+
# # Make a file executable
|
|
419
|
+
# sandbox.fs.set_file_permissions(
|
|
420
|
+
# path: "workspace/scripts/run.sh",
|
|
421
|
+
# mode: "755" # rwxr-xr-x
|
|
422
|
+
# )
|
|
423
|
+
#
|
|
424
|
+
# # Change file owner
|
|
425
|
+
# sandbox.fs.set_file_permissions(
|
|
426
|
+
# path: "workspace/data/file.txt",
|
|
427
|
+
# owner: "nightona",
|
|
428
|
+
# group: "nightona"
|
|
429
|
+
# )
|
|
430
|
+
def set_file_permissions(path:, mode: nil, owner: nil, group: nil)
|
|
431
|
+
opts = {}
|
|
432
|
+
opts[:mode] = mode if mode
|
|
433
|
+
opts[:owner] = owner if owner
|
|
434
|
+
opts[:group] = group if group
|
|
435
|
+
|
|
436
|
+
toolbox_api.set_file_permissions(path, opts)
|
|
437
|
+
rescue StandardError => e
|
|
438
|
+
raise Sdk::Error, "Failed to set file permissions: #{e.message}"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
instrument :create_folder, :delete_file, :get_file_info, :list_files, :download_file,
|
|
442
|
+
:download_file_stream, :upload_file, :upload_files, :find_files,
|
|
443
|
+
:search_files, :move_files, :replace_in_files, :set_file_permissions,
|
|
444
|
+
component: 'FileSystem'
|
|
445
|
+
|
|
446
|
+
private
|
|
447
|
+
|
|
448
|
+
# @return [Nightona::OtelState, nil]
|
|
449
|
+
attr_reader :otel_state
|
|
450
|
+
end
|
|
451
|
+
end
|