@sjcrh/proteinpaint-server 2.99.0 → 2.99.1-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/package.json +7 -2
- package/routes/clearwsisession.js +51 -0
- package/routes/img.js +46 -0
- package/routes/samplewsimages.js +11 -0
- package/routes/termdb.config.js +1 -0
- package/routes/termdb.singlecellSamples.js +11 -0
- package/routes/tileserver.js +1 -1
- package/routes/wsimages.js +92 -67
- package/src/app.js +578 -329
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjcrh/proteinpaint-server",
|
|
3
|
-
"version": "2.99.
|
|
3
|
+
"version": "2.99.1-1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "a genomics visualization tool for exploring a cohort's genotype and phenotype data",
|
|
6
6
|
"main": "src/app.js",
|
|
@@ -61,16 +61,20 @@
|
|
|
61
61
|
"@sjcrh/augen": "2.87.0",
|
|
62
62
|
"@sjcrh/proteinpaint-rust": "2.99.0",
|
|
63
63
|
"@sjcrh/proteinpaint-shared": "2.99.0",
|
|
64
|
-
"@sjcrh/proteinpaint-types": "2.99.
|
|
64
|
+
"@sjcrh/proteinpaint-types": "2.99.1-1",
|
|
65
|
+
"@types/express": "^5.0.0",
|
|
66
|
+
"@types/express-session": "^1.18.1",
|
|
65
67
|
"better-sqlite3": "^9.4.1",
|
|
66
68
|
"body-parser": "^1.15.2",
|
|
67
69
|
"canvas": "~2.11.2",
|
|
68
70
|
"compression": "^1.6.2",
|
|
71
|
+
"connect-redis": "^6.1.3",
|
|
69
72
|
"cookie-parser": "^1.4.5",
|
|
70
73
|
"d3": "^7.6.1",
|
|
71
74
|
"deep-object-diff": "^1.1.0",
|
|
72
75
|
"express": "^4.17.1",
|
|
73
76
|
"express-basic-auth": "^1.1.5",
|
|
77
|
+
"express-session": "^1.18.1",
|
|
74
78
|
"got": "^14.2.0",
|
|
75
79
|
"image-size": "^0.5.5",
|
|
76
80
|
"jsonwebtoken": "^9.0.0",
|
|
@@ -81,6 +85,7 @@
|
|
|
81
85
|
"minimatch": "^3.1.2",
|
|
82
86
|
"node-fetch": "^2.6.1",
|
|
83
87
|
"partjson": "^0.58.2",
|
|
88
|
+
"redis": "^4.7.0",
|
|
84
89
|
"tiny-async-pool": "^1.2.0",
|
|
85
90
|
"tough-cookie": "^4.1.4"
|
|
86
91
|
},
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { clearWSImagesSessionsPayload } from "@sjcrh/proteinpaint-types/routes/clearwsisessions.js";
|
|
2
|
+
import SessionManager from "#src/wsisessions/SessionManager.js";
|
|
3
|
+
import ky from "ky";
|
|
4
|
+
import serverconfig from "#src/serverconfig.js";
|
|
5
|
+
const api = {
|
|
6
|
+
endpoint: "clearwsisession",
|
|
7
|
+
methods: {
|
|
8
|
+
delete: {
|
|
9
|
+
...clearWSImagesSessionsPayload,
|
|
10
|
+
init
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function init() {
|
|
15
|
+
return async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
if (serverconfig.redis) {
|
|
18
|
+
const sessionsString = req.query.sessions;
|
|
19
|
+
const sessionsArray = JSON.parse(sessionsString);
|
|
20
|
+
const sessions = new Map(sessionsArray);
|
|
21
|
+
const sessionManager = SessionManager.getInstance(serverconfig.redis.url);
|
|
22
|
+
for (const [key, value] of sessions.entries()) {
|
|
23
|
+
const sessionData = await sessionManager.getSession(key);
|
|
24
|
+
if (sessionData) {
|
|
25
|
+
const userSessionIds = sessionData.userSessionIds;
|
|
26
|
+
if (!userSessionIds.some((sessionId) => sessionId === value)) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const newSessions = userSessionIds.filter((sessionId) => sessionId !== value);
|
|
30
|
+
if (newSessions.length === 0) {
|
|
31
|
+
await ky.put(`${serverconfig.tileServerURL}/tileserver/reset/${sessionData.imageSessionId}`);
|
|
32
|
+
await sessionManager.deleteSession(key);
|
|
33
|
+
} else {
|
|
34
|
+
sessionData.userSessionIds = newSessions;
|
|
35
|
+
await sessionManager.setSession(key, sessionData);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
res.send({ message: "Sessions cleared" });
|
|
40
|
+
} else {
|
|
41
|
+
res.status(404).send("Redis not configured");
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log(e);
|
|
45
|
+
res.status(404).send("Error clearing sessions");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
api
|
|
51
|
+
};
|
package/routes/img.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { imgPayload } from "#types/checkers";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import * as utils from "../src/utils.js";
|
|
5
|
+
import imagesize from "image-size";
|
|
6
|
+
const api = {
|
|
7
|
+
endpoint: "img",
|
|
8
|
+
methods: {
|
|
9
|
+
get: {
|
|
10
|
+
...imgPayload,
|
|
11
|
+
init
|
|
12
|
+
},
|
|
13
|
+
post: {
|
|
14
|
+
...imgPayload,
|
|
15
|
+
init
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function init() {
|
|
20
|
+
return async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
sendImage(req, res);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
res.send({ status: "error", error: e.message || e });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function sendImage(req, res) {
|
|
29
|
+
const [e, file] = utils.fileurl(req, true);
|
|
30
|
+
try {
|
|
31
|
+
if (e)
|
|
32
|
+
throw "invalid image file";
|
|
33
|
+
const data = await fs.promises.readFile(file);
|
|
34
|
+
const ext = path.extname(file).substring(1);
|
|
35
|
+
const image = {
|
|
36
|
+
src: `data:image/${ext};base64,${Buffer.from(data).toString("base64")}`,
|
|
37
|
+
size: imagesize(file)
|
|
38
|
+
};
|
|
39
|
+
res.send({ ...image });
|
|
40
|
+
} catch (e2) {
|
|
41
|
+
res.send({ error: e2.message || e2 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
api
|
|
46
|
+
};
|
package/routes/samplewsimages.js
CHANGED
|
@@ -23,6 +23,17 @@ function init({ genomes }) {
|
|
|
23
23
|
if (!ds)
|
|
24
24
|
throw "invalid dataset name";
|
|
25
25
|
const sampleId = query.sample_id;
|
|
26
|
+
if (ds.queries.WSImages.sources) {
|
|
27
|
+
const images2 = [];
|
|
28
|
+
if (ds.queries.WSImages.sources) {
|
|
29
|
+
images2.push({
|
|
30
|
+
filename: sampleId + "_fsspec.json",
|
|
31
|
+
metadata: ""
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
res.send({ sampleWSImages: images2 });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
26
37
|
const images = await ds.queries.WSImages.getWSImages({ sampleId });
|
|
27
38
|
res.send({ sampleWSImages: images });
|
|
28
39
|
} catch (e) {
|
package/routes/termdb.config.js
CHANGED
|
@@ -219,6 +219,7 @@ function addNonDictionaryQueries(c, ds, genome) {
|
|
|
219
219
|
sampleColumns: q.singleCell.samples.sampleColumns,
|
|
220
220
|
experimentColumns: q.singleCell.samples.experimentColumns
|
|
221
221
|
},
|
|
222
|
+
images: q.singleCell.images,
|
|
222
223
|
data: {
|
|
223
224
|
sameLegend: q.singleCell.data.sameLegend,
|
|
224
225
|
refName: q.singleCell.data.refName,
|
|
@@ -76,6 +76,17 @@ async function validate_query_singleCell(ds, genome) {
|
|
|
76
76
|
if (q.DEgenes) {
|
|
77
77
|
validate_query_singleCell_DEgenes(ds);
|
|
78
78
|
}
|
|
79
|
+
if (q.images) {
|
|
80
|
+
validateImages(q.images);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateImages(images) {
|
|
84
|
+
if (!images.folder)
|
|
85
|
+
throw "images.folder missing";
|
|
86
|
+
if (!images.label)
|
|
87
|
+
images.label = "Images";
|
|
88
|
+
if (!images.fileName)
|
|
89
|
+
throw "images.fileName missing";
|
|
79
90
|
}
|
|
80
91
|
async function validateSamplesNative(S, D, ds) {
|
|
81
92
|
const samples = /* @__PURE__ */ new Map();
|
package/routes/tileserver.js
CHANGED
|
@@ -19,7 +19,7 @@ function init() {
|
|
|
19
19
|
try {
|
|
20
20
|
const { sampleId, TileGroup, z, x, y } = req.params;
|
|
21
21
|
const url = `${serverconfig.tileServerURL}/tileserver/layer/slide/${sampleId}/zoomify/${TileGroup}/${z}-${x}-${y}@1x.jpg`;
|
|
22
|
-
const response = await ky.get(url);
|
|
22
|
+
const response = await ky.get(url, { timeout: 12e4 });
|
|
23
23
|
const buffer = await response.arrayBuffer();
|
|
24
24
|
res.status(response.status).send(Buffer.from(buffer));
|
|
25
25
|
} catch (error) {
|
package/routes/wsimages.js
CHANGED
|
@@ -5,6 +5,8 @@ import serverconfig from "#src/serverconfig.js";
|
|
|
5
5
|
import { CookieJar } from "tough-cookie";
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
import { wsImagesPayload } from "#types/checkers";
|
|
8
|
+
import SessionManager, { SessionData } from "../src/wsisessions/SessionManager.ts";
|
|
9
|
+
import crypto from "crypto";
|
|
8
10
|
const routePath = "wsimages";
|
|
9
11
|
const api = {
|
|
10
12
|
endpoint: `${routePath}`,
|
|
@@ -25,93 +27,116 @@ function init({ genomes }) {
|
|
|
25
27
|
const query = req.query;
|
|
26
28
|
const g = genomes[query.genome];
|
|
27
29
|
if (!g)
|
|
28
|
-
throw "
|
|
30
|
+
throw new Error("Invalid genome name");
|
|
29
31
|
const ds = g.datasets[query.dslabel];
|
|
30
32
|
if (!ds)
|
|
31
|
-
throw "
|
|
33
|
+
throw new Error("Invalid dataset name");
|
|
32
34
|
const sampleId = query.sampleId;
|
|
33
35
|
if (!sampleId)
|
|
34
|
-
throw "
|
|
36
|
+
throw new Error("Invalid sampleId");
|
|
35
37
|
const wsimage = query.wsimage;
|
|
36
38
|
if (!wsimage)
|
|
37
|
-
throw "
|
|
39
|
+
throw new Error("Invalid wsimage");
|
|
38
40
|
const cookieJar = new CookieJar();
|
|
39
41
|
const setCookie = promisify(cookieJar.setCookie.bind(cookieJar));
|
|
40
42
|
const getCookieString = promisify(cookieJar.getCookieString.bind(cookieJar));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
const cookieString = await getCookieString(`${serverconfig.tileServerURL}/tileserver/session_id`);
|
|
60
|
-
const sessionId = cookieString.match(/session_id=([^;]*)/)?.[1];
|
|
61
|
-
const sampleWsiTileServer = path.join(
|
|
62
|
-
`${serverconfig.tileServerMount}/${ds.queries.WSImages.imageBySampleFolder}/${sampleId}`,
|
|
63
|
-
wsimage
|
|
64
|
-
);
|
|
65
|
-
const data = qs.stringify({ slide_path: sampleWsiTileServer });
|
|
66
|
-
await ky.put(`${serverconfig.tileServerURL}/tileserver/slide`, {
|
|
67
|
-
body: data,
|
|
68
|
-
headers: {
|
|
69
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
70
|
-
Cookie: `session_id=${sessionId}`
|
|
71
|
-
// Include the session_id in the headers
|
|
72
|
-
},
|
|
73
|
-
hooks: {
|
|
74
|
-
beforeRequest: [
|
|
75
|
-
async (request) => {
|
|
76
|
-
const cookie = await getCookieString(request.url);
|
|
77
|
-
request.headers.set("Cookie", cookie);
|
|
78
|
-
}
|
|
79
|
-
],
|
|
80
|
-
afterResponse: [
|
|
81
|
-
async (request, options, response) => {
|
|
82
|
-
const setCookieHeader = response.headers.get("set-cookie");
|
|
83
|
-
if (setCookieHeader) {
|
|
84
|
-
await setCookie(setCookieHeader, request.url);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
const getWsiImageResponse = await ky.get(`${serverconfig.tileServerURL}/tileserver/slide`, {
|
|
91
|
-
hooks: {
|
|
92
|
-
beforeRequest: [
|
|
93
|
-
async (request) => {
|
|
94
|
-
const cookie = await getCookieString(request.url);
|
|
95
|
-
request.headers.set("Cookie", cookie);
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
}).json();
|
|
43
|
+
let sessionManager;
|
|
44
|
+
let sessionData;
|
|
45
|
+
let userSessionId = void 0;
|
|
46
|
+
if (serverconfig.redis) {
|
|
47
|
+
sessionManager = SessionManager.getInstance(serverconfig.redis.url);
|
|
48
|
+
userSessionId = crypto.createHash("sha256").update(crypto.randomBytes(32).toString("hex")).digest("hex");
|
|
49
|
+
sessionData = await sessionManager.getSession(wsimage);
|
|
50
|
+
}
|
|
51
|
+
const sessionId = sessionData ? sessionData.imageSessionId : await getSessionId(cookieJar, getCookieString, setCookie, wsimage, ds, sampleId);
|
|
52
|
+
if (serverconfig.redis && sessionManager) {
|
|
53
|
+
await manageUserSession(sessionManager, sessionData, wsimage, userSessionId, sessionId);
|
|
54
|
+
}
|
|
55
|
+
const getWsiImageResponse = await getWsiImageDimensions(sessionId, getCookieString);
|
|
100
56
|
const payload = {
|
|
101
57
|
status: "ok",
|
|
102
|
-
sessionId,
|
|
58
|
+
wsiSessionId: sessionId,
|
|
59
|
+
browserImageInstanceId: serverconfig.redis ? userSessionId : void 0,
|
|
103
60
|
slide_dimensions: getWsiImageResponse.slide_dimensions
|
|
104
61
|
};
|
|
105
62
|
res.status(200).json(payload);
|
|
106
63
|
} catch (e) {
|
|
107
|
-
console.
|
|
108
|
-
res.send({
|
|
64
|
+
console.error(e);
|
|
65
|
+
res.status(500).send({
|
|
109
66
|
status: "error",
|
|
110
|
-
error: e.
|
|
67
|
+
error: e.message || e
|
|
111
68
|
});
|
|
112
69
|
}
|
|
113
70
|
};
|
|
114
71
|
}
|
|
72
|
+
async function getSessionId(cookieJar, getCookieString, setCookie, wsimage, ds, sampleId) {
|
|
73
|
+
await ky.get(`${serverconfig.tileServerURL}/tileserver/session_id`, {
|
|
74
|
+
timeout: 5e4,
|
|
75
|
+
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
76
|
+
});
|
|
77
|
+
const cookieString = await getCookieString(`${serverconfig.tileServerURL}/tileserver/session_id`);
|
|
78
|
+
const sessionId = cookieString.match(/session_id=([^;]*)/)?.[1];
|
|
79
|
+
if (!sessionId)
|
|
80
|
+
throw new Error("session_id not found");
|
|
81
|
+
const sampleWsiTileServer = path.join(
|
|
82
|
+
`${serverconfig.tileServerMount}/${ds.queries.WSImages.imageBySampleFolder}/${sampleId}`,
|
|
83
|
+
wsimage
|
|
84
|
+
);
|
|
85
|
+
const data = qs.stringify({ slide_path: sampleWsiTileServer });
|
|
86
|
+
await ky.put(`${serverconfig.tileServerURL}/tileserver/slide`, {
|
|
87
|
+
body: data,
|
|
88
|
+
timeout: 5e4,
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
91
|
+
Cookie: `session_id=${sessionId}`
|
|
92
|
+
},
|
|
93
|
+
hooks: getHooks(cookieJar, getCookieString, setCookie)
|
|
94
|
+
});
|
|
95
|
+
return sessionId;
|
|
96
|
+
}
|
|
97
|
+
async function manageUserSession(sessionManager, sessionData, wsimage, userId, sessionId) {
|
|
98
|
+
if (!sessionData) {
|
|
99
|
+
await sessionManager.setSession(wsimage, new SessionData(sessionId, [userId]));
|
|
100
|
+
} else if (!sessionData.userSessionIds || !sessionData.userSessionIds.includes(userId)) {
|
|
101
|
+
sessionData.userSessionIds = sessionData.userSessionIds || [];
|
|
102
|
+
sessionData.userSessionIds.push(userId);
|
|
103
|
+
await sessionManager.setSession(wsimage, sessionData);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function getWsiImageDimensions(sessionId, getCookieString) {
|
|
107
|
+
return await ky.get(`${serverconfig.tileServerURL}/tileserver/slide`, {
|
|
108
|
+
timeout: 12e4,
|
|
109
|
+
hooks: {
|
|
110
|
+
beforeRequest: [
|
|
111
|
+
async (request) => {
|
|
112
|
+
let cookie = await getCookieString(request.url);
|
|
113
|
+
if (!cookie) {
|
|
114
|
+
cookie = `session_id=${sessionId}`;
|
|
115
|
+
}
|
|
116
|
+
request.headers.set("Cookie", cookie);
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}).json();
|
|
121
|
+
}
|
|
122
|
+
function getHooks(cookieJar, getCookieString, setCookie) {
|
|
123
|
+
return {
|
|
124
|
+
beforeRequest: [
|
|
125
|
+
async (request) => {
|
|
126
|
+
const cookie = await getCookieString(request.url);
|
|
127
|
+
request.headers.set("Cookie", cookie);
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
afterResponse: [
|
|
131
|
+
async (request, options, response) => {
|
|
132
|
+
const setCookieHeader = response.headers.get("set-cookie");
|
|
133
|
+
if (setCookieHeader) {
|
|
134
|
+
await setCookie(setCookieHeader, request.url);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
115
140
|
export {
|
|
116
141
|
api
|
|
117
142
|
};
|