@myelinbridge/cli 0.6.1 → 0.7.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/README.md +4 -0
- package/bin/myelin.js +35 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
|
|
|
31
31
|
too (S3 multipart). Uploads go direct to storage over short-lived presigned
|
|
32
32
|
URLs — no credential is stored on your machine, and revoking the API key cuts
|
|
33
33
|
off signing immediately.
|
|
34
|
+
- **Every file is fingerprinted.** Since 0.7.0, `push` computes an MD5 of each
|
|
35
|
+
file while it uploads and records it with the delivery. If the client's
|
|
36
|
+
quality contract includes a checksum-manifest check, your delivery verifies
|
|
37
|
+
against your own `md5_manifest.csv` instead of reading as "unverifiable".
|
|
34
38
|
- **Read the contract before you build the delivery.** `contract` prints what the
|
|
35
39
|
client expects — every check as one plain sentence, grouped by what it answers
|
|
36
40
|
(completeness, structure, validity, consistency, integrity, privacy), and marked
|
package/bin/myelin.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { readdirSync, statSync, createReadStream, readFileSync } from 'node:fs'
|
|
12
12
|
import { resolve, join, relative, sep, basename } from 'node:path'
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
13
14
|
import process from 'node:process'
|
|
14
15
|
|
|
15
16
|
const API_URL = (process.env.MYELIN_API_URL ?? 'https://myelinbridge.com/api/v1').replace(/\/$/, '')
|
|
@@ -158,6 +159,27 @@ async function readPart(absPath, start, end) {
|
|
|
158
159
|
return Buffer.concat(chunks)
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
// Whole-file MD5, lowercase hex — the exact format md5sum-style manifests use
|
|
163
|
+
// and the only one the platform's manifest_checksums_match check can verify.
|
|
164
|
+
// A separate sequential read (parts upload 4-wide, out of order — a streaming
|
|
165
|
+
// hash cannot ride along), overlapped with the upload by the caller. Returns
|
|
166
|
+
// null on read failure: a hash miss must degrade the delivery to
|
|
167
|
+
// "unverifiable", never kill the push.
|
|
168
|
+
async function md5File(absPath) {
|
|
169
|
+
try {
|
|
170
|
+
const hash = createHash('md5')
|
|
171
|
+
await new Promise((res, rej) => {
|
|
172
|
+
const s = createReadStream(absPath)
|
|
173
|
+
s.on('data', (c) => hash.update(c))
|
|
174
|
+
s.on('end', res)
|
|
175
|
+
s.on('error', rej)
|
|
176
|
+
})
|
|
177
|
+
return hash.digest('hex')
|
|
178
|
+
} catch {
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
async function uploadFileMultipart(absPath, size, fileId, partSize, onPartDone) {
|
|
162
184
|
const numParts = Math.max(1, Math.ceil(size / partSize))
|
|
163
185
|
const allParts = Array.from({ length: numParts }, (_, i) => i + 1)
|
|
@@ -389,8 +411,19 @@ async function cmdPush() {
|
|
|
389
411
|
const d = queue.shift()
|
|
390
412
|
if (!d) return
|
|
391
413
|
const local = byPath.get(d.path)
|
|
392
|
-
|
|
393
|
-
|
|
414
|
+
// Hash and upload overlap — two independent reads of the same file. The
|
|
415
|
+
// checksum rides on confirm so files.checksum is populated for CLI
|
|
416
|
+
// pushes (0.7.0): without it every CLI delivery reads as "unverifiable"
|
|
417
|
+
// to the manifest_checksums_match quality check.
|
|
418
|
+
const [{ upload_id, parts }, checksum] = await Promise.all([
|
|
419
|
+
uploadFileMultipart(local.abs, local.size, d.file_id, partSize),
|
|
420
|
+
md5File(local.abs),
|
|
421
|
+
])
|
|
422
|
+
const c = await api('POST', `/files/${d.file_id}/confirm`, {
|
|
423
|
+
upload_id,
|
|
424
|
+
parts,
|
|
425
|
+
...(checksum ? { checksum, checksum_algorithm: 'md5' } : {}),
|
|
426
|
+
})
|
|
394
427
|
if (c.status !== 200) die(`confirm ${d.path}: [${c.json?.code}] ${c.json?.detail ?? c.status}`)
|
|
395
428
|
done++
|
|
396
429
|
out(` ✓ ${d.path} (${done}/${pending.length})`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myelinbridge/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Myelin Partner Ingestion CLI — push R&D data deliveries from a pipeline: preflight against the client's quality rules, resumable upload, submit, track review outcomes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|