@data-fair/lib-node-registry 0.1.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/index.ts +185 -0
- package/package.json +21 -0
package/index.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createGunzip } from 'node:zlib'
|
|
2
|
+
import { pipeline } from 'node:stream/promises'
|
|
3
|
+
import { createWriteStream } from 'node:fs'
|
|
4
|
+
import { mkdir, readFile, writeFile, rm, rename, stat, utimes } from 'node:fs/promises'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import * as tar from 'tar-stream'
|
|
7
|
+
import resolvePath from 'resolve-path'
|
|
8
|
+
import { axiosBuilder } from '@data-fair/lib-node/axios.js'
|
|
9
|
+
import type { Readable } from 'node:stream'
|
|
10
|
+
|
|
11
|
+
export interface EnsureArtefactOpts {
|
|
12
|
+
registryUrl: string
|
|
13
|
+
secretKey: string
|
|
14
|
+
artefactId: string
|
|
15
|
+
version: string
|
|
16
|
+
cacheDir: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EnsureArtefactResult {
|
|
20
|
+
path: string
|
|
21
|
+
version: string
|
|
22
|
+
downloaded: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CacheMeta {
|
|
26
|
+
version: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function ensureArtefact (opts: EnsureArtefactOpts): Promise<EnsureArtefactResult> {
|
|
30
|
+
const ax = axiosBuilder({
|
|
31
|
+
baseURL: opts.registryUrl,
|
|
32
|
+
headers: { 'x-secret-key': opts.secretKey }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const encodedId = encodeURIComponent(opts.artefactId)
|
|
36
|
+
const versionRes = await ax.get(`/api/v1/artefacts/${encodedId}/versions/${opts.version}`)
|
|
37
|
+
const resolvedVersion: string = versionRes.data.version
|
|
38
|
+
|
|
39
|
+
const artefactDir = join(opts.cacheDir, opts.artefactId)
|
|
40
|
+
const metaPath = join(artefactDir, '.current-version.json')
|
|
41
|
+
const extractDir = join(artefactDir, resolvedVersion)
|
|
42
|
+
|
|
43
|
+
// Check cache
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(metaPath, 'utf-8')
|
|
46
|
+
const meta: CacheMeta = JSON.parse(raw)
|
|
47
|
+
if (meta.version === resolvedVersion) {
|
|
48
|
+
return { path: extractDir, version: resolvedVersion, downloaded: false }
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// no cache or invalid metadata
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Download tarball
|
|
55
|
+
const tarballRes = await ax.get(
|
|
56
|
+
`/api/v1/artefacts/${encodedId}/versions/${resolvedVersion}/tarball`,
|
|
57
|
+
{ responseType: 'stream' }
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Extract to temp dir then atomic rename
|
|
61
|
+
const tmpDir = `${extractDir}.tmp.${process.pid}`
|
|
62
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
63
|
+
await mkdir(tmpDir, { recursive: true })
|
|
64
|
+
try {
|
|
65
|
+
await extractTarball(tarballRes.data as Readable, tmpDir)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
68
|
+
throw err
|
|
69
|
+
}
|
|
70
|
+
await rm(extractDir, { recursive: true, force: true })
|
|
71
|
+
await rename(tmpDir, extractDir)
|
|
72
|
+
|
|
73
|
+
// Clean up old version
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(metaPath, 'utf-8')
|
|
76
|
+
const oldMeta: CacheMeta = JSON.parse(raw)
|
|
77
|
+
if (oldMeta.version !== resolvedVersion) {
|
|
78
|
+
await rm(join(artefactDir, oldMeta.version), { recursive: true, force: true })
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// no old version to clean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Write cache metadata
|
|
85
|
+
await writeFile(metaPath, JSON.stringify({ version: resolvedVersion } satisfies CacheMeta))
|
|
86
|
+
|
|
87
|
+
return { path: extractDir, version: resolvedVersion, downloaded: true }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface EnsureArtefactFileOpts {
|
|
91
|
+
registryUrl: string
|
|
92
|
+
secretKey: string
|
|
93
|
+
artefactId: string
|
|
94
|
+
cacheDir: string
|
|
95
|
+
/** defaults to artefactId */
|
|
96
|
+
fileName?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface EnsureArtefactFileResult {
|
|
100
|
+
path: string
|
|
101
|
+
downloaded: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function ensureArtefactFile (opts: EnsureArtefactFileOpts): Promise<EnsureArtefactFileResult> {
|
|
105
|
+
const ax = axiosBuilder({
|
|
106
|
+
baseURL: opts.registryUrl,
|
|
107
|
+
headers: { 'x-secret-key': opts.secretKey }
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const destPath = join(opts.cacheDir, opts.fileName ?? opts.artefactId)
|
|
111
|
+
|
|
112
|
+
let prevMtime: Date | undefined
|
|
113
|
+
try {
|
|
114
|
+
const st = await stat(destPath)
|
|
115
|
+
prevMtime = st.mtime
|
|
116
|
+
} catch { /* cold cache */ }
|
|
117
|
+
|
|
118
|
+
const headers: Record<string, string> = {}
|
|
119
|
+
if (prevMtime) headers['if-modified-since'] = prevMtime.toUTCString()
|
|
120
|
+
|
|
121
|
+
const res = await ax.get(
|
|
122
|
+
`/api/v1/artefacts/${encodeURIComponent(opts.artefactId)}/download`,
|
|
123
|
+
{ responseType: 'stream', headers, validateStatus: s => s === 200 || s === 304 }
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if (res.status === 304) {
|
|
127
|
+
return { path: destPath, downloaded: false }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await mkdir(dirname(destPath), { recursive: true })
|
|
131
|
+
const tmpPath = `${destPath}.tmp.${process.pid}`
|
|
132
|
+
await rm(tmpPath, { force: true })
|
|
133
|
+
try {
|
|
134
|
+
await pipeline(res.data as Readable, createWriteStream(tmpPath))
|
|
135
|
+
} catch (err) {
|
|
136
|
+
await rm(tmpPath, { force: true })
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
139
|
+
await rename(tmpPath, destPath)
|
|
140
|
+
|
|
141
|
+
const lastModified = res.headers['last-modified']
|
|
142
|
+
if (lastModified) {
|
|
143
|
+
const mtime = new Date(lastModified)
|
|
144
|
+
if (!isNaN(mtime.getTime())) {
|
|
145
|
+
await utimes(destPath, new Date(), mtime)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { path: destPath, downloaded: true }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function extractTarball (stream: Readable, destDir: string): Promise<void> {
|
|
153
|
+
const extract = tar.extract()
|
|
154
|
+
|
|
155
|
+
const entries: Promise<void>[] = []
|
|
156
|
+
|
|
157
|
+
extract.on('entry', (header, entryStream, next) => {
|
|
158
|
+
// npm tarballs prefix entries with "package/"
|
|
159
|
+
const entryPath = header.name.replace(/^package\//, '')
|
|
160
|
+
|
|
161
|
+
if (header.type === 'directory') {
|
|
162
|
+
entries.push(mkdir(resolvePath(destDir, entryPath), { recursive: true }).then(() => {}))
|
|
163
|
+
entryStream.resume()
|
|
164
|
+
entryStream.on('end', next)
|
|
165
|
+
} else if (header.type === 'file') {
|
|
166
|
+
const fullPath = resolvePath(destDir, entryPath)
|
|
167
|
+
const p = mkdir(dirname(fullPath), { recursive: true }).then(() => {
|
|
168
|
+
return new Promise<void>((resolve, reject) => {
|
|
169
|
+
const ws = createWriteStream(fullPath)
|
|
170
|
+
entryStream.pipe(ws)
|
|
171
|
+
ws.on('finish', resolve)
|
|
172
|
+
ws.on('error', reject)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
entries.push(p)
|
|
176
|
+
entryStream.on('end', next)
|
|
177
|
+
} else {
|
|
178
|
+
entryStream.resume()
|
|
179
|
+
entryStream.on('end', next)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
await pipeline(stream, createGunzip(), extract)
|
|
184
|
+
await Promise.all(entries)
|
|
185
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-fair/lib-node-registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js client library for the data-fair registry service.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"*.ts"
|
|
12
|
+
],
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@data-fair/lib-node": ">=2.8.0"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@types/resolve-path": "^1.4.3",
|
|
18
|
+
"resolve-path": "^1.4.0",
|
|
19
|
+
"tar-stream": "^3.1.0"
|
|
20
|
+
}
|
|
21
|
+
}
|