pinokiod 7.5.41 → 7.5.43

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.41",
3
+ "version": "7.5.43",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -85,6 +85,7 @@ const { createTaskWorkspaceLinkService } = require("./lib/task_workspace_links")
85
85
  const { createContentValidationService } = require("./lib/content_validation")
86
86
  const { buildSecureRouterDebugSnapshot, createSecureRouterDebugStore } = require("./lib/secure_router_debug")
87
87
  const logRedaction = require("./lib/log_redaction")
88
+ const privacyFilterCache = require("./lib/privacy_filter_cache")
88
89
  const AppRegistryService = require("./lib/app_registry")
89
90
  const AppLogService = require("./lib/app_logs")
90
91
  const AppLogReportService = require("./lib/app_log_report")
@@ -6401,6 +6402,12 @@ class Server {
6401
6402
  }
6402
6403
  });
6403
6404
  this.app.use('/files', serve2, serveIndex(this.kernel.homedir, {icons: true, hidden: true, theme: this.theme }))
6405
+ this.app.use('/pinokio/privacy-filter/models', express.static(privacyFilterCache.cacheRoot(this.kernel), {
6406
+ fallthrough: true,
6407
+ index: false,
6408
+ immutable: true,
6409
+ maxAge: '1y'
6410
+ }))
6404
6411
  } else {
6405
6412
  this.app.set("views", [
6406
6413
  path.resolve(__dirname, "views")
@@ -8415,6 +8422,24 @@ class Server {
8415
8422
  entries
8416
8423
  })
8417
8424
  }))
8425
+ this.app.get("/pinokio/privacy-filter/status", ex(async (req, res) => {
8426
+ const payload = await privacyFilterCache.status(this.kernel, {
8427
+ dtype: req.query.dtype
8428
+ })
8429
+ res.set("Cache-Control", "no-store")
8430
+ res.json(payload)
8431
+ }))
8432
+ this.app.post("/pinokio/privacy-filter/ensure", ex(async (req, res) => {
8433
+ if (!privacyFilterCache.isInstallRequestAllowed(req)) {
8434
+ res.status(403).json({ error: "Privacy filter cache install requires a same-origin request" })
8435
+ return
8436
+ }
8437
+ const payload = await privacyFilterCache.ensure(this.kernel, {
8438
+ dtype: req.query.dtype
8439
+ })
8440
+ res.set("Cache-Control", "no-store")
8441
+ res.json(payload)
8442
+ }))
8418
8443
  this.app.get("/pinokio/logs/file", ex(logRedaction.createTopLevelLogFileHandler(this)))
8419
8444
  this.app.get("/api/logs/stream", ex(async (req, res) => {
8420
8445
  const workspace = typeof req.query.workspace === 'string' ? req.query.workspace.trim() : ''
@@ -15991,12 +16016,21 @@ class Server {
15991
16016
  }
15992
16017
  return
15993
16018
  }
15994
- const redactionOverridesRequested = Boolean(req.body && Object.prototype.hasOwnProperty.call(req.body, 'redacted_overrides'))
16019
+ const reviewedArchiveRequested = Boolean(req.body && (
16020
+ Object.prototype.hasOwnProperty.call(req.body, 'redacted_overrides') ||
16021
+ Object.prototype.hasOwnProperty.call(req.body, 'excluded_paths')
16022
+ ))
16023
+ const allowPartialOverrides = Boolean(req.body && (
16024
+ req.body.allow_partial_overrides === true ||
16025
+ req.body.allow_partial_overrides === 'true'
16026
+ ))
15995
16027
  let redactionOverrides = []
16028
+ let redactionExclusions = []
15996
16029
  try {
15997
16030
  redactionOverrides = logRedaction.normalizeLogRedactionOverrides(req.body || {})
16031
+ redactionExclusions = logRedaction.normalizeLogRedactionExclusions(req.body || {})
15998
16032
  } catch (error) {
15999
- res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction overrides' })
16033
+ res.status(400).json({ error: error && error.message ? error.message : 'Invalid redaction request' })
16000
16034
  return
16001
16035
  }
16002
16036
 
@@ -16014,22 +16048,26 @@ class Server {
16014
16048
 
16015
16049
  let folder = this.kernel.path("exported_logs")
16016
16050
  let zipPath = this.kernel.path("logs.zip")
16017
- if (redactionOverridesRequested) {
16051
+ if (reviewedArchiveRequested) {
16018
16052
  await fs.promises.rm(zipPath, { force: true }).catch(() => {})
16019
16053
  }
16020
16054
  let appliedRedactionOverrides = 0
16055
+ let appliedRedactionExclusions = 0
16021
16056
  try {
16022
- if (redactionOverridesRequested) {
16023
- await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides)
16057
+ if (reviewedArchiveRequested) {
16058
+ await logRedaction.assertCompleteLogRedactionOverrides(folder, redactionOverrides, redactionExclusions, {
16059
+ requireComplete: !allowPartialOverrides
16060
+ })
16024
16061
  }
16062
+ appliedRedactionExclusions = await logRedaction.applyLogRedactionExclusions(folder, redactionExclusions)
16025
16063
  appliedRedactionOverrides = await logRedaction.applyLogRedactionOverrides(folder, redactionOverrides)
16026
16064
  } catch (error) {
16027
- res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction overrides' })
16065
+ res.status(400).json({ error: error && error.message ? error.message : 'Failed to apply redaction request' })
16028
16066
  return
16029
16067
  }
16030
16068
  await fs.promises.rm(zipPath, { force: true }).catch(() => {})
16031
16069
  await compressing.zip.compressDir(folder, zipPath)
16032
- res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides })
16070
+ res.json({ success: true, download: '/pinokio/logs.zip', redacted_overrides: appliedRedactionOverrides, excluded_paths: appliedRedactionExclusions })
16033
16071
  }))
16034
16072
  this.app.get("/pinokio/version", ex(async (req, res) => {
16035
16073
  let version = this.version
@@ -5,6 +5,7 @@ const { isBinaryFile } = require('isbinaryfile')
5
5
  const LOG_REDACTION_FILE_MAX_BYTES = 2 * 1024 * 1024
6
6
  const LOG_REDACTION_OVERRIDE_MAX_BYTES = 8 * 1024 * 1024
7
7
  const LOG_REDACTION_TEXT_EXTENSIONS = new Set(['.json', '.log', '.txt'])
8
+ const LOG_REDACTION_TAIL_LINE_COUNTS = new Set([500, 1000, 2000])
8
9
  const CADDY_LOG_PATTERN = /^caddy(?:-.+)?\.log$/i
9
10
 
10
11
  function isTopLevelRedactableLogPath(relativePath = '') {
@@ -21,8 +22,84 @@ function isTopLevelRedactableLogPath(relativePath = '') {
21
22
  return LOG_REDACTION_TEXT_EXTENSIONS.has(path.extname(value).toLowerCase())
22
23
  }
23
24
 
25
+ function normalizeTailLineCount(value) {
26
+ const parsed = Number.parseInt(String(value || ''), 10)
27
+ return LOG_REDACTION_TAIL_LINE_COUNTS.has(parsed) ? parsed : 0
28
+ }
29
+
30
+ async function readTailTextFile(filePath, stats, tailLines) {
31
+ const readBytes = Math.min(stats.size, LOG_REDACTION_FILE_MAX_BYTES - 2048)
32
+ const start = Math.max(0, stats.size - readBytes)
33
+ const buffer = Buffer.alloc(readBytes)
34
+ const handle = await fs.promises.open(filePath, 'r')
35
+ let bytesRead = 0
36
+ try {
37
+ const result = await handle.read(buffer, 0, readBytes, start)
38
+ bytesRead = result.bytesRead
39
+ } finally {
40
+ await handle.close()
41
+ }
42
+ let text = buffer.slice(0, bytesRead).toString('utf8')
43
+ if (start > 0) {
44
+ const firstNewline = text.indexOf('\n')
45
+ if (firstNewline >= 0) {
46
+ text = text.slice(firstNewline + 1)
47
+ }
48
+ }
49
+ const lines = text.split(/\r?\n/)
50
+ const selected = lines.length > tailLines ? lines.slice(-tailLines) : lines
51
+ const omittedLines = Math.max(0, lines.length - selected.length)
52
+ const prefix = start > 0 || omittedLines > 0
53
+ ? `[Older log content omitted by user. Showing the last ${tailLines.toLocaleString()} lines.]\n`
54
+ : ''
55
+ return {
56
+ text: `${prefix}${selected.join('\n')}`,
57
+ truncated: start > 0 || omittedLines > 0,
58
+ included_lines: selected.length
59
+ }
60
+ }
61
+
62
+ function clonePlainObject(value) {
63
+ return value && typeof value === 'object' && !Array.isArray(value)
64
+ ? { ...value }
65
+ : {}
66
+ }
67
+
68
+ function createRuntimeEnvSnapshot(kernel) {
69
+ const baseEnv = {
70
+ ...clonePlainObject(process.env),
71
+ ...clonePlainObject(kernel && kernel.envs)
72
+ }
73
+ if (kernel && kernel.bin && typeof kernel.bin.envs === 'function') {
74
+ try {
75
+ return clonePlainObject(kernel.bin.envs(baseEnv))
76
+ } catch (_) {}
77
+ }
78
+ return baseEnv
79
+ }
80
+
81
+ function createFallbackStateSnapshot(kernel) {
82
+ const env = createRuntimeEnvSnapshot(kernel)
83
+ if (Object.keys(env).length === 0) {
84
+ return null
85
+ }
86
+ return {
87
+ state: 'snapshot',
88
+ id: 'runtime-environment',
89
+ group: 'system',
90
+ env,
91
+ path: kernel && kernel.homedir,
92
+ cmd: 'pinokio environment snapshot',
93
+ done: true,
94
+ ready: true
95
+ }
96
+ }
97
+
24
98
  function createCurrentLogSnapshot(kernel, version) {
25
- const states = kernel.shell.shells.map((s) => {
99
+ const liveShells = kernel && kernel.shell && Array.isArray(kernel.shell.shells)
100
+ ? kernel.shell.shells
101
+ : []
102
+ const states = liveShells.map((s) => {
26
103
  return {
27
104
  state: s.state,
28
105
  id: s.id,
@@ -34,6 +111,12 @@ function createCurrentLogSnapshot(kernel, version) {
34
111
  ready: s.ready,
35
112
  }
36
113
  })
114
+ if (states.length === 0) {
115
+ const fallbackState = createFallbackStateSnapshot(kernel)
116
+ if (fallbackState) {
117
+ states.push(fallbackState)
118
+ }
119
+ }
37
120
 
38
121
  const info = {
39
122
  platform: kernel.platform,
@@ -104,12 +187,45 @@ function normalizeLogRedactionOverrides(body = {}) {
104
187
  return overrides
105
188
  }
106
189
 
107
- async function assertCompleteLogRedactionOverrides(exportRoot, overrides = []) {
190
+ function normalizeLogRedactionExclusions(body = {}) {
191
+ if (!body || !Object.prototype.hasOwnProperty.call(body, 'excluded_paths')) {
192
+ return []
193
+ }
194
+ const source = body.excluded_paths
195
+ if (!Array.isArray(source)) {
196
+ throw new Error('excluded_paths must be an array')
197
+ }
198
+ const exclusions = []
199
+ const seen = new Set()
200
+ for (const candidate of source) {
201
+ const relativePath = typeof candidate === 'string' ? candidate.trim() : ''
202
+ if (!isTopLevelRedactableLogPath(relativePath)) {
203
+ throw new Error(`Invalid excluded path: ${relativePath || '(empty)'}`)
204
+ }
205
+ if (seen.has(relativePath)) {
206
+ throw new Error(`Duplicate excluded path: ${relativePath}`)
207
+ }
208
+ seen.add(relativePath)
209
+ exclusions.push(relativePath)
210
+ }
211
+ return exclusions
212
+ }
213
+
214
+ async function assertCompleteLogRedactionOverrides(exportRoot, overrides = [], exclusions = [], options = {}) {
108
215
  const overridePaths = new Set((Array.isArray(overrides) ? overrides : []).map((override) => override && override.path).filter(Boolean))
216
+ const excludedPaths = new Set(Array.isArray(exclusions) ? exclusions.filter(Boolean) : [])
217
+ for (const excludedPath of excludedPaths) {
218
+ if (overridePaths.has(excludedPath)) {
219
+ throw new Error(`File cannot be both redacted and excluded: ${excludedPath}`)
220
+ }
221
+ }
222
+ if (options && options.requireComplete === false) {
223
+ return
224
+ }
109
225
  const missing = []
110
226
  const entries = await fs.promises.readdir(exportRoot, { withFileTypes: true })
111
227
  for (const entry of entries) {
112
- if (entry.isFile() && isTopLevelRedactableLogPath(entry.name) && !overridePaths.has(entry.name)) {
228
+ if (entry.isFile() && isTopLevelRedactableLogPath(entry.name) && !overridePaths.has(entry.name) && !excludedPaths.has(entry.name)) {
113
229
  missing.push(entry.name)
114
230
  }
115
231
  }
@@ -119,6 +235,35 @@ async function assertCompleteLogRedactionOverrides(exportRoot, overrides = []) {
119
235
  }
120
236
  }
121
237
 
238
+ async function applyLogRedactionExclusions(exportRoot, exclusions = []) {
239
+ if (!Array.isArray(exclusions) || exclusions.length === 0) {
240
+ return 0
241
+ }
242
+ let removed = 0
243
+ for (const relativePath of exclusions) {
244
+ if (!isTopLevelRedactableLogPath(relativePath)) {
245
+ throw new Error(`Invalid excluded path: ${relativePath || '(empty)'}`)
246
+ }
247
+ const target = path.resolve(exportRoot, relativePath)
248
+ const relative = path.relative(exportRoot, target)
249
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
250
+ throw new Error(`Invalid excluded path: ${relativePath}`)
251
+ }
252
+ let stats
253
+ try {
254
+ stats = await fs.promises.stat(target)
255
+ } catch (_) {
256
+ throw new Error(`Excluded path target not found: ${relativePath}`)
257
+ }
258
+ if (!stats.isFile()) {
259
+ throw new Error(`Excluded path target is not a file: ${relativePath}`)
260
+ }
261
+ await fs.promises.rm(target, { force: true })
262
+ removed += 1
263
+ }
264
+ return removed
265
+ }
266
+
122
267
  async function applyLogRedactionOverrides(exportRoot, overrides = []) {
123
268
  if (!Array.isArray(overrides) || overrides.length === 0) {
124
269
  return 0
@@ -215,6 +360,7 @@ function createTopLevelLogFileHandler(server) {
215
360
  res.status(400).json({ error: 'Only top-level text log files can be read for redaction' })
216
361
  return
217
362
  }
363
+ const tailLines = normalizeTailLineCount(req.query.tail_lines || req.query.lines)
218
364
  let stats
219
365
  try {
220
366
  stats = await fs.promises.stat(descriptor.absolutePath)
@@ -226,7 +372,7 @@ function createTopLevelLogFileHandler(server) {
226
372
  res.status(400).json({ error: 'Path is not a file' })
227
373
  return
228
374
  }
229
- if (stats.size > LOG_REDACTION_FILE_MAX_BYTES) {
375
+ if (stats.size > LOG_REDACTION_FILE_MAX_BYTES && !tailLines) {
230
376
  res.status(413).json({
231
377
  error: 'File is too large to redact in the browser',
232
378
  size: stats.size,
@@ -242,8 +388,14 @@ function createTopLevelLogFileHandler(server) {
242
388
  }
243
389
  } catch (_) {}
244
390
  let text
391
+ let tail = null
245
392
  try {
246
- text = await fs.promises.readFile(descriptor.absolutePath, 'utf8')
393
+ if (tailLines) {
394
+ tail = await readTailTextFile(descriptor.absolutePath, stats, tailLines)
395
+ text = tail.text
396
+ } else {
397
+ text = await fs.promises.readFile(descriptor.absolutePath, 'utf8')
398
+ }
247
399
  } catch (error) {
248
400
  res.status(500).json({ error: 'Failed to read file', detail: error.message })
249
401
  return
@@ -254,6 +406,9 @@ function createTopLevelLogFileHandler(server) {
254
406
  name: path.basename(relativePath),
255
407
  size: stats.size,
256
408
  modified: stats.mtime,
409
+ tail_lines: tailLines || null,
410
+ truncated: tail ? tail.truncated : false,
411
+ included_lines: tail ? tail.included_lines : null,
257
412
  text
258
413
  })
259
414
  }
@@ -262,10 +417,13 @@ function createTopLevelLogFileHandler(server) {
262
417
  module.exports = {
263
418
  LOG_REDACTION_FILE_MAX_BYTES,
264
419
  isTopLevelRedactableLogPath,
420
+ normalizeTailLineCount,
265
421
  createCurrentLogSnapshot,
266
422
  writeCurrentLogSnapshot,
267
423
  normalizeLogRedactionOverrides,
424
+ normalizeLogRedactionExclusions,
268
425
  assertCompleteLogRedactionOverrides,
426
+ applyLogRedactionExclusions,
269
427
  applyLogRedactionOverrides,
270
428
  createLogRedactionBodyParser,
271
429
  createLogRedactionBodyParsers,
@@ -0,0 +1,286 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { pipeline } = require('stream/promises')
4
+ const axios = require('axios')
5
+
6
+ const PRIVACY_FILTER_MODEL_ID = 'openai/privacy-filter'
7
+ const PRIVACY_FILTER_REVISION = '7ffa9a043d54d1be65afb281eddf0ffbe629385b'
8
+ const PRIVACY_FILTER_LOCAL_MODEL_PATH = `/pinokio/privacy-filter/models/${PRIVACY_FILTER_REVISION}/`
9
+ const PRIVACY_FILTER_SHARED_FILES = [
10
+ 'config.json',
11
+ 'tokenizer_config.json',
12
+ 'tokenizer.json'
13
+ ]
14
+ const PRIVACY_FILTER_DTYPE_FILES = {
15
+ q4f16: [
16
+ 'onnx/model_q4f16.onnx',
17
+ 'onnx/model_q4f16.onnx_data'
18
+ ],
19
+ q4: [
20
+ 'onnx/model_q4.onnx',
21
+ 'onnx/model_q4.onnx_data'
22
+ ],
23
+ q8: [
24
+ 'onnx/model_quantized.onnx',
25
+ 'onnx/model_quantized.onnx_data'
26
+ ]
27
+ }
28
+ const PRIVACY_FILTER_ASSET_SET = new Set([
29
+ ...PRIVACY_FILTER_SHARED_FILES,
30
+ ...Object.values(PRIVACY_FILTER_DTYPE_FILES).flat()
31
+ ])
32
+
33
+ const ensurePromises = new Map()
34
+ const installStates = new Map()
35
+
36
+ function normalizeDtype(value) {
37
+ const dtype = String(value || '').trim().toLowerCase()
38
+ return Object.prototype.hasOwnProperty.call(PRIVACY_FILTER_DTYPE_FILES, dtype) ? dtype : 'q8'
39
+ }
40
+
41
+ function cacheRoot(kernel) {
42
+ const home = kernel && typeof kernel.homedir === 'string' ? kernel.homedir : ''
43
+ if (!home.trim()) {
44
+ throw new Error('Pinokio home directory is required for privacy filter cache.')
45
+ }
46
+ return path.resolve(home, 'cache', 'privacy-filter', 'models')
47
+ }
48
+
49
+ function modelRoot(kernel) {
50
+ return path.resolve(cacheRoot(kernel), PRIVACY_FILTER_REVISION, 'openai', 'privacy-filter')
51
+ }
52
+
53
+ function assetFilesForDtype(dtype) {
54
+ const normalized = normalizeDtype(dtype)
55
+ return [
56
+ ...PRIVACY_FILTER_SHARED_FILES,
57
+ ...PRIVACY_FILTER_DTYPE_FILES[normalized]
58
+ ]
59
+ }
60
+
61
+ function assertKnownAsset(relativePath) {
62
+ const normalized = String(relativePath || '').replace(/\\/g, '/')
63
+ if (!PRIVACY_FILTER_ASSET_SET.has(normalized)) {
64
+ throw new Error(`Unknown privacy filter asset: ${relativePath}`)
65
+ }
66
+ return normalized
67
+ }
68
+
69
+ function isInstallRequestAllowed(req) {
70
+ const fetchSite = String(req && typeof req.get === 'function' ? req.get('Sec-Fetch-Site') || '' : '').trim().toLowerCase()
71
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
72
+ return false
73
+ }
74
+
75
+ const origin = String(req && typeof req.get === 'function' ? req.get('Origin') || '' : '').trim()
76
+ if (!origin) {
77
+ return true
78
+ }
79
+
80
+ const host = String(req && typeof req.get === 'function' ? req.get('Host') || '' : '').trim().toLowerCase()
81
+ if (!host) {
82
+ return false
83
+ }
84
+
85
+ try {
86
+ const parsed = new URL(origin)
87
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host.toLowerCase() === host
88
+ } catch (_) {
89
+ return false
90
+ }
91
+ }
92
+
93
+ function assetPath(kernel, relativePath) {
94
+ const normalized = assertKnownAsset(relativePath)
95
+ return path.resolve(modelRoot(kernel), ...normalized.split('/'))
96
+ }
97
+
98
+ function ensureKey(kernel, dtype) {
99
+ return `${modelRoot(kernel)}:${normalizeDtype(dtype)}`
100
+ }
101
+
102
+ function remoteAssetUrl(relativePath) {
103
+ const normalized = assertKnownAsset(relativePath)
104
+ return `https://huggingface.co/${PRIVACY_FILTER_MODEL_ID}/resolve/${PRIVACY_FILTER_REVISION}/${normalized}`
105
+ }
106
+
107
+ function copyInstallState(install) {
108
+ return install ? { ...install } : null
109
+ }
110
+
111
+ async function cleanupStaleAssetTemps(target) {
112
+ const dir = path.dirname(target)
113
+ const base = path.basename(target)
114
+ const currentProcessPrefix = `${base}.${process.pid}.`
115
+ let entries
116
+ try {
117
+ entries = await fs.promises.readdir(dir, { withFileTypes: true })
118
+ } catch (error) {
119
+ if (error && error.code === 'ENOENT') {
120
+ return
121
+ }
122
+ throw error
123
+ }
124
+ await Promise.all(entries
125
+ .filter((entry) => {
126
+ return entry.isFile() &&
127
+ entry.name.startsWith(`${base}.`) &&
128
+ entry.name.endsWith('.tmp') &&
129
+ !entry.name.startsWith(currentProcessPrefix)
130
+ })
131
+ .map((entry) => fs.promises.rm(path.resolve(dir, entry.name), { force: true })))
132
+ }
133
+
134
+ async function fileStatus(kernel, relativePath) {
135
+ const target = assetPath(kernel, relativePath)
136
+ try {
137
+ const stats = await fs.promises.stat(target)
138
+ return {
139
+ path: relativePath,
140
+ exists: stats.isFile() && stats.size > 0,
141
+ size: stats.isFile() ? stats.size : 0
142
+ }
143
+ } catch (_) {
144
+ return {
145
+ path: relativePath,
146
+ exists: false,
147
+ size: 0
148
+ }
149
+ }
150
+ }
151
+
152
+ async function status(kernel, options = {}) {
153
+ const dtype = normalizeDtype(options.dtype)
154
+ const install = copyInstallState(installStates.get(ensureKey(kernel, dtype)))
155
+ const files = await Promise.all(assetFilesForDtype(dtype).map((file) => fileStatus(kernel, file)))
156
+ const missing = files.filter((file) => !file.exists).map((file) => file.path)
157
+ return {
158
+ model_id: PRIVACY_FILTER_MODEL_ID,
159
+ revision: PRIVACY_FILTER_REVISION,
160
+ dtype,
161
+ local_model_path: PRIVACY_FILTER_LOCAL_MODEL_PATH,
162
+ ready: missing.length === 0,
163
+ files,
164
+ missing,
165
+ missing_count: missing.length,
166
+ installing: Boolean(install),
167
+ install
168
+ }
169
+ }
170
+
171
+ async function downloadAsset(kernel, relativePath, onProgress) {
172
+ const target = assetPath(kernel, relativePath)
173
+ const existing = await fileStatus(kernel, relativePath)
174
+ if (existing.exists) {
175
+ return { path: relativePath, status: 'cached', size: existing.size }
176
+ }
177
+
178
+ await fs.promises.mkdir(path.dirname(target), { recursive: true })
179
+ await cleanupStaleAssetTemps(target)
180
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`
181
+ try {
182
+ const response = await axios.get(remoteAssetUrl(relativePath), {
183
+ responseType: 'stream',
184
+ maxRedirects: 5,
185
+ timeout: 0,
186
+ maxContentLength: Infinity,
187
+ maxBodyLength: Infinity,
188
+ validateStatus: (statusCode) => statusCode >= 200 && statusCode < 300
189
+ })
190
+ let loaded = 0
191
+ const total = Number(response.headers && response.headers['content-length']) || 0
192
+ const encoded = Boolean(response.headers && response.headers['content-encoding'])
193
+ if (typeof onProgress === 'function') {
194
+ onProgress({ loaded, total })
195
+ }
196
+ response.data.on('data', (chunk) => {
197
+ loaded += chunk && chunk.length ? chunk.length : 0
198
+ if (typeof onProgress === 'function') {
199
+ onProgress({ loaded, total })
200
+ }
201
+ })
202
+ await pipeline(response.data, fs.createWriteStream(tmp))
203
+ const stats = await fs.promises.stat(tmp)
204
+ if (!stats.isFile() || stats.size <= 0) {
205
+ throw new Error(`Downloaded empty privacy filter asset: ${relativePath}`)
206
+ }
207
+ if (total > 0 && !encoded && stats.size !== total) {
208
+ throw new Error(`Incomplete privacy filter asset: ${relativePath} (${stats.size}/${total} bytes)`)
209
+ }
210
+ await fs.promises.rename(tmp, target)
211
+ return { path: relativePath, status: 'downloaded', size: stats.size }
212
+ } catch (error) {
213
+ await fs.promises.rm(tmp, { force: true }).catch(() => {})
214
+ throw error
215
+ }
216
+ }
217
+
218
+ async function ensure(kernel, options = {}) {
219
+ const dtype = normalizeDtype(options.dtype)
220
+ const key = ensureKey(kernel, dtype)
221
+ if (ensurePromises.has(key)) {
222
+ return ensurePromises.get(key)
223
+ }
224
+ const promise = (async () => {
225
+ const files = assetFilesForDtype(dtype)
226
+ const install = {
227
+ dtype,
228
+ total_files: files.length,
229
+ completed_files: 0,
230
+ cached_files: 0,
231
+ downloaded_files: 0,
232
+ current_file: '',
233
+ current_loaded: 0,
234
+ current_total: 0,
235
+ started_at: new Date().toISOString(),
236
+ updated_at: new Date().toISOString()
237
+ }
238
+ installStates.set(key, install)
239
+ const results = []
240
+ for (let index = 0; index < files.length; index += 1) {
241
+ const file = files[index]
242
+ install.current_file = file
243
+ install.current_index = index + 1
244
+ install.current_loaded = 0
245
+ install.current_total = 0
246
+ install.updated_at = new Date().toISOString()
247
+ const result = await downloadAsset(kernel, file, ({ loaded, total }) => {
248
+ install.current_loaded = loaded
249
+ install.current_total = total
250
+ install.updated_at = new Date().toISOString()
251
+ })
252
+ results.push(result)
253
+ install.completed_files += 1
254
+ if (result.status === 'cached') {
255
+ install.cached_files += 1
256
+ } else if (result.status === 'downloaded') {
257
+ install.downloaded_files += 1
258
+ }
259
+ install.updated_at = new Date().toISOString()
260
+ }
261
+ const current = await status(kernel, { dtype })
262
+ return {
263
+ ...current,
264
+ results,
265
+ downloaded: results.filter((result) => result.status === 'downloaded').length,
266
+ cached: results.filter((result) => result.status === 'cached').length
267
+ }
268
+ })().finally(() => {
269
+ ensurePromises.delete(key)
270
+ installStates.delete(key)
271
+ })
272
+ ensurePromises.set(key, promise)
273
+ return promise
274
+ }
275
+
276
+ module.exports = {
277
+ PRIVACY_FILTER_MODEL_ID,
278
+ PRIVACY_FILTER_REVISION,
279
+ PRIVACY_FILTER_LOCAL_MODEL_PATH,
280
+ normalizeDtype,
281
+ cacheRoot,
282
+ modelRoot,
283
+ isInstallRequestAllowed,
284
+ status,
285
+ ensure
286
+ }