Skip to content

Commit 05ec76a

Browse files
committed
puterjs/fs: add "share" arg to fs.write api, add test as well
1 parent c0c601a commit 05ec76a

File tree

9 files changed

+270
-4
lines changed

9 files changed

+270
-4
lines changed

src/puter-js/src/modules/FileSystem/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import upload from "./operations/upload.js";
99
import read from "./operations/read.js";
1010
import move from "./operations/move.js";
1111
import write from "./operations/write.js";
12+
import share from "./operations/share.js";
1213
import sign from "./operations/sign.js";
1314
import symlink from './operations/symlink.js';
1415
// Why is this called deleteFSEntry instead of just delete? because delete is
@@ -34,6 +35,7 @@ export class PuterJSFileSystemModule extends AdvancedBase {
3435
sign = sign;
3536
symlink = symlink;
3637
getReadURL = getReadURL;
38+
share = share;
3739

3840
FSItem = FSItem
3941

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
2+
3+
const share = async function (targetPath, options = {}) {
4+
// targetPath is required
5+
if (!targetPath) {
6+
throw new Error('No target path provided.');
7+
}
8+
9+
// If targetPath is not provided or it's not starting with a slash, it means it's a relative path
10+
// in that case, we need to prepend the app's root directory to it
11+
targetPath = getAbsolutePathForApp(targetPath);
12+
13+
// Extract options
14+
const recipients = options.recipients || [];
15+
const access = options.access || 'read';
16+
17+
// Validate access level
18+
if (!['read', 'write'].includes(access)) {
19+
throw new Error('Invalid access level. Must be "read" or "write".');
20+
}
21+
22+
// Validate recipients
23+
if (!Array.isArray(recipients) || recipients.length === 0) {
24+
throw new Error('Recipients must be a non-empty array.');
25+
}
26+
27+
// Prepare the share request
28+
const shareData = {
29+
recipients: recipients,
30+
shares: [
31+
{
32+
$: 'fs-share',
33+
path: targetPath,
34+
access: access,
35+
}
36+
]
37+
};
38+
39+
// Make the API call to share the file
40+
console.log(`api origin: ${puter.APIOrigin}`);
41+
const response = await fetch(`${puter.APIOrigin}/share`, {
42+
method: 'POST',
43+
headers: {
44+
'Content-Type': 'application/json',
45+
'Authorization': `Bearer ${puter.authToken}`
46+
},
47+
body: JSON.stringify(shareData)
48+
});
49+
50+
if (!response.ok) {
51+
const errorText = await response.text();
52+
throw new Error(`Share failed: ${response.status} ${errorText}`);
53+
}
54+
55+
const result = await response.json();
56+
return result;
57+
};
58+
59+
export default share;

src/puter-js/src/modules/FileSystem/operations/write.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import path from "../../../lib/path.js"
1+
import path from "../../../lib/path.js";
22
import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
33

44
const write = async function (targetPath, data, options = {}) {
@@ -56,7 +56,35 @@ const write = async function (targetPath, data, options = {}) {
5656
}
5757

5858
// perform upload
59-
return this.upload(data, parent, options);
59+
const result = await this.upload(data, parent, options);
60+
61+
if (options.share) {
62+
try {
63+
// Call the share API after successful write
64+
let share_result = await this.share(result.path, {
65+
recipients: options.share.recipients || [],
66+
access: options.share.access || 'read'
67+
});
68+
console.log('share_result', share_result);
69+
70+
// Add share information to the result
71+
result.share = {
72+
status: share_result.status,
73+
recipients: share_result.recipients || [],
74+
shares: share_result.shares || []
75+
};
76+
} catch (error) {
77+
console.error('Failed to share file after write:', error);
78+
// Add error information to the result
79+
result.share = {
80+
status: 'error',
81+
error: error.message,
82+
recipients: []
83+
};
84+
}
85+
}
86+
87+
return result;
6088
}
6189

6290
export default write;

tools/api-tester/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# API Tester
22

3-
A test framework for testing the backend API of puter.
3+
A test framework for testing the API of puter backend and puter-js client.
44

55
## Table of Contents
66

@@ -43,6 +43,12 @@ All commands below should be run from the root directory of puter.
4343
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml
4444
```
4545
46+
4. (experimental) Run tests against the puter-js client:
47+
48+
```bash
49+
node ./tools/api-tester/apitest.js --config=./tools/api-tester/config.yml --client
50+
```
51+
4652
### Shorthands
4753
4854
- Run unit tests only:

tools/api-tester/apitest.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ const { parseArgs } = require('node:util');
99

1010
const args = process.argv.slice(2);
1111

12-
let config, report, suiteName;
12+
let config, report, suiteName, onlycase, bench, unit, stopOnFailure, id, puterjs;
1313

1414
try {
1515
const parsed = parseArgs({
1616
options: {
1717
config: {
1818
type: 'string',
19+
default: './tools/api-tester/config.yml',
1920
},
2021
report: {
2122
type: 'string',
@@ -24,6 +25,8 @@ try {
2425
bench: { type: 'boolean' },
2526
unit: { type: 'boolean' },
2627
suite: { type: 'string' },
28+
'stop-on-failure': { type: 'boolean' },
29+
puterjs: { type: 'boolean' },
2730
},
2831
allowPositionals: true,
2932
});
@@ -35,6 +38,8 @@ try {
3538
bench,
3639
unit,
3740
suite: suiteName,
41+
'stop-on-failure': stopOnFailure,
42+
puterjs,
3843
}, positionals: [id] } = parsed);
3944

4045
onlycase = onlycase !== undefined ? Number.parseInt(onlycase) : undefined;
@@ -47,6 +52,7 @@ try {
4752
'\n' +
4853
'Options:\n' +
4954
' --config=<path> (required) Path to configuration file\n' +
55+
' --puterjs (optional) Use puter-js puterjs\n' +
5056
' --report=<path> (optional) Output file for full test results\n' +
5157
' --suite=<name> (optional) Run only tests with matching suite name\n' +
5258
''
@@ -58,6 +64,87 @@ const conf = YAML.parse(fs.readFileSync(config).toString());
5864

5965

6066
const main = async () => {
67+
if (puterjs) {
68+
// const run = require('./puter_js/__entry__.js');
69+
70+
const context = {
71+
mountpoint: {
72+
path: '/',
73+
}
74+
};
75+
76+
const ts = new TestSDK(conf, context, {});
77+
const registry = new TestRegistry(ts);
78+
79+
await require('./puter_js/__entry__.js')(registry);
80+
81+
await registry.run_all_tests();
82+
83+
// await run(conf);
84+
ts.printTestResults();
85+
ts.printBenchmarkResults();
86+
process.exit(0);
87+
return;
88+
}
89+
90+
const unit_test_results = [];
91+
const benchmark_results = [];
92+
for (const mountpoint of conf.mountpoints) {
93+
const { unit_test_results: results, benchmark_results: benchs } = await test({ mountpoint });
94+
unit_test_results.push(...results);
95+
benchmark_results.push(...benchs);
96+
}
97+
98+
// hard-coded identifier for ci script
99+
console.log("==================== nightly build results begin ====================")
100+
101+
// print unit test results
102+
let tbl = {};
103+
for ( const result of unit_test_results ) {
104+
tbl[result.name + ' - ' + result.settings] = {
105+
passed: result.caseCount - result.failCount,
106+
failed: result.failCount,
107+
total: result.caseCount,
108+
'duration (s)': result.duration ? result.duration.toFixed(2) : 'N/A',
109+
}
110+
}
111+
console.table(tbl);
112+
113+
// print benchmark results
114+
if (benchmark_results.length > 0) {
115+
tbl = {};
116+
for ( const result of benchmark_results ) {
117+
const fs_provider = result.fs_provider || 'unknown';
118+
tbl[result.name + ' - ' + fs_provider] = {
119+
'duration (s)': result.duration ? (result.duration / 1000).toFixed(2) : 'N/A',
120+
}
121+
}
122+
console.table(tbl);
123+
124+
// print description of each benchmark since it's too long to fit in the table
125+
const seen = new Set();
126+
for ( const result of benchmark_results ) {
127+
if ( seen.has(result.name) ) continue;
128+
seen.add(result.name);
129+
130+
if ( result.description ) {
131+
console.log(result.name + ': ' + result.description);
132+
}
133+
}
134+
}
135+
136+
// hard-coded identifier for ci script
137+
console.log("==================== nightly build results end ====================")
138+
}
139+
140+
/**
141+
* Run test using the given config, and return the test results
142+
*
143+
* @param {Object} options
144+
* @param {Object} options.mountpoint
145+
* @returns {Promise<Object>}
146+
*/
147+
async function test({ mountpoint }) {
61148
const context = {
62149
options: {
63150
onlycase,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const load_puterjs = require('./load.cjs');
2+
3+
async function run(conf) {
4+
const puter = await load_puterjs();
5+
if (conf.token) {
6+
puter.setAuthToken(conf.token);
7+
} else {
8+
throw new Error('No token found in config file. Please add a "token" field to your config.yaml');
9+
}
10+
return;
11+
};
12+
13+
module.exports = async registry => {
14+
const puter = await load_puterjs();
15+
if (registry.t?.conf?.token) {
16+
puter.setAuthToken(registry.t.conf.token);
17+
} else {
18+
throw new Error('No token found in config file. Please add a "token" field to your config.yaml');
19+
}
20+
21+
registry.t.puter = puter;
22+
23+
console.log('__entry__.js');
24+
require('./filesystem/__entry__.js')(registry);
25+
26+
// registry.add_test('filesystem', require('./filesystem/__entry__.js'));
27+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = registry => {
2+
console.log('filesystem __entry__.js');
3+
registry.add_test('write', require('./write.js'));
4+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const chai = require('chai');
2+
chai.use(require('chai-as-promised'))
3+
const expect = chai.expect;
4+
5+
module.exports = {
6+
name: 'write',
7+
description: 'a collection of tests for writing to the filesystem',
8+
do: async t => {
9+
const puter = t.puter;
10+
11+
await t.case('demo (whoami)', async () => {
12+
const result = await puter.auth.whoami();
13+
expect(result.username).to.equal('admin');
14+
});
15+
16+
await t.case('write and share', async () => {
17+
let result = await puter.fs.write('~/test.txt', 'hello');
18+
expect(result.name).to.equal('test.txt');
19+
20+
result = await puter.fs.share('~/test.txt', {
21+
recipients: ['tom', 'jerry'],
22+
access: 'read',
23+
withPermissions: true,
24+
});
25+
console.log('result', result);
26+
expect(result.recipients.length).to.equal(2);
27+
});
28+
29+
await t.case('write with share args', async () => {
30+
let result = await puter.fs.write('~/test.txt', 'hello', {
31+
share: {
32+
recipients: ['tom', 'jerry'],
33+
access: 'read',
34+
},
35+
withPermissions: true,
36+
});
37+
expect(result.share.recipients.length).to.equal(2);
38+
});
39+
}
40+
}

tools/api-tester/puter_js/load.cjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const vm = require('vm');
2+
3+
async function load_puterjs() {
4+
const goodContext = {}
5+
Object.getOwnPropertyNames(globalThis).forEach(name => { try { goodContext[name] = globalThis[name]; } catch { } })
6+
goodContext.globalThis = goodContext
7+
const code = await fetch("http://puter.localhost:4100/puter.js/v2").then(res => res.text());
8+
const context = vm.createContext(goodContext);
9+
const result = vm.runInNewContext(code, context);
10+
return goodContext.puter;
11+
}
12+
13+
module.exports = load_puterjs;

0 commit comments

Comments
 (0)