@rpcbase/server 0.364.0 → 0.366.0-fileupload.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/boot/worker.js +3 -1
- package/express/index.js +3 -1
- package/files.ts +1 -0
- package/package.json +1 -1
- package/queue/register_queue_listener.js +17 -7
- package/rts/index.js +13 -14
- package/src/files/finalize_file_upload.ts +19 -0
- package/src/files/helpers/get_grid_fs_bucket.ts +18 -0
- package/src/files/index.js +5 -0
- package/src/files/tasks/finalize_file_upload.ts +5 -0
- package/src/files/tasks/index.ts +6 -0
- package/src/files/upload_chunk.ts +82 -0
- package/src/helpers/sim_test_inject.ts +21 -0
- package/src/tasks/index.js +2 -0
- package/tsconfig.json +8 -0
package/boot/worker.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
require("./shared")
|
|
3
3
|
|
|
4
4
|
require("rb-plugin-worker")
|
|
5
|
+
require("../src/tasks")
|
|
5
6
|
|
|
6
7
|
const register_queue_listener = require("../queue/register_queue_listener")
|
|
7
8
|
|
|
8
9
|
const queue = require("../queue")
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
queue.start()
|
|
11
13
|
|
|
12
14
|
let _queue_listener
|
|
@@ -17,7 +19,7 @@ register_queue_listener()
|
|
|
17
19
|
})
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
const handle_graceful_exit = async
|
|
22
|
+
const handle_graceful_exit = async() => {
|
|
21
23
|
const close_fns = [
|
|
22
24
|
queue.instance().close,
|
|
23
25
|
]
|
package/express/index.js
CHANGED
|
@@ -5,9 +5,10 @@ const body_parser = require("body-parser")
|
|
|
5
5
|
const request_ip = require("request-ip")
|
|
6
6
|
const Sentry = require("@sentry/node")
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// functional middlewares
|
|
9
9
|
const auth = require("../src/auth")
|
|
10
10
|
const api = require("../src/api")
|
|
11
|
+
const files = require("../src/files")
|
|
11
12
|
const sessions = require("../src/sessions")
|
|
12
13
|
|
|
13
14
|
const dev_save_coverage = require("./dev_save_coverage")
|
|
@@ -84,6 +85,7 @@ module.exports = () => {
|
|
|
84
85
|
|
|
85
86
|
auth(app)
|
|
86
87
|
api(app)
|
|
88
|
+
files(app)
|
|
87
89
|
|
|
88
90
|
dev_save_coverage(app)
|
|
89
91
|
|
package/files.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/files/finalize_file_upload"
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ const queue = require("./index")
|
|
|
9
9
|
const dispatch_worker_queue = require("./dispatch_worker_queue")
|
|
10
10
|
const dispatch_indexer_queue = require("./dispatch_indexer_queue")
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
const log = debug("rb:queue:listener")
|
|
13
14
|
|
|
14
15
|
const {RB_APP_NAME, RB_TENANT_ID} = process.env
|
|
@@ -30,10 +31,12 @@ const mongoose_delete_plugin = (schema) => {
|
|
|
30
31
|
})
|
|
31
32
|
|
|
32
33
|
// TODO: add other delete operations
|
|
34
|
+
// TODO: this should be deleteOne ?
|
|
33
35
|
// https://mongoosejs.com/docs/queries.html
|
|
34
36
|
schema.post("findOneAndDelete", function(doc) {
|
|
35
37
|
log("queue:findOneAndDelete", "dispatch_doc_change NYI")
|
|
36
38
|
log("del PLUGIN", doc)
|
|
39
|
+
const change = null // TMP
|
|
37
40
|
const coll_name = change.ns.coll
|
|
38
41
|
|
|
39
42
|
const model_name = Object.keys(mongoose.models).find((k) => {
|
|
@@ -53,10 +56,11 @@ const mongoose_delete_plugin = (schema) => {
|
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
const assert_doc_id = (change) => {
|
|
56
|
-
if (process.env.NODE_ENV !== "development") return
|
|
59
|
+
// if (process.env.NODE_ENV !== "development") return
|
|
57
60
|
|
|
58
61
|
const doc_id = change.documentKey?._id?.toString()
|
|
59
62
|
|
|
63
|
+
// some changes aren't on documents
|
|
60
64
|
if (!doc_id) return
|
|
61
65
|
|
|
62
66
|
const sub = doc_id.substring(8, 16)
|
|
@@ -70,6 +74,11 @@ const assert_doc_id = (change) => {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
const dispatch_change_handler = (change) => {
|
|
77
|
+
// skip if this is a file upload
|
|
78
|
+
if (change.ns?.coll?.endsWith(".files") || change.ns?.coll?.endsWith(".chunks")) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
// verify we have correct object ids
|
|
74
83
|
assert_doc_id(change)
|
|
75
84
|
|
|
@@ -79,8 +88,6 @@ const dispatch_change_handler = (change) => {
|
|
|
79
88
|
return mongoose.models[k].collection.collectionName === coll_name
|
|
80
89
|
})
|
|
81
90
|
|
|
82
|
-
console.log("O3333K", Object.keys(require("../mongoose").models), Object.keys(mongoose.models))
|
|
83
|
-
|
|
84
91
|
if (!model_name) {
|
|
85
92
|
return
|
|
86
93
|
}
|
|
@@ -107,8 +114,11 @@ const register_db_emitter = () => {
|
|
|
107
114
|
|
|
108
115
|
// Set up the change stream with a filter to only listen to the specific database
|
|
109
116
|
const pipeline = [
|
|
110
|
-
{ $match: {
|
|
111
|
-
{ $match: {
|
|
117
|
+
{ $match: { "ns.db": RB_APP_NAME } },
|
|
118
|
+
{ $match: {
|
|
119
|
+
"ns.coll": { $nin: ["file-uploads.files", "file-uploads.chunks"] },
|
|
120
|
+
"operationType": { $in: ["insert", "update" /* "delete"*/] } },
|
|
121
|
+
},
|
|
112
122
|
]
|
|
113
123
|
|
|
114
124
|
// https://www.mongodb.com/docs/manual/reference/method/Mongo.watch/
|
|
@@ -154,8 +164,8 @@ const register_queue_listener = () => new Promise((resolve, reject) => {
|
|
|
154
164
|
mongoose.plugin(mongoose_delete_plugin)
|
|
155
165
|
|
|
156
166
|
mongoose.connection.once("open", () => {
|
|
157
|
-
const
|
|
158
|
-
resolve(
|
|
167
|
+
const db_emitter = register_db_emitter()
|
|
168
|
+
resolve(db_emitter)
|
|
159
169
|
})
|
|
160
170
|
|
|
161
171
|
mongoose.connection.on("error", (err) => {
|
package/rts/index.js
CHANGED
|
@@ -37,7 +37,7 @@ const get_query_options = (ctx, options) => {
|
|
|
37
37
|
// https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions()
|
|
38
38
|
const query_options = {
|
|
39
39
|
ctx,
|
|
40
|
-
is_client: true
|
|
40
|
+
is_client: true,
|
|
41
41
|
//
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -46,8 +46,7 @@ const get_query_options = (ctx, options) => {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const limit = typeof options.limit === "number" ? Math.min(QUERY_MAX_LIMIT, Math.abs(options.limit)) : QUERY_MAX_LIMIT
|
|
49
|
-
|
|
50
|
-
query_options.limit = options.limit
|
|
49
|
+
query_options.limit = limit
|
|
51
50
|
|
|
52
51
|
return query_options
|
|
53
52
|
}
|
|
@@ -99,15 +98,15 @@ const dispatch_doc_change = (model, doc) => {
|
|
|
99
98
|
return
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
|
|
103
101
|
const query_options = get_query_options(ctx, options)
|
|
102
|
+
// TODO: do not redo query here, use document from change event
|
|
104
103
|
const query_promise = model.find(query, options.projection, query_options)
|
|
105
104
|
if (query_options.sort) {
|
|
106
105
|
query_promise.sort(query_options.sort)
|
|
107
106
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
|
|
108
|
+
query_promise.limit(query_options.limit)
|
|
109
|
+
|
|
111
110
|
// WARNING: DANGER
|
|
112
111
|
// TODO: we should not be doing a read here, use doc from event
|
|
113
112
|
const data = await query_promise
|
|
@@ -179,8 +178,8 @@ const mongoose_txn_plugin = (schema, options) => {
|
|
|
179
178
|
|
|
180
179
|
|
|
181
180
|
// responds to a change stream "change" event
|
|
182
|
-
const get_dispatch_change_handler = (
|
|
183
|
-
const queries = _queries[model_name]
|
|
181
|
+
const get_dispatch_change_handler = (model_name) => async(change) => {
|
|
182
|
+
// const queries = _queries[model_name]
|
|
184
183
|
|
|
185
184
|
const model = mongoose.model(model_name)
|
|
186
185
|
|
|
@@ -203,7 +202,7 @@ const get_dispatch_change_handler = (mongoose, model_name) => async(change) => {
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
|
|
206
|
-
const add_change_stream = (
|
|
205
|
+
const add_change_stream = (socket_id, {model_name, query, query_key, options}) => {
|
|
207
206
|
if (!model_name) throw new Error("empty model name")
|
|
208
207
|
|
|
209
208
|
if (!_change_streams[model_name]) {
|
|
@@ -225,7 +224,7 @@ const add_change_stream = (mongoose, socket_id, {model_name, query, query_key, o
|
|
|
225
224
|
fullDocumentBeforeChange: "whenAvailable",
|
|
226
225
|
})
|
|
227
226
|
|
|
228
|
-
emitter.on("change", get_dispatch_change_handler(
|
|
227
|
+
emitter.on("change", get_dispatch_change_handler(model_name))
|
|
229
228
|
|
|
230
229
|
emitter.on("error", (err) => {
|
|
231
230
|
console.log("change listener emitter got error", err)
|
|
@@ -261,7 +260,7 @@ const add_change_stream = (mongoose, socket_id, {model_name, query, query_key, o
|
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
|
|
264
|
-
const run_query = async(
|
|
263
|
+
const run_query = async(socket_id, {model_name, query, query_key, options}) => {
|
|
265
264
|
const model = mongoose.model(model_name)
|
|
266
265
|
|
|
267
266
|
log("run_query", {model_name, query, query_key, options})
|
|
@@ -399,7 +398,7 @@ const rts_server = async(server) => {
|
|
|
399
398
|
console.log("socket connected", socket.id)
|
|
400
399
|
//
|
|
401
400
|
socket.on("register_query", (payload) => {
|
|
402
|
-
add_change_stream(
|
|
401
|
+
add_change_stream(socket.id, payload)
|
|
403
402
|
})
|
|
404
403
|
|
|
405
404
|
socket.on("remove_query", (payload) => {
|
|
@@ -408,7 +407,7 @@ const rts_server = async(server) => {
|
|
|
408
407
|
|
|
409
408
|
socket.on("run_query", (payload) => {
|
|
410
409
|
try {
|
|
411
|
-
run_query(
|
|
410
|
+
run_query(socket.id, payload)
|
|
412
411
|
} catch (err) {
|
|
413
412
|
console.log("run_query caught error", err)
|
|
414
413
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Promise from "bluebird"
|
|
2
|
+
|
|
3
|
+
import queue from "../../queue"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// after we've uploaded a file, we process it with ocr, llm vectorization, png rendering, etc
|
|
7
|
+
export const finalize_file_upload = async(files: Array<{hash: string}>) => {
|
|
8
|
+
|
|
9
|
+
await Promise.map(files,
|
|
10
|
+
async(file: {hash: string}) => {
|
|
11
|
+
await queue.add("finalize_file_upload", {hash: file.hash, hello: "world"}, {
|
|
12
|
+
jobId: `finalize_file_upload-${file.hash}`,
|
|
13
|
+
removeOnComplete: true,
|
|
14
|
+
removeOnFail: true,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import mongoose from "../../../mongoose"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const CHUNK_SIZE = 1024 * 1024
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const get_grid_fs_bucket = (bucket_name: string, chunk_size: number) => {
|
|
9
|
+
assert(chunk_size === CHUNK_SIZE, "chunk_size must match default CHUNK_SIZE")
|
|
10
|
+
|
|
11
|
+
const {db} = mongoose.connection
|
|
12
|
+
const bucket = new mongoose.mongo.GridFSBucket(db, {
|
|
13
|
+
bucketName: bucket_name,
|
|
14
|
+
chunkSizeBytes: chunk_size,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return bucket
|
|
18
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import {formidable, File} from "formidable"
|
|
4
|
+
|
|
5
|
+
import {get_grid_fs_bucket} from "./helpers/get_grid_fs_bucket"
|
|
6
|
+
import { sim_test_inject } from "../helpers/sim_test_inject"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const upload_file_to_bucket = async(file: File, metadata): Promise<void> => {
|
|
10
|
+
const bucket = get_grid_fs_bucket("file-uploads", metadata.chunk_size)
|
|
11
|
+
|
|
12
|
+
const chunk_filename = `${metadata.hash}.${metadata.chunk_index}`
|
|
13
|
+
|
|
14
|
+
const upload_stream = bucket.openUploadStream(chunk_filename, {
|
|
15
|
+
metadata,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const read_stream = fs.createReadStream(file.filepath)
|
|
19
|
+
|
|
20
|
+
read_stream.pipe(upload_stream)
|
|
21
|
+
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
upload_stream.on("finish", () => {
|
|
24
|
+
// console.log("finished uploading:", upload_stream.id)
|
|
25
|
+
resolve()
|
|
26
|
+
})
|
|
27
|
+
upload_stream.on("error", (error) => {
|
|
28
|
+
reject(error)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export const upload_chunk = async(req, res, next) => {
|
|
35
|
+
const {user_id} = req.session
|
|
36
|
+
assert(user_id, "upload_chunk: unable to resolve user_id")
|
|
37
|
+
|
|
38
|
+
// https://github.com/node-formidable/formidable#options
|
|
39
|
+
const form = formidable({})
|
|
40
|
+
|
|
41
|
+
let fields, files
|
|
42
|
+
try {
|
|
43
|
+
[fields, files] = await form.parse(req)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return res.status(500).json({error: "Failed to parse uploaded file"})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await sim_test_inject()
|
|
49
|
+
// await new Promise(resolve => setTimeout(resolve, 3000))
|
|
50
|
+
|
|
51
|
+
const file_chunk = files.file_chunk[0] as File
|
|
52
|
+
|
|
53
|
+
const metadata = {
|
|
54
|
+
user_id,
|
|
55
|
+
original_filename: fields.original_filename[0],
|
|
56
|
+
is_compressed: fields.is_compressed[0] === "yes",
|
|
57
|
+
chunk_index: parseInt(fields.chunk_index[0]),
|
|
58
|
+
total_chunks: parseInt(fields.total_chunks[0]),
|
|
59
|
+
chunk_size: parseInt(fields.chunk_size[0]),
|
|
60
|
+
mime_type: fields.mime_type[0],
|
|
61
|
+
hash: fields.hash[0],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await upload_file_to_bucket(file_chunk, metadata)
|
|
65
|
+
|
|
66
|
+
const result: {
|
|
67
|
+
status: string;
|
|
68
|
+
finalize_token?: string;
|
|
69
|
+
} = {
|
|
70
|
+
status: "ok",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const is_last_chunk = metadata.chunk_index === metadata.total_chunks - 1
|
|
74
|
+
|
|
75
|
+
if (is_last_chunk) {
|
|
76
|
+
result.finalize_token = metadata.hash
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.json({
|
|
80
|
+
status: "ok",
|
|
81
|
+
})
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const ERROR_PROBABILITY = 0.1
|
|
2
|
+
const THROTTLE_PROBABILITY = 0.2
|
|
3
|
+
const MAX_THROTTLE_MS = 2000
|
|
4
|
+
|
|
5
|
+
// randomly inject errors or delays
|
|
6
|
+
export const sim_test_inject = async() => {
|
|
7
|
+
if (!__DEV__) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const should_error = Math.random() > 1 - ERROR_PROBABILITY
|
|
12
|
+
if (should_error) {
|
|
13
|
+
throw new Error("random sim test error")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const should_throttle = Math.random() > 1 - THROTTLE_PROBABILITY
|
|
17
|
+
if (should_throttle) {
|
|
18
|
+
const throttle_delay = Math.floor(Math.random() * MAX_THROTTLE_MS)
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, throttle_delay))
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/tasks/index.js
CHANGED