@naanlang/naan 1.0.0
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 +21 -0
- package/README.md +46 -0
- package/dist/env_web.js +431 -0
- package/dist/naan.min.js +73 -0
- package/frameworks/browser/browser.nlg +30 -0
- package/frameworks/browser/pouchdb/pouchdb.min.js +7 -0
- package/frameworks/browser/spark-md5/spark-md5.min.js +1 -0
- package/frameworks/browser/sworker.js +192 -0
- package/frameworks/browser/terminals.nlg +787 -0
- package/frameworks/browser/workers.nlg +386 -0
- package/frameworks/client/apiclient.nlg +183 -0
- package/frameworks/client/client.nlg +29 -0
- package/frameworks/client/psm_client.nlg +572 -0
- package/frameworks/common/common.nlg +464 -0
- package/frameworks/common/preferences.nlg +131 -0
- package/frameworks/common/repltools.nlg +100 -0
- package/frameworks/common/watching.nlg +97 -0
- package/frameworks/node/apiserver.nlg +399 -0
- package/frameworks/node/filesystem.nlg +1012 -0
- package/frameworks/node/gitter.nlg +234 -0
- package/frameworks/node/node.nlg +81 -0
- package/frameworks/node/psm_server.nlg +465 -0
- package/frameworks/node/worker.nlg +560 -0
- package/frameworks/project/build.nlg +1229 -0
- package/frameworks/project/jszip.js +30 -0
- package/frameworks/project/jszip.nlg +87 -0
- package/frameworks/project/proj_cliser.nlg +139 -0
- package/frameworks/project/proj_console.nlg +137 -0
- package/frameworks/project/proj_folder.nlg +143 -0
- package/frameworks/project/proj_lambda.nlg +137 -0
- package/frameworks/project/proj_static.nlg +138 -0
- package/frameworks/project/project.nlg +29 -0
- package/frameworks/project/projects.nlg +587 -0
- package/frameworks/project/uglifyjs.js +1 -0
- package/frameworks/project/uglifyjs.nlg +79 -0
- package/frameworks/running/debugcom.nlg +163 -0
- package/frameworks/running/debugnub.nlg +565 -0
- package/frameworks/running/debugutil.nlg +510 -0
- package/frameworks/running/executors.nlg +767 -0
- package/frameworks/running/running.nlg +151 -0
- package/frameworks/running/sourcecode.nlg +346 -0
- package/frameworks/running/taskexec.nlg +196 -0
- package/frameworks/storage/dbt_pouch.nlg +773 -0
- package/frameworks/storage/file_manager.nlg +772 -0
- package/frameworks/storage/psm.nlg +326 -0
- package/frameworks/storage/psm_dbtables.nlg +814 -0
- package/frameworks/storage/resources.nlg +445 -0
- package/frameworks/storage/storage.nlg +52 -0
- package/lib/browser/env_web.js +431 -0
- package/lib/browser/env_webworker.js +146 -0
- package/lib/browser/require.js +1653 -0
- package/lib/core/naanlib.js +73 -0
- package/lib/env_node.js +282 -0
- package/lib/env_nodeworker.js +165 -0
- package/lib/node_repl.js +25 -0
- package/lib/node_repl_init.nlg +52 -0
- package/package.json +13 -0
- package/plugins/gitClient/clientops.nlg +317 -0
- package/plugins/gitClient/gitClient.nlg +63 -0
- package/plugins/gitClient/gitutils.nlg +1331 -0
- package/plugins/gitClient/minimatch.js +945 -0
- package/plugins/gitClient/minimatch_license.txt +15 -0
- package/plugins/gitClient/minimatch_readme.md +208 -0
- package/plugins/nodeLib/nodeLib.nlg +26 -0
- package/plugins/nodeLib/node_cp.nlg +65 -0
- package/plugins/nodeLib/node_fs.nlg +137 -0
- package/plugins/nodeLib/node_https.nlg +172 -0
- package/plugins/nodeLib/node_util.nlg +180 -0
- package/plugins/serviceAws/aws/aws-sdk.min.js +88 -0
- package/plugins/serviceAws/aws/lambda_rest_index.js +122 -0
- package/plugins/serviceAws/aws/lambda_rest_init.nlg +58 -0
- package/plugins/serviceAws/aws/lambda_ws_index.js +498 -0
- package/plugins/serviceAws/aws/lambda_ws_init.nlg +49 -0
- package/plugins/serviceAws/aws_cloudwatchlogs.nlg +122 -0
- package/plugins/serviceAws/aws_cognito.nlg +76 -0
- package/plugins/serviceAws/aws_dynamo.nlg +509 -0
- package/plugins/serviceAws/aws_lambda.nlg +95 -0
- package/plugins/serviceAws/aws_s3.nlg +134 -0
- package/plugins/serviceAws/aws_utils.nlg +86 -0
- package/plugins/serviceAws/dbt_aws.nlg +769 -0
- package/plugins/serviceAws/lambda_naan_control.nlg +175 -0
- package/plugins/serviceAws/psm_aws.nlg +258 -0
- package/plugins/serviceAws/serviceAws.nlg +38 -0
- package/plugins/serviceGitHub/gitops.nlg +522 -0
- package/plugins/serviceGitHub/psm_github.nlg +205 -0
- package/plugins/serviceGitHub/serviceGitHub.nlg +65 -0
- package/plugins/serviceGitLab/gitops.nlg +528 -0
- package/plugins/serviceGitLab/psm_gitlab.nlg +205 -0
- package/plugins/serviceGitLab/serviceGitLab.nlg +65 -0
- package/test/harness.nlg +312 -0
- package/test/node_test.js +33 -0
- package/test/test_01_core.nlg +768 -0
- package/test/test_02_context.nlg +269 -0
- package/test/test_03_jsinterop.nlg +137 -0
- package/test/test_04_numerics.nlg +450 -0
- package/test/test_05_strings.nlg +63 -0
- package/test/test_06_lingoparse.nlg +500 -0
- package/test/test_07_lingo.nlg +623 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* psm_dbtables.nlg
|
|
3
|
+
* frameworks/storage
|
|
4
|
+
*
|
|
5
|
+
* PSM Connector for file systems on databases.
|
|
6
|
+
*
|
|
7
|
+
* column positioning: // // !
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2020-2021 by Richard C. Zulch
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* FSview
|
|
16
|
+
*
|
|
17
|
+
* Filesystem services via the specified DBtables instance. This view object allows access to an
|
|
18
|
+
* underlying database using filesystem semantics. Naming provided by this filesystem adheres to the
|
|
19
|
+
* following rules:
|
|
20
|
+
* - terminology: a generic file or directory is a "node"
|
|
21
|
+
* - the path separator is "/"
|
|
22
|
+
* - filenames are non-empty string not containing "/"
|
|
23
|
+
* - directory names are non-empty strings with the sole "/" at the end
|
|
24
|
+
* - the names ".", "..", and anything containing ":" are forbidden
|
|
25
|
+
* - every directory has (or should have) an associated database record for itself
|
|
26
|
+
* - files lacking a valid directory parent are "orphans" listed during tree enumeration
|
|
27
|
+
* - pathnames are relative to the root of the view an d consist of a string with:
|
|
28
|
+
* - zero or more directory names joined by "/", then
|
|
29
|
+
* - either "", "/" for a directory, or "/" with a filename
|
|
30
|
+
* - the empty pathname "" refers to the root of the view
|
|
31
|
+
* - the underlying database uses records named with "/" characters for the directories
|
|
32
|
+
* - e.g. "a/" is the directory "a", and "a/b" is the file "b" in directory "a"
|
|
33
|
+
* - record names starting with "/" or having sequential "/" characters are no-name directories
|
|
34
|
+
* - no-name directories can be enumerated and accessed, but not created
|
|
35
|
+
* - pathnames starting with "/" are within a no-name directory in the root of the view
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
closure FSview(table, rootpath, local view, error) {
|
|
40
|
+
if !table
|
|
41
|
+
return (list(Error("invalid arguments")))
|
|
42
|
+
view = new(object, this)
|
|
43
|
+
view.table = table
|
|
44
|
+
view.rootpath = rootpath
|
|
45
|
+
view.path = JSpath.posix // for view.path.join(), etc.
|
|
46
|
+
|
|
47
|
+
// dirName
|
|
48
|
+
//
|
|
49
|
+
// Return the directory portion of the specified path.
|
|
50
|
+
|
|
51
|
+
function dirName(path, local slashdex) {
|
|
52
|
+
if path == "" || (slashdex = path.lastIndexOf("/")) < 0
|
|
53
|
+
"" // empty parent directory
|
|
54
|
+
else if path.slice(-1) != "/"
|
|
55
|
+
path.substring(0, slashdex+1) // file at end
|
|
56
|
+
else if path == "/" || (slashdex = path.lastIndexOf("/", slashdex-1)) < 0
|
|
57
|
+
"" // empty parent directory of directory
|
|
58
|
+
else
|
|
59
|
+
path.substring(0, slashdex+1) // directory at end
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// baseName
|
|
63
|
+
//
|
|
64
|
+
// Return the final segment of the specified path, with no terminal slash.
|
|
65
|
+
|
|
66
|
+
function baseName(path, local slashdex) {
|
|
67
|
+
if path == "" || (slashdex = path.lastIndexOf("/")) < 0
|
|
68
|
+
path // no parent directory
|
|
69
|
+
else if path.slice(-1) != "/"
|
|
70
|
+
path.slice(slashdex+1) // file at end
|
|
71
|
+
else if path == "/" || (slashdex = path.lastIndexOf("/", slashdex-1)) < 0
|
|
72
|
+
path.slice(0, -1) // directory with empty parent
|
|
73
|
+
else
|
|
74
|
+
path.slice(slashdex+1, -1) // directory at end
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// validName
|
|
78
|
+
//
|
|
79
|
+
// Test whether the specified file or directory name is valid. If isPath is true then we look at
|
|
80
|
+
// the basename of the assumed path.
|
|
81
|
+
|
|
82
|
+
function validName(name, isPath) {
|
|
83
|
+
if isPath {
|
|
84
|
+
if name == ""
|
|
85
|
+
return (true)
|
|
86
|
+
else if name.substring(0, 1) == "/" || path.indexOf("//") >= 0
|
|
87
|
+
return (false)
|
|
88
|
+
else
|
|
89
|
+
name = baseName(name) }
|
|
90
|
+
name != "" && name != "." && name != ".." && name.indexOf("/") < 0 && name.indexOf(":") < 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// checkPath
|
|
94
|
+
//
|
|
95
|
+
// Execute callback with error if path invalid, otherwise return false.
|
|
96
|
+
|
|
97
|
+
function checkPath(path, callback, msg) {
|
|
98
|
+
if !string(path) {
|
|
99
|
+
if !msg
|
|
100
|
+
msg = "invalid path"
|
|
101
|
+
if !string(path)
|
|
102
|
+
path = "<".concat(typeof(path), ">") // e.g. "<xobject>"
|
|
103
|
+
asyncResult(callback, Error(msg, typeof(content), {
|
|
104
|
+
status: 400
|
|
105
|
+
name: "invalid_arguments"
|
|
106
|
+
path: path
|
|
107
|
+
}))
|
|
108
|
+
true }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// checkPathName
|
|
112
|
+
//
|
|
113
|
+
// Return a `(path, name) tuple for a directory, or false if callback executed due to error.
|
|
114
|
+
|
|
115
|
+
function checkPathName(path, callback, local name, slashdex) {
|
|
116
|
+
if path == ""
|
|
117
|
+
name = ""
|
|
118
|
+
else {
|
|
119
|
+
if checkPath(path, callback)
|
|
120
|
+
return (false) // non-string
|
|
121
|
+
if path.slice(-1) != "/"
|
|
122
|
+
path = path.concat("/") // force to be directory
|
|
123
|
+
slashdex = path.lastIndexOf("/", path.length-2)
|
|
124
|
+
if path == "/" || slashdex < 0
|
|
125
|
+
name = path
|
|
126
|
+
else
|
|
127
|
+
name = path.slice(slashdex+1, -1) // omit trailing "/"
|
|
128
|
+
}
|
|
129
|
+
list(path, name)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// dbDirRecords
|
|
133
|
+
//
|
|
134
|
+
// Return all the node records in the directory, optionally with transitive children.
|
|
135
|
+
|
|
136
|
+
closure dbDirRecords(path, deep, callback, local options) {
|
|
137
|
+
if !callback
|
|
138
|
+
return (syncAdapter(dbDirRecords, path, deep))
|
|
139
|
+
if checkPath(path, callback)
|
|
140
|
+
return
|
|
141
|
+
options = {
|
|
142
|
+
prefix: path
|
|
143
|
+
head: true
|
|
144
|
+
}
|
|
145
|
+
if deep
|
|
146
|
+
options.deep = true
|
|
147
|
+
view.table.records(options, callback)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// dbReadRecord
|
|
151
|
+
//
|
|
152
|
+
// Read a record at the specified path. If nodata is true then no content is included. If the
|
|
153
|
+
// path does not end with a slash and it's not found, then we try again with a slash.
|
|
154
|
+
|
|
155
|
+
closure dbReadRecord(path, nodata, callback, local proc, error, data) {
|
|
156
|
+
if !callback
|
|
157
|
+
return (syncAdapter(dbReadRecord, path, nodata))
|
|
158
|
+
if checkPath(path, callback)
|
|
159
|
+
return
|
|
160
|
+
if nodata
|
|
161
|
+
proc = view.table.head
|
|
162
|
+
else
|
|
163
|
+
proc = view.table.get
|
|
164
|
+
`(error, data) = proc(path)
|
|
165
|
+
if error && error.status == 404 && path.slice(-1) != "/"
|
|
166
|
+
`(error, data) = proc(path.concat("/"))
|
|
167
|
+
callback(error, data)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// dbWriteRecord
|
|
171
|
+
//
|
|
172
|
+
// Write a record at the specified path. If necessary this first reads the record. If the table
|
|
173
|
+
// has conflict detection then this updates to avoid conflict with the existing file. If options
|
|
174
|
+
// is non-false then it is a dictionary with optional keys:
|
|
175
|
+
// exclusive: true -- file must not already exist
|
|
176
|
+
// emptyok: true -- allow empty directory names (leading "/" or embdedded "//")
|
|
177
|
+
// md5: <hash> -- existing file content must match hash
|
|
178
|
+
// writenew: true -- write as a new file, with updated create time
|
|
179
|
+
|
|
180
|
+
closure dbWriteRecord(path, content, options, callback) {
|
|
181
|
+
if !callback
|
|
182
|
+
return (syncAdapter(dbWriteRecord, path, content, options))
|
|
183
|
+
|
|
184
|
+
function dbwriter(error, doc, local nowMs, nowstr, data, metadata) {
|
|
185
|
+
nowMs = Date.now()
|
|
186
|
+
nowstr = Date(nowMs).toISOString()
|
|
187
|
+
if error {
|
|
188
|
+
if error.status != 404
|
|
189
|
+
return (callback(error)) // failed, but not due to missing file
|
|
190
|
+
if !options.emptyok && !validName(path, true)
|
|
191
|
+
return (callback(Error("empty directory in path", { // invalid pathname
|
|
192
|
+
status: 403
|
|
193
|
+
name: "forbidden"
|
|
194
|
+
path: path
|
|
195
|
+
})))
|
|
196
|
+
metadata = {
|
|
197
|
+
finfo: { // new node
|
|
198
|
+
birthtimeMs: nowMs
|
|
199
|
+
birthtime: nowstr
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if path.slice(-1) == "/"
|
|
203
|
+
metadata.finfo.type = "directory"
|
|
204
|
+
else
|
|
205
|
+
metadata.finfo.type = "file"
|
|
206
|
+
} else if options.exclusive
|
|
207
|
+
return (callback(Error("file already exists", { // only write when creating new file
|
|
208
|
+
status: 409
|
|
209
|
+
name: "conflict"
|
|
210
|
+
path: path
|
|
211
|
+
})))
|
|
212
|
+
else if options.md5 && options.md5 != doc._md5
|
|
213
|
+
return (callback(Error("file changed before overwrite", { // existing file didn't match expected hash
|
|
214
|
+
status: 412
|
|
215
|
+
name: "precondition_failed"
|
|
216
|
+
path: path
|
|
217
|
+
check: options.md5
|
|
218
|
+
file: doc._md5
|
|
219
|
+
})))
|
|
220
|
+
else { // update for new information
|
|
221
|
+
metadata = doc
|
|
222
|
+
doc.content = content }
|
|
223
|
+
metadata.finfo.mtimeMs = nowMs // update timestamps
|
|
224
|
+
metadata.finfo.mtime = nowstr
|
|
225
|
+
if options.writenew {
|
|
226
|
+
metadata.finfo.birthtimeMs = nowMs
|
|
227
|
+
metadata.finfo.birthtime = nowstr
|
|
228
|
+
}
|
|
229
|
+
if string(content) {
|
|
230
|
+
if options.encoding
|
|
231
|
+
metadata.finfo.encoding = options.encoding // caller-specified string encoding
|
|
232
|
+
else
|
|
233
|
+
metadata.finfo.encoding = "utf8" } // default string encoding
|
|
234
|
+
else if content && !xobject(content)
|
|
235
|
+
return (callback(Error("unsupported content type", typeof(content), {
|
|
236
|
+
status: 400
|
|
237
|
+
name: "invalid_arguments"
|
|
238
|
+
path: path
|
|
239
|
+
})))
|
|
240
|
+
if doc
|
|
241
|
+
`(error, data) = view.table.update(doc) // update existing record
|
|
242
|
+
else
|
|
243
|
+
`(error, data) = view.table.add(path, content, metadata) // create new record
|
|
244
|
+
if error
|
|
245
|
+
callback(error)
|
|
246
|
+
else
|
|
247
|
+
callback(false, { ok: true })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if checkPath(path, callback)
|
|
251
|
+
return
|
|
252
|
+
if table.revcheck || options.exclusive || options.md5 || !options.writenew || options.overwriteDirCheck
|
|
253
|
+
view.table.head(path, dbwriter.proc) // get head first, check, then write
|
|
254
|
+
else
|
|
255
|
+
future(function dbGetMissing() { dbwriter({ status: 404 }) }).run() // write with simulated missing record
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// dbMakePathDirs
|
|
259
|
+
//
|
|
260
|
+
// Generate all the directories in the specified path, if they do not exist. This will fail if a
|
|
261
|
+
// file exists where a directory is needed.
|
|
262
|
+
|
|
263
|
+
closure dbMakePathDirs(path, callback) {
|
|
264
|
+
if !callback
|
|
265
|
+
return (syncAdapter(dbMakePathDirs, path))
|
|
266
|
+
if path == ""
|
|
267
|
+
return (asyncResult(callback, false, { created: false })) // root always already exists
|
|
268
|
+
if checkPath(path, callback)
|
|
269
|
+
return
|
|
270
|
+
if path.slice(-1) != "/"
|
|
271
|
+
path = path.concat("/")
|
|
272
|
+
view.table.get(path, function(error, data, local pathdirs, buildpath, item) {
|
|
273
|
+
if !error
|
|
274
|
+
return (callback(false, { created: false })) // fastest: already exists
|
|
275
|
+
pathdirs = path.split("/")
|
|
276
|
+
buildpath = ""
|
|
277
|
+
for item in pathdirs
|
|
278
|
+
if item.length > 0 {
|
|
279
|
+
buildpath = buildpath.concat(item)
|
|
280
|
+
`(error, data) = view.table.get(buildpath) // check for file in the way
|
|
281
|
+
if !error
|
|
282
|
+
return (callback(Error("file in the way while building directory path",
|
|
283
|
+
{
|
|
284
|
+
status: 409
|
|
285
|
+
name: "conflict"
|
|
286
|
+
path: buildpath
|
|
287
|
+
})))
|
|
288
|
+
if buildpath.slice(-1) != "/"
|
|
289
|
+
buildpath = buildpath.concat("/")
|
|
290
|
+
`(error, data) = view.table.get(buildpath) // get desired directory
|
|
291
|
+
if error.status == 404
|
|
292
|
+
`(error, data) = dbWriteRecord(buildpath) // create desired directory
|
|
293
|
+
if error
|
|
294
|
+
return (callback(Error("failed building directory path", path, "on", buildpath, error)))
|
|
295
|
+
}
|
|
296
|
+
else
|
|
297
|
+
buildpath = buildpath.concat("/") // had an extra slash here
|
|
298
|
+
if !error
|
|
299
|
+
data = { created: true } // leaf directory newly-created
|
|
300
|
+
callback(error, data)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// copyFile
|
|
305
|
+
//
|
|
306
|
+
// Copy a file given the source file's document, with optional move.
|
|
307
|
+
|
|
308
|
+
closure copyFile(doc, dest, options, local srcpath, newpath, error, data) {
|
|
309
|
+
srcpath = doc.key
|
|
310
|
+
if dest.indexOf("/") < 0
|
|
311
|
+
newpath = dirName(srcpath).concat(dest) // renaming within same directory
|
|
312
|
+
else {
|
|
313
|
+
if dest.slice(-1) == "/" // moving to new directory location:
|
|
314
|
+
newpath = dest.concat(baseName(srcpath)) // new path is new pathname
|
|
315
|
+
else { // moving and renaming:
|
|
316
|
+
newpath = dest // new path is new pathname
|
|
317
|
+
dest = dirName(dest) } // dest is new parent directory path
|
|
318
|
+
`(error, data) = dbMakePathDirs(dest) // ensure parent directories exist
|
|
319
|
+
if error
|
|
320
|
+
return (list(Error("copyFile: can't make destination", {
|
|
321
|
+
source: srcpath
|
|
322
|
+
destination: dest
|
|
323
|
+
}, error)))
|
|
324
|
+
else if !data.created {
|
|
325
|
+
`(error, data) = view.table.get(newpath.concat("/")) // check for directory in the way
|
|
326
|
+
if !error
|
|
327
|
+
return (callback(Error("copyFile: would overwrite directory",
|
|
328
|
+
{
|
|
329
|
+
status: 409
|
|
330
|
+
name: "conflict"
|
|
331
|
+
source: srcpath
|
|
332
|
+
destination: dest
|
|
333
|
+
existing: newpath.concat("/")
|
|
334
|
+
}))) } }
|
|
335
|
+
`(error, data) = view.table.copy(srcpath, newpath)
|
|
336
|
+
if error
|
|
337
|
+
return (list(Error("copyFile: copy failed", {
|
|
338
|
+
source: srcpath
|
|
339
|
+
destination: newpath
|
|
340
|
+
}, error)))
|
|
341
|
+
if options.move {
|
|
342
|
+
`(error, data) = view.table.delete(doc)
|
|
343
|
+
if error
|
|
344
|
+
error = Error("copyFile: couldn't remove original", {
|
|
345
|
+
source: srcpath
|
|
346
|
+
destination: newpath
|
|
347
|
+
}, error) }
|
|
348
|
+
if !error
|
|
349
|
+
data = { ok: true }
|
|
350
|
+
list(error, data)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// copyDirectory
|
|
354
|
+
//
|
|
355
|
+
// Copy a directory by its source path, with optional move.
|
|
356
|
+
|
|
357
|
+
closure copyDirectory(srcpath, dest, options, local error, nodes, data, destpath, errors, docpath, newpath, srcroot) {
|
|
358
|
+
`(error, nodes) = view.table.records({ prefix: srcpath })
|
|
359
|
+
if error
|
|
360
|
+
return (list(Error("copyDirectory: can't enumerate source directory", {
|
|
361
|
+
source: srcpath
|
|
362
|
+
destination: dest
|
|
363
|
+
}, error)))
|
|
364
|
+
if nodes.rows.length == 0 || nodes.rows[0].key != srcpath
|
|
365
|
+
return (list(Error("copyDirectory: source directory not found", {
|
|
366
|
+
status: 404 // shouldn't happen (sanity check)
|
|
367
|
+
name: "not_found"
|
|
368
|
+
source: srcpath
|
|
369
|
+
destination: dest
|
|
370
|
+
})))
|
|
371
|
+
if dest.indexOf("/") < 0 {
|
|
372
|
+
destpath = dirName(srcpath).concat(dest) // new directory name in same parent directory
|
|
373
|
+
dest = dirName(srcpath) } // dest is new parent pathname
|
|
374
|
+
else {
|
|
375
|
+
if dest.slice(-1) == "/"
|
|
376
|
+
destpath = dest.concat(baseName(srcpath)) // same name; new location
|
|
377
|
+
else {
|
|
378
|
+
destpath = dest // new full pathname
|
|
379
|
+
dest = dirName(dest) } // dest is new parent pathname
|
|
380
|
+
}
|
|
381
|
+
if destpath != "" && destpath.slice(-1) != "/"
|
|
382
|
+
destpath = destpath.concat("/") // get actual path of new directory
|
|
383
|
+
//
|
|
384
|
+
// Change srcpath to destpath for all records. Note that dest is the name of our new parent
|
|
385
|
+
// directory without a terminal slash, while destpath is the name of our new directory under
|
|
386
|
+
// which any children will be stored and does end in slash.
|
|
387
|
+
//
|
|
388
|
+
if srcpath == destpath
|
|
389
|
+
return (list(false, { ok: true })) // no change
|
|
390
|
+
if destpath.indexOf(srcpath) == 0
|
|
391
|
+
return (list(Error("copyDirectory: destination within source", {
|
|
392
|
+
status: 403 // trying to move within itself
|
|
393
|
+
name: "forbidden"
|
|
394
|
+
source: srcpath
|
|
395
|
+
destination: destpath
|
|
396
|
+
})))
|
|
397
|
+
`(error, data) = dbMakePathDirs(dest) // ensure future parent directories exist
|
|
398
|
+
if error
|
|
399
|
+
return (list(Error("copyDirectory: can't create destination", {
|
|
400
|
+
source: srcpath
|
|
401
|
+
destination: dest
|
|
402
|
+
}, error)))
|
|
403
|
+
if !data.created {
|
|
404
|
+
errors = [] // existing directory - check for overwrites
|
|
405
|
+
for item in nodes.rows {
|
|
406
|
+
docpath = item.key
|
|
407
|
+
newpath = destpath.concat(docpath.slice(srcpath.length))
|
|
408
|
+
`(error, data) = view.table.md5(newpath)
|
|
409
|
+
if !error
|
|
410
|
+
errors.push(newpath) // something exists there
|
|
411
|
+
else if error.status != 404
|
|
412
|
+
return (list(Error("copyDirectory: preflight failed", { // unexpected error
|
|
413
|
+
source: srcpath
|
|
414
|
+
destination: newpath
|
|
415
|
+
}, error))) }
|
|
416
|
+
if errors.length > 0
|
|
417
|
+
return (list(Error("copyDirectory: would overwrite", {
|
|
418
|
+
status: 409 // destination files already exist
|
|
419
|
+
name: "conflict"
|
|
420
|
+
source: srcpath
|
|
421
|
+
destination: destpath
|
|
422
|
+
existing: errors
|
|
423
|
+
}))) }
|
|
424
|
+
for item in nodes.rows {
|
|
425
|
+
docpath = item.key
|
|
426
|
+
newpath = destpath.concat(docpath.slice(srcpath.length))
|
|
427
|
+
`(error, data) = view.table.copy(docpath, newpath)
|
|
428
|
+
if error
|
|
429
|
+
errors.push(Error("copyDirectory: can't create copy", { // failed to add copy
|
|
430
|
+
source: docpath
|
|
431
|
+
destination: newpath
|
|
432
|
+
}, error))
|
|
433
|
+
else if options.move {
|
|
434
|
+
if !srcroot
|
|
435
|
+
srcroot = item // delete source root later
|
|
436
|
+
else {
|
|
437
|
+
`(error, data) = view.table.delete(item.doc) // delete original
|
|
438
|
+
if error
|
|
439
|
+
errors.push(Error("copyDirectory: can't remove original", {
|
|
440
|
+
source: docpath
|
|
441
|
+
destination: newpath
|
|
442
|
+
}, error)) } } }
|
|
443
|
+
if errors.length > 0
|
|
444
|
+
list(Error("copyDirectory: failures occurred", {
|
|
445
|
+
source: srcpath
|
|
446
|
+
destination: destpath
|
|
447
|
+
failures: errors
|
|
448
|
+
}, errors))
|
|
449
|
+
else {
|
|
450
|
+
if srcroot {
|
|
451
|
+
`(error, data) = view.table.delete(srcroot.doc) // delete original root
|
|
452
|
+
if error
|
|
453
|
+
return (list(Error("copyDirectory: can't remove original root", {
|
|
454
|
+
source: srcpath
|
|
455
|
+
destination: destpath
|
|
456
|
+
}, error))) }
|
|
457
|
+
list(false, { ok: true }) }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// info
|
|
461
|
+
//
|
|
462
|
+
// Get file/directory info at the specified path.
|
|
463
|
+
|
|
464
|
+
view.info = closure info(path, callback) {
|
|
465
|
+
if !callback
|
|
466
|
+
return (syncAdapter(info, path))
|
|
467
|
+
dbReadRecord(path, true, function(error, data, local finfo) {
|
|
468
|
+
if data {
|
|
469
|
+
finfo = new(data.finfo)
|
|
470
|
+
if !finfo
|
|
471
|
+
finfo = { }
|
|
472
|
+
if data._md5
|
|
473
|
+
finfo.md5 = data._md5
|
|
474
|
+
if data._length
|
|
475
|
+
finfo.length = data._length }
|
|
476
|
+
else if path == "" {
|
|
477
|
+
error = false // we were asked for info on superoot
|
|
478
|
+
finfo = {
|
|
479
|
+
md5: HashMD5("")
|
|
480
|
+
length: 0
|
|
481
|
+
type: "directory"
|
|
482
|
+
} }
|
|
483
|
+
callback(error, finfo)
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// read
|
|
488
|
+
//
|
|
489
|
+
// Read file content at the specified path.
|
|
490
|
+
|
|
491
|
+
view.read = closure read(path, options, callback) {
|
|
492
|
+
if !callback
|
|
493
|
+
return (syncAdapter(read, path, options))
|
|
494
|
+
dbReadRecord(path, false, function(error, data) {
|
|
495
|
+
if error || data.finfo.type != "directory"
|
|
496
|
+
callback(error, data.content)
|
|
497
|
+
else
|
|
498
|
+
callback(Error("cannot read a directory", { source: path }))
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// readFile
|
|
503
|
+
//
|
|
504
|
+
// Write a file at the specified path
|
|
505
|
+
|
|
506
|
+
view.readFile = function readFile(path, options, callback) {
|
|
507
|
+
read(path, options, callback)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// mkdir
|
|
511
|
+
//
|
|
512
|
+
// Make a directory at the specified path. The data value is a dictionary with the single
|
|
513
|
+
// key created having value true if we created the directory or false if the directory
|
|
514
|
+
// already exists.
|
|
515
|
+
|
|
516
|
+
view.mkdir = closure mkdir(path, callback) {
|
|
517
|
+
if !callback
|
|
518
|
+
return (syncAdapter(mkdir, path))
|
|
519
|
+
if !validName(path, true)
|
|
520
|
+
return (asyncResult(callback, Error("invalid directory", path)))
|
|
521
|
+
dbMakePathDirs(path, callback)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// write
|
|
525
|
+
//
|
|
526
|
+
// Write a file at the specified path, creating if needed by default. The following options are
|
|
527
|
+
// available:
|
|
528
|
+
// exclusive: true -- file must not already exist
|
|
529
|
+
// md5: <hash> -- existing file content must match hash
|
|
530
|
+
|
|
531
|
+
view.write = closure write(path, content, options, callback) {
|
|
532
|
+
if !callback
|
|
533
|
+
return (syncAdapter(write, path, content, options))
|
|
534
|
+
if checkPath(path, callback)
|
|
535
|
+
return
|
|
536
|
+
if !content
|
|
537
|
+
return (asyncResult(callback, Error("content required")))
|
|
538
|
+
if path.slice(-1) == "/"
|
|
539
|
+
return (asyncResult(callback, Error("cannot write to a directory", { path: path })))
|
|
540
|
+
if !validName(path, true)
|
|
541
|
+
return (asyncResult(callback, Error("invalid filename", { path: path })))
|
|
542
|
+
dbMakePathDirs(dirName(path), function(error, data) { // ensure parent directories exist
|
|
543
|
+
if error
|
|
544
|
+
return (callback(error))
|
|
545
|
+
if !data.created {
|
|
546
|
+
`(error, data) = view.table.head(path.concat("/")) // don't write file over direcctory
|
|
547
|
+
if !error
|
|
548
|
+
return (callback(list(Error("cannot overwrite a directory", { path: path }))))
|
|
549
|
+
}
|
|
550
|
+
dbWriteRecord(path, content, options, callback) })
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// writeFile
|
|
554
|
+
//
|
|
555
|
+
// Write a file at the specified path
|
|
556
|
+
|
|
557
|
+
view.writeFile = function writeFile(path, data, options, callback) {
|
|
558
|
+
write(path, data, options, callback)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// copy
|
|
562
|
+
//
|
|
563
|
+
// Copy / move / rename a file or directory. The source is the full path of the item. The
|
|
564
|
+
// destination is:
|
|
565
|
+
// - a new name if it has no embedded slashes, or
|
|
566
|
+
// - a new location if it ends in a slash, otherwise
|
|
567
|
+
// - a new pathname for the source if it does not end in a slash.
|
|
568
|
+
// This works by creating a new file or directory of files in the new location and then
|
|
569
|
+
// optionally deleting the files in the existing location. If options.move is true then we
|
|
570
|
+
// delete the source after the copy.
|
|
571
|
+
|
|
572
|
+
view.copy = closure copy(srcpath, dest, options, callback) {
|
|
573
|
+
if !callback
|
|
574
|
+
return (syncAdapter(copy, srcpath, dest, options))
|
|
575
|
+
if checkPath(srcpath, callback, "invalid source path")
|
|
576
|
+
return
|
|
577
|
+
if checkPath(dest, callback, "invalid destination path")
|
|
578
|
+
return
|
|
579
|
+
if !validName(dest, true)
|
|
580
|
+
return (asyncResult(callback, Error("invalid destination", { destination: dest })))
|
|
581
|
+
dbReadRecord(srcpath, true, function(error, data, local result) {
|
|
582
|
+
if error
|
|
583
|
+
callback(list(Error("copy can't access source", {
|
|
584
|
+
source: srcpath
|
|
585
|
+
destination: dest
|
|
586
|
+
}, error)))
|
|
587
|
+
else if srcpath.slice(-1) == "/"
|
|
588
|
+
result = copyDirectory(srcpath, dest, options) // hard case: directory with possibly many children
|
|
589
|
+
else
|
|
590
|
+
result = copyFile(data, dest, options) // easy case: single file
|
|
591
|
+
apply(callback, result)
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// delete
|
|
596
|
+
//
|
|
597
|
+
// Delete the file at the specified path, or a directory if the options dictionary includes:
|
|
598
|
+
// recursive: true -- "recursively" delete directory and children
|
|
599
|
+
|
|
600
|
+
view.delete = closure delete(path, options, callback) {
|
|
601
|
+
if !callback
|
|
602
|
+
return (syncAdapter(delete, path, options))
|
|
603
|
+
dbReadRecord(path, true, function(error, data, local errors, item, basedir) {
|
|
604
|
+
if error
|
|
605
|
+
return (callback(error))
|
|
606
|
+
if data.key.slice(-1) != "/" { // just delete the file
|
|
607
|
+
`(error, data) = view.table.delete(data)
|
|
608
|
+
if !error
|
|
609
|
+
data = { ok: true }
|
|
610
|
+
return (callback(error, data)) }
|
|
611
|
+
if !options.recursive
|
|
612
|
+
return (callback(Error("delete directory must specify recursive option", {
|
|
613
|
+
status: 403
|
|
614
|
+
name: "forbidden"
|
|
615
|
+
path: path
|
|
616
|
+
})))
|
|
617
|
+
if path.slice(-1) != "/"
|
|
618
|
+
path = path.concat("/") // avoid deleting longer filenames with same parent and prefix
|
|
619
|
+
`(error, data) = dbDirRecords(path, true)
|
|
620
|
+
if error
|
|
621
|
+
return (callback(Error("delete directory enumeration failed", {
|
|
622
|
+
path: path
|
|
623
|
+
}, error)))
|
|
624
|
+
errors = []
|
|
625
|
+
for item in data.rows {
|
|
626
|
+
if item.key == path {
|
|
627
|
+
basedir = item // delete base directory at end
|
|
628
|
+
continue
|
|
629
|
+
}
|
|
630
|
+
`(error, data) = view.table.delete(item.doc)
|
|
631
|
+
if error
|
|
632
|
+
errors.push(error) }
|
|
633
|
+
if errors.length != 0
|
|
634
|
+
error = Error("some directory children not deleted", errors)
|
|
635
|
+
else {
|
|
636
|
+
`(error, data) = view.table.delete(basedir.doc)
|
|
637
|
+
if error
|
|
638
|
+
error = Error("delete directory failed", {
|
|
639
|
+
path: path
|
|
640
|
+
}, error)
|
|
641
|
+
}
|
|
642
|
+
if !error
|
|
643
|
+
data = { ok: true }
|
|
644
|
+
callback(error, data)
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// dirList
|
|
649
|
+
//
|
|
650
|
+
// Read a directory at the specified path
|
|
651
|
+
// ### options currently ignored, for better or woise
|
|
652
|
+
|
|
653
|
+
view.dirList = closure dirList(path, options, callback, local name, dirdict) {
|
|
654
|
+
if !callback
|
|
655
|
+
return (syncAdapter(readir, path))
|
|
656
|
+
`(path, name) = checkPathName(path, callback)
|
|
657
|
+
if !path
|
|
658
|
+
return // already executed callback with error
|
|
659
|
+
dirdict = {
|
|
660
|
+
name: name.slice(0,-1)
|
|
661
|
+
path: path
|
|
662
|
+
data: {
|
|
663
|
+
platform: table.type
|
|
664
|
+
semantics: "dbfs"
|
|
665
|
+
pathsep: "/"
|
|
666
|
+
}
|
|
667
|
+
children: []
|
|
668
|
+
}
|
|
669
|
+
dbDirRecords(path, false, function(error, data, local item, result, slashdex, offset, finfo) {
|
|
670
|
+
if error
|
|
671
|
+
return (callback(error))
|
|
672
|
+
if data.rows.length == 0 && path != ""
|
|
673
|
+
return (callback(Error("directory not found", // should be something there
|
|
674
|
+
{
|
|
675
|
+
status: 404
|
|
676
|
+
name: "not_found"
|
|
677
|
+
path: path
|
|
678
|
+
})))
|
|
679
|
+
offset = path.length
|
|
680
|
+
for item in data.rows {
|
|
681
|
+
slashdex = item.key.indexOf("/", offset)
|
|
682
|
+
if slashdex < 0 || slashdex == item.key.length-1 { // file, or child directory
|
|
683
|
+
finfo = new(item.doc.finfo)
|
|
684
|
+
if !finfo
|
|
685
|
+
finfo = { }
|
|
686
|
+
name = item.key.slice(offset)
|
|
687
|
+
if name.slice(-1) == "/"
|
|
688
|
+
name = name.slice(0,-1) // remove trailing "/" from directory
|
|
689
|
+
if item.doc._md5
|
|
690
|
+
finfo.md5 = item.doc._md5
|
|
691
|
+
if item.doc._length
|
|
692
|
+
finfo.length = item.doc._length
|
|
693
|
+
if item.key.length > offset // not the directory itself
|
|
694
|
+
dirdict.children.push({
|
|
695
|
+
name: name
|
|
696
|
+
info: finfo }) } }
|
|
697
|
+
callback(false, dirdict)
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// readir
|
|
702
|
+
//
|
|
703
|
+
// Read a directory at the specified path
|
|
704
|
+
|
|
705
|
+
view.readir = closure readir(path, callback, local name, dirdict) {
|
|
706
|
+
dirList(path, { head: true }, callback)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// tree
|
|
710
|
+
//
|
|
711
|
+
// Return a tree for the specified path. The path is relative to the root of the view.
|
|
712
|
+
|
|
713
|
+
view.tree = closure tree(path, options, callback, local name, treedict, item, offset) {
|
|
714
|
+
if !callback
|
|
715
|
+
return (syncAdapter(tree, path, options))
|
|
716
|
+
`(path, name) = checkPathName(path, callback)
|
|
717
|
+
if !path
|
|
718
|
+
return // already executed callback with error
|
|
719
|
+
options = { // ### might want to allow some options
|
|
720
|
+
prefix: path
|
|
721
|
+
deep: true
|
|
722
|
+
}
|
|
723
|
+
treedict = {
|
|
724
|
+
name: name.slice(0,-1)
|
|
725
|
+
data: {
|
|
726
|
+
platform: table.type
|
|
727
|
+
semantics: "dbfs"
|
|
728
|
+
pathsep: "/"
|
|
729
|
+
}
|
|
730
|
+
parents: []
|
|
731
|
+
children: []
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
for item in path.split("/").slice(0,-1)
|
|
735
|
+
treedict.parents.push(item)
|
|
736
|
+
offset = path.length // offset to path's child names (after any slash)
|
|
737
|
+
view.table.records(options, function(error, data, local index, leaf, parent, slashdex) {
|
|
738
|
+
if error
|
|
739
|
+
return (callback(error))
|
|
740
|
+
if data.rows.length == 0 && path != ""
|
|
741
|
+
return (callback(Error("directory not found",
|
|
742
|
+
{
|
|
743
|
+
status: 404
|
|
744
|
+
name: "not_found"
|
|
745
|
+
path: path
|
|
746
|
+
})))
|
|
747
|
+
index = { }
|
|
748
|
+
index[""] = treedict // treedict is parent of children in top level
|
|
749
|
+
for item in data.rows {
|
|
750
|
+
name = item.key.slice(offset) // the node's pathname starting with slash
|
|
751
|
+
if name == ""
|
|
752
|
+
continue // root already exists
|
|
753
|
+
slashdex = name.lastIndexOf("/")
|
|
754
|
+
if slashdex < 0 { // file child in root
|
|
755
|
+
parent = ""
|
|
756
|
+
child = { name: name } }
|
|
757
|
+
else if slashdex == name.length-1 { // directory child
|
|
758
|
+
if name == "/" {
|
|
759
|
+
parent = "" // "no-name" directory in root
|
|
760
|
+
leaf = "" }
|
|
761
|
+
else { // named or non-root directory
|
|
762
|
+
slashdex = name.lastIndexOf("/", name.length-2) // penultimate slash
|
|
763
|
+
parent = name.substring(0, slashdex+1) // includes parent's ending slash
|
|
764
|
+
leaf = name.slice(slashdex+1, -1) } // omits child's ending slash
|
|
765
|
+
child = index[name] = {
|
|
766
|
+
name: leaf
|
|
767
|
+
children: [] } }
|
|
768
|
+
else { // file child not in root
|
|
769
|
+
parent = name.substring(0, slashdex+1)
|
|
770
|
+
leaf = name.slice(slashdex+1)
|
|
771
|
+
child = { name: leaf } }
|
|
772
|
+
if index[parent]
|
|
773
|
+
index[parent].children.push(child)
|
|
774
|
+
else { // orphaned child doesn't have <parent>/ record
|
|
775
|
+
if !treedict.orphans
|
|
776
|
+
treedict.orphans = []
|
|
777
|
+
child.key = item.key
|
|
778
|
+
treedict.orphans.push(child)
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
callback(false, treedict)
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// util
|
|
786
|
+
//
|
|
787
|
+
// Utility operations for the filesystem
|
|
788
|
+
|
|
789
|
+
view.util = MakePSMutil(view)
|
|
790
|
+
|
|
791
|
+
// finis
|
|
792
|
+
list(false, view)
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
/*
|
|
797
|
+
* psmdInit
|
|
798
|
+
*
|
|
799
|
+
* Initialize the module.
|
|
800
|
+
*
|
|
801
|
+
*/
|
|
802
|
+
|
|
803
|
+
function psmdInit(local manifest) {
|
|
804
|
+
|
|
805
|
+
manifest = `(FSview, psmdInit)
|
|
806
|
+
|
|
807
|
+
Naan.module.build(module.id, "psm_dbtables", function(modobj, compobj) {
|
|
808
|
+
require("./storage.nlg")
|
|
809
|
+
require("./psm.nlg")
|
|
810
|
+
compobj.manifest = manifest
|
|
811
|
+
modobj.exports.FSview = FSview
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
} ();
|