Skip to content

Commit 3cc61ed

Browse files
committed
New Feature: Programmatic bucket data access, via new API: write_bucket_data.
1 parent 340587f commit 3cc61ed

4 files changed

Lines changed: 146 additions & 3 deletions

File tree

docs/api.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,43 @@ And an example response:
458458

459459
Deletions are permanent and cannot be undone.
460460

461+
### write_bucket_data
462+
463+
```
464+
POST /api/app/write_bucket_data/v1
465+
```
466+
467+
This API allows you to write bucket data into a storage bucket. The [edit_buckets](privileges.md#edit_buckets) privilege is required, as well as a valid user session or API Key. The input parameters are as follows:
468+
469+
| Property Name | Type | Description |
470+
|---------------|------|-------------|
471+
| `bucket` | String | **(Required)** The alphanumeric ID of the bucket to write data to. |
472+
| `data` | Object | **(Required)** The data object to shallow-merge into the bucket data. |
473+
| `fetch` | Boolean | Optional flag requesting the entire data object be returned in the API response. |
474+
475+
Here is an example request:
476+
477+
```json
478+
{
479+
"id": "bme4wi6pg35",
480+
"fetch": true,
481+
"data": { "foo": "bar" }
482+
}
483+
```
484+
485+
And an example response:
486+
487+
```json
488+
{
489+
"code": 0,
490+
"data": { "foo": "bar", "other": 12345 }
491+
}
492+
```
493+
494+
Notably, data passed to this API is *shallow-merged* into the bucket data. In this way multiple "clients" can read/write data to the same bucket without affecting each other (as long as they use unique property names). Locking is used to ensure only one read/write operation occurs at a time. If multiple clients write the same property names the latter prevails.
495+
496+
This API is designed to be called from within jobs (i.e. Event Plugin scripts), so it does not update the bucket record itself, nor log a user transaction.
497+
461498
### upload_bucket_files
462499

463500
```
@@ -474,6 +511,8 @@ The file properties are automatically set based on the user files themselves, in
474511

475512
Note that bucket files are automatically added or replaced based on their normalized filenames. Normalization involves converting anything other than alphanumerics, dashes and periods to underscores, and converting the filename to lowercase.
476513

514+
This API is designed to be called from within jobs (i.e. Event Plugin scripts), so it does not update the bucket record itself, nor log a user transaction.
515+
477516
### delete_bucket_file
478517

479518
```

docs/buckets.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ See [Privileges](privileges.md#buckets) for specifics. Listing and fetching typi
4343

4444
## Using Buckets In Jobs
4545

46-
Buckets integrate with jobs through two action types: Fetch Bucket and Store Bucket. You attach these as job actions with conditions controlling when they run.
46+
Buckets integrate with jobs through two action types: [Fetch Bucket](actions.md#fetch-bucket) and [Store Bucket](actions.md#store-bucket). You attach these as job actions with conditions controlling when they run.
4747

4848
### Fetch At Job Start
4949

50-
Use Fetch Bucket with the `start` condition to pull bucket content into the job's input context before launch:
50+
Use [Fetch Bucket](actions.md#fetch-bucket) with the `start` condition to pull bucket content into the job's input context before launch:
5151

5252
- **Data**: Shallow-merged into the job's `input.data`. Avoid key collisions or namespace your keys deliberately.
5353
- **Files**: Selected files are added to the job's input file list and staged into the job's temp directory on the remote server before the Plugin starts.
@@ -67,7 +67,7 @@ Example (JSON):
6767

6868
### Store On Completion
6969

70-
Use Store Bucket with a completion condition (e.g., `success`, `error`, `complete`) to persist job outputs:
70+
Use [Store Bucket](actions.md#store-bucket) with a completion condition (e.g., `success`, `error`, `complete`) to persist job outputs:
7171

7272
- **Data**: The job can emit output data which is written into the bucket when `bucket_sync` includes `data`.
7373
- **Files**: The job's output files can be filtered by `bucket_glob` and stored in the bucket when `bucket_sync` includes `files`.
@@ -110,6 +110,44 @@ Every bucket file includes a `path` (e.g., `files/bucket/<bucket_id>/<hash>/<fil
110110
GET https://your.xyops.example.com/files/bucket/bme4wi6pg35/bdY8zZ9nKynfFUb4xH6fA/report.csv
111111
```
112112

113+
These URLs have built-in authentication and are "stable" (i.e. permalinks) even if the files are replaced in the bucket (however, not if they are deleted and then re-added).
114+
115+
## Programmatic Access
116+
117+
Using the [get_bucket](api.md#get_bucket), [write_bucket_data](api.md#write_bucket_data) and [upload_bucket_files](api.md#upload_bucket_files) APIs, you can programmatically read and write bucket data and files at any time, including during a job run. Here is how to set that up:
118+
119+
- First, create a storage bucket, and save the new [Bucket.id](data.md#bucket-id).
120+
- Next, create an [API Key](api.md#api-keys), and grant it the [edit_buckets](privileges.md#edit_buckets) privilege. Save the API key secret when prompted.
121+
- Then, create a [Secret Vault](secrets.md), and add your API Key and Bucket ID as variables (e.g. `XYOPS_API_KEY` and `XYOPS_BUCKET_ID`).
122+
- Assign the secret vault to your Event, Category or Plugin (the scope is up to you).
123+
124+
When your job runs, you will now have access to your secret variables, and also a special magic variable called [Job.base_url](data.md#job-base_url) (also available as the `JOB_BASE_URL` environment variable). Using these variables, you can write bucket data like this:
125+
126+
```sh
127+
#!/bin/sh
128+
JSON_PAYLOAD='{ "data": { "foo": "bar", "number": 1234 } }'
129+
API_URL="$JOB_BASE_URL/api/app/write_bucket_data/v1?id=$XYOPS_BUCKET_ID"
130+
131+
curl -sS "$API_URL" \
132+
-H "Content-Type: application/json" \
133+
-H "X-API-Key: $XYOPS_API_KEY" \
134+
-d "$JSON_PAYLOAD" >/dev/null
135+
```
136+
137+
And read it back like this:
138+
139+
```sh
140+
#!/bin/sh
141+
API_URL="$JOB_BASE_URL/api/app/get_bucket/v1?id=$XYOPS_BUCKET_ID"
142+
RESPONSE=$(curl -sS -H "X-API-Key: $XYOPS_API_KEY" "$API_URL")
143+
144+
echo "Response: $RESPONSE"
145+
```
146+
147+
The [write_bucket_data](api.md#write_bucket_data) API is designed to be rugged, and can easily handle getting bombarded with many jobs hitting it at once. It uses locking to ensure the bucket data doesn't get corrupted. Also, each request performs a "shallow merge" write into the data, so multiple "clients" (events / workflows) can read/write different properties in the same bucket at the same time.
148+
149+
Of course, you can get similar functionality using the [Store Bucket](actions.md#store-bucket) and [Fetch Bucket](actions.md#fetch-bucket) actions, but this way you have complete control over when the data is read and written, and you're not limited to the start and completion of the job.
150+
113151
## Tips
114152

115153
- **Namespacing**: Use distinct keys in bucket JSON to avoid shallow-merge collisions with job input.

docs/data.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,10 @@ This is set to `true` when the job was fired from an event test. This is used t
856856

857857
If any secrets were assigned to the job, this object will *temporarily* hold the decrypted key/value pairs for the job code to use. They are **not** stored anywhere, and only passed to the Event Plugin running the job for immediate use.
858858

859+
### Job.base_url
860+
861+
While the job is running, i.e. in the data passed to the Event Plugin, the job object will contain a `base_url` property. This will be a fully-qualified URL pointing to the current primary conductor server, using the same protocol, hostname and port that xySat used to connect. This is useful if your job needs to call any xyOps APIs directly.
862+
859863
### Job.workflow
860864

861865
When the job is itself a workflow, or a sub-job inside a workflow, this object will contain additional information. See [JobWorkflow](#jobworkflow) for details.

lib/api/buckets.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,68 @@ class Buckets {
284284
} ); // loadSession
285285
}
286286

287+
api_write_bucket_data(args, callback) {
288+
// write bucket data, safely with locks, and without updating bucket itself
289+
// note: data is shallow-merged
290+
var self = this;
291+
var params = Tools.mergeHashes( args.params, args.query );
292+
if (!this.requireMaster(args, callback)) return;
293+
294+
if (!this.requireParams(params, {
295+
id: /^[a-z0-9_]+$/
296+
}, callback)) return;
297+
298+
this.loadSession(args, function(err, session, user) {
299+
if (err) return self.doError('session', err.message, callback);
300+
if (!self.requireValidUser(session, user, callback)) return;
301+
if (!self.requirePrivilege(user, 'edit_buckets', callback)) return;
302+
303+
args.user = user;
304+
args.session = session;
305+
306+
var bucket = Tools.findObject(self.buckets, { id: params.id });
307+
if (!bucket) {
308+
return self.doError('bucket', "Bucket not found: " + params.id, callback);
309+
}
310+
311+
var bucket_path = 'buckets/' + bucket.id;
312+
var bucket_data = null;
313+
314+
// read and write using locking
315+
self.logDebug(6, "Merging data into storage bucket: " + bucket.id, params);
316+
317+
async.series([
318+
function(callback) {
319+
// lock bucket
320+
self.storage.lock( bucket_path, true, callback );
321+
},
322+
function(callback) {
323+
// load bucket data
324+
self.storage.get( bucket_path + '/data', function(err, data) {
325+
if (err) return callback(err);
326+
bucket_data = data;
327+
callback();
328+
} );
329+
},
330+
function(callback) {
331+
// shallow-merge data and save
332+
Tools.mergeHashInto( bucket_data, params.data || {} );
333+
334+
// write data back to storage
335+
self.storage.put( bucket_path + '/data', bucket_data, callback );
336+
}
337+
],
338+
function(err) {
339+
// all done
340+
self.storage.unlock( bucket_path );
341+
if (err) return self.doError('bucket', "Failed to store data in bucket: " + (err.message || err), callback);
342+
343+
// return success
344+
callback({ code: 0, data: params.fetch ? bucket_data : null });
345+
});
346+
} ); // loadSession
347+
}
348+
287349
api_upload_bucket_files(args, callback) {
288350
// upload one or more files to storage bucket
289351
var self = this;

0 commit comments

Comments
 (0)