@openneuro/server 4.21.0 → 4.21.2
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 +4 -4
- package/src/config.ts +2 -0
- package/src/datalad/files.ts +27 -26
- package/src/handlers/datalad.ts +30 -10
- package/src/server.ts +10 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openneuro/server",
|
|
3
|
-
"version": "4.21.
|
|
3
|
+
"version": "4.21.2",
|
|
4
4
|
"description": "Core service for the OpenNeuro platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@elastic/elasticsearch": "7.15.0",
|
|
22
22
|
"@graphql-tools/schema": "^10.0.0",
|
|
23
23
|
"@keyv/redis": "^2.7.0",
|
|
24
|
-
"@openneuro/search": "^4.21.
|
|
24
|
+
"@openneuro/search": "^4.21.2",
|
|
25
25
|
"@passport-next/passport-google-oauth2": "^1.0.0",
|
|
26
26
|
"@sentry/node": "^4.5.3",
|
|
27
27
|
"base64url": "^3.0.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"date-fns": "^2.16.1",
|
|
31
31
|
"draft-js": "^0.11.7",
|
|
32
32
|
"draft-js-export-html": "^1.4.1",
|
|
33
|
-
"elastic-apm-node": "3.
|
|
33
|
+
"elastic-apm-node": "^4.3.0",
|
|
34
34
|
"express": "4.18.2",
|
|
35
35
|
"graphql": "16.8.1",
|
|
36
36
|
"graphql-bigint": "^1.0.0",
|
|
@@ -85,5 +85,5 @@
|
|
|
85
85
|
"publishConfig": {
|
|
86
86
|
"access": "public"
|
|
87
87
|
},
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "5607a295f9130a0e51bffdaf8d3d6235c8eab8f8"
|
|
89
89
|
}
|
package/src/config.ts
CHANGED
package/src/datalad/files.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import request from "superagent"
|
|
2
1
|
import { redis } from "../libs/redis"
|
|
3
2
|
import CacheItem, { CacheType } from "../cache/item"
|
|
4
3
|
import { getDatasetWorker } from "../libs/datalad-service"
|
|
@@ -86,36 +85,38 @@ export const computeTotalSize = (files: [DatasetFile]): number =>
|
|
|
86
85
|
* @param {string} datasetId - Dataset accession number
|
|
87
86
|
* @param {string} treeish - Git treeish hexsha
|
|
88
87
|
*/
|
|
89
|
-
export const getFiles = (datasetId, treeish): Promise<[DatasetFile]> => {
|
|
88
|
+
export const getFiles = (datasetId, treeish): Promise<[DatasetFile?]> => {
|
|
90
89
|
const cache = new CacheItem(redis, CacheType.commitFiles, [
|
|
91
90
|
datasetId,
|
|
92
91
|
treeish.substring(0, 7),
|
|
93
92
|
])
|
|
94
93
|
return cache.get(
|
|
95
|
-
(doNotCache) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
break
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return files as [DatasetFile]
|
|
94
|
+
async (doNotCache): Promise<[DatasetFile?]> => {
|
|
95
|
+
const response = await fetch(
|
|
96
|
+
`http://${
|
|
97
|
+
getDatasetWorker(
|
|
98
|
+
datasetId,
|
|
99
|
+
)
|
|
100
|
+
}/datasets/${datasetId}/tree/${treeish}`,
|
|
101
|
+
{
|
|
102
|
+
signal: AbortSignal.timeout(10000),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
const body = await response.json()
|
|
106
|
+
const files = body?.files
|
|
107
|
+
if (files) {
|
|
108
|
+
for (const f of files) {
|
|
109
|
+
// Skip caching this tree if it doesn't contain S3 URLs - likely still exporting
|
|
110
|
+
if (!f.directory && !f.urls[0].includes("s3.amazonaws.com")) {
|
|
111
|
+
doNotCache(true)
|
|
112
|
+
break
|
|
118
113
|
}
|
|
119
|
-
}
|
|
114
|
+
}
|
|
115
|
+
return files
|
|
116
|
+
} else {
|
|
117
|
+
// Possible to have zero files here, return an empty array
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
},
|
|
120
121
|
)
|
|
121
122
|
}
|
package/src/handlers/datalad.ts
CHANGED
|
@@ -24,11 +24,30 @@ export const getFile = async (req, res) => {
|
|
|
24
24
|
let tree = snapshotId || "HEAD"
|
|
25
25
|
let file
|
|
26
26
|
for (const level of pathComponents) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
try {
|
|
28
|
+
const files = await getFiles(datasetId, tree)
|
|
29
|
+
if (level == pathComponents.slice(-1)) {
|
|
30
|
+
file = files.find((f) => !f.directory && f.filename === level)
|
|
31
|
+
} else {
|
|
32
|
+
// This tree may exist but have no children
|
|
33
|
+
if (files) {
|
|
34
|
+
tree = files.find((f) => f.directory && f.filename === level).id
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// ConnectTimeoutError is Node/Undici and TimeoutError is the standard DOMException name
|
|
39
|
+
if (
|
|
40
|
+
err?.cause?.name === "ConnectTimeoutError" ||
|
|
41
|
+
err?.name === "TimeoutError"
|
|
42
|
+
) {
|
|
43
|
+
// Unreachable backend, forward this error
|
|
44
|
+
// Usually this is the service restarting due to node migrations or upgrades
|
|
45
|
+
res.status(503).send("Worker could not be reached")
|
|
46
|
+
return
|
|
47
|
+
} else {
|
|
48
|
+
// Unknown error should bubble up
|
|
49
|
+
throw err
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
// Get the file URL and redirect if external or serve if local
|
|
@@ -44,12 +63,13 @@ export const getFile = async (req, res) => {
|
|
|
44
63
|
.then((r) => {
|
|
45
64
|
// Set the content length (allow clients to catch HTTP issues better)
|
|
46
65
|
res.setHeader("Content-Length", Number(r.headers.get("content-length")))
|
|
47
|
-
|
|
66
|
+
if (r.status === 404) {
|
|
67
|
+
res.status(404).send("Requested dataset or file cannot be found")
|
|
68
|
+
} else {
|
|
69
|
+
// @ts-expect-error
|
|
70
|
+
Readable.fromWeb(r.body, { highWaterMark: 4194304 }).pipe(res)
|
|
71
|
+
}
|
|
48
72
|
})
|
|
49
|
-
.then((stream) =>
|
|
50
|
-
// @ts-expect-error
|
|
51
|
-
Readable.fromWeb(stream, { highWaterMark: 4194304 }).pipe(res)
|
|
52
|
-
)
|
|
53
73
|
.catch((err) => {
|
|
54
74
|
console.error(err)
|
|
55
75
|
res.status(500).send("Internal error transferring requested file")
|
package/src/server.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
/** Needs to run before the other imports in Node */
|
|
2
1
|
import apm from "elastic-apm-node"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import config from "./config"
|
|
3
|
+
/** Needs to run before the other imports in Node */
|
|
4
|
+
if (config.elasticsearch.apmServerUrl) {
|
|
5
|
+
apm.start({
|
|
6
|
+
serverUrl: config.elasticsearch.apmServerUrl,
|
|
7
|
+
apiKey: config.elasticsearch.apmApiKey,
|
|
8
|
+
serviceName: "openneuro-server",
|
|
9
|
+
cloudProvider: "none",
|
|
10
|
+
})
|
|
11
|
+
}
|
|
8
12
|
import { createServer } from "http"
|
|
9
13
|
import mongoose from "mongoose"
|
|
10
14
|
import { connect as redisConnect } from "./libs/redis"
|
|
11
|
-
import config from "./config"
|
|
12
15
|
import { expressApolloSetup } from "./app"
|
|
13
16
|
|
|
14
17
|
const redisConnectionSetup = async () => {
|