From 19d84d9f657dcd5c46f89aca08c52267294921b8 Mon Sep 17 00:00:00 2001 From: ferhat elmas Date: Mon, 16 Feb 2026 14:37:45 +0100 Subject: [PATCH] fix(file-backend): return correct last modified in head object closes #858 Signed-off-by: ferhat elmas --- src/storage/backend/file.ts | 14 ++++----- src/test/file-backend.test.ts | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/storage/backend/file.ts b/src/storage/backend/file.ts index 89d5110f..5a2ff3ff 100644 --- a/src/storage/backend/file.ts +++ b/src/storage/backend/file.ts @@ -89,15 +89,14 @@ export class FileBackend implements StorageBackendAdapter { const eTag = await this.etag(file, data) const fileSize = data.size const { cacheControl, contentType } = await this.getFileMetadata(file) - const lastModified = new Date(0) - lastModified.setUTCMilliseconds(data.mtimeMs) + const lastModified = data.mtime if (headers?.ifNoneMatch && headers.ifNoneMatch === eTag) { return { metadata: { cacheControl: cacheControl || 'no-cache', mimetype: contentType || 'application/octet-stream', - lastModified: lastModified, + lastModified, httpStatusCode: 304, size: data.size, eTag, @@ -115,7 +114,7 @@ export class FileBackend implements StorageBackendAdapter { metadata: { cacheControl: cacheControl || 'no-cache', mimetype: contentType || 'application/octet-stream', - lastModified: lastModified, + lastModified, httpStatusCode: 304, size: data.size, eTag, @@ -155,7 +154,7 @@ export class FileBackend implements StorageBackendAdapter { metadata: { cacheControl: cacheControl || 'no-cache', mimetype: contentType || 'application/octet-stream', - lastModified: lastModified, + lastModified, httpStatusCode: 200, size: data.size, eTag, @@ -319,8 +318,7 @@ export class FileBackend implements StorageBackendAdapter { const data = await fs.stat(file) const { cacheControl, contentType } = await this.getFileMetadata(file) - const lastModified = new Date(0) - lastModified.setUTCMilliseconds(data.mtimeMs) + const lastModified = data.mtime const eTag = await this.etag(file, data) return { @@ -329,7 +327,7 @@ export class FileBackend implements StorageBackendAdapter { cacheControl: cacheControl || 'no-cache', mimetype: contentType || 'application/octet-stream', eTag, - lastModified: data.birthtime, + lastModified, contentLength: data.size, } } diff --git a/src/test/file-backend.test.ts b/src/test/file-backend.test.ts index 88fff428..cc1da895 100644 --- a/src/test/file-backend.test.ts +++ b/src/test/file-backend.test.ts @@ -157,3 +157,60 @@ describe('FileBackend xattr metadata', () => { } }) }) + +describe('FileBackend lastModified', () => { + let tmpDir: string + let backend: FileBackend + let originalStoragePath: string | undefined + let originalFilePath: string | undefined + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'storage-file-backend-')) + originalStoragePath = process.env.STORAGE_FILE_BACKEND_PATH + originalFilePath = process.env.FILE_STORAGE_BACKEND_PATH + process.env.STORAGE_FILE_BACKEND_PATH = tmpDir + process.env.FILE_STORAGE_BACKEND_PATH = tmpDir + getConfig({ reload: true }) + backend = new FileBackend() + }) + + afterEach(async () => { + if (originalStoragePath === undefined) { + delete process.env.STORAGE_FILE_BACKEND_PATH + } else { + process.env.STORAGE_FILE_BACKEND_PATH = originalStoragePath + } + if (originalFilePath === undefined) { + delete process.env.FILE_STORAGE_BACKEND_PATH + } else { + process.env.FILE_STORAGE_BACKEND_PATH = originalFilePath + } + await fs.remove(tmpDir) + }) + + it('headObject/getObject should return mtime as lastModified', async () => { + const bucket = 'test-bucket' + const key = 'test-file.txt' + const version = 'v1' + + await backend.uploadObject( + bucket, + key, + version, + Readable.from('initial content'), + 'text/plain', + 'no-cache' + ) + + const filePath = path.join(tmpDir, withOptionalVersion(`${bucket}/${key}`, version)) + const stat = await fs.stat(filePath) + const knownMtime = new Date(stat.birthtimeMs + 60_000) // mtime must be in the future + await fs.utimes(filePath, knownMtime, knownMtime) + + const headResult = await backend.headObject(bucket, key, version) + expect(headResult.lastModified).toEqual(knownMtime) + + const getResult = await backend.getObject(bucket, key, version) + expect(getResult.metadata.lastModified).toEqual(knownMtime) + }) +})