@@ -189,6 +189,8 @@ export class BlobsServer {
189
189
return new Response ( null , { status : 404 } )
190
190
}
191
191
192
+ this . logDebug ( 'Error when reading data:' , error )
193
+
192
194
return new Response ( null , { status : 500 } )
193
195
}
194
196
}
@@ -291,16 +293,36 @@ export class BlobsServer {
291
293
return new Response ( null , { status : 400 } )
292
294
}
293
295
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' )
296
299
297
300
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
+
298
321
// We can't have multiple requests writing to the same file, which could
299
322
// happen if multiple clients try to write to the same key at the same
300
323
// time. To prevent this, we write to a temporary file first and then
301
324
// atomically move it to its final destination.
302
325
const tempPath = join ( tmpdir ( ) , Math . random ( ) . toString ( ) )
303
-
304
326
const body = await req . arrayBuffer ( )
305
327
await fs . writeFile ( tempPath , Buffer . from ( body ) )
306
328
await fs . mkdir ( dirname ( dataPath ) , { recursive : true } )
@@ -311,10 +333,18 @@ export class BlobsServer {
311
333
await fs . writeFile ( metadataPath , JSON . stringify ( metadata ) )
312
334
}
313
335
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 )
317
337
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
+ }
318
348
return new Response ( null , { status : 500 } )
319
349
}
320
350
}
@@ -356,6 +386,20 @@ export class BlobsServer {
356
386
return { dataPath, key : key . join ( '/' ) , metadataPath, rootPath : storePath }
357
387
}
358
388
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
+
359
403
private async handleRequest ( req : Request ) : Promise < Response > {
360
404
if ( ! req . url || ! this . validateAccess ( req ) ) {
361
405
return new Response ( null , { status : 403 } )
@@ -502,9 +546,8 @@ export class BlobsServer {
502
546
503
547
// If the entry is a file, add it to the `blobs` bucket.
504
548
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 )
508
551
509
552
result . blobs ?. push ( {
510
553
etag,
0 commit comments