@@ -189,6 +189,8 @@ export class BlobsServer {
189189 return new Response ( null , { status : 404 } )
190190 }
191191
192+ this . logDebug ( 'Error when reading data:' , error )
193+
192194 return new Response ( null , { status : 500 } )
193195 }
194196 }
@@ -291,16 +293,36 @@ export class BlobsServer {
291293 return new Response ( null , { status : 400 } )
292294 }
293295
294- const metadataHeader = req . headers . get ( METADATA_HEADER_INTERNAL )
295- const metadata = decodeMetadata ( metadataHeader )
296+ // Check conditional write headers.
297+ const ifMatch = req . headers . get ( 'if-match' )
298+ const ifNoneMatch = req . headers . get ( 'if-none-match' )
296299
297300 try {
301+ let fileExists = false
302+ try {
303+ await fs . access ( dataPath )
304+
305+ fileExists = true
306+ } catch { }
307+
308+ const currentEtag = fileExists ? await BlobsServer . generateETag ( dataPath ) : undefined
309+
310+ if ( ifNoneMatch === '*' && fileExists ) {
311+ return new Response ( null , { status : 412 } )
312+ }
313+
314+ if ( ifMatch && ( ! fileExists || ifMatch !== currentEtag ) ) {
315+ return new Response ( null , { status : 412 } )
316+ }
317+
318+ const metadataHeader = req . headers . get ( METADATA_HEADER_INTERNAL )
319+ const metadata = decodeMetadata ( metadataHeader )
320+
298321 // We can't have multiple requests writing to the same file, which could
299322 // happen if multiple clients try to write to the same key at the same
300323 // time. To prevent this, we write to a temporary file first and then
301324 // atomically move it to its final destination.
302325 const tempPath = join ( tmpdir ( ) , Math . random ( ) . toString ( ) )
303-
304326 const body = await req . arrayBuffer ( )
305327 await fs . writeFile ( tempPath , Buffer . from ( body ) )
306328 await fs . mkdir ( dirname ( dataPath ) , { recursive : true } )
@@ -311,10 +333,18 @@ export class BlobsServer {
311333 await fs . writeFile ( metadataPath , JSON . stringify ( metadata ) )
312334 }
313335
314- return new Response ( null , { status : 200 } )
315- } catch ( error ) {
316- this . logDebug ( 'Error when writing data:' , error )
336+ const newEtag = await BlobsServer . generateETag ( dataPath )
317337
338+ return new Response ( null , {
339+ status : 200 ,
340+ headers : {
341+ etag : newEtag ,
342+ } ,
343+ } )
344+ } catch ( error ) {
345+ if ( isNodeError ( error ) ) {
346+ this . logDebug ( 'Error when writing data:' , error )
347+ }
318348 return new Response ( null , { status : 500 } )
319349 }
320350 }
@@ -356,6 +386,20 @@ export class BlobsServer {
356386 return { dataPath, key : key . join ( '/' ) , metadataPath, rootPath : storePath }
357387 }
358388
389+ /**
390+ * Helper method to generate an ETag for a file based on its path and last modified time.
391+ */
392+ private static async generateETag ( filePath : string ) : Promise < string > {
393+ try {
394+ const stats = await fs . stat ( filePath )
395+ const hash = createHmac ( 'sha256' , stats . mtime . toISOString ( ) ) . update ( filePath ) . digest ( 'hex' )
396+
397+ return `"${ hash } "`
398+ } catch {
399+ return ''
400+ }
401+ }
402+
359403 private async handleRequest ( req : Request ) : Promise < Response > {
360404 if ( ! req . url || ! this . validateAccess ( req ) ) {
361405 return new Response ( null , { status : 403 } )
@@ -502,9 +546,8 @@ export class BlobsServer {
502546
503547 // If the entry is a file, add it to the `blobs` bucket.
504548 if ( ! stat . isDirectory ( ) ) {
505- // We don't support conditional requests in the local server, so we
506- // generate a random ETag for each entry.
507- const etag = Math . random ( ) . toString ( ) . slice ( 2 )
549+ // Generate a deterministic ETag based on file path and last modified time.
550+ const etag = await this . generateETag ( entryPath )
508551
509552 result . blobs ?. push ( {
510553 etag,
0 commit comments