@rpcbase/server 0.365.0 → 0.366.0-fileupload.1
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 +10 -3
- 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/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
|
@@ -56,10 +56,9 @@ const mongoose_delete_plugin = (schema) => {
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
const assert_doc_id = (change) => {
|
|
59
|
-
if (process.env.NODE_ENV !== "development") return
|
|
60
|
-
|
|
61
59
|
const doc_id = change.documentKey?._id?.toString()
|
|
62
60
|
|
|
61
|
+
// some changes aren't on documents
|
|
63
62
|
if (!doc_id) return
|
|
64
63
|
|
|
65
64
|
const sub = doc_id.substring(8, 16)
|
|
@@ -73,6 +72,11 @@ const assert_doc_id = (change) => {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
const dispatch_change_handler = (change) => {
|
|
75
|
+
// skip if this is a file upload
|
|
76
|
+
if (change.ns?.coll?.endsWith(".files") || change.ns?.coll?.endsWith(".chunks")) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
// verify we have correct object ids
|
|
77
81
|
assert_doc_id(change)
|
|
78
82
|
|
|
@@ -109,7 +113,10 @@ const register_db_emitter = () => {
|
|
|
109
113
|
// Set up the change stream with a filter to only listen to the specific database
|
|
110
114
|
const pipeline = [
|
|
111
115
|
{ $match: { "ns.db": RB_APP_NAME } },
|
|
112
|
-
{ $match: {
|
|
116
|
+
{ $match: {
|
|
117
|
+
"ns.coll": { $nin: ["file-uploads.files", "file-uploads.chunks"] },
|
|
118
|
+
"operationType": { $in: ["insert", "update" /* "delete"*/] } },
|
|
119
|
+
},
|
|
113
120
|
]
|
|
114
121
|
|
|
115
122
|
// https://www.mongodb.com/docs/manual/reference/method/Mongo.watch/
|
|
@@ -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
|
+
}
|