Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
4c757a8
feat(sandbox): add sandbox product sdk
miclle Jun 8, 2026
86f9679
test(sandbox): expand integration coverage and examples
miclle Jun 15, 2026
133d6dc
feat(sandbox): improve sandbox runtime APIs
miclle Jun 15, 2026
2218310
feat(sandbox): align runtime and template APIs
miclle Jun 15, 2026
0438153
docs(sandbox): remove stale compatibility plan
miclle Jun 15, 2026
b674adc
fix(sandbox): harden runtime compatibility
miclle Jun 15, 2026
289cb27
fix(sandbox): stream command handles
miclle Jun 15, 2026
965f1b7
fix(sandbox): address review hardening
miclle Jun 15, 2026
3ac6673
fix(sandbox): tighten review edge cases
miclle Jun 15, 2026
a175f81
fix(sandbox): handle review edge cases
miclle Jun 15, 2026
813e680
fix(sandbox): handle stream and auth review
miclle Jun 15, 2026
cb1c1e0
fix(sandbox): cap stream frames and credentials
miclle Jun 15, 2026
070a7b1
fix(sandbox): tighten parser edge cases
miclle Jun 15, 2026
9b74b33
fix(sandbox): address stream review feedback
miclle Jun 15, 2026
44b3b19
fix(sandbox): normalize timeout aliases
miclle Jun 15, 2026
7693291
fix(sandbox): harden stream and path handling
miclle Jun 15, 2026
2516874
fix(sandbox): align timeout and helper semantics
miclle Jun 15, 2026
3daec8e
fix(sandbox): preserve helper edge cases
miclle Jun 15, 2026
0434265
fix(sandbox): harden template helper handling
miclle Jun 15, 2026
802fde4
fix(sandbox): sign batch upload urls
miclle Jun 15, 2026
ffb9674
fix(sandbox): align envd and template edge cases
miclle Jun 15, 2026
831e7a5
fix(sandbox): harden stream termination
miclle Jun 15, 2026
9d6d523
fix(sandbox): preserve ids and clone auth
miclle Jun 15, 2026
0bff3b6
fix(sandbox): handle async watch exit errors
miclle Jun 15, 2026
7e14583
fix(sandbox): guard file and copy edge cases
miclle Jun 15, 2026
33e3ca6
fix(sandbox): normalize pip install options
miclle Jun 15, 2026
daff3fe
fix(sandbox): normalize install helpers
miclle Jun 15, 2026
dd40947
fix(sandbox): clear stale auth state
miclle Jun 15, 2026
d873b32
fix(sandbox): harden file and git helpers
miclle Jun 15, 2026
ed307ca
fix(sandbox): align request and url types
miclle Jun 15, 2026
a8ffc89
fix(sandbox): preserve copy boolean flags
miclle Jun 15, 2026
b201e54
fix(sandbox): validate helper inputs
miclle Jun 15, 2026
a57ba44
fix(sandbox): harden helper validation
miclle Jun 15, 2026
de5805f
fix(sandbox): normalize async validation
miclle Jun 15, 2026
d7bc0fe
fix(sandbox): align helper types
miclle Jun 15, 2026
9082027
fix(sandbox): harden stream helpers
miclle Jun 15, 2026
d849360
fix(sandbox): align type export test
miclle Jun 15, 2026
ba03919
fix(sandbox): align pty disconnect wait
miclle Jun 15, 2026
e62cee3
fix(sandbox): honor explicit mac credentials
miclle Jun 15, 2026
9e4fb25
fix(sandbox): harden helper edge cases
miclle Jun 15, 2026
93bea18
fix(sandbox): preserve pty args and binary writes
miclle Jun 15, 2026
e9ed56f
fix(sandbox): always create live pty
miclle Jun 15, 2026
f48f9a6
test(sandbox): cover helper edge cases
miclle Jun 16, 2026
370b768
test(sandbox): split coverage tests
miclle Jun 16, 2026
33e0921
test(sandbox): rename split tests
miclle Jun 16, 2026
e8ec878
test(sandbox): support legacy url parsing
miclle Jun 16, 2026
e670855
ci(sandbox): keep fork coverage focused
miclle Jun 16, 2026
32685e9
fix(sandbox): resolve wait on disconnect
miclle Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copy this file to .env in the project root before running sandbox examples
# or sandbox integration tests.

# Sandbox API key auth.
QINIU_SANDBOX_API_KEY=

# Optional custom endpoint.
QINIU_SANDBOX_ENDPOINT=

# Optional template alias or ID. Defaults to base.
QINIU_SANDBOX_TEMPLATE=base

# Required only for injection-rule and Kodo resource examples.
QINIU_SANDBOX_ACCESS_KEY=
QINIU_SANDBOX_SECRET_KEY=

# Optional Git remote examples.
GIT_REPO_URL=
GIT_USERNAME=
GIT_PASSWORD=

# Optional Git repository resource example.
GITHUB_TOKEN=
QINIU_SANDBOX_GIT_MOUNT_PATH=/workspace/repo

# Optional Kodo resource example.
QINIU_SANDBOX_KODO_BUCKET=
QINIU_SANDBOX_KODO_MOUNT_PATH=/workspace/kodo
QINIU_SANDBOX_KODO_PREFIX=

# Optional request injection examples.
QINIU_SANDBOX_HTTP_INJECTION_TOKEN=real_token
QINIU_SANDBOX_OPENAI_API_KEY=
7 changes: 6 additions & 1 deletion .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ jobs:
- name: Run cases
run: |
npm run check-type
nyc --reporter=lcov npm test
nyc --reporter=lcov --include 'qiniu/sandbox/**/*.js' npm run test:sandbox
if [ -n "$QINIU_ACCESS_KEY" ] && [ -n "$QINIU_SECRET_KEY" ] && [ -n "$QINIU_TEST_BUCKET" ] && [ -n "$QINIU_TEST_DOMAIN" ]; then
nyc --no-clean --reporter=lcov npm test
else
npm test
fi
env:
QINIU_ACCESS_KEY: ${{ secrets.QINIU_ACCESS_KEY }}
QINIU_SECRET_KEY: ${{ secrets.QINIU_SECRET_KEY }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ coverage/
yarn.lock

.claude/settings.local.json
.env
135 changes: 135 additions & 0 deletions examples/sandbox_common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const fs = require('fs');
const path = require('path');

const qiniu = require('../index');
const { shellQuote } = require('../qiniu/sandbox/util');

function loadDotEnvIfPresent () {
const files = [
path.join(process.cwd(), '.env')
];

files.forEach(filepath => {
if (!fs.existsSync(filepath)) {
return;
}

fs.readFileSync(filepath, 'utf8')
.split(/\r?\n/)
.forEach(line => {
line = line.trim();
if (!line || line[0] === '#') {
return;
}
const index = line.indexOf('=');
if (index < 0) {
return;
}
const key = line.slice(0, index).trim();
let value = line.slice(index + 1).trim();
if (
(value[0] === '"' && value[value.length - 1] === '"') ||
(value[0] === '\'' && value[value.length - 1] === '\'')
) {
value = value.slice(1, -1);
}
if (process.env[key] === undefined) {
process.env[key] = value;
}
});
});
}

function env (key, fallback) {
return process.env[key] || fallback;
}

function requiredEnv (key) {
if (process.env[key]) {
return process.env[key];
}
throw new Error(`Please set ${key}`);
}

function sandboxEndpoint () {
return env('QINIU_SANDBOX_ENDPOINT');
}

function sandboxApiKey () {
return requiredEnv('QINIU_SANDBOX_API_KEY');
}

function sandboxTemplate () {
return env('QINIU_SANDBOX_TEMPLATE', 'base');
}

function sandboxClient (options) {
options = Object.assign({
endpoint: sandboxEndpoint(),
apiKey: sandboxApiKey()
}, options || {});
return new qiniu.sandbox.SandboxClient(options);
}

function sandboxMac () {
const accessKey = requiredEnv('QINIU_SANDBOX_ACCESS_KEY');
const secretKey = requiredEnv('QINIU_SANDBOX_SECRET_KEY');
return new qiniu.auth.digest.Mac(accessKey, secretKey);
}

function createSandboxAndWait (options, pollOptions) {
const client = options && options.client ? options.client : sandboxClient();
const params = Object.assign({
client,
template: sandboxTemplate(),
timeout: 300
}, options || {});

return qiniu.sandbox.Sandbox.create(params).then(sandbox => {
console.log('Sandbox created:', sandbox.sandboxId);
return sandbox.waitForReady(Object.assign({
interval: 3000,
timeout: 180000
}, pollOptions || {})).then(info => {
console.log('Sandbox ready:', sandbox.sandboxId, info.state || '');
return sandbox;
});
});
}

function cleanupSandbox (sandbox) {
if (!sandbox) {
return Promise.resolve();
}
return sandbox.kill()
.then(() => {
console.log('Sandbox killed:', sandbox.sandboxId);
}, err => {
console.log('Failed to kill sandbox:', sandbox.sandboxId, err.message);
});
}

function runExample (fn) {
loadDotEnvIfPresent();
Promise.resolve()
.then(fn)
.catch(err => {
console.error(err && err.stack ? err.stack : err);
process.exitCode = 1;
});
}

module.exports = {
qiniu,
shellQuote,
env,
requiredEnv,
sandboxEndpoint,
sandboxApiKey,
sandboxTemplate,
sandboxClient,
sandboxMac,
createSandboxAndWait,
cleanupSandbox,
runExample
};
25 changes: 25 additions & 0 deletions examples/sandbox_create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const {
qiniu,
sandboxClient,
sandboxTemplate,
cleanupSandbox,
runExample
} = require('./sandbox_common');

runExample(() => {
const client = sandboxClient();
let sandbox;

return qiniu.sandbox.Sandbox.create(sandboxTemplate(), {
client,
timeout: 300,
metadata: {
example: 'sandbox_create'
}
}).then(created => {
sandbox = created;
console.log('Sandbox created:', sandbox.sandboxId);
console.log('Template:', sandbox.info.templateID || sandbox.info.template_id || sandboxTemplate());
return cleanupSandbox(sandbox);
});
});
68 changes: 68 additions & 0 deletions examples/sandbox_envd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const {
shellQuote,
createSandboxAndWait,
cleanupSandbox,
runExample
} = require('./sandbox_common');

runExample(() => {
let sandbox;
const workdir = '/tmp/qiniu-nodejs-sdk-envd';
const filePath = `${workdir}/hello.txt`;

return createSandboxAndWait({
metadata: {
example: 'sandbox_envd'
}
}).then(created => {
sandbox = created;
console.log('Public host for port 8080:', sandbox.getHost(8080));
return sandbox.commands.run(`mkdir -p ${shellQuote(workdir)}`);
}).then(result => {
console.log('mkdir exit:', result.exitCode);
return sandbox.files.write(filePath, 'Hello from Qiniu Node.js SDK\n');
}).then(entry => {
console.log('Wrote:', entry.path || entry);
return sandbox.files.readText(filePath);
}).then(text => {
console.log('ReadText:', JSON.stringify(text));
return sandbox.files.writeFiles([
{ path: `${workdir}/batch-a.txt`, data: 'file A content\n' },
{ path: `${workdir}/batch-b.txt`, data: 'file B content\n' }
]);
}).then(entries => {
console.log('WriteFiles:', entries.map(item => item.path));
return sandbox.files.read(`${workdir}/batch-a.txt`, { format: 'bytes' });
}).then(bytes => {
console.log('Read bytes:', bytes.length);
return sandbox.files.list(workdir);
}).then(entries => {
console.log('List:', entries.map(item => `${item.type}:${item.name}`));
return sandbox.files.exists(filePath);
}).then(exists => {
console.log('Exists:', exists);
return sandbox.files.getInfo(filePath);
}).then(info => {
console.log('GetInfo:', info.name, info.size);
return sandbox.files.rename(`${workdir}/batch-b.txt`, `${workdir}/batch-b-renamed.txt`);
}).then(info => {
console.log('Renamed to:', info.path);
return sandbox.files.remove(`${workdir}/batch-b-renamed.txt`);
}).then(() => {
console.log('Removed renamed file');
return sandbox.commands.run('echo $MY_VAR && pwd', {
cwd: workdir,
envs: {
MY_VAR: 'sandbox-value'
}
});
}).then(result => {
console.log('Command exit:', result.exitCode);
console.log('stdout:\n' + result.stdout);
return cleanupSandbox(sandbox);
}, err => {
return cleanupSandbox(sandbox).then(() => {
throw err;
});
});
});
34 changes: 34 additions & 0 deletions examples/sandbox_filesystem_encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const {
createSandboxAndWait,
cleanupSandbox,
runExample
} = require('./sandbox_common');

runExample(() => {
let sandbox;
const filePath = '/tmp/qiniu-nodejs-sdk-encoding.txt';

return createSandboxAndWait({
metadata: {
example: 'sandbox_filesystem_encoding'
}
}).then(created => {
sandbox = created;
return sandbox.files.write(filePath, 'hello with octet-stream and gzip\n', {
useOctetStream: true,
gzip: true
});
}).then(entry => {
console.log('Wrote:', entry.path || entry);
return sandbox.files.read(filePath, {
gzip: true
});
}).then(text => {
console.log('Read:', JSON.stringify(text));
return cleanupSandbox(sandbox);
}, err => {
return cleanupSandbox(sandbox).then(() => {
throw err;
});
});
});
Loading
Loading