@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 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
- // functionality middlewares
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.365.0",
3
+ "version": "0.366.0-fileupload.1",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "scripts": {
@@ -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: { operationType: { $in: ["insert", "update" /* "delete"*/] } } },
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,5 @@
1
+ import {upload_chunk} from "./upload_chunk"
2
+
3
+ module.exports = (app) => {
4
+ app.post("/rb-api/v1/files/upload_chunk", upload_chunk)
5
+ }
@@ -0,0 +1,5 @@
1
+
2
+ export const finalize_file_upload = async(payload) => {
3
+ console.log("TASK:FINALIZE FILE UPLOAD")
4
+ console.log("payload", payload)
5
+ }
@@ -0,0 +1,6 @@
1
+ import queue from "../../../queue"
2
+
3
+ import {finalize_file_upload} from "./finalize_file_upload"
4
+
5
+
6
+ queue.register_task("finalize_file_upload", finalize_file_upload)
@@ -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
+ }
@@ -1,6 +1,8 @@
1
1
  const queue = require("../../queue")
2
2
 
3
+ require("../files/tasks")
3
4
 
4
5
  const index_item = require("./index_item")
5
6
 
7
+
6
8
  queue.register_task("rb_index_item", index_item)