@eluvio/elv-client-js 4.2.25 → 4.2.27

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,332 @@
1
+ # Media Ingest
2
+
3
+ ## CLI: `utilities/MediaIngest.js`
4
+
5
+ Ingest a video file from disk or S3 into a new or existing fabric object.
6
+
7
+ Uses a single fabric object (no separate 'master' object) and does not store the source files with the object.
8
+
9
+ Three source modes:
10
+
11
+ | Source mode | Option | Fabric behavior | Source cleanup after ABR finalization |
12
+ | --- | --- | --- | --- |
13
+ | Local upload | none | Uploads and encrypts the file in the object | Deletes the uploaded Fabric file |
14
+ | S3 copy | `--s3-copy` | Server-side copies and encrypts the S3 object into the Fabric | Deletes the copied Fabric file |
15
+ | S3 reference | `--s3-reference` | Adds a link to the S3 object without copying or Fabric encryption | Retains the Fabric S3 reference |
16
+
17
+ Cleanup only affects files in the Fabric draft. MediaIngest never deletes the original local file or the original S3 object.
18
+
19
+ ## Prerequisites
20
+
21
+ ```sh
22
+ npm ci
23
+ export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
24
+ export FABRIC_CONFIG_URL="https://main.net955305.contentfabric.io/config"
25
+ ```
26
+
27
+ Instead of `FABRIC_CONFIG_URL` you can use `--config-url URL` or `--network main|demvo3`.
28
+
29
+ ## Local upload
30
+
31
+ ```sh
32
+ node utilities/MediaIngest.js \
33
+ --library-id ilib... \
34
+ --title "Local source" \
35
+ --files /absolute/path/to/source.mxf
36
+ ```
37
+
38
+ If media comes from multiple source files:
39
+
40
+ ```sh
41
+ node utilities/MediaIngest.js \
42
+ --library-id ilib... \
43
+ --title "Separate video and audio" \
44
+ --files /absolute/path/to/picture.mxf /absolute/path/to/english.wav
45
+ ```
46
+
47
+ ## S3 reference with a presigned URL
48
+
49
+ The signed URL is specified in the credentials JSON. The value passed to `--files` is the just the filename (no path). Fabric derives the bucket and parent directory from the signed URL.
50
+
51
+ Each `path_matchers` entry is a regular expression matched against the exact relative path passed to `--files`.
52
+
53
+ `credentials-reference-signed.json`:
54
+
55
+ ```json
56
+ [
57
+ {
58
+ "path_matchers": ["^source\\.mxf$"],
59
+ "remote_access": {
60
+ "protocol": "s3",
61
+ "platform": "aws",
62
+ "cloud_credentials": {
63
+ "signed_url": "https://example-ingest-bucket.s3.us-west-2.amazonaws.com/media/source.mxf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=REDACTED&X-Amz-Signature=REDACTED"
64
+ }
65
+ }
66
+ }
67
+ ]
68
+ ```
69
+
70
+ ```sh
71
+ node utilities/MediaIngest.js \
72
+ --library-id ilib... \
73
+ --title "S3 reference, signed URL" \
74
+ --s3-reference \
75
+ --credentials ./credentials-reference-signed.json \
76
+ --files source.mxf
77
+ ```
78
+
79
+ For signed URLs, `remote_access.path` (bucket) and `storage_endpoint.region` are optional. The URL must authorize `GET` access and remain valid long enough for source inspection and transcoding.
80
+
81
+ ## S3 reference with an access key and secret key
82
+
83
+ `credentials-reference-aksk.json`:
84
+
85
+ ```json
86
+ [
87
+ {
88
+ "path_matchers": ["^s3://example-ingest-bucket/media/source\\.mxf$"],
89
+ "remote_access": {
90
+ "protocol": "s3",
91
+ "platform": "aws",
92
+ "path": "example-ingest-bucket/",
93
+ "storage_endpoint": {
94
+ "region": "us-west-2"
95
+ },
96
+ "cloud_credentials": {
97
+ "access_key_id": "YOUR_AWS_ACCESS_KEY_ID",
98
+ "secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY"
99
+ }
100
+ }
101
+ }
102
+ ]
103
+ ```
104
+
105
+ ```sh
106
+ node utilities/MediaIngest.js \
107
+ --library-id ilib... \
108
+ --title "S3 reference, access key" \
109
+ --s3-reference \
110
+ --credentials ./credentials-reference-aksk.json \
111
+ --files s3://example-ingest-bucket/media/source.mxf
112
+ ```
113
+
114
+ The bucket in a full `s3://` source must match `remote_access.path`. A bare key such as `media/source.mxf` also works when the credential file supplies the bucket.
115
+
116
+ ## S3 copy with a presigned URL
117
+
118
+ Credentials and file arguments are the same as signed-URL reference. For the example URL below, that file is `source.mxf`.
119
+
120
+ `credentials-copy-signed.json`:
121
+
122
+ ```json
123
+ [
124
+ {
125
+ "path_matchers": ["^source\\.mxf$"],
126
+ "remote_access": {
127
+ "protocol": "s3",
128
+ "platform": "aws",
129
+ "cloud_credentials": {
130
+ "signed_url": "https://example-ingest-bucket.s3.us-west-2.amazonaws.com/media/source.mxf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=REDACTED&X-Amz-Signature=REDACTED"
131
+ }
132
+ }
133
+ }
134
+ ]
135
+ ```
136
+
137
+ ```sh
138
+ node utilities/MediaIngest.js \
139
+ --library-id ilib... \
140
+ --title "S3 copy, signed URL" \
141
+ --s3-copy \
142
+ --credentials ./credentials-copy-signed.json \
143
+ --files source.mxf
144
+ ```
145
+
146
+ The server copies the object from S3 into the Fabric and then deletes the Fabric copy after the ABR mezzanine is finalized.
147
+
148
+ ## S3 copy with an access key and secret key
149
+
150
+ `credentials-copy-aksk.json`:
151
+
152
+ ```json
153
+ [
154
+ {
155
+ "path_matchers": ["^s3://example-ingest-bucket/media/source\\.mxf$"],
156
+ "remote_access": {
157
+ "protocol": "s3",
158
+ "platform": "aws",
159
+ "path": "example-ingest-bucket/",
160
+ "storage_endpoint": {
161
+ "region": "us-west-2"
162
+ },
163
+ "cloud_credentials": {
164
+ "access_key_id": "YOUR_AWS_ACCESS_KEY_ID",
165
+ "secret_access_key": "YOUR_AWS_SECRET_ACCESS_KEY"
166
+ }
167
+ }
168
+ }
169
+ ]
170
+ ```
171
+
172
+ ```sh
173
+ node utilities/MediaIngest.js \
174
+ --library-id ilib... \
175
+ --title "S3 copy, access key" \
176
+ --s3-copy \
177
+ --credentials ./credentials-copy-aksk.json \
178
+ --files s3://example-ingest-bucket/media/source.mxf
179
+ ```
180
+
181
+ The reference and copy credential schemas are identical; the command-line mode determines whether the S3 object is linked or copied.
182
+
183
+ Access-key credentials can alternatively come from the environment:
184
+
185
+ ```sh
186
+ export AWS_REGION="us-west-2"
187
+ export AWS_BUCKET="example-ingest-bucket"
188
+ export AWS_KEY="YOUR_AWS_ACCESS_KEY_ID"
189
+ export AWS_SECRET="YOUR_AWS_SECRET_ACCESS_KEY"
190
+
191
+ node utilities/MediaIngest.js \
192
+ --library-id ilib... \
193
+ --title "S3 copy, environment credentials" \
194
+ --s3-copy \
195
+ --files media/source.mxf
196
+ ```
197
+
198
+ ## S3 credential matching and security
199
+
200
+ Each entry in a credentials file applies to the `--files` values matching one of its `path_matchers` regular expressions. Every S3 source must match an entry. For access-key credentials these values may be full `s3://` paths or object keys; for signed URLs they are paths relative to the signed URL's containing directory.
201
+
202
+ A normal S3 presigned URL authorizes one object. For multiple signed sources, use one credential entry and exact matcher per object:
203
+
204
+ ```json
205
+ [
206
+ {
207
+ "path_matchers": ["^video\\.mxf$"],
208
+ "remote_access": {
209
+ "protocol": "s3",
210
+ "platform": "aws",
211
+ "cloud_credentials": {
212
+ "signed_url": "https://example-ingest-bucket.s3.amazonaws.com/media/video.mxf?X-Amz-Signature=REDACTED"
213
+ }
214
+ }
215
+ },
216
+ {
217
+ "path_matchers": ["^audio\\.wav$"],
218
+ "remote_access": {
219
+ "protocol": "s3",
220
+ "platform": "aws",
221
+ "cloud_credentials": {
222
+ "signed_url": "https://example-ingest-bucket.s3.amazonaws.com/media/audio.wav?X-Amz-Signature=REDACTED"
223
+ }
224
+ }
225
+ }
226
+ ]
227
+ ```
228
+
229
+ The corresponding argument is `--files video.mxf audio.wav`.
230
+
231
+ The access key, secret key, and presigned URL are sent for the S3 operation but are not stored by the client or in Fabric metadata.
232
+
233
+ ## Audio
234
+
235
+ > TODO
236
+
237
+ ## HDR10
238
+
239
+ Pass an HDR metadata file with `--hdr-info-file`:
240
+
241
+ `hdr-info.json`:
242
+
243
+ ```json
244
+ {
245
+ "master_display": "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
246
+ "max_cll": "1000,400"
247
+ }
248
+ ```
249
+
250
+ ```sh
251
+ node utilities/MediaIngest.js \
252
+ --library-id ilib... \
253
+ --title "HDR10 source" \
254
+ --files /absolute/path/to/hdr-source.mxf \
255
+ --hdr-info-file ./hdr-info.json
256
+ ```
257
+
258
+ ## DRM and clear playback
259
+
260
+ The built-in profile contains both clear and DRM playout formats - MediaIngest selects only one group for each ingest:
261
+
262
+ | Command-line setting | Effective playout formats |
263
+ | --- | --- |
264
+ | No DRM argument | Clear DASH and clear HLS only |
265
+ | `--no-drm` | Clear DASH and clear HLS only |
266
+ | `--drm` | All DRM formats listed below; no clear formats |
267
+
268
+ The `--drm` option defaults to `false`.
269
+
270
+ With `--no-drm`, explicitly selects clear-only.
271
+
272
+ With `--drm`, MediaIngest retains these protected formats from the built-in profile:
273
+
274
+ - DASH Widevine
275
+ - HLS Widevine CENC
276
+ - HLS FairPlay
277
+ - HLS PlayReady CENC
278
+ - HLS Sample AES
279
+ - HLS AES-128
280
+
281
+ The profile stores mezzanine media encrypted at rest (`store_clear: false`) in either mode.
282
+
283
+ ## ABR profile and variants
284
+
285
+ MediaIngest always uses the built-in [`abr_profile_4k_all.json`](utilities/lib/abr_profiles/abr_profile_4k_all.json). There is currently no command-line option for supplying a different ABR profile but you can modify the JSON file as needed.
286
+
287
+ The video profile is parametric. These are its base rungs for a 16:9, 30 fps source:
288
+
289
+ | Base rung | Base bitrate |
290
+ | --- | ---: |
291
+ | 3840x2160 | 14 Mbps |
292
+ | 2560x1440 | 11.5 Mbps |
293
+ | 1920x1080 | 9.5 Mbps |
294
+ | 1280x720 | 4.5 Mbps |
295
+ | 854x480 | 1.75 Mbps |
296
+ | 640x360 | 810 Kbps |
297
+ | 426x240 | 500 Kbps |
298
+
299
+ At ingest time, the profile generator adapts rung dimensions to the source aspect ratio, snaps near-standard aspect ratios when they are within 6%, and adjusts bitrates for the source frame rate relative to the 30 fps base. It never upscales (eg. a 1920x1080 source has no 1440p or 2160p output). The profile accepts source frame rates from 15 through 60 fps and caps a generated video rung at 30 Mbps. An audio-only variant produces no video ladder.
300
+
301
+ The base profile does not force a video codec. When `--hdr-info-file` is present, MediaIngest adds HEVC (`h265`) and 10-bit encoding requirements to the generated profile. Both audio and video use a two-second segment target with 15 segments per chunk. The audio ladder contains pregenerated rungs for mono at 128 Kbps, stereo at 192 Kbps, and 5.1 at 384 Kbps.
302
+
303
+ Variants and offerings are separate selections:
304
+
305
+ - `--variant-key KEY` selects an existing production-master variant and defaults to `default`. It does not create or modify a variant. MediaIngest does not pass a `streamKeys` filter, so all streams in the selected variant are included in mezzanine creation.
306
+ - `--offering-key KEY` selects the mezzanine offering to create and also defaults to `default`. Changing it does not change which production-master streams are selected.
307
+
308
+
309
+ ## Existing objects and finalization
310
+
311
+ To ingest into an existing object, pass `--object-id` instead of `--library-id`. The utility discovers the object's library and opens a draft:
312
+
313
+ ```sh
314
+ node utilities/MediaIngest.js \
315
+ --object-id iq__... \
316
+ --title "Replacement ingest" \
317
+ --files /absolute/path/to/source.mxf
318
+ ```
319
+
320
+ `--title` is only required for a newly created object
321
+
322
+ By default the completed object is finalized and MediaIngest waits for publication. `--no-finalize` leaves the object draft open and prints its write token and node. Source cleanup still occurs after ABR finalization even when object finalization is skipped.
323
+
324
+ ## Operational notes
325
+
326
+ - Local upload, S3 copy, and S3 reference are mutually exclusive invocation-wide modes. Do not mix local and S3 paths in one run.
327
+ - Every input is stored at `path.basename(source)`. Inputs from different directories that share a filename therefore collide.
328
+ - Captions/subtitles, explicit language labels, custom stream maps, clipping, watermarks, and custom ABR profiles are not exposed by MediaIngest.
329
+ - Progress is written to `./ingest-status.json`, including the object ID and write token. Automatic restart/resume though is not currently implemented
330
+ - If ingest fails before cleanup, uploaded or copied sources may remain in the open draft. Use the reported object ID/write token and `ingest-status.json` for diagnosis and manual cleanup.
331
+
332
+ Run `node utilities/MediaIngest.js --help` for the complete option list.
@@ -0,0 +1,181 @@
1
+ {
2
+ "audio_individual_drm_keys": false,
3
+ "no_upscale": true,
4
+ "drm_optional": true,
5
+ "store_clear": false,
6
+ "video_parametric_ladder": {
7
+ "baseAspectRatio": "16/9",
8
+ "baseFrameRate": "30",
9
+ "rungSpecs": [
10
+ {
11
+ "dim": 2160,
12
+ "bitrate": 14000000
13
+ },
14
+ {
15
+ "dim": 1440,
16
+ "bitrate": 11500000
17
+ },
18
+ {
19
+ "dim": 1080,
20
+ "bitrate": 9500000
21
+ },
22
+ {
23
+ "dim": 720,
24
+ "bitrate": 4500000
25
+ },
26
+ {
27
+ "dim": 480,
28
+ "bitrate": 1750000
29
+ },
30
+ {
31
+ "dim": 360,
32
+ "bitrate": 810000
33
+ },
34
+ {
35
+ "dim": 240,
36
+ "bitrate": 500000
37
+ }
38
+ ],
39
+ "options": {
40
+ "upscale": false,
41
+ "snapAR": true,
42
+ "maxARSnap": 0.06,
43
+ "minDimStepdown": 0.12,
44
+ "frameRateScaleFactor": 0.5
45
+ },
46
+ "limits": {
47
+ "aspectRatioMax": "3",
48
+ "aspectRatioMin": "1/3",
49
+ "avgBitrateMax": 100000000,
50
+ "avgBitrateMin": 100000,
51
+ "fileSizeMax": 100000000,
52
+ "fileSizeMin": 10000,
53
+ "durationMax": 14400,
54
+ "durationMin": 1,
55
+ "finalBitrateMax": 30000000,
56
+ "frameRateMax": "60",
57
+ "frameRateMin": "15",
58
+ "heightMax": 5000,
59
+ "heightMin": 100,
60
+ "sampleAspectRatioMax": "3/2",
61
+ "sampleAspectRatioMin": "2/3",
62
+ "widthMax": 5000,
63
+ "widthMin": 100
64
+ }
65
+ },
66
+ "ladder_specs": {
67
+ "{\"media_type\":\"audio\",\"channels\":1}": {
68
+ "rung_specs": [
69
+ {
70
+ "bit_rate": 128000,
71
+ "media_type": "audio",
72
+ "pregenerate": true
73
+ }
74
+ ]
75
+ },
76
+ "{\"media_type\":\"audio\",\"channels\":2}": {
77
+ "rung_specs": [
78
+ {
79
+ "bit_rate": 192000,
80
+ "media_type": "audio",
81
+ "pregenerate": true
82
+ }
83
+ ]
84
+ },
85
+ "{\"media_type\":\"audio\",\"channels\":6}": {
86
+ "rung_specs": [
87
+ {
88
+ "bit_rate": 384000,
89
+ "media_type": "audio",
90
+ "pregenerate": true
91
+ }
92
+ ]
93
+ }
94
+ },
95
+ "playout_formats": {
96
+ "dash-clear": {
97
+ "drm": null,
98
+ "protocol": {
99
+ "min_buffer_length": 2,
100
+ "type": "ProtoDash"
101
+ }
102
+ },
103
+ "dash-widevine": {
104
+ "drm": {
105
+ "content_id": "",
106
+ "enc_scheme_name": "cenc",
107
+ "lic_srvs_in_manifest": true,
108
+ "license_servers": [],
109
+ "mp4ff_encryptor": true,
110
+ "type": "DrmWidevine"
111
+ },
112
+ "protocol": {
113
+ "min_buffer_length": 2,
114
+ "type": "ProtoDash"
115
+ }
116
+ },
117
+ "hls-aes128": {
118
+ "drm": {
119
+ "enc_scheme_name": "aes-128",
120
+ "type": "DrmAes128"
121
+ },
122
+ "protocol": {
123
+ "type": "ProtoHls"
124
+ }
125
+ },
126
+ "hls-clear": {
127
+ "drm": null,
128
+ "protocol": {
129
+ "type": "ProtoHls"
130
+ }
131
+ },
132
+ "hls-fairplay": {
133
+ "drm": {
134
+ "enc_scheme_name": "cbcs",
135
+ "license_servers": [],
136
+ "type": "DrmFairplay"
137
+ },
138
+ "protocol": {
139
+ "type": "ProtoHls"
140
+ }
141
+ },
142
+ "hls-playready-cenc": {
143
+ "drm": {
144
+ "enc_scheme_name": "cenc",
145
+ "type": "DrmPlayReady"
146
+ },
147
+ "protocol": {
148
+ "type": "ProtoHls"
149
+ }
150
+ },
151
+ "hls-sample-aes": {
152
+ "drm": {
153
+ "enc_scheme_name": "cbcs",
154
+ "type": "DrmSampleAes"
155
+ },
156
+ "protocol": {
157
+ "type": "ProtoHls"
158
+ }
159
+ },
160
+ "hls-widevine-cenc": {
161
+ "drm": {
162
+ "content_id": "",
163
+ "enc_scheme_name": "cenc",
164
+ "type": "DrmWidevine"
165
+ },
166
+ "protocol": {
167
+ "type": "ProtoHls"
168
+ }
169
+ }
170
+ },
171
+ "segment_specs": {
172
+ "audio": {
173
+ "segs_per_chunk": 15,
174
+ "target_dur": 2
175
+ },
176
+ "video": {
177
+ "segs_per_chunk": 15,
178
+ "target_dur": 2
179
+ }
180
+ }
181
+ }
@@ -1,11 +1,18 @@
1
1
  const {ElvClient} = require("../../../src/ElvClient");
2
2
 
3
3
  const elvRegions = require("../data/elv_regions");
4
+ const networkSpecs = require("../data/network_specs.json");
4
5
 
5
6
  const {NewOpt} = require("../options");
6
7
 
7
8
  const Logger = require("./Logger");
8
9
 
10
+ const DEFAULT_NETWORK = "main";
11
+ const networkChoices = [...new Set([
12
+ ...Object.keys(ElvClient.Networks()),
13
+ ...Object.keys(networkSpecs)
14
+ ])].sort();
15
+
9
16
  const blueprint = {
10
17
  name: "Client",
11
18
  concerns: [Logger],
@@ -15,6 +22,12 @@ const blueprint = {
15
22
  group: "API",
16
23
  type: "string"
17
24
  }),
25
+ NewOpt("network", {
26
+ choices: networkChoices,
27
+ descTemplate: `Network to use. Defaults to "${DEFAULT_NETWORK}" when neither --configUrl nor FABRIC_CONFIG_URL is supplied. Custom networks in utilities/lib/data/network_specs.json are initialized directly.`,
28
+ group: "API",
29
+ type: "string"
30
+ }),
18
31
  NewOpt("debug", {
19
32
  descTemplate: "Print debug logging for API calls",
20
33
  group: "API",
@@ -49,7 +62,7 @@ const New = (context) => {
49
62
  if(/^".+"$/.test(configUrl)) {
50
63
  configUrl = configUrl.slice(1,-1);
51
64
  }
52
- const {debug, ethContractTimeout} = context.args;
65
+ const {debug, ethContractTimeout, network, node} = context.args;
53
66
  const region = context.args.elvGeo;
54
67
  const logger = context.concerns.Logger;
55
68
  const privateKey = context.env.PRIVATE_KEY;
@@ -65,16 +78,64 @@ const New = (context) => {
65
78
  if(!privateKey) {
66
79
  throw Error("Please set environment variable PRIVATE_KEY");
67
80
  }
68
- if(!configUrl) {
69
- throw Error("Please either supply --configUrl or set environment variable FABRIC_CONFIG_URL");
81
+
82
+ if(network && networkSpecs[network]) {
83
+ // Initialize directly from a custom network spec, no config URL fetched.
84
+ const spec = networkSpecs[network];
85
+ const fabricURIs = spec.fabricURIs;
86
+ const ethereumURIs = spec.ethereumURIs;
87
+ const fileServiceURIs = spec.fileServiceURIs || fabricURIs;
88
+
89
+ logger.log(`Initializing elv-client-js from network spec "${network}" (no config URL)...`);
90
+ logger.log(` Content space: ${spec.contentSpaceId}`);
91
+ logger.log(` Fabric API: ${fabricURIs.join(", ")}`);
92
+ logger.log(` Ethereum API: ${ethereumURIs.join(", ")}`);
93
+
94
+ elvClient = new ElvClient({
95
+ contentSpaceId: spec.contentSpaceId,
96
+ networkId: spec.networkId,
97
+ networkName: spec.networkName || network,
98
+ fabricVersion: spec.fabricVersion || 3,
99
+ fabricURIs,
100
+ ethereumURIs,
101
+ fileServiceURIs,
102
+ authServiceURIs: spec.authServiceURIs || [],
103
+ searchURIs: spec.searchURIs || [],
104
+ ethereumContractTimeout: ethContractTimeout
105
+ });
106
+ } else if(network) {
107
+ logger.log(`Initializing elv-client-js from network "${network}"...`);
108
+ elvClient = await ElvClient.FromNetworkName({
109
+ networkName: network,
110
+ region,
111
+ ethereumContractTimeout: ethContractTimeout
112
+ });
113
+ } else {
114
+ if(configUrl) {
115
+ logger.log(`Initializing elv-client-js... (config URL: ${configUrl})`);
116
+ elvClient = await ElvClient.FromConfigurationUrl({
117
+ configUrl,
118
+ region,
119
+ ethereumContractTimeout: ethContractTimeout
120
+ });
121
+ } else {
122
+ logger.log(`Initializing elv-client-js from default network "${DEFAULT_NETWORK}"...`);
123
+ elvClient = await ElvClient.FromNetworkName({
124
+ networkName: DEFAULT_NETWORK,
125
+ region,
126
+ ethereumContractTimeout: ethContractTimeout
127
+ });
128
+ }
70
129
  }
71
130
 
72
- logger.log(`Initializing elv-client-js... (config URL: ${configUrl})`);
73
- elvClient = await ElvClient.FromConfigurationUrl({
74
- configUrl,
75
- region,
76
- ethereumContractTimeout: ethContractTimeout
77
- });
131
+ if(node) {
132
+ const nodeUrl = /^https?:\/\//i.test(node) ? node : `https://${node}`;
133
+ logger.log(`Pinning fabric and file-service requests to ${nodeUrl}...`);
134
+ elvClient.SetNodes({
135
+ fabricURIs: [nodeUrl],
136
+ fileServiceURIs: [nodeUrl]
137
+ });
138
+ }
78
139
 
79
140
  let wallet = elvClient.GenerateWallet();
80
141
  let signer = wallet.AddAccount({privateKey});
@@ -98,4 +159,4 @@ const New = (context) => {
98
159
  module.exports = {
99
160
  blueprint,
100
161
  New
101
- };
162
+ };
@@ -8,7 +8,7 @@ const blueprint = {
8
8
  concerns: [JSON],
9
9
  options: [
10
10
  NewOpt("credentials", {
11
- descTemplate: "Path to JSON file containing credential sets for files stored in cloud",
11
+ descTemplate: "Path to JSON file containing access key or signed URL credential sets for files stored in cloud",
12
12
  group: "Cloud",
13
13
  normalize: true,
14
14
  type: "string"
@@ -66,4 +66,4 @@ const New = context => {
66
66
  return {credentialSet};
67
67
  };
68
68
 
69
- module.exports = {blueprint, New};
69
+ module.exports = {blueprint, New};
@@ -34,7 +34,10 @@ const accessMatchIndex = R.curry(
34
34
  const groupByPathMatch = (access, fileList) => R.groupBy(accessMatchIndex(access), fileList);
35
35
 
36
36
  const validateBucketMatch = R.curry((credentialSet, sourceFilePath) => {
37
- const credentialSetBucket = removeTrailingSlash(credentialSet.remote_access.path);
37
+ const remotePath = credentialSet.remote_access.path;
38
+ if(!remotePath) return;
39
+
40
+ const credentialSetBucket = removeTrailingSlash(remotePath);
38
41
  // if full s3 path supplied, check bucket name
39
42
  const s3prefixMatch = s3BucketRegex.exec(sourceFilePath);
40
43
  if(s3prefixMatch) {
@@ -96,17 +99,20 @@ const New = context => {
96
99
  if(kindOf(copy) === "undefined") copy = isCopy();
97
100
  files = files || context.args.files;
98
101
  const groupedFiles = groupByPathMatch(access, files);
99
- validatePathMatchGroups(groupedFiles);
102
+ validatePathMatchGroups(access, groupedFiles);
100
103
 
101
104
  const client = await context.concerns.Client.get();
102
105
 
103
106
  // iterate over file groups, add to fabric object using credential set for group
104
107
  for(const [index, fileList] of R.toPairs(groupedFiles)) {
105
108
  const credentialSet = access[index];
106
- const region = credentialSet.remote_access.storage_endpoint.region;
107
- const bucket = removeTrailingSlash(credentialSet.remote_access.path);
108
- const accessKey = credentialSet.remote_access.cloud_credentials.access_key_id;
109
- const secret = credentialSet.remote_access.cloud_credentials.secret_access_key;
109
+ const remoteAccess = credentialSet.remote_access;
110
+ const cloudCredentials = remoteAccess.cloud_credentials;
111
+ const region = remoteAccess.storage_endpoint && remoteAccess.storage_endpoint.region;
112
+ const bucket = remoteAccess.path && removeTrailingSlash(remoteAccess.path);
113
+ const accessKey = cloudCredentials.access_key_id;
114
+ const secret = cloudCredentials.secret_access_key;
115
+ const signedUrl = cloudCredentials.signed_url;
110
116
 
111
117
  await client.UploadFilesFromS3({
112
118
  libraryId,
@@ -117,6 +123,7 @@ const New = context => {
117
123
  bucket,
118
124
  accessKey,
119
125
  secret,
126
+ signedUrl,
120
127
  copy,
121
128
  callback,
122
129
  encryption: encrypt ? "cgck" : "none"
@@ -134,4 +141,4 @@ const New = context => {
134
141
  return {add, callback, credentialSet, fileInfo, isCopy};
135
142
  };
136
143
 
137
- module.exports = {blueprint, New};
144
+ module.exports = {blueprint, New};