@@ -181,152 +181,6 @@ export interface DlxMetadata {
181181 }
182182}
183183
184- /**
185- * Get metadata file path for a cached binary.
186- */
187- function getMetadataPath ( cacheEntryPath : string ) : string {
188- return getPath ( ) . join ( cacheEntryPath , '.dlx-metadata.json' )
189- }
190-
191- /**
192- * Check if a cached binary is still valid.
193- */
194- async function isCacheValid (
195- cacheEntryPath : string ,
196- cacheTtl : number ,
197- ) : Promise < boolean > {
198- const fs = getFs ( )
199- try {
200- const metaPath = getMetadataPath ( cacheEntryPath )
201- if ( ! fs . existsSync ( metaPath ) ) {
202- return false
203- }
204-
205- const metadata = await readJson ( metaPath , { throws : false } )
206- if ( ! isObjectObject ( metadata ) ) {
207- return false
208- }
209- const now = Date . now ( )
210- const timestamp = ( metadata as Record < string , unknown > ) [ 'timestamp' ]
211- // If timestamp is missing or invalid, cache is invalid
212- if ( typeof timestamp !== 'number' || timestamp <= 0 ) {
213- return false
214- }
215- const age = now - timestamp
216-
217- return age < cacheTtl
218- } catch {
219- return false
220- }
221- }
222-
223- /**
224- * Download a file from a URL with integrity checking and concurrent download protection.
225- * Uses processLock to prevent multiple processes from downloading the same binary simultaneously.
226- * Internal helper function for downloading binary files.
227- */
228- async function downloadBinaryFile (
229- url : string ,
230- destPath : string ,
231- integrity ?: string | undefined ,
232- ) : Promise < string > {
233- // Use process lock to prevent concurrent downloads.
234- // Lock is placed in the cache entry directory as 'concurrency.lock'.
235- const crypto = getCrypto ( )
236- const fs = getFs ( )
237- const path = getPath ( )
238- const cacheEntryDir = path . dirname ( destPath )
239- const lockPath = path . join ( cacheEntryDir , 'concurrency.lock' )
240-
241- return await processLock . withLock (
242- lockPath ,
243- async ( ) => {
244- // Check if file was downloaded while waiting for lock.
245- if ( fs . existsSync ( destPath ) ) {
246- const stats = await fs . promises . stat ( destPath )
247- if ( stats . size > 0 ) {
248- // File exists, compute and return SRI integrity hash.
249- const fileBuffer = await fs . promises . readFile ( destPath )
250- const hash = crypto
251- . createHash ( 'sha512' )
252- . update ( fileBuffer )
253- . digest ( 'base64' )
254- return `sha512-${ hash } `
255- }
256- }
257-
258- // Download the file.
259- try {
260- await httpDownload ( url , destPath )
261- } catch ( e ) {
262- throw new Error (
263- `Failed to download binary from ${ url } \n` +
264- `Destination: ${ destPath } \n` +
265- 'Check your internet connection or verify the URL is accessible.' ,
266- { cause : e } ,
267- )
268- }
269-
270- // Compute SRI integrity hash of downloaded file.
271- const fileBuffer = await fs . promises . readFile ( destPath )
272- const hash = crypto
273- . createHash ( 'sha512' )
274- . update ( fileBuffer )
275- . digest ( 'base64' )
276- const actualIntegrity = `sha512-${ hash } `
277-
278- // Verify integrity if provided.
279- if ( integrity && actualIntegrity !== integrity ) {
280- // Clean up invalid file.
281- await safeDelete ( destPath )
282- throw new Error (
283- `Integrity mismatch: expected ${ integrity } , got ${ actualIntegrity } ` ,
284- )
285- }
286-
287- // Make executable on POSIX systems.
288- if ( ! WIN32 ) {
289- await fs . promises . chmod ( destPath , 0o755 )
290- }
291-
292- return actualIntegrity
293- } ,
294- {
295- // Align with npm npx locking strategy.
296- staleMs : 5000 ,
297- touchIntervalMs : 2000 ,
298- } ,
299- )
300- }
301-
302- /**
303- * Write metadata for a cached binary.
304- * Uses unified schema shared with C++ decompressor and CLI dlxBinary.
305- * Schema documentation: See DlxMetadata interface in this file (exported).
306- */
307- async function writeMetadata (
308- cacheEntryPath : string ,
309- cacheKey : string ,
310- url : string ,
311- integrity : string ,
312- size : number ,
313- ) : Promise < void > {
314- const metaPath = getMetadataPath ( cacheEntryPath )
315- const metadata : DlxMetadata = {
316- version : '1.0.0' ,
317- cache_key : cacheKey ,
318- timestamp : Date . now ( ) ,
319- integrity,
320- size,
321- source : {
322- type : 'download' ,
323- url,
324- } ,
325- }
326- const fs = getFs ( )
327- await fs . promises . writeFile ( metaPath , JSON . stringify ( metadata , null , 2 ) )
328- }
329-
330184/**
331185 * Clean expired entries from the DLX cache.
332186 */
@@ -626,6 +480,85 @@ export async function downloadBinary(
626480 }
627481}
628482
483+ /**
484+ * Download a file from a URL with integrity checking and concurrent download protection.
485+ * Uses processLock to prevent multiple processes from downloading the same binary simultaneously.
486+ * Internal helper function for downloading binary files.
487+ */
488+ export async function downloadBinaryFile (
489+ url : string ,
490+ destPath : string ,
491+ integrity ?: string | undefined ,
492+ ) : Promise < string > {
493+ // Use process lock to prevent concurrent downloads.
494+ // Lock is placed in the cache entry directory as 'concurrency.lock'.
495+ const crypto = getCrypto ( )
496+ const fs = getFs ( )
497+ const path = getPath ( )
498+ const cacheEntryDir = path . dirname ( destPath )
499+ const lockPath = path . join ( cacheEntryDir , 'concurrency.lock' )
500+
501+ return await processLock . withLock (
502+ lockPath ,
503+ async ( ) => {
504+ // Check if file was downloaded while waiting for lock.
505+ if ( fs . existsSync ( destPath ) ) {
506+ const stats = await fs . promises . stat ( destPath )
507+ if ( stats . size > 0 ) {
508+ // File exists, compute and return SRI integrity hash.
509+ const fileBuffer = await fs . promises . readFile ( destPath )
510+ const hash = crypto
511+ . createHash ( 'sha512' )
512+ . update ( fileBuffer )
513+ . digest ( 'base64' )
514+ return `sha512-${ hash } `
515+ }
516+ }
517+
518+ // Download the file.
519+ try {
520+ await httpDownload ( url , destPath )
521+ } catch ( e ) {
522+ throw new Error (
523+ `Failed to download binary from ${ url } \n` +
524+ `Destination: ${ destPath } \n` +
525+ 'Check your internet connection or verify the URL is accessible.' ,
526+ { cause : e } ,
527+ )
528+ }
529+
530+ // Compute SRI integrity hash of downloaded file.
531+ const fileBuffer = await fs . promises . readFile ( destPath )
532+ const hash = crypto
533+ . createHash ( 'sha512' )
534+ . update ( fileBuffer )
535+ . digest ( 'base64' )
536+ const actualIntegrity = `sha512-${ hash } `
537+
538+ // Verify integrity if provided.
539+ if ( integrity && actualIntegrity !== integrity ) {
540+ // Clean up invalid file.
541+ await safeDelete ( destPath )
542+ throw new Error (
543+ `Integrity mismatch: expected ${ integrity } , got ${ actualIntegrity } ` ,
544+ )
545+ }
546+
547+ // Make executable on POSIX systems.
548+ if ( ! WIN32 ) {
549+ await fs . promises . chmod ( destPath , 0o755 )
550+ }
551+
552+ return actualIntegrity
553+ } ,
554+ {
555+ // Align with npm npx locking strategy.
556+ staleMs : 5000 ,
557+ touchIntervalMs : 2000 ,
558+ } ,
559+ )
560+ }
561+
629562/**
630563 * Execute a cached binary without re-downloading.
631564 * Similar to executePackage from dlx-package.
@@ -680,6 +613,45 @@ export function getDlxCachePath(): string {
680613 return getSocketDlxDir ( )
681614}
682615
616+ /**
617+ * Get metadata file path for a cached binary.
618+ */
619+ export function getMetadataPath ( cacheEntryPath : string ) : string {
620+ return getPath ( ) . join ( cacheEntryPath , '.dlx-metadata.json' )
621+ }
622+
623+ /**
624+ * Check if a cached binary is still valid.
625+ */
626+ export async function isCacheValid (
627+ cacheEntryPath : string ,
628+ cacheTtl : number ,
629+ ) : Promise < boolean > {
630+ const fs = getFs ( )
631+ try {
632+ const metaPath = getMetadataPath ( cacheEntryPath )
633+ if ( ! fs . existsSync ( metaPath ) ) {
634+ return false
635+ }
636+
637+ const metadata = await readJson ( metaPath , { throws : false } )
638+ if ( ! isObjectObject ( metadata ) ) {
639+ return false
640+ }
641+ const now = Date . now ( )
642+ const timestamp = ( metadata as Record < string , unknown > ) [ 'timestamp' ]
643+ // If timestamp is missing or invalid, cache is invalid
644+ if ( typeof timestamp !== 'number' || timestamp <= 0 ) {
645+ return false
646+ }
647+ const age = now - timestamp
648+
649+ return age < cacheTtl
650+ } catch {
651+ return false
652+ }
653+ }
654+
683655/**
684656 * Get information about cached binaries.
685657 */
@@ -754,3 +726,31 @@ export async function listDlxCache(): Promise<
754726
755727 return results
756728}
729+
730+ /**
731+ * Write metadata for a cached binary.
732+ * Uses unified schema shared with C++ decompressor and CLI dlxBinary.
733+ * Schema documentation: See DlxMetadata interface in this file (exported).
734+ */
735+ export async function writeMetadata (
736+ cacheEntryPath : string ,
737+ cacheKey : string ,
738+ url : string ,
739+ integrity : string ,
740+ size : number ,
741+ ) : Promise < void > {
742+ const metaPath = getMetadataPath ( cacheEntryPath )
743+ const metadata : DlxMetadata = {
744+ version : '1.0.0' ,
745+ cache_key : cacheKey ,
746+ timestamp : Date . now ( ) ,
747+ integrity,
748+ size,
749+ source : {
750+ type : 'download' ,
751+ url,
752+ } ,
753+ }
754+ const fs = getFs ( )
755+ await fs . promises . writeFile ( metaPath , JSON . stringify ( metadata , null , 2 ) )
756+ }
0 commit comments