@hanzo/runtime 0.0.0-dev

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,414 @@
1
+ /*
2
+ * Copyright 2025 Daytona Platforms Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { FileInfo, Match, ReplaceRequest, ReplaceResult, SearchFilesResponse, ToolboxApi } from '@daytonaio/api-client'
7
+ import { prefixRelativePath } from './utils/Path'
8
+ import * as fs from 'fs'
9
+ import { Readable } from 'stream'
10
+ import FormData from 'form-data'
11
+
12
+ /**
13
+ * Parameters for setting file permissions in the Sandbox.
14
+ *
15
+ * @interface
16
+ * @property {string} [mode] - File mode/permissions in octal format (e.g. "644")
17
+ * @property {string} [owner] - User owner of the file
18
+ * @property {string} [group] - Group owner of the file
19
+ *
20
+ * @example
21
+ * const permissions: FilePermissionsParams = {
22
+ * mode: '644',
23
+ * owner: 'daytona',
24
+ * group: 'users'
25
+ * };
26
+ */
27
+ export type FilePermissionsParams = {
28
+ /** Group owner of the file */
29
+ group?: string
30
+ /** File mode/permissions in octal format (e.g. "644") */
31
+ mode?: string
32
+ /** User owner of the file */
33
+ owner?: string
34
+ }
35
+
36
+ /**
37
+ * Represents a file to be uploaded to the Sandbox.
38
+ *
39
+ * @interface
40
+ * @property {string | Buffer} source - File to upload. If a Buffer, it is interpreted as the file content which is loaded into memory.
41
+ * Make sure it fits into memory, otherwise use the local file path which content will be streamed to the Sandbox.
42
+ * @property {string} destination - Absolute destination path in the Sandbox. Relative paths are resolved based on the user's
43
+ * root directory.
44
+ */
45
+ export interface FileUpload {
46
+ source: string | Buffer
47
+ destination: string
48
+ }
49
+
50
+ /**
51
+ * Provides file system operations within a Sandbox.
52
+ *
53
+ * @class
54
+ */
55
+ export class FileSystem {
56
+ constructor(
57
+ private readonly sandboxId: string,
58
+ private readonly toolboxApi: ToolboxApi,
59
+ private readonly getRootDir: () => Promise<string>,
60
+ ) {}
61
+
62
+ /**
63
+ * Create a new directory in the Sandbox with specified permissions.
64
+ *
65
+ * @param {string} path - Path where the directory should be created. Relative paths are resolved based on the user's
66
+ * root directory.
67
+ * @param {string} mode - Directory permissions in octal format (e.g. "755")
68
+ * @returns {Promise<void>}
69
+ *
70
+ * @example
71
+ * // Create a directory with standard permissions
72
+ * await fs.createFolder('app/data', '755');
73
+ */
74
+ public async createFolder(path: string, mode: string): Promise<void> {
75
+ const response = await this.toolboxApi.createFolder(
76
+ this.sandboxId,
77
+ prefixRelativePath(await this.getRootDir(), path),
78
+ mode,
79
+ )
80
+ return response.data
81
+ }
82
+
83
+ /**
84
+ * Deletes a file or directory from the Sandbox.
85
+ *
86
+ * @param {string} path - Path to the file or directory to delete. Relative paths are resolved based on the user's
87
+ * root directory.
88
+ * @returns {Promise<void>}
89
+ *
90
+ * @example
91
+ * // Delete a file
92
+ * await fs.deleteFile('app/temp.log');
93
+ */
94
+ public async deleteFile(path: string): Promise<void> {
95
+ const response = await this.toolboxApi.deleteFile(this.sandboxId, prefixRelativePath(await this.getRootDir(), path))
96
+ return response.data
97
+ }
98
+
99
+ /**
100
+ * Downloads a file from the Sandbox. This method loads the entire file into memory, so it is not recommended
101
+ * for downloading large files.
102
+ *
103
+ * @param {string} remotePath - Path to the file to download. Relative paths are resolved based on the user's
104
+ * root directory.
105
+ * @param {number} [timeout] - Timeout for the download operation in seconds. 0 means no timeout.
106
+ * Default is 30 minutes.
107
+ * @returns {Promise<Buffer>} The file contents as a Buffer.
108
+ *
109
+ * @example
110
+ * // Download and process a file
111
+ * const fileBuffer = await fs.downloadFile('tmp/data.json');
112
+ * console.log('File content:', fileBuffer.toString());
113
+ */
114
+ public async downloadFile(remotePath: string, timeout?: number): Promise<Buffer>
115
+ /**
116
+ * Downloads a file from the Sandbox and saves it to a local file. This method uses streaming to download the file,
117
+ * so it is recommended for downloading larger files.
118
+ *
119
+ * @param {string} remotePath - Path to the file to download in the Sandbox. Relative paths are resolved based on the user's
120
+ * root directory.
121
+ * @param {string} localPath - Path to save the downloaded file.
122
+ * @param {number} [timeout] - Timeout for the download operation in seconds. 0 means no timeout.
123
+ * Default is 30 minutes.
124
+ * @returns {Promise<void>}
125
+ *
126
+ * @example
127
+ * // Download and save a file
128
+ * await fs.downloadFile('tmp/data.json', 'local_file.json');
129
+ */
130
+ public async downloadFile(remotePath: string, localPath: string, timeout?: number): Promise<void>
131
+ public async downloadFile(src: string, dst?: string | number, timeout: number = 30 * 60): Promise<Buffer | void> {
132
+ const remotePath = prefixRelativePath(await this.getRootDir(), src)
133
+
134
+ if (typeof dst !== 'string') {
135
+ timeout = dst as number
136
+ const { data } = await this.toolboxApi.downloadFile(this.sandboxId, remotePath, undefined, {
137
+ responseType: 'arraybuffer',
138
+ timeout: timeout * 1000,
139
+ })
140
+
141
+ if (Buffer.isBuffer(data)) {
142
+ return data
143
+ }
144
+
145
+ if (data instanceof ArrayBuffer) {
146
+ return Buffer.from(data)
147
+ }
148
+
149
+ return Buffer.from(await data.arrayBuffer())
150
+ }
151
+
152
+ const response = await this.toolboxApi.downloadFile(this.sandboxId, remotePath, undefined, {
153
+ responseType: 'stream',
154
+ timeout: timeout * 1000,
155
+ })
156
+ const writer = fs.createWriteStream(dst)
157
+ ;(response.data as any).pipe(writer)
158
+ await new Promise<void>((resolve, reject) => {
159
+ writer.on('finish', () => resolve())
160
+ writer.on('error', (err) => reject(err))
161
+ })
162
+ }
163
+
164
+ /**
165
+ * Searches for text patterns within files in the Sandbox.
166
+ *
167
+ * @param {string} path - Directory to search in. Relative paths are resolved based on the user's
168
+ * root directory.
169
+ * @param {string} pattern - Search pattern
170
+ * @returns {Promise<Array<Match>>} Array of matches with file and line information
171
+ *
172
+ * @example
173
+ * // Find all TODO comments in TypeScript files
174
+ * const matches = await fs.findFiles('app/src', 'TODO:');
175
+ * matches.forEach(match => {
176
+ * console.log(`${match.file}:${match.line}: ${match.content}`);
177
+ * });
178
+ */
179
+ public async findFiles(path: string, pattern: string): Promise<Array<Match>> {
180
+ const response = await this.toolboxApi.findInFiles(
181
+ this.sandboxId,
182
+ prefixRelativePath(await this.getRootDir(), path),
183
+ pattern,
184
+ )
185
+ return response.data
186
+ }
187
+
188
+ /**
189
+ * Retrieves detailed information about a file or directory.
190
+ *
191
+ * @param {string} path - Path to the file or directory. Relative paths are resolved based on the user's
192
+ * root directory.
193
+ * @returns {Promise<FileInfo>} Detailed file information including size, permissions, modification time
194
+ *
195
+ * @example
196
+ * // Get file details
197
+ * const info = await fs.getFileDetails('app/config.json');
198
+ * console.log(`Size: ${info.size}, Modified: ${info.modTime}`);
199
+ */
200
+ public async getFileDetails(path: string): Promise<FileInfo> {
201
+ const response = await this.toolboxApi.getFileInfo(
202
+ this.sandboxId,
203
+ prefixRelativePath(await this.getRootDir(), path),
204
+ )
205
+ return response.data
206
+ }
207
+
208
+ /**
209
+ * Lists contents of a directory in the Sandbox.
210
+ *
211
+ * @param {string} path - Directory path to list. Relative paths are resolved based on the user's
212
+ * root directory.
213
+ * @returns {Promise<FileInfo[]>} Array of file and directory information
214
+ *
215
+ * @example
216
+ * // List directory contents
217
+ * const files = await fs.listFiles('app/src');
218
+ * files.forEach(file => {
219
+ * console.log(`${file.name} (${file.size} bytes)`);
220
+ * });
221
+ */
222
+ public async listFiles(path: string): Promise<FileInfo[]> {
223
+ const response = await this.toolboxApi.listFiles(
224
+ this.sandboxId,
225
+ undefined,
226
+ prefixRelativePath(await this.getRootDir(), path),
227
+ )
228
+ return response.data
229
+ }
230
+
231
+ /**
232
+ * Moves or renames a file or directory.
233
+ *
234
+ * @param {string} source - Source path. Relative paths are resolved based on the user's
235
+ * root directory.
236
+ * @param {string} destination - Destination path. Relative paths are resolved based on the user's
237
+ * root directory.
238
+ * @returns {Promise<void>}
239
+ *
240
+ * @example
241
+ * // Move a file to a new location
242
+ * await fs.moveFiles('app/temp/data.json', 'app/data/data.json');
243
+ */
244
+ public async moveFiles(source: string, destination: string): Promise<void> {
245
+ const response = await this.toolboxApi.moveFile(
246
+ this.sandboxId,
247
+ prefixRelativePath(await this.getRootDir(), source),
248
+ prefixRelativePath(await this.getRootDir(), destination),
249
+ )
250
+ return response.data
251
+ }
252
+
253
+ /**
254
+ * Replaces text content in multiple files.
255
+ *
256
+ * @param {string[]} files - Array of file paths to process. Relative paths are resolved based on the user's
257
+ * @param {string} pattern - Pattern to replace
258
+ * @param {string} newValue - Replacement text
259
+ * @returns {Promise<Array<ReplaceResult>>} Results of the replace operation for each file
260
+ *
261
+ * @example
262
+ * // Update version number across multiple files
263
+ * const results = await fs.replaceInFiles(
264
+ * ['app/package.json', 'app/version.ts'],
265
+ * '"version": "1.0.0"',
266
+ * '"version": "1.1.0"'
267
+ * );
268
+ */
269
+ public async replaceInFiles(files: string[], pattern: string, newValue: string): Promise<Array<ReplaceResult>> {
270
+ for (let i = 0; i < files.length; i++) {
271
+ files[i] = prefixRelativePath(await this.getRootDir(), files[i])
272
+ }
273
+
274
+ const replaceRequest: ReplaceRequest = {
275
+ files,
276
+ newValue,
277
+ pattern,
278
+ }
279
+
280
+ const response = await this.toolboxApi.replaceInFiles(this.sandboxId, replaceRequest)
281
+ return response.data
282
+ }
283
+
284
+ /**
285
+ * Searches for files and directories by name pattern in the Sandbox.
286
+ *
287
+ * @param {string} path - Directory to search in. Relative paths are resolved based on the user's
288
+ * @param {string} pattern - File name pattern (supports globs)
289
+ * @returns {Promise<SearchFilesResponse>} Search results with matching files
290
+ *
291
+ * @example
292
+ * // Find all TypeScript files
293
+ * const result = await fs.searchFiles('app', '*.ts');
294
+ * result.files.forEach(file => console.log(file));
295
+ */
296
+ public async searchFiles(path: string, pattern: string): Promise<SearchFilesResponse> {
297
+ const response = await this.toolboxApi.searchFiles(
298
+ this.sandboxId,
299
+ prefixRelativePath(await this.getRootDir(), path),
300
+ pattern,
301
+ )
302
+ return response.data
303
+ }
304
+
305
+ /**
306
+ * Sets permissions and ownership for a file or directory.
307
+ *
308
+ * @param {string} path - Path to the file or directory. Relative paths are resolved based on the user's
309
+ * root directory.
310
+ * @param {FilePermissionsParams} permissions - Permission settings
311
+ * @returns {Promise<void>}
312
+ *
313
+ * @example
314
+ * // Set file permissions and ownership
315
+ * await fs.setFilePermissions('app/script.sh', {
316
+ * owner: 'daytona',
317
+ * group: 'users',
318
+ * mode: '755' // Execute permission for shell script
319
+ * });
320
+ */
321
+ public async setFilePermissions(path: string, permissions: FilePermissionsParams): Promise<void> {
322
+ const response = await this.toolboxApi.setFilePermissions(
323
+ this.sandboxId,
324
+ prefixRelativePath(await this.getRootDir(), path),
325
+ undefined,
326
+ permissions.owner!,
327
+ permissions.group!,
328
+ permissions.mode!,
329
+ )
330
+ return response.data
331
+ }
332
+
333
+ /**
334
+ * Uploads a file to the Sandbox. This method loads the entire file into memory, so it is not recommended
335
+ * for uploading large files.
336
+ *
337
+ * @param {Buffer} file - Buffer of the file to upload.
338
+ * @param {string} remotePath - Destination path in the Sandbox. Relative paths are resolved based on the user's
339
+ * root directory.
340
+ * @param {number} [timeout] - Timeout for the upload operation in seconds. 0 means no timeout.
341
+ * Default is 30 minutes.
342
+ * @returns {Promise<void>}
343
+ *
344
+ * @example
345
+ * // Upload a configuration file
346
+ * await fs.uploadFile(Buffer.from('{"setting": "value"}'), 'tmp/config.json');
347
+ */
348
+ public async uploadFile(file: Buffer, remotePath: string, timeout?: number): Promise<void>
349
+ /**
350
+ * Uploads a file from the local file system to the Sandbox. This method uses streaming to upload the file,
351
+ * so it is recommended for uploading larger files.
352
+ *
353
+ * @param {string} localPath - Path to the local file to upload.
354
+ * @param {string} remotePath - Destination path in the Sandbox. Relative paths are resolved based on the user's
355
+ * root directory.
356
+ * @param {number} [timeout] - Timeout for the upload operation in seconds. 0 means no timeout.
357
+ * Default is 30 minutes.
358
+ * @returns {Promise<void>}
359
+ *
360
+ * @example
361
+ * // Upload a local file
362
+ * await fs.uploadFile('local_file.txt', 'tmp/file.txt');
363
+ */
364
+ public async uploadFile(localPath: string, remotePath: string, timeout?: number): Promise<void>
365
+ public async uploadFile(src: string | Buffer, dst: string, timeout: number = 30 * 60): Promise<void> {
366
+ await this.uploadFiles([{ source: src, destination: dst }], timeout)
367
+ }
368
+
369
+ /**
370
+ * Uploads multiple files to the Sandbox. If files already exist at the destination paths,
371
+ * they will be overwritten.
372
+ *
373
+ * @param {FileUpload[]} files - Array of files to upload.
374
+ * @param {number} [timeout] - Timeout for the upload operation in seconds. 0 means no timeout.
375
+ * Default is 30 minutes.
376
+ * @returns {Promise<void>}
377
+ *
378
+ * @example
379
+ * // Upload multiple text files
380
+ * const files = [
381
+ * {
382
+ * source: Buffer.from('Content of file 1'),
383
+ * destination: '/tmp/file1.txt'
384
+ * },
385
+ * {
386
+ * source: 'app/data/file2.txt',
387
+ * destination: '/tmp/file2.txt'
388
+ * },
389
+ * {
390
+ * source: Buffer.from('{"key": "value"}'),
391
+ * destination: '/tmp/config.json'
392
+ * }
393
+ * ];
394
+ * await fs.uploadFiles(files);
395
+ */
396
+ public async uploadFiles(files: FileUpload[], timeout: number = 30 * 60): Promise<void> {
397
+ const form = new FormData()
398
+ const rootDir = await this.getRootDir()
399
+
400
+ files.forEach(({ source, destination }, i) => {
401
+ const dst = prefixRelativePath(rootDir, destination)
402
+ form.append(`files[${i}].path`, dst)
403
+ const stream = typeof source === 'string' ? fs.createReadStream(source) : Readable.from(source)
404
+ // the third arg sets filename in Content-Disposition
405
+ form.append(`files[${i}].file`, stream as any, dst)
406
+ })
407
+
408
+ await this.toolboxApi.uploadFiles(this.sandboxId, undefined, {
409
+ data: form,
410
+ maxRedirects: 0,
411
+ timeout: timeout * 1000,
412
+ })
413
+ }
414
+ }