System.IO.Packaging – vytváření datových “balíčků” dle OPC v .NET 3.5

Open Packaging Conventions (OPC) je standard, který popisuje postupy pro ukládání více datových objektů a jejich vzájemných vztahů v jednom fyzickém kontajneru (souboru). Nejčastější formou datového úložiště je ZIP soubor. Tento archiv obsahuje strukturu souborů a složek a dále pak XML dokumenty, které popisují vztahy mezi těmito soubory a obsahují další metadata. Na specifikaci OPC je založena většina novějších souborových formátů Microsoftu. Například OpenXML, nativní formát souborů v Microsoft Office 2007 (soubory s příponami docx, xlsx, pptx atd.). OPC formát je také základem pro dokumenty ve formátu XPS (XML Papar Specification) nebo úložištěm pro Windows Presentation Foundation (WPF). O pravdivosti mých slov se můžete přesvědčit tak, že DOCX soubor (nejlépe takový, který obsahuje další vložené objekty, jako například obrázky, přejmenujete na ZIP a pak ho otevřete.

Pro práci s “balíčky” z prostředí .NET (verze 3.0 a vyšší) slouží třídy v namespace System.IO.Packaging. Jádrem všeho je abstraktní třída Package, která reprezentuje úložiště, které obsahuje jednotlivé položky, reprezentované třídou PackagePart. Úložištěm pro Package může být principiálně cokoliv – stačí implementovat abstraktní třídu Package a obsloužit podkladové úložiště. Nativní reprezentací balíčku je ale ZIP archiv. Ten je dostupný prostřednictvím třídy ZipPackage.

Rád bych na tomto místě zdůraznil, že popisované třídy nejsou určeny pro obecnou práci se ZIP soubory. Jimi vytvořený ZIP archiv bude kromě vašich dat vždy obsahovat minimálně soubor [Content_Types].xml (a patrně ještě další XML dokumenty ve složkách _rels a package, pokud budete využívat metadata a relace mezi částmi). Pro obecnou práci se ZIP archivy se vám bude spíše hodit specializovaná komponenta. Nepsaným standardem je například volně dostupná knihovna #ZipLib od ICsharpCode. System.IO.Packaging se vám bude hodit v případě, že potřebujete pro vlastní potřebu uchovávat nějaké kompozitní datové struktury. Použitím OPC bez námahy získáte podporu komprese, digitálních podpisů či metadat.

Jedna moje aplikace využívá ke své činnosti několik vzájemně souvisejících XML souborů a XSLT šablon. Pro vytváření záloh těchto dat jsem použil právě OPC formát, který mi umožnil uložit samotné soubory, informace o jejich vzájemném vztahu a metadata – jako například kdo a kdy zálohu vytvořil.

Vytvoření a inicializace OPC balíčku

V první řadě musíte do svého projektu přidat referenci na assembly WindowsBase.dll, čímž se vám zpřístupní namespace System.IO.Packaging.

Třída ZipPackage má metodu Open, pomocí které můžete (v závislosti na předaných argumentech) balíček vytvořit nebo otevřít již existující. Pomocí vlastnosti PackageProperties pak můžete nastavovat metadata společná pro celý balíček – název, typ, autora, datum vytvoření a další. Tato data se interně ukládají do XML souboru /package/services/metadata/core-properties/*.psmdcp.

Následujícím kódem vytvoříte balíček a nastavíte jeho metadata:

// Vytvořit package (pokud existuje, bude přepsána)

Package p = ZipPackage.Open(@"C:\Users\Altair\Downloads\demo.zip", System.IO.FileMode.Create);

 

// Nastavit vlastnosti

p.PackageProperties.Created = DateTime.Now;

p.PackageProperties.Creator = System.Security.Principal.WindowsIdentity.GetCurrent().Name;

p.PackageProperties.ContentType = "Moje Package";

p.PackageProperties.Identifier = Guid.NewGuid().ToString();

Metadata jsou věc užitečná, nicméně nepovinná. Kompletní seznam a popis dostupných vlastností najdete v OPC specifikaci. Zastavím se pouze u dvou obzvláště užitečných.

ContentType je typ obsahu a může obsahovat informaci o použitém formátu dokumentu. Zejména poud pro název souboru použijete generickou příponu ZIP, může se jednat o vítanou indikaci, že soubor je v nějakém vámi definovaném formátu a že tedy můžete očekávat jistou strukturu. Sem můžete zapsat jakýkoliv text, přes shodu názvu se nejedná o MIME typ, jako je například text/html a podobně.

Identifier je jedinečný identifikátor, který můžete balíčku přiřadit. Může se hodit, pokud chcete obsažená data identifikovat i v případě, že uživatel například soubor přejmenuje.

Přidání obsahu

Přidání části (PackagePart) se děje pomocí metody CreatePart, jejíž argumenty jsou:

  • partUri - relativní adresa dané části, v ZIP archivu se jedná o název složky a souboru. Slouží jako hlavní identifikátor části a v rámci jedné package musí být jedinečná.
  • contentType – MIME typ obsahu, např. text/plain, text/html, text/xml, image/jpeg… Pokud typ neznáte, použijte application/octet-stream.
  • compressionOption – nepovinný, určuje úroveň komprese, stanovuje poměr mezi výpočetní náročností zpracování a efektivitou komprimace. Pokud parametr není uveden, data se pouze uloží, nebudou se komprimovat. To je užitečné, pokud ukládáte data, která mají náhodnou charakteristiku, jako například data šifrovaná nebo již jednou komprimovaná.

Metoda vrací instanci třídy PackagePart. Ta má důležitou metodu GetStream, která vrátí stream, jehož prostřednictvím můžete číst a zapisovat data. Neexistuje přímý způsob, jak do balíčku zahrnout existujcíí soubor, musíte ho zkopírovat pomocí streamu.

Následující kód vytvoří balíček se třemi částmi:

  1. Textový soubor jménem /readme.txt. Obsah do něj zapíšeme pomocí TextWriteru.
  2. XML soubor jménem /XmlData/MojeXml.xml. Obsah do něj zapíšeme uložením objektu XmlDocument.
  3. Obrázek jménem /Wallpapers/Vista.jpg. Vytvoříme ho zkopírováním obsahu existujícího souboru. Vzhledem k tomu, že se jedná již o jednou zkomprimovaná data (formát JPG je komprimovaný), používám možnost data pouze uložit a nesnažit se o opětovnou kompresi.

// Vytvořit package (pokud existuje, bude přepsána)

Package p = ZipPackage.Open(@"C:\Users\Altair\Downloads\demo.zip", System.IO.FileMode.Create);

 

// Nastavit vlastnosti

p.PackageProperties.Created = DateTime.Now;

p.PackageProperties.Creator = System.Security.Principal.WindowsIdentity.GetCurrent().Name;

p.PackageProperties.ContentType = "Moje Package";

p.PackageProperties.Identifier = Guid.NewGuid().ToString();

 

// Přidat textový soubor

PackagePart textPart = p.CreatePart(new Uri("/readme.txt", UriKind.Relative), "text/plain", CompressionOption.Maximum);

using (var s = textPart.GetStream())

using (var w = new StreamWriter(s)) {

    for (int i = 0; i < 1000; i++) {

        w.WriteLine("Nějaký obsah textového souboru.");

    }

}

 

// Přidat XML dokument

XmlDocument doc = new XmlDocument();

doc.AppendChild(doc.CreateElement("root"));

PackagePart xmlPart = p.CreatePart(new Uri("/XmlData/MojeXml.xml", UriKind.Relative), "text/xml", CompressionOption.Maximum);

using (var s = xmlPart.GetStream()) {

    doc.Save(s);

}

 

// Přidat existující soubor

PackagePart jpgPart = p.CreatePart(new Uri("/Wallpapers/Vista.jpg", UriKind.Relative), "image/jpeg");

using (var fileStream = File.OpenRead(@"C:\Windows\Web\Wallpaper\img36.jpg"))

using (var partStream = jpgPart.GetStream()) {

    byte[] buffer = new byte[4096];

    int bytesRead = 0;

    while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0) {

        partStream.Write(buffer, 0, bytesRead);

    }

}

 

// Zavřít package

p.Close();

Přečtení obsahu

Pro přečtení obsahu je nutné balíček otevřít a získat jeho jednotlivé části. Buďto můžete použít metodu GetPart, pokud přímo znáte její Uri a nebo metodu GetParts, která vám vrátí kolekci všech částí. Ve druhém případě ale dostanete i systémové části, které jste nevytvořili. Poznáte je podle toho, že jejich content type začíná na application/vnd.openxmlformats-package.

Napsal jsem jednoduchou řádkovou utilitku ShowOPC, která vypíše informace o hlaviččce a všech částech OPC balíčku. Její zdrojový kód je následující:

static void Main(string[] args) {

    if (args.Length != 1) {

        Console.WriteLine("USAGE: showopc filename");

        Environment.Exit(1);

    }

 

    Package p = null;

    try {

        p = Package.Open(args[0], System.IO.FileMode.Open);

        // Display properties

        Console.WriteLine("Category:       {0}", p.PackageProperties.Category);

        Console.WriteLine("ContentStatus:  {0}", p.PackageProperties.ContentStatus);

        Console.WriteLine("ContentType:    {0}", p.PackageProperties.ContentType);

        Console.WriteLine("Created:        {0}", p.PackageProperties.Created);

        Console.WriteLine("Creator:        {0}", p.PackageProperties.Creator);

        Console.WriteLine("Description:    {0}", p.PackageProperties.Description);

        Console.WriteLine("Identifier:     {0}", p.PackageProperties.Identifier);

        Console.WriteLine("Keywords:       {0}", p.PackageProperties.Keywords);

        Console.WriteLine("Language:       {0}", p.PackageProperties.Language);

        Console.WriteLine("LastModifiedBy: {0}", p.PackageProperties.LastModifiedBy);

        Console.WriteLine("LastPrinted:    {0}", p.PackageProperties.LastPrinted);

        Console.WriteLine("Modified:       {0}", p.PackageProperties.Modified);

        Console.WriteLine("Revision:       {0}", p.PackageProperties.Revision);

        Console.WriteLine("Subject:        {0}", p.PackageProperties.Subject);

        Console.WriteLine("Title:          {0}", p.PackageProperties.Title);

        Console.WriteLine("Version:        {0}", p.PackageProperties.Version);

 

        // Display parts

        int i = 0;

        foreach (var part in p.GetParts()) {

            i++;

            Console.WriteLine("Package part {0}:", i);

            Console.WriteLine("  URI:          {0}", part.Uri);

            Console.WriteLine("  Content type: {0}", part.ContentType);

            Console.WriteLine("  Size:         {0:N0} B", part.GetStream().Length);

            Console.WriteLine("  Compression:  {0}", part.CompressionOption);

        }

 

    }

    catch (System.IO.FileNotFoundException) {

        Console.WriteLine("File {0} was not found.", fileName);

    }

    catch (Exception ex) {

        Console.WriteLine("Failed to read file {0}:", fileName);

        Console.WriteLine(ex.Message);

    }

    finally {

        if (p != null) p.Close();

    }

}

Výsledek výše uvedeného kódu pro námi vytvořenou package je následující:

File:            C:\Users\Altair\Downloads\demo.zip

Category:

ContentStatus:

ContentType:     Moje Package

Created:         2.8.2008 15:13:21

Creator:         appaloosa\Altair

Description:

Identifier:      e555d798-c6bc-4c41-9f03-49472fed1b9c

Keywords:

Language:

LastModifiedBy:

LastPrinted:

Modified:

Revision:

Subject:

Title:

Version:

Package part 1:

  URI:          /package/services/metadata/core-properties/d5bce375e53b489b9e8bcdf0b2439a20.psmdcp

  Content type: application/vnd.openxmlformats-package.core-properties+xml

  Size:         530 B

  Compression:  NotCompressed

Package part 2:

  URI:          /readme.txt

  Content type: text/plain

  Size:         36 000 B

  Compression:  Maximum

Package part 3:

  URI:          /Wallpapers/Vista.jpg

  Content type: image/jpeg

  Size:         717 578 B

  Compression:  NotCompressed

Package part 4:

  URI:          /XmlData/MojeXml.xml

  Content type: text/xml

  Size:         8 B

  Compression:  Maximum

Package part 5:

  URI:          /_rels/.rels

  Content type: application/vnd.openxmlformats-package.relationships+xml

  Size:         365 B

  Compression:  NotCompressed

Zde uvedeným způsobem můžete manipulovat s OpenXML dokumenty, XPS dokumenty, případně vytvářet vlastní datové formáty. Dodržení OPC standardu vám umožní snadno řešit otázky ukládání metadat, vzájemných vztahů, digitálního podpisu a další.

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET