@sjcrh/proteinpaint-server 2.216.0 → 2.217.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/package.json +4 -4
- package/src/app.js +1563 -1117
- package/src/serverconfig.js +132 -1
package/src/serverconfig.js
CHANGED
|
@@ -7,6 +7,7 @@ import fs from 'fs'
|
|
|
7
7
|
import os from 'os'
|
|
8
8
|
import path from 'path'
|
|
9
9
|
import { fileURLToPath } from 'url'
|
|
10
|
+
import crypto from 'crypto'
|
|
10
11
|
|
|
11
12
|
// import.meta.dirname is undefined when using docker dev environment
|
|
12
13
|
// use __dirname and __filename global variable convention from commonjs
|
|
@@ -364,6 +365,128 @@ if (process.env.PP_MODE?.startsWith('container')) {
|
|
|
364
365
|
})
|
|
365
366
|
}
|
|
366
367
|
|
|
368
|
+
/*
|
|
369
|
+
The key for deriving the cache file names and the cachedir subdir name, kept module-local, not in serverconfig,
|
|
370
|
+
so that it is never part of a config dump or a response.
|
|
371
|
+
|
|
372
|
+
Set PP_CACHEID_CREDS to the same value on every instance that shares a cachedir, so that they derive the same names
|
|
373
|
+
and keep finding the existing cache files across restarts. When the server is started with container/envHelpers.mjs,
|
|
374
|
+
as in the container images, PP_CACHEID_CREDS_FILE may name a file with the value instead: envHelpers.mjs reads that
|
|
375
|
+
file and passes its content like the other <NAME>_CREDS values, so that it is not in the initial env of the server
|
|
376
|
+
process. This module does not read PP_CACHEID_CREDS_FILE, so without envHelpers.mjs it is ignored. When not set, a
|
|
377
|
+
random key is generated per process: the cache still works, but every restart or other instance misses on the
|
|
378
|
+
files that were written before.
|
|
379
|
+
|
|
380
|
+
Read here, after the handoff values are set in process.env above, and removed from process.env in every mode, so
|
|
381
|
+
that it is not inherited by a child process. Only native modules are imported by this file, since it is also
|
|
382
|
+
loaded unbundled from the published package, such as by genome/copyDataFilesFromRepo2Tp.js, besides its bundled
|
|
383
|
+
copy in app.js. Only the first loaded copy, which is the bundled one in a server process, reads the key and sets
|
|
384
|
+
up the cache subdir, see firstLoad below.
|
|
385
|
+
*/
|
|
386
|
+
const cacheIdKeyValue = process.env.PP_CACHEID_CREDS
|
|
387
|
+
const hasPersistentCacheKey = !!cacheIdKeyValue
|
|
388
|
+
const cacheIdKey = cacheIdKeyValue ? Buffer.from(cacheIdKeyValue, 'utf8') : crypto.randomBytes(32)
|
|
389
|
+
delete process.env.PP_CACHEID_CREDS
|
|
390
|
+
|
|
391
|
+
/** Derive a 32-hex-char cacheId from the given object via
|
|
392
|
+
* HMAC-sha256(key, JSON.stringify([scope, args])). Truncation at 32 chars is safe
|
|
393
|
+
* for cache keys — collision probability is negligible at realistic cache sizes.
|
|
394
|
+
* Callers shape `args` to include only the fields whose identity
|
|
395
|
+
* determines the cache key, and must construct it with a stable key order
|
|
396
|
+
* (object literals do this naturally).
|
|
397
|
+
*
|
|
398
|
+
* `scope` is optional, and separates cacheIds for the same args, e.g. per user or session
|
|
399
|
+
* when the result depends on what the requester may access. Without a scope, identical
|
|
400
|
+
* args share one cacheId, which is the intended behavior for results that are the same for
|
|
401
|
+
* every requester. */
|
|
402
|
+
export function generateHash(args, scope = '') {
|
|
403
|
+
return crypto
|
|
404
|
+
.createHmac('sha256', cacheIdKey)
|
|
405
|
+
.update(JSON.stringify([scope, args]))
|
|
406
|
+
.digest('hex')
|
|
407
|
+
.slice(0, 32)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/*
|
|
411
|
+
true for the first loaded copy of this module in a process. A later copy, such as the unbundled file that a genome
|
|
412
|
+
file imports, finds no PP_CACHEID_CREDS since the first copy removed it, so it would derive a different cache
|
|
413
|
+
subdir. The flag only marks that the cache subdir is set up, and does not hold its name.
|
|
414
|
+
*/
|
|
415
|
+
const firstLoadFlag = Symbol.for('proteinpaint.serverconfig.firstLoad')
|
|
416
|
+
const firstLoad = !globalThis[firstLoadFlag]
|
|
417
|
+
// configurable, so that a test can evaluate another first copy
|
|
418
|
+
if (firstLoad) Object.defineProperty(globalThis, firstLoadFlag, { value: true, configurable: true })
|
|
419
|
+
|
|
420
|
+
/*
|
|
421
|
+
Use an unlisted subdir of the configured cachedir, whose name is derived from the PP_CACHEID_CREDS key, so that the
|
|
422
|
+
cache files cannot be found by listing directories. On by default in a container, and may be set with
|
|
423
|
+
serverconfig.hideCachedir in any mode. The configured cachedir must then be owned by another user than the server
|
|
424
|
+
process, such as root, with mode 1733: the server user may create and use a subdir with a known name, but may not
|
|
425
|
+
list the entries or change the mode. A dir that is owned by the server user can always be listed by that user,
|
|
426
|
+
since the owner may change its mode.
|
|
427
|
+
|
|
428
|
+
All code that uses the path must copy serverconfig.cachedir to a module-local variable when it is loaded:
|
|
429
|
+
app.ts deletes serverconfig.cachedir before the server starts listening.
|
|
430
|
+
*/
|
|
431
|
+
if (serverconfig.hideCachedir ?? process.env.PP_MODE?.startsWith('container')) {
|
|
432
|
+
// a later copy does not use the cache, and must not use the configured cachedir, which is the parent of the subdir
|
|
433
|
+
if (!firstLoad) delete serverconfig.cachedir
|
|
434
|
+
else {
|
|
435
|
+
const parent = serverconfig.cachedir
|
|
436
|
+
if (!parent) throw 'serverconfig.cachedir missing'
|
|
437
|
+
// a parent dir that is created here is owned by the server user, which the warning below reports
|
|
438
|
+
if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true })
|
|
439
|
+
let parentEntries
|
|
440
|
+
try {
|
|
441
|
+
parentEntries = fs.readdirSync(parent)
|
|
442
|
+
console.warn(
|
|
443
|
+
`WARNING: serverconfig.cachedir='${parent}' can be listed by the server process, so the cache subdir name ` +
|
|
444
|
+
`is not hidden; the dir should be owned by another user, such as root, with mode 1733`
|
|
445
|
+
)
|
|
446
|
+
} catch (e) {
|
|
447
|
+
// EACCES is the expected result; another error is reported by the mkdirSync() below
|
|
448
|
+
}
|
|
449
|
+
if (!hasPersistentCacheKey) {
|
|
450
|
+
console.warn(
|
|
451
|
+
`WARNING: PP_CACHEID_CREDS is not set, so the cache subdir name is generated for this process only, ` +
|
|
452
|
+
`and the files cached by an earlier process are not found or removed`
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
// not a generateHash() value, since the HMAC input is not a JSON array
|
|
456
|
+
const dirName = crypto.createHmac('sha256', cacheIdKey).update('cachedir').digest('hex').slice(0, 32)
|
|
457
|
+
serverconfig.cachedir = path.join(parent, dirName)
|
|
458
|
+
// the name is not logged, so that it is not in any log output
|
|
459
|
+
fs.mkdirSync(serverconfig.cachedir, { recursive: true, mode: 0o700 })
|
|
460
|
+
if (parentEntries) moveEarlierCacheEntries(parent, parentEntries, dirName)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
delete serverconfig.hideCachedir
|
|
464
|
+
|
|
465
|
+
/*
|
|
466
|
+
Moves the entries of the earlier cache layout, such as the saved sessions in massSession/, from the configured
|
|
467
|
+
cachedir into the derived subdir, where they are still found by the server and evicted by the cache monitor.
|
|
468
|
+
This is only possible while the configured cachedir can be listed, such as on the first start with the derived
|
|
469
|
+
subdir, before the dir owner and mode are changed. An entry with the name shape of a derived subdir, such as from
|
|
470
|
+
another instance with a different key, is not moved, and neither is an entry whose name already exists in the
|
|
471
|
+
derived subdir.
|
|
472
|
+
*/
|
|
473
|
+
function moveEarlierCacheEntries(parent, entries, dirName) {
|
|
474
|
+
let moved = 0
|
|
475
|
+
for (const name of entries) {
|
|
476
|
+
if (name == dirName || /^[0-9a-f]{32}$/.test(name)) continue
|
|
477
|
+
const dest = path.join(parent, dirName, name)
|
|
478
|
+
if (fs.existsSync(dest)) continue
|
|
479
|
+
try {
|
|
480
|
+
fs.renameSync(path.join(parent, name), dest)
|
|
481
|
+
moved++
|
|
482
|
+
} catch (e) {
|
|
483
|
+
// such as EXDEV for a separately mounted entry, which then stays where it is
|
|
484
|
+
console.warn(`WARNING: unable to move the earlier cache entry '${name}' into the cache subdir: ${e.code || e}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (moved) console.log(`moved ${moved} earlier cache entries into the cache subdir`)
|
|
488
|
+
}
|
|
489
|
+
|
|
367
490
|
// when a mandatory setting is not defined in any ds, declare its default here
|
|
368
491
|
|
|
369
492
|
if (process.argv.find(a => a == 'validate')) {
|
|
@@ -390,6 +513,13 @@ if (!serverconfig.backend_only && fs.existsSync(publicDir)) serverconfig.publicD
|
|
|
390
513
|
const binDir = path.join(process.cwd(), './bin')
|
|
391
514
|
if (!serverconfig.backend_only) serverconfig.binDir = binDir
|
|
392
515
|
|
|
516
|
+
// The proteinpaint-front package's own public/index.html and public/cards, which its init copies into public/ when
|
|
517
|
+
// missing there. The server serves these paths from this dir when public/ does not have them, such as a public/ mount
|
|
518
|
+
// that the init cannot write to (see app.middlewares.js). Auto-computed from cwd like binDir, never operator-set.
|
|
519
|
+
const frontPublicDir = path.join(process.cwd(), 'node_modules/@sjcrh/proteinpaint-front/public')
|
|
520
|
+
delete serverconfig.frontPublicDir
|
|
521
|
+
if (!serverconfig.backend_only && fs.existsSync(frontPublicDir)) serverconfig.frontPublicDir = frontPublicDir
|
|
522
|
+
|
|
393
523
|
if (serverconfig.publicDir) {
|
|
394
524
|
const defaultTarget = path.join(serverconfig.binpath, 'cards')
|
|
395
525
|
if (!serverconfig.cards) {
|
|
@@ -410,7 +540,8 @@ if (fs.existsSync('./package.json')) {
|
|
|
410
540
|
serverconfig.version = JSON.parse(pkg).version
|
|
411
541
|
}
|
|
412
542
|
|
|
413
|
-
|
|
543
|
+
// a later copy may have no cachedir, see firstLoad above
|
|
544
|
+
if (!serverconfig.cache_snpgt && serverconfig.cachedir) {
|
|
414
545
|
serverconfig.cache_snpgt = {
|
|
415
546
|
dir: path.join(serverconfig.cachedir, 'snpgt'),
|
|
416
547
|
fileNameRegexp: /[^\w]/, // client-provided cache file name matching with this are denied
|