DEV Community

Cover image for Preserve File Creation and Modification Date When Downloading in a ZIP File
Gabriel Weidmann
Gabriel Weidmann

Posted on

Preserve File Creation and Modification Date When Downloading in a ZIP File

As part of a recent project, it was necessary to ensure that the creation date and modification date of files were preserved when downloading them from the browser. The default behavior of browsers is to set both dates to the current time upon download (some research showed that this behavior can't be easily bypassed when downloading files directly in the browser).

For example, here’s the title image: just downloaded, but definitely not just created.

default metadata

A workaround was to always download the files in ZIP archives and adjust the creation and modification dates there. Unfortunately, in .NET and common libraries, only the LastModifiedAt can be set. It seems the standard only supports this value.

Fortunately, the ZIP specification does support an NTFS extra field:

   4.5.5 -NTFS Extra Field (0x000a):

      The following is the layout of the NTFS attributes 
      "extra" block. (Note: At this time the Mtime, Atime
      and Ctime values MAY be used on any WIN32 system.)  

      Note: all fields stored in Intel low-byte/high-byte order.

        Value      Size       Description
        -----      ----       -----------
(NTFS)  0x000a     2 bytes    Tag for this "extra" block type
        TSize      2 bytes    Size of the total "extra" block
        Reserved   4 bytes    Reserved for future use
        Tag1       2 bytes    NTFS attribute tag value #1
        Size1      2 bytes    Size of attribute #1, in bytes
        (var)      Size1      Attribute #1 data
         .
         .
         .
         TagN       2 bytes    NTFS attribute tag value #N
         SizeN      2 bytes    Size of attribute #N, in bytes
         (var)      SizeN      Attribute #N data

       For NTFS, values for Tag1 through TagN are as follows:
       (currently only one set of attributes is defined for NTFS)

         Tag        Size       Description
         -----      ----       -----------
         0x0001     2 bytes    Tag for attribute #1 
         Size1      2 bytes    Size of attribute #1, in bytes
         Mtime      8 bytes    File last modification time
         Atime      8 bytes    File last access time
         Ctime      8 bytes    File creation time
Enter fullscreen mode Exit fullscreen mode

With this, you can provide additional values—specifically the Creation time and Last modification time you want.

The implementation isn’t completely straightforward.

  1. You need a ZIP library that supports writing this NTFS extra field. I came across SharpZipLib. It hasn’t been updated since August 2023, but it works perfectly fine for a server-side packaging solution.
  2. You need the code to write the field.
var zipMemoryStream = new System.IO.MemoryStream();

using var zipStream = new ZipOutputStream(zipMemoryStream)
{
    IsStreamOwner = false // Prevent from disposing the memory stream
};

var zipEntry = new ZipEntry(myFilename);

byte[] ntfsExtraData = CreateNtfsExtraField(
    creationTime: myDocument.CreatedAt,
    modifiedTime: myDocument.ChangedAt,
    lastAccessTime: myDocument.ChangedAt
);

zipEntry.ExtraData = ntfsExtraData;

zipStream.PutNextEntry(zipEntry);

await fileStream.CopyToAsync(zipStream, ct);

fileStream.Dispose();
...

private static byte[] CreateNtfsExtraField(DateTime creationTime, DateTime modifiedTime, DateTime lastAccessTime)
{
    // NTFS extra field structure:
    // [Header ID (2 bytes)][Data Size (2 bytes)][Reserved (4 bytes)][Tag (2 bytes)][Content Size (2 bytes)][Timestamps (24 bytes)]

    // Constants
    ushort headerId = 0x000A; // NTFS field
    ushort tag = 0x0001;      // Attribute Tag - "Standard information"

    // Time conversion: DateTime -> Windows FILETIME (ticks since 1601-01-01)
    long creationFileTime = creationTime.ToFileTimeUtc();
    long accessFileTime = lastAccessTime.ToFileTimeUtc();
    long modifiedFileTime = modifiedTime.ToFileTimeUtc();

    using (var ms = new MemoryStream())
    using (var writer = new BinaryWriter(ms))
    {
        // Write Header ID and Size Placeholder
        writer.Write(headerId);
        writer.Write((ushort)32); // Data Size: Reserved(4) + Tag(2) + Size(2) + 24 bytes

        writer.Write(0); // Reserved (4 bytes)

        // Attribute Tag
        writer.Write(tag);
        writer.Write((ushort)24); // Content Size (24 bytes for 3 timestamps)

        // Write the timestamps (each 8 bytes)
        writer.Write(modifiedFileTime);
        writer.Write(accessFileTime);
        writer.Write(creationFileTime);

        return ms.ToArray();
    }
}
Enter fullscreen mode Exit fullscreen mode

And voilà: the metadata is preserved as desired :-)

adjusted metadata

(Note: "Last Access" changes when opening the metadata dialog; before that, it’s correct 😉)

Top comments (0)