Im Rahmen eines aktuellen Projektes war es notwendig, dass beim Download von Dateien aus dem Browser das Erstellungsdatum und Bearbeitungsdatum erhalten bleiben. Das Standardverhalten von Browsern ist nämlich, beim Download beides auf die aktuelle Zeit zu setzen (einiges an Recherche hat ergeben, dass sich dieses Verhalten beim Download im Browser nicht praktikabel umgehen lässt).

Beispielsweise hier das Titelbild: Gerade erst heruntergeladen, aber sicherlich nicht gerade erst erstellt.
Standard-Metadaten

Ein Workaround war, die Dateien stets in ZIP-Dateien herunterzuladen und das Erstellungs- und Bearbeitungsdatum dort anzupassen. Unglücklicherweise ist in .NET und gängigen Bibliotheken stets nur das LastModifiedAt zur Änderung verfügbar. Scheinbar unterstützt der Standard nur diesen Wert.

Glücklicherweise unterstützt die ZIP-Definition aber ein NTFS-Extra-Feld:

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

Mit diesem lassen sich weitere Werte mitgeben; eben genau die gewünschten Creation time und Last modification time.

Die Umsetzung ist nicht ganz straightforward.

  1. braucht es eine ZIP-Bibliothek die das Schreiben dieses NTFS extra Feldes unterstützt. Hier bin ich auf SharpZipLib gestoßen. Wird zwar seit August 2023 nicht mehr aktualisiert, aber für eine serverseitige Pack-Logik passt es wunderbar.
  2. braucht es den Code, um das Feld zu schreiben:
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();
    }
}

Und voilà: Die Metadaten sind wie gewünscht :-)

Angepasste Metadaten

(Hinweis: "Letzter Zugriff" ändert sich beim Öffnen des Metadaten-Dialogs; vorher passt es ;) )