@naanlang/naan 1.0.6 → 1.0.7
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.
- package/LICENSE.md +1 -1
- package/README.md +1 -1
- package/bin/index.js +2 -2
- package/dist/env_web.js +6 -2
- package/dist/naan.min.js +5 -5
- package/frameworks/browser/browser.nlg +67 -2
- package/frameworks/browser/https_request.nlg +2 -2
- package/frameworks/browser/sworker.js +17 -17
- package/frameworks/browser/terminals.nlg +14 -12
- package/frameworks/browser/workers.nlg +6 -6
- package/frameworks/common/common.nlg +8 -5
- package/frameworks/project/build.nlg +13 -9
- package/frameworks/project/projects.nlg +42 -17
- package/frameworks/running/running.nlg +1 -1
- package/frameworks/running/taskexec.nlg +1 -1
- package/frameworks/storage/file_manager.nlg +2 -1
- package/frameworks/storage/psm_dbtables.nlg +13 -3
- package/lib/browser/env_web.js +10 -6
- package/lib/browser/env_webworker.js +10 -0
- package/lib/core/naanlib.js +5 -5
- package/package.json +2 -1
- package/plugins/serviceAws/aws_dynamo.nlg +18 -6
- package/plugins/serviceAws/aws_dynextra.nlg +48 -28
- package/plugins/serviceAws/aws_s3.nlg +74 -28
- package/plugins/serviceAws/dbt_aws.nlg +73 -24
- package/plugins/serviceAws/psm_aws.nlg +30 -11
- package/plugins/serviceAws/serviceAws.nlg +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naanlang/naan",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"author": "Richard C. Zulch",
|
|
5
5
|
"description": "Naan™ software platform",
|
|
6
6
|
"main": "./lib/core/naanlib.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "node lib/node_repl.js",
|
|
15
|
+
"debug": "node --inspect-brk lib/node_repl.js",
|
|
15
16
|
"test": "node test/node_test.js"
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -515,6 +515,8 @@ closure DynaConverter(local conv) {
|
|
|
515
515
|
closure DynaTable(dyna, tablename, local table) {
|
|
516
516
|
table = new(object, this)
|
|
517
517
|
table.name = tablename
|
|
518
|
+
if dyna.ycdb
|
|
519
|
+
table.ycdb = true // Yandex Cloud version of DynamoDB
|
|
518
520
|
|
|
519
521
|
// info
|
|
520
522
|
//
|
|
@@ -665,7 +667,7 @@ closure DynaTable(dyna, tablename, local table) {
|
|
|
665
667
|
// Make a dynamoDB key dictionary for the table using the specified primary key, which can be a
|
|
666
668
|
// tuple of hash/range or just a hash if only a hash is needed for this table.
|
|
667
669
|
//
|
|
668
|
-
|
|
670
|
+
table.makeDynKey = function makeDynKey(primaryKey, local hashValue, rangeValue, key) {
|
|
669
671
|
if tuple(primaryKey)
|
|
670
672
|
`(hashValue, rangeValue) = primaryKey
|
|
671
673
|
else
|
|
@@ -994,14 +996,22 @@ closure DynamoDB(local dyna) {
|
|
|
994
996
|
|
|
995
997
|
// login
|
|
996
998
|
//
|
|
997
|
-
dyna.login = function login(creds) {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
999
|
+
dyna.login = function login(creds, local params) {
|
|
1000
|
+
if !awsSDK
|
|
1001
|
+
return (list(Error("DynamoDB.login: no AWS SDK available")))
|
|
1002
|
+
params = { }
|
|
1003
|
+
if creds {
|
|
1004
|
+
params.region = creds.region
|
|
1005
|
+
params.credentials = {
|
|
1001
1006
|
accessKeyId: creds.keyID
|
|
1002
1007
|
secretAccessKey: creds.keySecret
|
|
1003
1008
|
}
|
|
1004
|
-
|
|
1009
|
+
if creds.dyna_endpoint {
|
|
1010
|
+
params.endpoint = creds.dyna_endpoint
|
|
1011
|
+
dyna.ycdb = true
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
dyna.aws = xnew(awsSDK.DynamoDB, params)
|
|
1005
1015
|
list(false, { ok: true })
|
|
1006
1016
|
}
|
|
1007
1017
|
|
|
@@ -1011,6 +1021,8 @@ closure DynamoDB(local dyna) {
|
|
|
1011
1021
|
// yet exist; see DynaTable for methods.
|
|
1012
1022
|
//
|
|
1013
1023
|
dyna.table = function table(name) {
|
|
1024
|
+
if !dyna.aws
|
|
1025
|
+
debuglog("DynamoDB.table: database not open:", name)
|
|
1014
1026
|
DynaTable(dyna, name)
|
|
1015
1027
|
}
|
|
1016
1028
|
|
|
@@ -37,7 +37,7 @@ function dynexBatchGetRecords(dyna, request,
|
|
|
37
37
|
request = new(request) // allow local modifications
|
|
38
38
|
index = { }
|
|
39
39
|
for titem in request { // preflight all tables
|
|
40
|
-
tname = titem.table.
|
|
40
|
+
tname = titem.table.name
|
|
41
41
|
if index[tname]
|
|
42
42
|
return (Error("DynaTable.batchGetRecords: duplicate table in request:", tname))
|
|
43
43
|
`(error) = titem.table.info()
|
|
@@ -57,7 +57,7 @@ function dynexBatchGetRecords(dyna, request,
|
|
|
57
57
|
activity = false
|
|
58
58
|
for `(tname, titem) in index {
|
|
59
59
|
if titem.keys.length > 0 {
|
|
60
|
-
primary = titem.table.makeDynKey(titem.keys.
|
|
60
|
+
primary = titem.table.makeDynKey(titem.keys.shift())
|
|
61
61
|
if params.RequestItems[tname].Keys
|
|
62
62
|
params.RequestItems[tname].Keys.push(primary)
|
|
63
63
|
else
|
|
@@ -81,7 +81,7 @@ function dynexBatchGetRecords(dyna, request,
|
|
|
81
81
|
dyna.aws.batchGetItem(params, function(err, data) {
|
|
82
82
|
if err
|
|
83
83
|
debuglog("aws.batchGetItem failed", err)
|
|
84
|
-
pending.signal(err, data)
|
|
84
|
+
pending.signal(list(err, data))
|
|
85
85
|
})
|
|
86
86
|
`(err, response) = pending.wait()
|
|
87
87
|
for `(tname, table) in response.Responses
|
|
@@ -124,7 +124,8 @@ function dynexBatchGetRecords(dyna, request,
|
|
|
124
124
|
*
|
|
125
125
|
*/
|
|
126
126
|
|
|
127
|
-
closure dynexBatchWriteRecords(dyna, request,
|
|
127
|
+
closure dynexBatchWriteRecords(dyna, request,
|
|
128
|
+
local index, group, tname, batches, error, activity) {
|
|
128
129
|
if !dyna.aws
|
|
129
130
|
return (list(Error("DynaTable.batchWriteRecords: not logged into AWS")))
|
|
130
131
|
|
|
@@ -132,19 +133,18 @@ closure dynexBatchWriteRecords(dyna, request, local batches, error, activity) {
|
|
|
132
133
|
// addOp
|
|
133
134
|
// Add a put or delete operation to the parameters
|
|
134
135
|
//
|
|
135
|
-
function addOp(params, table, del, record, local request) {
|
|
136
|
+
function addOp(params, table, del, record, primary, local request) {
|
|
136
137
|
if del { // deleting a record
|
|
137
|
-
if tuple(record)
|
|
138
|
-
record = table.
|
|
139
|
-
|
|
140
|
-
request = { DeleteRequest: { Key: dyna.conv.recordNaanToDyn(record) } }
|
|
138
|
+
if !tuple(record)
|
|
139
|
+
record = list(record[table.hashKey], record[table.rangeKey])
|
|
140
|
+
request = { DeleteRequest: { Key: table.makeDynKey(record) } }
|
|
141
141
|
}
|
|
142
142
|
else // putting a record
|
|
143
143
|
request = { PutRequest: { Item: dyna.conv.recordNaanToDyn(record) } }
|
|
144
|
-
if params.RequestItems[table.
|
|
145
|
-
params.RequestItems[table.
|
|
144
|
+
if params.RequestItems[table.name]
|
|
145
|
+
params.RequestItems[table.name].push(request)
|
|
146
146
|
else
|
|
147
|
-
params.RequestItems[table.
|
|
147
|
+
params.RequestItems[table.name] = [request]
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// transform into batches
|
|
@@ -155,48 +155,68 @@ closure dynexBatchWriteRecords(dyna, request, local batches, error, activity) {
|
|
|
155
155
|
// request have no more entries.
|
|
156
156
|
//
|
|
157
157
|
request = new(request) // allow local modifications
|
|
158
|
+
index = { }
|
|
159
|
+
for group in request { // preflight all tables
|
|
160
|
+
tname = group.table.name
|
|
161
|
+
if index[tname]
|
|
162
|
+
return (Error("DynaTable.dynexBatchWriteRecords: duplicate table in request:", tname))
|
|
163
|
+
`(error) = group.table.info()
|
|
164
|
+
if error
|
|
165
|
+
return (Error("DynaTable.dynexBatchWriteRecords: table", tname, ":", error))
|
|
166
|
+
index[tname] = group
|
|
167
|
+
if group.put // ### in case these are xobjects
|
|
168
|
+
group.put = new(group.put) // new() doesn't deep clone them
|
|
169
|
+
if group.delete
|
|
170
|
+
group.delete = new(group.delete)
|
|
171
|
+
}
|
|
172
|
+
request = new(request) // allow local modifications
|
|
158
173
|
batches = []
|
|
159
174
|
activity = true
|
|
160
175
|
while activity
|
|
161
|
-
let (params, batchsize
|
|
176
|
+
let (params, batchsize) {
|
|
162
177
|
params = {
|
|
163
178
|
RequestItems: { }
|
|
164
179
|
}
|
|
165
180
|
batchsize = 0
|
|
166
181
|
while :fillbatch activity { // loop until no activity or batch full
|
|
167
182
|
activity = false
|
|
168
|
-
for
|
|
169
|
-
if
|
|
170
|
-
addOp(params, table, true,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
for group in request {
|
|
184
|
+
if group.delete.length > 0 {
|
|
185
|
+
addOp(params, group.table, true, group.delete.shift())
|
|
186
|
+
activity = true
|
|
187
|
+
++batchsize
|
|
188
|
+
}
|
|
189
|
+
if group.put.length > 0 {
|
|
190
|
+
addOp(params, group.table, false, group.put.shift())
|
|
191
|
+
activity = true
|
|
192
|
+
++batchsize
|
|
193
|
+
}
|
|
194
|
+
if batchsize >= 25
|
|
177
195
|
break :fillbatch // our batch is full
|
|
178
196
|
}
|
|
179
197
|
}
|
|
180
198
|
if batchsize > 0
|
|
181
199
|
batches.push(params)
|
|
182
200
|
} ()
|
|
183
|
-
asyncArray(batches,
|
|
201
|
+
asyncArray(batches, 1, closure bwr(params, local delayer, pending, err, response) {
|
|
184
202
|
delayer = dyna.conv.genBackoffDelayer()
|
|
185
203
|
loop {
|
|
186
204
|
pending = new(nonce)
|
|
187
|
-
dyna.aws.batchWriteItem(params,
|
|
205
|
+
dyna.aws.batchWriteItem(params, closure(err, data) {
|
|
188
206
|
if err
|
|
189
207
|
debuglog("aws.batchWriteItem failed", err)
|
|
190
|
-
pending.signal(err, data)
|
|
208
|
+
pending.signal(list(err, data))
|
|
191
209
|
})
|
|
192
210
|
`(err, response) = pending.wait()
|
|
193
211
|
if response.UnprocessedKeys
|
|
194
212
|
params.RequestItems = response.UnprocessedKeys // still more to do
|
|
195
213
|
else
|
|
196
214
|
break // total success or failure
|
|
215
|
+
debuglog("dynexBatchWriteRecords unprocessedKeys:", response.UnprocessedKeys)
|
|
197
216
|
if delayer() > dyna.timeout {
|
|
198
217
|
if !error // too slow
|
|
199
218
|
error = err
|
|
219
|
+
debuglog("dynexBatchWriteRecords timeout", error, err)
|
|
200
220
|
break
|
|
201
221
|
}
|
|
202
222
|
}
|
|
@@ -235,7 +255,7 @@ closure dynexTransactGetRecords(dyna, request,
|
|
|
235
255
|
request = new(request) // allow local modifications
|
|
236
256
|
index = { }
|
|
237
257
|
for titem in request { // preflight all tables
|
|
238
|
-
tname = titem.table.
|
|
258
|
+
tname = titem.table.name
|
|
239
259
|
if index[tname]
|
|
240
260
|
return (Error("DynaTable.batchGetRecords: duplicate table in request:", tname))
|
|
241
261
|
`(error) = titem.table.info()
|
|
@@ -252,7 +272,7 @@ closure dynexTransactGetRecords(dyna, request,
|
|
|
252
272
|
params.TransactItems.push({
|
|
253
273
|
Get: {
|
|
254
274
|
TableName: tname
|
|
255
|
-
Key: makeDynKey(titem.keys.
|
|
275
|
+
Key: makeDynKey(titem.keys.shift())
|
|
256
276
|
}
|
|
257
277
|
})
|
|
258
278
|
}
|
|
@@ -262,7 +282,7 @@ closure dynexTransactGetRecords(dyna, request,
|
|
|
262
282
|
dyna.aws.transactGetItems(params, function(err, data) {
|
|
263
283
|
if err
|
|
264
284
|
debuglog("aws.transactGetItems failed", err)
|
|
265
|
-
pending.signal(err, data)
|
|
285
|
+
pending.signal(list(err, data))
|
|
266
286
|
})
|
|
267
287
|
`(error, response) = pending.wait()
|
|
268
288
|
if error
|
|
@@ -13,17 +13,36 @@
|
|
|
13
13
|
|
|
14
14
|
/*
|
|
15
15
|
* S3bucket
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Bucket ooptions:
|
|
18
|
+
* {
|
|
19
|
+
* cacheControl: "max-age=2592000" - default cache control, e.g. to "max-age=2592000"
|
|
20
|
+
* }
|
|
18
21
|
*
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
|
-
closure S3bucket(s3, name, local bucket) {
|
|
24
|
+
closure S3bucket(s3, name, bucketOptions, local bucket) {
|
|
22
25
|
bucket = new(object, this)
|
|
23
26
|
bucket.name = name
|
|
24
|
-
|
|
27
|
+
|
|
28
|
+
//
|
|
29
|
+
// head
|
|
30
|
+
//
|
|
31
|
+
bucket.head = closure head(key, local pending) {
|
|
32
|
+
pending = new(nonce)
|
|
33
|
+
s3.aws.headObject({
|
|
34
|
+
Bucket: bucket.name
|
|
35
|
+
Key: key
|
|
36
|
+
}, function (error, data) {
|
|
37
|
+
if error
|
|
38
|
+
error = Error("S3bucket.head failed:", error.name, "key:", key, "in", bucket.name)
|
|
39
|
+
pending.signal(list(error, data))
|
|
40
|
+
})
|
|
41
|
+
pending.wait()
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
//
|
|
26
|
-
//
|
|
45
|
+
// listObjects
|
|
27
46
|
//
|
|
28
47
|
bucket.listObjects = closure listObjects(local pending) {
|
|
29
48
|
pending = new(nonce)
|
|
@@ -31,31 +50,46 @@ closure S3bucket(s3, name, local bucket) {
|
|
|
31
50
|
Bucket: bucket.name
|
|
32
51
|
}, function (error, data) {
|
|
33
52
|
if error
|
|
34
|
-
error = Error("S3bucket.list failed", error)
|
|
53
|
+
error = Error("S3bucket.list failed:", error.name, "for", bucket.name)
|
|
35
54
|
pending.signal(list(error, data))
|
|
36
55
|
})
|
|
37
56
|
pending.wait()
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
//
|
|
41
|
-
//
|
|
60
|
+
// put
|
|
42
61
|
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
// options:
|
|
63
|
+
// {
|
|
64
|
+
// contentType: "text/plain" - override contentType if specified
|
|
65
|
+
// cacheControl: "max-age=2592000" - default cache control, e.g. to "max-age=2592000"
|
|
66
|
+
// metadata: { <items> } - optional dictionary of keys to store
|
|
67
|
+
// }
|
|
68
|
+
//
|
|
69
|
+
bucket.put = closure put(key, data, options, local params, pending) {
|
|
70
|
+
if !contentType
|
|
71
|
+
contentType = "application/octet-stream"
|
|
72
|
+
params = {
|
|
46
73
|
Bucket: bucket.name
|
|
47
|
-
ACL: "public-read"
|
|
48
|
-
Body: data
|
|
49
74
|
Key: key
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
75
|
+
ContentMD5: HashMD5_base64(data)
|
|
76
|
+
Body: data
|
|
77
|
+
}
|
|
78
|
+
if options.contentType
|
|
79
|
+
params.ContentType = options.contentType
|
|
80
|
+
if options.metadata
|
|
81
|
+
params.Metadata = options.metadata
|
|
82
|
+
else
|
|
83
|
+
params.Metadata = "application/octet-stream"
|
|
84
|
+
if options.cacheControl
|
|
85
|
+
params.CacheControl = options.cacheControl
|
|
86
|
+
else if bucket.cacheControl
|
|
87
|
+
params.CacheControl = bucket.cacheControl
|
|
88
|
+
pending = new(nonce)
|
|
89
|
+
s3.aws.putObject(params, function(error, resp) {
|
|
56
90
|
if error
|
|
57
|
-
error = Error("S3bucket.
|
|
58
|
-
pending.signal(list(error,
|
|
91
|
+
error = Error("S3bucket.put failed:", error.name, "key:", key, "in", bucket.name)
|
|
92
|
+
pending.signal(list(error, resp))
|
|
59
93
|
})
|
|
60
94
|
pending.wait()
|
|
61
95
|
}
|
|
@@ -88,6 +122,8 @@ closure S3(local s3) {
|
|
|
88
122
|
accessKeyId: creds.keyID
|
|
89
123
|
secretAccessKey: creds.keySecret
|
|
90
124
|
}
|
|
125
|
+
if creds.endpoint
|
|
126
|
+
params.endpoint = creds.endpoint
|
|
91
127
|
}
|
|
92
128
|
s3.aws = xnew(awsSDK.S3, params)
|
|
93
129
|
if awsSDK.S3Client
|
|
@@ -111,12 +147,23 @@ closure S3(local s3) {
|
|
|
111
147
|
// This obtains a pre-signed URL for the specified command and its parameters. Options control
|
|
112
148
|
// how the URL is generated.
|
|
113
149
|
//
|
|
150
|
+
// CommandParams:
|
|
151
|
+
// {
|
|
152
|
+
// Bucket: <string> // bucket name
|
|
153
|
+
// Key: <string> // object being operated upon
|
|
154
|
+
// ContentType: <string> // video/mp4 or whatever
|
|
155
|
+
// ResponseCacheControl: <string> // reportedly for getObject only
|
|
156
|
+
// ACL: <string> // e.g. "public-read"
|
|
157
|
+
// Body: <data> // fiik
|
|
158
|
+
// }
|
|
159
|
+
//
|
|
114
160
|
// Options:
|
|
115
161
|
// {
|
|
116
|
-
// expiresIn:
|
|
162
|
+
// expiresIn: <seconds> // default expiration is 900 (15 minutes)
|
|
163
|
+
// signingDate: <epoch-secs> // reference time for URL being created
|
|
117
164
|
// }
|
|
118
165
|
//
|
|
119
|
-
s3.getSignedURL = closure getSignedURL(cmdname, cmdparams, local command,
|
|
166
|
+
s3.getSignedURL = closure getSignedURL(cmdname, cmdparams, options, local command, pending) {
|
|
120
167
|
if s3.client {
|
|
121
168
|
if cmdname == "putObject"
|
|
122
169
|
cmdname = "PutObjectCommand"
|
|
@@ -124,16 +171,15 @@ closure S3(local s3) {
|
|
|
124
171
|
cmdname = "GetObjectCommand"
|
|
125
172
|
else
|
|
126
173
|
return (list(Error("S3bucket.getSignedURL: command not present:", cmdname)))
|
|
127
|
-
if cmdparams.Expires {
|
|
128
|
-
cmdparams = new(cmdparams)
|
|
129
|
-
options.expiresIn = cmdparams.Expires
|
|
130
|
-
cmdparams.Expires = undefined
|
|
131
|
-
} else
|
|
132
|
-
options = { }
|
|
133
174
|
command = xnew(awsSDK.s3Commands[cmdname], cmdparams)
|
|
134
175
|
await(awsSDK.getSignedUrl(s3.client, command, options))
|
|
135
176
|
} else {
|
|
177
|
+
debuglog("s3.getSignedURL: warning: old API")
|
|
136
178
|
pending = new(nonce)
|
|
179
|
+
if options.expiresIn {
|
|
180
|
+
cmdparams = new(cmdparams)
|
|
181
|
+
cmdparams.Expires = options.expiresIn
|
|
182
|
+
}
|
|
137
183
|
s3.aws.getSignedUrl(cmdname, cmdparams, function(error, data) {
|
|
138
184
|
pending.signal(list(error, data))
|
|
139
185
|
})
|
|
@@ -37,6 +37,19 @@ closure dawsBucket(s3b, local bucket, s3) {
|
|
|
37
37
|
closure asyncResult args {
|
|
38
38
|
future(function() { apply(pop(args), args) }).run()
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
// hashFromETag
|
|
42
|
+
//
|
|
43
|
+
// Return the MD5 hash for an ETag
|
|
44
|
+
//
|
|
45
|
+
function hashFromETag(etag) {
|
|
46
|
+
if etag.startsWith("W/")
|
|
47
|
+
etag = etag.slice(2) // "weak" from gzip with NGINX, apparently
|
|
48
|
+
if etag.length == 34 // multipart S3 ETag will is longer and will be ignored
|
|
49
|
+
etag.substring(1,33)
|
|
50
|
+
else
|
|
51
|
+
undefined
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
// close
|
|
42
55
|
//
|
|
@@ -65,8 +78,8 @@ closure dawsBucket(s3b, local bucket, s3) {
|
|
|
65
78
|
function s3GetCB(error, resp) {
|
|
66
79
|
if error
|
|
67
80
|
error = Error("can't get object", error, key)
|
|
68
|
-
else
|
|
69
|
-
resp.md5 = resp.ETag
|
|
81
|
+
else
|
|
82
|
+
resp.md5 = hashFromETag(resp.ETag)
|
|
70
83
|
callback(error, resp)
|
|
71
84
|
}
|
|
72
85
|
|
|
@@ -79,12 +92,18 @@ closure dawsBucket(s3b, local bucket, s3) {
|
|
|
79
92
|
// put
|
|
80
93
|
//
|
|
81
94
|
// Put an object in the bucket. Parameters include:
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
95
|
+
// key - the object name, which may not be empty or start with "/"
|
|
96
|
+
// data - optional content to store
|
|
97
|
+
// options:
|
|
98
|
+
// {
|
|
99
|
+
// contentType: "text/plain" - override contentType if specified
|
|
100
|
+
// cacheControl: "max-age=2592000" - default cache control, e.g. to "max-age=2592000"
|
|
101
|
+
// metadata: { <items> } - optional dictionary of keys to store
|
|
102
|
+
// }
|
|
85
103
|
|
|
86
|
-
bucket.put = closure put(key, data,
|
|
87
|
-
|
|
104
|
+
bucket.put = closure put(key, data, options, callback,
|
|
105
|
+
local params, mimeType, md5) {
|
|
106
|
+
if !string(key) || length(key) == 0 || key.substring(0,1) == "/"
|
|
88
107
|
return (asyncResult(callback, Error("invalid arguments")))
|
|
89
108
|
if !s3
|
|
90
109
|
return (asyncResult(callback, Error("database closed")))
|
|
@@ -128,9 +147,13 @@ closure dawsBucket(s3b, local bucket, s3) {
|
|
|
128
147
|
mimeType = mimeType.concat("; charset=UTF-8")
|
|
129
148
|
params.ContentType = mimeType
|
|
130
149
|
}
|
|
150
|
+
if options.contentType
|
|
151
|
+
params.ContentType = options.contentType
|
|
131
152
|
}
|
|
132
|
-
if metadata
|
|
133
|
-
params.Metadata = metadata
|
|
153
|
+
if options.metadata
|
|
154
|
+
params.Metadata = options.metadata
|
|
155
|
+
if options.cacheControl
|
|
156
|
+
params.CacheControl = options.cacheControl
|
|
134
157
|
if data
|
|
135
158
|
params.Body = data
|
|
136
159
|
else
|
|
@@ -142,11 +165,10 @@ closure dawsBucket(s3b, local bucket, s3) {
|
|
|
142
165
|
error = Error("can't put object", error, key)
|
|
143
166
|
else {
|
|
144
167
|
resp.length = length(data)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
resp = false } } }
|
|
168
|
+
resp.md5 = hashFromETag(resp.ETag)
|
|
169
|
+
if resp.md5 && md5 != resp.md5 {
|
|
170
|
+
error = Error("hash match failure", md5, resp.md5) // has match failed on round-trip
|
|
171
|
+
resp = false } }
|
|
150
172
|
callback(error, resp)
|
|
151
173
|
})
|
|
152
174
|
}
|
|
@@ -285,12 +307,31 @@ closure dawsBucket(s3b, local bucket, s3) {
|
|
|
285
307
|
*
|
|
286
308
|
*/
|
|
287
309
|
|
|
288
|
-
closure dawsTable(database, path, local table) {
|
|
310
|
+
closure dawsTable(database, path, tableOptions, local table) {
|
|
289
311
|
table = new(object, this)
|
|
290
312
|
table.type = database.type
|
|
291
313
|
table.db = database
|
|
292
314
|
table.path = path
|
|
293
315
|
table.s3prefix = makePrefix(path)
|
|
316
|
+
|
|
317
|
+
// readableStreamToArrayBuffer
|
|
318
|
+
//
|
|
319
|
+
// For browsers, convert a ReadableStream to an array buffer.
|
|
320
|
+
//
|
|
321
|
+
function readableStreamToArrayBuffer(stream, local result, reader, error, data, chunk) {
|
|
322
|
+
result = xnew(js.w.Uint8Array, 0)
|
|
323
|
+
reader = stream.getReader()
|
|
324
|
+
loop {
|
|
325
|
+
`(error, data) = await(reader.read())
|
|
326
|
+
if error || data.done
|
|
327
|
+
break
|
|
328
|
+
chunk = xnew(js.w.Uint8Array, result.length + data.value.length)
|
|
329
|
+
chunk.set(result)
|
|
330
|
+
chunk.set(data.value, result.length)
|
|
331
|
+
result = chunk
|
|
332
|
+
}
|
|
333
|
+
result
|
|
334
|
+
}
|
|
294
335
|
|
|
295
336
|
// makePrefix
|
|
296
337
|
//
|
|
@@ -344,7 +385,7 @@ closure dawsTable(database, path, local table) {
|
|
|
344
385
|
if resp.ContentType
|
|
345
386
|
doc.contentType = resp.ContentType
|
|
346
387
|
if resp.md5
|
|
347
|
-
doc.
|
|
388
|
+
doc._md5 = resp.md5
|
|
348
389
|
doc
|
|
349
390
|
}
|
|
350
391
|
|
|
@@ -394,11 +435,14 @@ closure dawsTable(database, path, local table) {
|
|
|
394
435
|
if content
|
|
395
436
|
doc.content = content
|
|
396
437
|
metadata = metadata2s3(metadata) // filter out any invalid metadata
|
|
397
|
-
database.bucket.put(id, content,
|
|
438
|
+
database.bucket.put(id, content, {
|
|
439
|
+
metadata: metadata
|
|
440
|
+
cacheControl: tableOptions.cacheControl
|
|
441
|
+
}, function(error, resp) {
|
|
398
442
|
if error
|
|
399
443
|
error = Error("add failed", error, id, { status: error.$metadata.httpStatusCode })
|
|
400
444
|
else {
|
|
401
|
-
doc.
|
|
445
|
+
doc._md5 = resp.md5 // update the original doc
|
|
402
446
|
doc.length = resp.length }
|
|
403
447
|
callback(error, doc)
|
|
404
448
|
})
|
|
@@ -411,14 +455,17 @@ closure dawsTable(database, path, local table) {
|
|
|
411
455
|
table.update = closure update(doc, callback, local metadata) {
|
|
412
456
|
if !callback
|
|
413
457
|
return (syncAdapter(update, doc))
|
|
414
|
-
if !doc._id || !doc._id.startsWith(table.s3prefix) || !doc.
|
|
458
|
+
if !doc._id || !doc._id.startsWith(table.s3prefix) || !doc._md5
|
|
415
459
|
return (asyncResult(callback, Error("invalid argument")))
|
|
416
460
|
metadata = metadata2s3(doc)
|
|
417
|
-
database.bucket.put(doc._id, doc.content,
|
|
461
|
+
database.bucket.put(doc._id, doc.content, {
|
|
462
|
+
metadata: metadata
|
|
463
|
+
cacheControl: tableOptions.cacheControl
|
|
464
|
+
}, function(error, resp) {
|
|
418
465
|
if error
|
|
419
466
|
error = Error("update failed", error, doc._id, { status: error.$metadata.httpStatusCode })
|
|
420
467
|
else {
|
|
421
|
-
doc.
|
|
468
|
+
doc._md5 = resp.md5 // update the original doc
|
|
422
469
|
doc.modified = undefined // we don't know exact modify date
|
|
423
470
|
doc.length = resp.length }
|
|
424
471
|
callback(error, doc)
|
|
@@ -433,7 +480,7 @@ closure dawsTable(database, path, local table) {
|
|
|
433
480
|
table.delete = closure delete(doc, callback) {
|
|
434
481
|
if !callback
|
|
435
482
|
return (syncAdapter(delete, doc))
|
|
436
|
-
if !doc._id || !doc._id.startsWith(table.s3prefix) || !doc.
|
|
483
|
+
if !doc._id || !doc._id.startsWith(table.s3prefix) || !(doc._md5 || doc._id.slice(-1) == "/")
|
|
437
484
|
return (asyncResult(callback, Error("invalid argument")))
|
|
438
485
|
database.bucket.delete(doc._id, function(error, resp) {
|
|
439
486
|
if error
|
|
@@ -469,6 +516,8 @@ closure dawsTable(database, path, local table) {
|
|
|
469
516
|
{ } // already "decoded", e.g. base64
|
|
470
517
|
else if jsTypedArray(resp.content)
|
|
471
518
|
resp.content = xnew(TextDecoder, "utf-8").decode(resp.content)
|
|
519
|
+
else if jsInstanceOf(resp.content, js.w.ReadableStream)
|
|
520
|
+
resp.content = xnew(TextDecoder, "utf-8").decode(readableStreamToArrayBuffer(resp.content))
|
|
472
521
|
resp._id = id }
|
|
473
522
|
callback(error, resp)
|
|
474
523
|
})
|
|
@@ -748,12 +797,12 @@ closure S3DB(s3b, local database, result) {
|
|
|
748
797
|
//
|
|
749
798
|
// Return a table-access object for the table.
|
|
750
799
|
|
|
751
|
-
database.table = function table(path) {
|
|
800
|
+
database.table = function table(path, options) {
|
|
752
801
|
if !database.bucket
|
|
753
802
|
return (list(Error("database closed")))
|
|
754
803
|
if !string(path)
|
|
755
804
|
return (list(Error("invalid table path")))
|
|
756
|
-
dawsTable(database, path)
|
|
805
|
+
dawsTable(database, path, options)
|
|
757
806
|
}
|
|
758
807
|
|
|
759
808
|
// finis
|