H
U S Z A D I K
F E J E Z E T
Fájlműveletek és elszigetelt tárolás Teljes értékű asztali alkalmazás készítésekor fontos, hogy a program képes legyen eltárolni a felhasználói munkamenetek információit. Ez a fejezet a .NET keretrendszer szemszögéből mutat be számos, I/O-val kapcsolatos témakört. Elsőként megismerkedünk a System.IO névtérben definiált alapvető típusokkal, és megvizsgáljuk, hogy hogyan módosíthatjuk programozottan a számítógép mappa- és fájlstruktrúráját, majd a különböző karakter-, bináris, sztring- és memóriaalapú adattárolók olvasásával és írásával foglalkozunk. Miután megismertük a fájlok és könyvtárak kezelését az alapvető I/O típusok segítségével, bemutatjuk az elszigetelt tároló használatát (a System.IO.IsolatedStorage névtéren keresztül). Ennek segítségével a szigorúbb biztonsági környezetben futó alkalmazások a felhasználói és az alkalmazásadatokat biztonságosan, korlátozott fájlműveletekkel hozhatják létre. Ennek az API-nak a bemutatásához először a .NET platform egyik biztonsági megoldását, a CAS-t (Code Access Security, kóderedet-alapú biztonság) kell megvizsgálnunk. A CAS-t gyakran az elszigetelt tárolóval együtt használjuk.
A System.IO névtér A .NET keretrendszerben a fájlalapú (és a memóriaalapú) be- és kiviteli (I/O) szolgáltatásokat biztosító alapvető osztálykönyvtárak helye a System.IO névtér. Mint minden névtér, a System.IO is számos osztályt, interfészt, felsorolt típust, struktúrát és metódusreferenciát határoz meg, amelyek túlnyomó többsége az mscorlib.dll fájlban található. A System.IO névtér további tagjait a System.dll szerelvény definiálja (a Visual Studio 2008 projektek az alapértelmezés szerint mindkét szerelvényre hivatkoznak, így az itt említett típusokat további teendők nélkül, azonnal használhatjuk).
20. fejezet: Fájlműveletek és elszigetelt tárolás
A System.IO névtér több típusa a fizikai mappák és fájlok programozott kezelésére összpontosít. Vannak azonban további típusok, amelyek a sztring- és memóriamanipulációkat támogatják. A 20.1. táblázat bemutatja a System.IO alapvető (nem absztrakt) osztályait. Nem absztrakt I/O osztálytípusok BinaryReader, BinaryWriter
Jelentés Primitív adattípusok (egész számok, logikai értékek, sztringek stb.) bináris értékként történő tárolását és visszaolvasását teszik lehetővé.
BufferedStream
Byte-folyamok ideiglenes tárolójaként szolgál, ezeket a későbbiek során más tárolókra menthetjük.
Directory,
Ezekkel a számítógép könyvtárstruktúráját módosíthatjuk. A Directory típus a funkcionalitást statikus tagok segítségével teszi elérhetővé. A DirectoryInfo típus hasonló működést eredményez az érvényes objektumreferenciából.
DirectoryInfo
DriveInfo
A számítógép meghajtóiról szolgáltat részletes információt.
File, FileInfo
Ezekkel a számítógépen található fájlokat módosíthatjuk. A File típus a funkcionalitást statikus tagok segítségével teszi elérhetővé. A FileInfo típus hasonló működést eredményez az érvényes objektumreferenciából.
FileStream
Véletlen fájlhozzáférést (pl. keresési képességeket) tesz lehetővé, és az adatokat byte-folyamként jeleníti meg.
FileSystemWatcher
Egy megadott könyvtárban található külső fájlok megváltozásának figyelését teszi lehetővé.
MemoryStream
Véletlen hozzáférést biztosít a memóriában tárolt adatfolyamhoz (nem pedig egy fizikai fájlhoz).
Path
Olyan System.String típusú objektumokon hajt végre műveleteket, amelyek platformfüggetlen fájl-, illetve könyvtárelérési utakról tartalmaznak információkat.
StreamWriter,
Ezekkel fájlokban tárolhatunk (illetve azokból visszaolvashatunk) szöveges információkat. A véletlen fájlhozzáférést nem támogatják.
StreamReader
4
A Directory(Info) és File(Info) típusok
Nem absztrakt I/O osztálytípusok StringWriter, StringReader
Jelentés A StreamReader és StreamWriter típusokhoz hasonlóan ezek az osztályok is szöveges információkat kezelnek, háttértárolóik azonban nem fizikai fájlok, hanem sztringpufferek.
20.1. táblázat: A System.IO névtér kulcsfontosságú tagjai
Az imént felsorolt konkrét osztálytípusok mellett a System.IO több felsorolt típust és olyan absztrakt osztályt (Stream, TextReader, TextWriter stb.) is definiál, amelyek egy megosztott polimorf interfészt definiálnak az összes leszármazott számára. A későbbiekben számos ilyen típust bemutatunk.
A Directory(Info) és File(Info) típusok A System.IO névtér négy típus segítségével teszi lehetővé a számítógép könyvtárstruktúrájának és különálló fájljainak a kezelését. Az első két típus, a Directory és a File, különböző statikus tagokon keresztül biztosítja az objektumok létrehozását, törlését, másolását és mozgatását. Az ezekhez szorosan kapcsolódó FileInfo és DirectoryInfo típusok hasonló működést tesznek elérhetővé példányszintű metódusokkal (így ezeket a new kulcsszóval le kell foglalni). A 20.1. ábrán látható, hogy a Directory és File típusok közvetlenül a System.Object osztályt bővítik ki, míg a DirectoryInfo és FileInfo osztályok az absztrakt FileSystemInfo típusból származnak.
20.1. ábra: A File- és Directory-központú típusok
5
20. fejezet: Fájlműveletek és elszigetelt tárolás
Általánosságban elmondhatjuk, hogy a FileInfo és a DirectoryInfo osztályokat célszerűbb használni, ha egy fájlról vagy könyvtárról részletes információkra (pl. létrehozás ideje, írhatóság/olvashatóság stb.) van szükségünk, ugyanis ezek tagjai erősen típusos objektumokat adnak vissza. Ezzel szemben a Directory és a File osztályok tagjai erősen típusos objektumok helyett inkább egyszerű sztringértékekkel térnek vissza.
Az absztrakt FileSystemInfo ősosztály A DirectoryInfo és a FileInfo típusok az absztrakt FileSystemInfo ősosztálytól öröklik számos képességüket. Legnagyobbrészt arra használjuk FileSystemInfo osztály tagjait, hogy lekérdezzük egy fájl vagy egy könyvtár általános tulajdonságait (pl. a létrehozás idejét, s különböző attribútumokat stb). A 20.2. táblázat felsorolja az érdekesebb alaptulajdonságokat. Tulajdonság
Jelentés
Attributes
Visszaadja vagy beállítja az aktuális fájlnak azokat az attribútumait, amelyeket a FileAttributes felsorolás ír le.
CreationTime
Visszaadja vagy beállítja az aktuális fájl vagy könyvtár létrehozásának az idejét.
Exists
Segítségével megállapíthatjuk, hogy egy adott fájl vagy könyvtár létezik-e.
Extension
Visszaadja a fájl kiterjesztését.
FullName
Visszaadja a fájl vagy könyvtár teljes elérési útvonalát.
LastAccessTime
Visszaadja vagy beállítja az aktuális fájl vagy könyvtár legutolsó elérésének az időpontját.
LastWriteTime
Visszaadja vagy beállítja az aktuális fájl vagy könyvtár utolsó módosításának az idejét.
Name
Az aktuális fájl vagy könyvtár nevét adja vissza.
20.2. táblázat: A FileSystemInfo tulajdonságai
A FileSystemInfo osztály definiálja a Delete() metódust is. Ezt a származtatott típusok valósítják meg azért, hogy egy adott fájlt vagy könyvtárat törölni lehessen a merevlemezről. Az attribútumok lekérdezése előtt továbbá meghívhatjuk a Refresh() metódust, amely biztosítja, hogy az aktuális fájlra (vagy könyvtárra) vonatkozó statisztikák ne legyenek elavultak. 6
A DirectoryInfo típus használata
A DirectoryInfo típus használata Az első, I/O műveletekkel kapcsolatos típus, amelyet közelebbről megvizsgálunk, a DirectoryInfo osztály. Tagjainak a segítségével létrehozhatunk, áthelyezhetünk, törölhetünk és kilistázhatunk könyvtárakat és alkönyvtárakat. Az ősosztályától (FileSystemInfo) örökölt funkcionalitáson túl a DirectoryInfo a 20.3. táblázatban feltüntetett kulcsfontosságú tagokkal rendelkezik. Tag
Jelentés
Create(), CreateSubdirectory()
Egy könyvtár (vagy több alkönyvtár) létrehozása a megadott útvonalon.
Delete()
Egy adott könyvtárnak és teljes tartalmának a törlése.
GetDirectories()
Visszaad egy, az aktuális könyvtár összes alkönyvtárának nevét tartalmazó sztringtömböt.
GetFiles()
FileInfo típusok tömbjével tér vissza, amely az adott
könyvtár fájljait tartalmazza. MoveTo()
A könyvtárat és a tartalmát új elérési út alá mozgatja.
Parent
Az adott elérési út szülőkönyvtárát adja vissza.
Root
Megkapja az elérési útvonal gyökérrészét.
20.3. táblázat: A DirectoryInfo típus kulcsfontosságú tagjai
A DirectoryInfo típus használatához az elérési útvonalat kell megadnunk konstruktorparaméterként. Ha az aktuális könyvtárhoz (pl. a futó alkalmazás könyvtárához) szeretnénk hozzáférni, használjuk a „.” jelölést. Az alábbiakban bemutatunk néhány példát: // Az aktuális könyvtárhoz kötés. DirectoryInfo dir1 = new DirectoryInfo("."); // A C:\Windows könyvtárhoz kötés, // szó szerinti sztring használatával. DirectoryInfo dir2 = new DirectoryInfo(@"C:\Windows");
A második példában azt feltételezzük, hogy a konstruktornak átadott útvonal (C:\Windows) már létezik a számítógépen. Ha nem létező könyvtáron próbálunk műveleteket végrehajtani, a rendszer egy System.IO.DirectoryNotFoundException kivételt dob. Így, ha még nem létező könyvtárat adunk meg, először meg kell hívnunk a Create() metódust: 7
20. fejezet: Fájlműveletek és elszigetelt tárolás // Kössük hozzá egy nem létező könyvtárhoz, majd hozzuk létre. DirectoryInfo dir3 = new DirectoryInfo(@"C:\MyCode\Testing"); dir3.Create();
Miután létrehoztuk a DirectoryInfo objektumot, megvizsgálhatjuk a könyvtár tartalmát bármely, a FileSystemInfo típusból örökölt tulajdonsággal. Példaként hozzunk létre egy új, DirectoryApp nevű konzolalkalmazást. A Program osztályt egészítsük ki egy új statikus metódussal, amely létrehoz egy olyan új DirectoryInfo objektumot a C:\Windows könyvtárra leképezve (szükség esetén módosítsuk az útvonalat), amely számos érdekes statisztikát jelenít meg (a kimenet a 20.2. ábrán látható): class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with Directory(Info) *****\n"); ShowWindowsDirectoryInfo(); Console.ReadLine(); } static void ShowWindowsDirectoryInfo() { // A könyvtárinformációk kiírása. DirectoryInfo dir = new DirectoryInfo(@"C:\Windows"); Console.WriteLine("***** Directory Info *****"); Console.WriteLine("FullName: {0}", dir.FullName); Console.WriteLine("Name: {0}", dir.Name); Console.WriteLine("Parent: {0}", dir.Parent); Console.WriteLine("Creation: {0}", dir.CreationTime); Console.WriteLine("Attributes: {0}", dir.Attributes); Console.WriteLine("Root: {0}", dir.Root); Console.WriteLine("**************************\n"); } }
20.2. ábra: Információk megjelenítése a Windows-könyvtárról
8
A DirectoryInfo típus használata
Fájlok listázása a DirectoryInfo típus segítségével A fenti példát kiegészíthetjük azzal, hogy az alapvető információk lekérdezésén túl a DirectoryInfo típus néhány metódusát is használjuk. Először kérdezzünk le adatokat a C:\Windows\Web\Wallpaper könyvtárban található *.jpg-fájlokról a GetFiles() metódussal. Megjegyzés Ha a számítógépen nem található C:\Windows\Web\Wallpaper könyvtár, alakítsuk át megfelelően a kódot, például úgy, hogy a C:\Windows könyvtár *.bmp fájljait olvassa be.
Ez a metódus olyan FileInfo tömbbel tér vissza, amelyek mindegyike egy adott fájlról tartalmaz információkat (a FileInfo típus használatáról részletesen később lesz szó). Vegyük a Program osztály következő, a Main() függvényből meghívott, statikus metódusát: static void DisplayImageFiles() { DirectoryInfo dir = new DirectoryInfo(@"C:\Windows\Web\Wallpaper"); // Az összes *.jpg kiterjesztésű fájl lekérdezése. FileInfo[] imageFiles = dir.GetFiles("*.jpg"); // Mennyit találtunk? Console.WriteLine("Found {0} *.jpg files\n", imageFiles.Length); // Minden fájlról kiírjuk az információkat. foreach (FileInfo f in imageFiles) { Console.WriteLine("***************************"); Console.WriteLine("File name: {0}", f.Name); Console.WriteLine("File size: {0}", f.Length); Console.WriteLine("Creation: {0}", f.CreationTime); Console.WriteLine("Attributes: {0}", f.Attributes); Console.WriteLine("***************************\n"); } }
Ha futtatjuk az alkalmazást, akkor a 20.3. ábrához hasonló listát kell látnunk. (A képek nevei különbözhetnek.)
9
20. fejezet: Fájlműveletek és elszigetelt tárolás
20.3. ábra: Információk képfájlokról
Alkönyvtárak létrehozása a DirectoryInfo segítségével Egy könyvtár szerkezetét a DirectoryInfo.CreateSubdirectory() metódussal programozottan bővíthetjük. Ezzel a metódussal akár egy, akár több egymásba ágyazott alkönyvtárat is létrehozhatunk egyszeres függvényhívással. A következő metódus például kibővíti az alkalmazás telepítési útvonalának könyvtárszerkezetét néhány alkönyvtárral: static void ModifyAppDirectory() { DirectoryInfo dir = new DirectoryInfo("."); // A \MyFolder létrehozása az alkalmazás könyvtárában. dir.CreateSubdirectory("MyFolder"); // A \MyFolder2\Data létrehozása az alkalmazás könyvtárában. dir.CreateSubdirectory(@"MyFolder2\Data"); }
Ha meghívjuk ezt a metódust a Main() függvényből, és Windows Intézővel megnézzük a Windows-könyvtárat, látható, hogy megjelentek az új alkönyvtárak (lásd a 20.4. ábrát). A CreateSubdirectory() metódus visszatérési értékéről annyit kell tudni, hogy sikeres végrehajtás esetén a metódus visszaad egy DirectoryInfo objektumot, amely az újonnan létrehozott könyvtárat jelképezi. Nézzük meg az alábbi példát: 10
A Directory típus használata static void ModifyAppDirectory() { DirectoryInfo dir = new DirectoryInfo("."); // A \MyFolder létrehozása a kezdőkönyvtárban. dir.CreateSubdirectory("MyFolder"); // A visszaadott DirectoryInfo objektum tárolása. DirectoryInfo myDataFolder = dir.CreateSubdirectory(@"MyFolder2\Data"); // A ..\MyFolder2\Data elérési útjának kiírása. Console.WriteLine("New Folder is: {0}", myDataFolder); }
20.4. ábra: Alkönyvtárak létrehozása
A Directory típus használata A következőkben ismerkedjünk meg a Directory típussal. A legtöbb esetben a Directory osztály statikus tagjainak működése megfelel annak a funkcionalitásnak, amelyet a DirectoryInfo példányszintű tagjai nyújtanak. Ne feledjük azonban, hogy a Directory tagjai általában sztringtípusokat adnak vissza, nem pedig erősen típusos FileInfo/DirectoryInfo típusokat. A Directory típus működésének szemléltetésére ez a végső segédfüggvény megjeleníti a számítógépen található összes meghajtó nevét (a Directory.GetLogicalDrives() metódussal), és a Directory.Delete() statikus metódussal letörli a korábban létrehozott \MyFolder és \MyFolder2\Data alkönyvtárakat:
11
20. fejezet: Fájlműveletek és elszigetelt tárolás static void FunWithDirectoryType() { // A számítógép meghajtóinak kilistázása. string[] drives = Directory.GetLogicalDrives(); Console.WriteLine("Here are your drives:"); foreach (string s in drives) Console.WriteLine("--> {0} ", s); // Letöröljük, amit létrehoztunk. Console.WriteLine("Press Enter to delete directories"); Console.ReadLine(); try { Directory.Delete(string.Format(@"{0}\MyFolder", Environment.CurrentDirectory)); // A második paraméterben adhatjuk meg, hogy // az alkönyvtárakat is törölni szeretnénk-e. Directory.Delete(string.Format(@"{0}\MyFolder2", Environment.CurrentDirectory), true); } catch (IOException e) { Console.WriteLine(e.Message); } }
Forráskód A DirectoryApp projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
A DriveInfo osztálytípus használata A
névtérben található a DriveInfo nevű osztály. A Directory.Getmetódushoz hasonlóan a statikus DriveInfo.GetDrives() metódus is a számítógép meghajtóinak a nevét adja vissza, de a Directory. GetLogicalDrives() metódussal ellentétben a DriveInfo számos egyéb részletet is megad a meghajtókról (pl. a meghajtó típusát, a szabad területet, a kötetcímkét stb.). Nézzük a következő, DriveInfoApp nevű új konzolalkalmazásban definiált Program osztályt: System.IO
LogicalDrives()
class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with DriveInfo *****\n");
12
A DriveInfo osztálytípus használata // Információk lekérése minden meghajtóról. DriveInfo[] myDrives = DriveInfo.GetDrives(); // A meghajtók statisztikáinak kiírása. foreach(DriveInfo d in myDrives) { Console.WriteLine("Name: {0}", d.Name); Console.WriteLine("Type: {0}", d.DriveType); // Ellenőrizzük, hogy a meghajtó csatlakoztatva van-e. if (d.IsReady) { Console.WriteLine("Free space: {0}", d.TotalFreeSpace); Console.WriteLine("Format: {0}", d.DriveFormat); Console.WriteLine("Label: {0}", d.VolumeLabel); Console.WriteLine(); } } Console.ReadLine(); } }
A 20.5. ábra egy lehetséges kimenetet mutat.
20.5. ábra: Meghajtók adatainak lekérdezése a DriveInfo használatával
A Directory, a DirectoryInfo és a DriveInfo osztályok alapvető működésének áttekintése után a következőkben megvizsgáljuk, hogyan hozhatunk létre, nyithatunk meg, zárhatunk be és törölhetünk adott könyvtárban lévő fájlokat. Forráskód A DriveInfoApp projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
13
20. fejezet: Fájlműveletek és elszigetelt tárolás
A FileInfo osztály használata Ahogy a DirectoryApp-példában is láttuk, a FileInfo osztály segítségével a számítógépen található fájlokról kérdezhetünk le adatokat (létrehozás ideje, méret, attribútumok stb.), továbbá az osztály segítségével létrehozhatunk, másolhatunk, áthelyezhetünk és törölhetünk fájlokat. A FileSystemInfo típusból örökölt funkcionalitáson túl a FileInfo osztály néhány egyedi alaptagját a 20.4. táblázatban foglaltuk össze. Tag
Jelentés
AppendText()
Egy StreamWriter típust hoz létre (lásd később), amelynek segítségével szöveget fűzhetünk a fájlhoz.
CopyTo()
A meglévő fájlt átmásolja egy új fájlba.
Create()
Új fájlt hoz létre, és egy FileStream típussal (lásd később) tér vissza, amellyel az újonnan létrehozott fájlt kezelhetjük.
CreateText()
Egy StreamWriter típust hoz létre, amely új szövegfájlt ír.
Delete()
Törli a FileInfo példány által reprezentált állományt.
Directory
A szülőkönyvtár egy példányát adja vissza.
DirectoryName
Visszaadja a szülőkönyvtár teljes elérési útját.
Length
Visszaadja az aktuális fájl vagy könyvtár méretét.
MoveTo()
A fájlt áthelyezi, és lehetőséget ad arra, hogy új nevet adjunk neki.
Name
Visszaadja a fájl nevét.
Open()
Megnyitja a fájlt különböző olvasási/írási és megosztási jogosultságokkal.
OpenRead()
Csak olvasható FileStream típust hoz létre.
OpenText()
Egy StreamReader típust hoz létre (lásd később), amellyel egy létező szövegfájlból olvashatunk.
OpenWrite()
Csak írható FileStream típust hoz létre.
20.4. táblázat: FileInfo alapvető tagjai
14
A FileInfo osztály használata
A FileInfo osztály metódusainak többsége tehát egy specifikus I/O-központú objektumot (FileStream, StreamWriter stb.) ad vissza. Ezek segítségével a fájlból többféleképpen olvashatunk, illetve írhatunk adatokat. A példák tanulmányozása előtt azonban vizsgáljuk meg azokat a különböző módszereket, amelyekkel a FileInfo osztállyal megszerezhetjük a fájlazonosítóját.
A FileInfo.Create() metódus A fájlazonosító létrehozásának első módja a nálata:
FileInfo.Create()
metódus hasz-
static void Main(string[] args) { // Létrehozunk egy új fájlt a C meghajtón. FileInfo f = new FileInfo(@"C:\Test.dat"); FileStream fs = f.Create(); // Használjuk a FileStream objektumot... // Zárjuk le a fájlfolyamot. fs.Close(); }
A FileInfo.Create() metódus FileStream típussal tér vissza, amely a fájlon mind szinkron, mind aszinkron írási/olvasási műveleteket tesz lehetővé (a részleteket lásd később). A FileInfo.Create() által visszaadott FileStream objektum minden felhasználó számára teljes olvasási/írási hozzáférést biztosít. Miután befejeztük a FileStream objektummal a munkát, zárjuk le a fájlazonosítót, hiszen így a rendszer felszabadítja az adatfolyam nem felügyelt erőforrásait. Minthogy a FileStream megvalósítja az IDisposable interfészt, a C# using blokkjának a segítségével a fordítóra bízhatjuk a „takarító”-logika létrehozását (a részleteket lásd az első kötet 8. fejezetében): static void Main(string[] args) { // Egy ’using’ hatókör definiálása // ideális a fájl I/O típusokhoz. FileInfo f = new FileInfo(@"C:\Test.dat"); using (FileStream fs = f.Create()) { // Használjuk a FileStream objektumot... } }
15
20. fejezet: Fájlműveletek és elszigetelt tárolás
A FileInfo.Open() metódus A FileInfo.Open() metódust a FileInfo.Create() által nyújtott lehetőségeknél precízebben használhatjuk a fájlok megnyitására és létrehozására, ugyanis az Open() jellemzően több paramétert vesz fel a fájl kezelésének a leírására. Az Open() meghívása után egy FileStream objektumot kapunk vissza. Nézzük meg a következő logikát: static void Main(string[] args) { // Létrehozunk egy új fájlt a FileInfo.Open() metódussal. FileInfo f2 = new FileInfo(@"C:\Test2.dat"); using(FileStream fs2 = f2.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { // Használjuk a FileStream objektumot... } }
A túlterhelt Open() metódus ezen verziójának három paraméterre van szüksége. Az első paraméter az I/O-kérés típusát határozza meg (pl. új fájl létrehozása, létező fájl megnyitása, hozzáfűzés fájlhoz stb.), amelyet a FileMode felsorolt típussal határozhatunk meg (lásd 20.5. táblázat): public enum FileMode { CreateNew, Create, Open, OpenOrCreate, Truncate, Append }
Tag
Jelentés
CreateNew
Az operációs rendszert új fájl létrehozására utasítja. Ha a fájl már létezik, IOException kivételt dob.
Create
Az operációs rendszert új fájl létrehozására utasítja. Ha a fájl már létezik, akkor felülírja azt.
Open
Megnyit egy már létező fájlt. Ha a fájl nem létezik, FileNotFoundException kivételt dob.
OpenOrCreate
Megnyitja a fájlt, ha az már létezik; ellenkező esetben új fájlt hoz létre.
16
A FileInfo osztály használata
Tag
Jelentés
Truncate
Megnyitja és 0 byte méretűre vágja le a fájlt.
Append
Megnyitja a fájlt, a végére pozicionál, és írási műveletet kezd meg (ez a kapcsoló kizárólag csak írható folyamatokkal használható). Ha a fájl nem létezik, új fájlt hoz létre.
20.5. táblázat: A FileMode felsorolt típus tagjai
A második paraméter a FileAccess felsorolt típus egy olyan értéke, amellyel a folyam olvasási/írási viselkedését határozhatjuk meg: public enum FileAccess { Read, Write, ReadWrite }
Végül a harmadik paraméter (FileShare) azt határozza meg, hogy milyen módon osztjuk meg a fájlt más fájlkezelőkkel. Az alapnevek a következők: public enum FileShare { None, Read, Write, ReadWrite }
A FileInfo.OpenRead() és a FileInfo.OpenWrite() metódusok Habár a FileInfo.Open() metódussal rendkívül rugalmasan kezelhetők a fájlazonosítók, a FileInfo osztály további, OpenRead() és OpenWrite() nevű metódusokat is tartalmaz. Ezek megfelelően konfigurált írásvédett, illetve csak írható FileStream típusokat adnak vissza anélkül, hogy különféle felsorolásértékeket kellene ehhez megadnunk. A FileInfo.Create() és a FileInfo.Open() metódusokhoz hasonlóan az OpenRead() és az OpenWrite() is FileStream objektummal tér vissza (a következő kódban feltételezzük, hogy a C meghajtón létezik a Test3.dat és Test4.dat nevű fájl):
17
20. fejezet: Fájlműveletek és elszigetelt tárolás static void Main(string[] args) { // Írásvédett FileStream objektum megszerzése. FileInfo f3 = new FileInfo(@"C:\Test3.dat"); using(FileStream readOnlyStream = f3.OpenRead()) { // Használjuk a FileStream objektumot... } // Most pedig a csak írható FileStream objektumot kapjuk vissza. FileInfo f4 = new FileInfo(@"C:\Test4.dat"); using(FileStream writeOnlyStream = f4.OpenWrite()) { // Használjuk a FileStream objektumot... } }
A FileInfo.OpenText() metódus Az OpenText() a FileInfo típus egy másik, a fájlmegnyitással kapcsolatos tagja. A Create(), az Open(), az OpenRead() és az OpenWrite() metódusokkal szemben az OpenText() metódus a StreamReader típus egy példányát adja vissza a FileStream típus helyett. Feltételezve, hogy a C meghajtón található egy boot.ini nevű fájl, annak tartalmát a következőképpen olvashatjuk ki: static void Main(string[] args) { // Visszakapunk egy StreamReader objektumot. FileInfo f5 = new FileInfo(@"C:\boot.ini"); using(StreamReader sreader = f5.OpenText()) { // Használjuk a StreamReader objektumot... } }
A StreamReader típus segítségével karakteradatokat olvashatunk a mögöttes fájlból.
A FileInfo.CreateText() és a FileInfo.AppendText() metódusok A két utolsó érdekes metódus a CreateText() és az StreamWriter referenciát adnak vissza:
18
AppendText(),
amelyek
A File típus használata static void Main(string[] args) { FileInfo f6 = new FileInfo(@"C:\Test5.txt"); using(StreamWriter swriter = f6.CreateText()) { // Használjuk a StreamWriter objektumot... } FileInfo f7 = new FileInfo(@"C:\FinalTest.txt"); using(StreamWriter swriterAppend = f7.AppendText()) { // Használjuk a StreamWriter objektumot... } }
A StreamWriter típussal karakteradatokat írhatunk a mögöttes fájlba.
A File típus használata A File típus a FileInfo osztályhoz hasonló funkcionalitást biztosít a statikus tagokon keresztül. Ugyanúgy, mint a FileInfo típusban, a File típusban is megtalálhatók az AppendText(), a Create(), a CreateText(), az Open(), az OpenRead(), az OpenWrite() és az OpenText() metódusok. Sok esetben a File és a FileInfo típusok egymással felcserélhetők. Az előző, a FileStream használatát bemutató példákat a File típus alkalmazásával például egyszerűbbé tehetjük: static void Main(string[] args) { // FileStream objektum megszerzése a File.Create() metódus révén. using(FileStream fs = File.Create(@"C:\Test.dat")) { } // FileStream objektum megszerzése a File.Open() metódus révén. using(FileStream fs2 = File.Open(@"C:\Test2.dat", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { } // Írásvédett FileStream objektum megszerzése. using(FileStream readOnlyStream = File.OpenRead(@"Test3.dat")) { }
19
20. fejezet: Fájlműveletek és elszigetelt tárolás // Csak írható FileStream objektum megszerzése. using(FileStream writeOnlyStream = File.OpenWrite(@"Test4.dat")) { } // Visszakapunk egy StreamReader objektumot. using(StreamReader sreader = File.OpenText(@"C:\boot.ini")) { } // Néhány StreamWriter megszerzése. using(StreamWriter swriter = File.CreateText(@"C:\Test3.txt")) { } using(StreamWriter swriterAppend = File.AppendText(@"C:\FinalTest.txt")) { } }
További fájlközpontú tagok A File típus rendelkezik néhány egyedi taggal is (lásd 20.6. táblázat), amelyek jelentősen megkönnyítik a szöveges adatok olvasását és írását. Metódus
Jelentés
ReadAllBytes()
Megnyitja a megadott fájlt, visszaadja a bináris adatokat bytetömbként, majd lezárja a fájlt.
ReadAllLines()
Megnyitja a megadott fájlt, visszaadja a karakteradatokat sztringtömbként, majd lezárja a fájlt.
ReadAllText()
Megnyitja a megadott fájlt, visszaadja a karakteradatokat System.String típusként, majd lezárja a fájlt.
WriteAllBytes()
Megnyitja a megadott fájlt, kiírja a byte-tömböt, majd lezárja a fájlt.
WriteAllLines()
Megnyitja a megadott fájlt, kiírja a sztringtömböt, majd lezárja a fájlt.
WriteAllText()
Megnyitja a megadott fájlt, kiírja a karakteradatokat, majd lezárja a fájlt.
20.6. táblázat: A File típus metódusai
20
A File típus használata
A File típus új metódusaival, néhány sornyi kóddal olvashatunk vagy írhatunk adatcsomagokat. Sőt mi több, ezek a tagok a művelet végén automatikusan le is zárják a mögöttes fájlazonosítót. A következő konzolalkalmazás (SimpleFileIO) például a lehető legkevesebb munkával menti a sztringadatokat egy új fájlba a C meghajtón (majd onnan beolvassa memóriába): using System; using System.IO; class Program { static void Main(string[] args) { Console.WriteLine("***** Simple IO with the File Type *****\n"); string[] myTasks = { "Fix bathroom sink", "Call Dave", "Call Mom and Dad", "Play Xbox 360"}; // Az összes adatot kiírjuk egy fájlba a C meghajtón. File.WriteAllLines(@"C:\tasks.txt", myTasks); // Visszaolvassuk az egészet, és kiírjuk a képernyőre. foreach (string task in File.ReadAllLines(@"C:\tasks.txt")) { Console.WriteLine("TODO: {0}", task); } Console.ReadLine(); } }
Ha gyorsan szeretnénk fájlazonosítóhoz jutni, a File típus egyértelműen megkönnyíti a dolgunkat. Ha pedig előbb létrehozunk egy FileInfo objektumot, ennek megvan az az előnye, hogy megvizsgálhatjuk a fájlt az absztrakt FileSystemInfo alaposztály tagjainak a segítségével. Forráskód A SimpleFileIO projekt megtalálható a 20. fejezet alkönyvtárában. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
21
20. fejezet: Fájlműveletek és elszigetelt tárolás
Az absztrakt Stream osztály Eddig a FileStream, a StreamReader és a StreamWriter objektumokat még nem használtuk fájlok adatainak írására vagy olvasására. Hogy megértsük ennek menetét, meg kell ismernünk az adatfolyam koncepcióját. Az I/O kezelésében az adatfolyam olyan adattöredéket jelképez, amely a forrás és a cél között folyik. Az adatfolyamokkal egyformán kezelhetünk egy byte-sorozatot attól függetlenül, hogy milyen eszköz (fájl, hálózati kapcsolat, nyomtató stb.) tárolja vagy jeleníti meg azt. Az absztrakt System.IO.Stream osztály több tagot definiál, amelyek szinkron és aszinkron adatkezelést tesznek lehetővé a tárolóeszközön (pl. mögöttes fájl- vagy memóriahelyen). A 20.6. ábra a Stream típus különböző leszármazottait mutatja a Visual Studio 2008 objektumböngészőjében.
20.6. ábra: A Streamből származó típusok
Megjegyzés Az adatfolyamok segítségével nem csak fájlokat kezelhetünk. A .NET-könyvtárak adatfolyam-alapú hozzáférést biztosítanak a hálózatokhoz, a memóriaterületekhez és az egyéb adatfolyam-alapú absztrakciókhoz.
A Stream osztály leszármazottai tehát az adatokat nyers byte-folyamként jelenítik meg; így gyakran elég nehézkes a nyers adatfolyamokkal dolgozni. Bizonyos Stream-leszármazottak támogatják a keresést, és ez azt jelenti, hogy lekérdezhetjük vagy beállíthatjuk az adatfolyamban lévő pozíciót.
22
Az absztrakt Stream osztály
A Stream osztály által nyújtott funkcionalitás megértéséhez nézzük meg annak alapvető tagjait a 20.7. táblázatban. Tag
Jelentés
CanRead,
Megadja, hogy az aktuális adatfolyam támogatja-e az olvasást, a keresést és/vagy az írást.
CanWrite, CanSeek Close()
Lezárja az aktuális adatfolyamot, és felszabadít minden, az adatfolyammal kapcsolatos erőforrást (pl. csatlakozópontokat és fájlazonosítókat). Belsőleg ez tulajdonképpen a Dispose() metódus álneve; vagyis az „adatfolyam lezárása” funkcionálisan megegyezik az „adatfolyam eldobásával”.
Flush()
Frissíti a mögöttes adatforrást vagy adattárat a puffer jelenlegi állapotával, majd törli a puffert. Ha egy adatfolyamhoz nem tartozik puffer, ez a metódus nem csinál semmit.
Length
Byte-ban visszaadja az adatfolyam hosszát.
Position
Meghatározza a pozíciót az aktuális adatfolyamban.
Read(), ReadByte()
Kiolvas egy byte-sorozatot (vagy egyetlen byte-ot) az adatfolyamból, és az olvasott byte-ok számával előrébb lépteti az aktuális pozíciót az adatfolyamban.
Seek()
Pozicionál az aktuális adatfolyamban.
SetLength()
Beállítja az aktuális adatfolyam hosszát.
Write(),
Kiír egy byte-sorozatot (vagy egyetlen byte-ot) az adatfolyamba, és a kiírt byte-ok számával előrébb lépteti az aktuális pozíciót az adatfolyamban.
WriteByte()
20.7. táblázat: A Stream absztrakt tagjai
A FileStream típusok használata A FileStream osztály úgy valósítja meg az absztrakt Stream típus tagjait, ahogy az a fájlalapú folyamok számára a legmegfelelőbb. Ez egy elég egyszerű adatfolyam; csak egy byte-ot vagy byte-ok tömbjét tudja olvasni. A valóságban közvetlenül nem túlságosan gyakran használjuk a FileStream típus tagjait. Ehelyett inkább különböző adatfolyam-burkolókat alkalmazunk, amelyek megkönnyítik a szöveges adatokkal vagy a .NET-típusokkal végzett munkát. Ennek ellenére példaként nézzük meg a FileStream típus szinkron olvasási/írási lehetőségeit. 23
20. fejezet: Fájlműveletek és elszigetelt tárolás
Tételezzük fel, hogy van egy új, FileStreamApp nevű konzolalkalmazásunk. A célunk az, hogy egy egyszerű szöveges üzenetet írjuk a myMessage.dat nevű új fájlba. Mivel a FileStream csak nyers byte-okkal tud dolgozni, a System.String típusú adatot előbb egy megfelelő byte-tömbbé kell alakítani. A System.Text névtér egy Encoding nevű típust definiál, amelynek tagjai sztringeket tudnak bytetömbbe kódolni vagy abból dekódolni (az Encoding típus részletes leírását a .NET Framework 3.5 SDK dokumentációja tartalmazza). A kódolás végeztével a byte-tömböt a FileStream.Write() metódussal írhatjuk ki a fájlba. Ha a byte-okat vissza szeretnénk olvasni a memóriába, először alaphelyzetbe kell állítanunk az adatfolyam belső pozícióját (a Position tulajdonság segítségével), majd meg kell hívnunk a ReadByte() metódust. Végül megjelenítjük a konzolon a nyers byte-tömböt és a dekódolt sztringet. A teljes Main() metódus a következő: // Ne felejtsük el importálni a System.Text és // a System.IO névtereket. static void Main(string[] args) { Console.WriteLine("***** Fun with FileStreams *****\n"); // A FileStream objektum megszerzése. using(FileStream fStream = File.Open(@"C:\myMessage.dat", FileMode.Create)) { // A sztring byte-tömbbe történő kódolása. string msg = "Hello!"; byte[] msgAsByteArray = Encoding.Default.GetBytes(msg); // A byte[] fájlba írása. fStream.Write(msgAsByteArray, 0, msgAsByteArray.Length); // Az adatfolyam belső pozíciójának alaphelyzetbe állítása. fStream.Position = 0; // A típusok kiolvasása a fájlból és megjelenítésük a konzolon. Console.Write("Your message as an array of bytes: "); byte[] bytesFromFile = new byte[msgAsByteArray.Length]; for (int i = 0; i < msgAsByteArray.Length; i++) { bytesFromFile[i] = (byte)fStream.ReadByte(); Console.Write(bytesFromFile[i]); } // Dekódolt üzenetek megjelenítése. Console.Write("\nDecoded Message: "); Console.WriteLine(Encoding.Default.GetString(bytesFromFile)); } Console.ReadLine(); } 24
A StreamWriter és StreamReader típusok használata
Bár a fenti példa valóban feltölti a fájlt adatokkal, kiderül a FileStream típus közvetlen használatának legnagyobb hátránya is: nyers byte-okkal kell dolgoznunk. Más Stream-leszármazott típusok is hasonlóképpen működnek. Ha például egy byte-sorozatot szeretnénk egy adott memóriaterületre kiírni, lefoglalhatunk egy MemoryStream objektumot. Hasonlóképp, ha egy byte-tömböt egy hálózati kapcsolaton keresztül kell továbbítanunk, a NetworkStream típus lesz a segítségünkre. A System.IO névtér tartalmaz számos „olvasó” és „író” típust, amelyek magukba foglalják a Streamből származó típusok működésének részleteit. Forráskód A FileStreamApp projekt megtalálható a 20. fejezet alkönyvtárában. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
A StreamWriter és StreamReader típusok használata A StreamWriter és StreamReader osztályok segítségével karakteralapú adatokat (pl. sztringeket) írhatunk vagy olvashatunk. Az alapértelmezés szerint mindkét típus Unicode-karakterekkel dolgozik, de ezt megváltoztathatjuk, ha megadunk egy megfelelően konfigurált System.Text.Encoding objektumreferenciát. Az egyszerűség kedvéért tegyük fel, hogy az alapértelmezett Unicode-kódolás megfelelő. A StreamReader az absztrakt TextReader típusból származik ugyanúgy, mint a kapcsolódó StringReader típus (erről a későbbiekben még lesz szó). A TextReader alaposztály nagyon korlátozott funkcionalitást tesz lehetővé a leszármazottainak, különösen a karakterfolyam olvasásában és megtekintésében. A StreamWriter típus (a StringWriterhez hasonlóan, ezt lásd később) az absztrakt TextWriter ősosztályból származik. Ez az osztály olyan tagokat definiál, amelyekkel a származtatott típusok szöveges adatot írhatnak egy megadott karakterfolyamba. A StreamWriter és a StringWriter osztályok jobb megértésének érdekében a 20.8. táblázat összefoglalja a TextWriter ősosztály legfontosabb tagjait.
25
20. fejezet: Fájlműveletek és elszigetelt tárolás
Tag
Jelentés
Close()
A metódus lezárja az írót, és felszabadít minden kapcsolódó erőforrást. A folyamat során a puffer automatikusan kiürül (ez a tag funkcionálisan megegyezik a Dispose() metódus meghívásával).
Flush()
Ez a metódus kiüríti a puffereket az aktuális íróból, és minden pufferelt adatot kiír a mögöttes eszközre, de nem zárja le az írót.
NewLine
Ez a tulajdonság jelöli a származtatott író osztály újsor-konstansát. A Windows operációs rendszer alapértelmezett sorvégjelzője a „kocsivissza”, amelyet soremelés követ (\r\n).
Write()
Ez a túlterhelt metódus adatokat ír ki a szövegfolyamba újsorkonstans nélkül.
WriteLine()
Ez a túlterhelt metódus adatokat ír ki a szövegfolyamba újsorkonstanssal.
20.8. táblázat: A TextWriter alapvető tagjai
Megjegyzés A TextWriter osztály utolsó két tagja ismerős lehet. A System.Console típusnak is van Write() és WriteLine() tagja, amelyek az alapértelmezett kimeneti eszközre írnak szöveges adatot. Valójában a Console.In tulajdonság egy TextWriter, a Console.Out tulajdonság pedig egy TextReader objektum.
A származtatott StreamWriter osztály megfelelő Write(), Close() és Flush() metódusokat valósít meg, emellett egy AutoFlush tulajdonsággal is rendelkezik. Ha ezt a tulajdonságot igaz értékre állítjuk, akkor arra kényszeríti a StreamWriter típust, hogy kiürítsen minden adatot az írási műveletek után. Jobb teljesítményt érhetünk el, ha az AutoFlush tulajdonságot nem kapcsoljuk be, ugyanis ez esetben meghívjuk a Close() metódust, miután befejeztük az írást a StreamWriter típussal.
Szövegfájl írása A
típus működésének tanulmányozásához készítsünk egy új, StreamWriterReaderApp nevű konzolalkalmazást. A következő Main() metódus létrehoz egy reminders.txt nevű fájlt a File.CreateText() metódus használatával. A visszakapott StreamWriter objektummal szöveges adatot írunk ki az új fájlba:
26
StreamWriter
A StreamWriter és StreamReader típusok használata static void Main(string[] args) { Console.WriteLine("***** Fun with StreamWriter / StreamReader *****\n"); // Megkapjuk a StreamWritert, majd kiírjuk a sztringadatot. using(StreamWriter writer = File.CreateText("reminders.txt")) { writer.WriteLine("Don't forget Mother's Day this year..."); writer.WriteLine("Don't forget Father's Day this year..."); writer.WriteLine("Don't forget these numbers:"); for(int i = 0; i < 10; i++) writer.Write(i + " "); // Új sort szúrunk be. writer.Write(writer.NewLine); } Console.WriteLine("Created file and wrote some thoughts..."); Console.ReadLine(); }
Ha lefuttatjuk a programot, megvizsgálhatjuk az új fájl tartalmát (lásd 20.7. ábra). A fájl az aktuális alkalmazás bin\Debug mappájában található, feltéve, ha nem abszolút elérési útvonalat adtunk meg a CreateText() függvénynek.
20.7. ábra: A *.txt fájl tartalma
Olvasás szövegfájlból A következőkben megnézzük, hogyan olvashatunk programozottan adatot fájlból a megfelelő StreamReader típus segítségével. Ez az osztály az absztrakt TextReader osztályból származik, amely a 20.9. táblázatban összegzett funkcionalitást nyújtja.
27
20. fejezet: Fájlműveletek és elszigetelt tárolás
Tag
Jelentés
Peek ()
Visszaadja a következő elérhető karaktert anélkül, hogy az olvasó pozícióját módosítaná. Ha -1 értékkel tér vissza, az azt jelenti, hogy az adatfolyam végére értünk.
Read ()
Adatot olvas a bemeneti adatfolyamból.
ReadBlock()
Legfeljebb annyi karaktert olvas be az aktuális adatfolyamból, amennyit a count paraméterben megadunk, az adatokat pedig kiírja a pufferbe a megadott indextől kezdve.
ReadLine()
Beolvas egysornyi karaktert az aktuális adatfolyamból, és sztringként adja vissza az adatot (null sztringgel tér vissza, ha a fájl végén vagyunk).
ReadToEnd()
Beolvassa az adatfolyamban található valamennyi karaktert az aktuális pozíciótól kezdve, és egyetlen sztringként adja vissza őket.
20.9. táblázat: A TextReader alapvető tagjai
Ha úgy módosítjuk a MyStreamWriterReader osztályt, hogy az a StreamReadert használja, akkor az alábbiak szerint olvashatjuk be a reminders.txt fájl szöveges tartalmát: static void Main(string[] args) { Console.WriteLine("***** Fun with StreamWriter / StreamReader *****\n"); ... // Most adatokat olvasunk a fájlból. Console.WriteLine("Here are your thoughts:\n"); using(StreamReader sr = File.OpenText("reminders.txt")) { string input = null; while ((input = sr.ReadLine()) != null) { Console.WriteLine (input); } } Console.ReadLine(); }
Miután lefuttatjuk a programot, a reminders.txt fájl tartalmát látjuk a konzolon.
28
A StringWriter és StringReader típusok használata
A StreamWriter/StreamReader típusok közvetlen létrehozása A System.IO névtér típusait használva zavaró lehet, hogy ugyanazt az eredményt gyakran többféle megközelítéssel is elérhetjük. Megszerezhetünk például egy StreamWriter objektumot a File vagy a FileInfo révén, a CreateText() metódussal. Van azonban még egy módszer, amelyet a StreamWriter és a StreamReader típusokkal használhatunk: ha közvetlenül hozzuk létre őket. Az aktuális alkalmazást például a következőképpen is módosíthatjuk: static void Main(string[] args) { Console.WriteLine("***** Fun with StreamWriter / StreamReader *****\n"); // Megkapunk egy StreamWritert, és kiírjuk a sztringadatot. using(StreamWriter writer = new StreamWriter("reminders.txt")) { ... } // Most adatokat olvasunk a fájlból. using(StreamReader sr = new StreamReader("reminders.txt")) { ... } }
Bár zavaró lehet, hogy a fájl I/O-műveleteit ennyiféleképpen végrehajthatjuk, ám ezek mind a nagyobb rugalmasságot biztosítják. A következőkben a StringWriter és a StringReader osztályok szerepét vizsgáljuk meg. Forráskód A StreamWriterReaderApp projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
A StringWriter és StringReader típusok használata A StringWriter és a StringReader típusok segítségével a szöveges információt memóriában tárolt karakterek folyamaként kezelhetjük. Ez akkor lehet hasznos, ha egy mögöttes pufferhez karakteralapú információt kell hozzáfűz29
20. fejezet: Fájlműveletek és elszigetelt tárolás
nünk. Vegyük példaként azt, amikor a StringReaderWriterApp nevű konzolalkalmazás a helyi merevlemezen lévő fájl helyett egy StringWriter objektumba ír sztringadatblokkot. static void Main(string[] args) { Console.WriteLine("***** Fun with StringWriter / StringReader *****\n"); // Létrehozzuk a StringWritert, és memóriába írjuk // a karakteradatot. using(StringWriter strWriter = new StringWriter()) { strWriter.WriteLine("Don't forget Mother's Day this year..."); // Megkapjuk a tartalom másolatát (sztringben tárolva), // és kiírjuk a konzolra. Console.WriteLine("Contents of StringWriter:\n{0}", strWriter); } Console.ReadLine(); }
Mivel a StringWriter és a StreamWriter ugyanabból az ősosztályból (TextWriter) származik, az írás logikája többé-kevésbé megegyezik. A StringWriter osztályból azonban – természeténél fogva – kinyerhető egy System. Text.StringBuilder objektum, amelyet a GetStringBuilder() metódussal kérhetünk le: using (StringWriter strWriter = new StringWriter()) { strWriter.WriteLine("Don't forget Mother's Day this year..."); Console.WriteLine("Contents of StringWriter:\n{0}", strWriter); // Megkapjuk a belső StringBuildert. StringBuilder sb = strWriter.GetStringBuilder(); sb.Insert(0, "Hey!! "); Console.WriteLine("-> {0}", sb.ToString()); sb.Remove(0, "Hey!! ".Length); Console.WriteLine("-> {0}", sb.ToString()); }
Ha egy szöveges adatfolyamból szeretnénk olvasni, használjuk a megfelelő StringReader típust, amely (ahogy az várható) a kapcsolódó StreamReader osztályhoz hasonlóan működik. A StringReader osztály tulajdonképpen nem tesz mást, mint felüldefiniálja az örökölt tagokat, hogy azok ne fájlból, hanem szöveges adatból olvassanak:
30
A BinaryWriter és BinaryReader osztályok használata using (StringWriter strWriter = new StringWriter()) { strWriter.WriteLine("Don't forget Mother's Day this year..."); Console.WriteLine("Contents of StringWriter:\n{0}", strWriter); // Kiolvassuk az adatot a StringWriter-ből. using (StringReader strReader = new StringReader(strWriter.ToString())) { string input = null; while ((input = strReader.ReadLine()) != null) { Console.WriteLine(input); } } }
Forráskód A StringReaderWriterApp projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
A BinaryWriter és BinaryReader osztályok használata Végezetül megvizsgálunk még két írással/olvasással kapcsolatos osztályt, a BinaryReadert és a BinaryWritert. Mindkettő közvetlenül a System.Object osztályból származik. Ezekkel a típusokkal különálló adattípusokat írhatunk és olvashatunk tömör bináris formátumban a mögöttes adatfolyamból. A BinaryWriter osztály egy többszörösen túlterhelt Write() metódust határoz meg, amellyel egy adattípust írhatunk a mögöttes adatfolyamba. A Write() metódus mellett a BinaryWriter további tagokkal is rendelkezik, ezek segítségével lekérdezhetjük vagy beállíthatjuk a Streamből származó típust, továbbá a BinaryWriter véletlen elérést biztosít az adathoz (lásd 20.10. táblázat). Tag
Jelentés
BaseStream
Ez az írásvédett tulajdonság hozzáférést biztosít a BinaryWriter objektummal használt mögöttes adatfolyamhoz.
Close()
Ez a metódus lezárja a bináris adatfolyamot.
Flush()
Ez a metódus kiüríti a bináris adatfolyam pufferét.
31
20. fejezet: Fájlműveletek és elszigetelt tárolás
Tag
Jelentés
Seek()
Ez a metódus beállítja a pozíciót az aktuális adatfolyamban.
Write()
Ez a metódus kiír egy értéket az aktuális adatfolyamba.
20.10. táblázat: A BinaryWriter legfontosabb tagjai
A BinaryReader osztály a BinaryWriter által nyújtott működést a 20.11. táblázatban szereplő tagokkal egészíti ki. Tag
Jelentés
BaseStream
Ez az írásvédett tulajdonság hozzáférést biztosít a BinaryReader objektummal használt mögöttes adatfolyamhoz.
Close()
Ez a metódus lezárja a bináris olvasót.
PeekChar()
Ez a metódus visszaadja a következő elérhető karaktert anélkül, hogy módosítaná a pozíciót az adatfolyamban.
Read ()
Ez a metódus kiolvas egy adott byte- vagy karaktersorozatot, és eltárolja őket a bemeneti tömbben.
ReadXXXX()
A BinaryReader osztály számos olyan olvasási metódust definiál, amelyek kiolvassák a következő típust az adatfolyamból (ReadBoolean(), ReadByte(), ReadInt32() stb.).
20.11. táblázat: A BinaryReader alapvető tagjai
A következő példa (BinaryWriterReader nevű konzolalkalmazás) többféle adattípust ír ki egy új *.dat fájlba: static void Main(string[] args) { Console.WriteLine("***** Fun with Binary Writers / Readers *****\n"); // Megnyitunk egy bináris írót egy fájl számára. FileInfo f = new FileInfo("BinFile.dat"); using(BinaryWriter bw = new BinaryWriter(f.OpenWrite())) { // Kiírjuk a BaseStream tulajdonság típusát. // (Jelen esetben System.IO.FileStream.) Console.WriteLine("Base stream is: {0}", bw.BaseStream); // Létrehozunk néhány adatot, amelyet elmenthetünk a fájlba. double aDouble = 1234.67; int anInt = 34567; string aString = "A, B, C";
32
A BinaryWriter és BinaryReader osztályok használata // Az adatok írása. bw.Write(aDouble); bw.Write(anInt); bw.Write(aString); } Console.ReadLine(); }
Figyeljük meg, hogy a FileInfo.OpenWrite() által visszaadott FileStream objektumot hogyan adjuk át a BinaryWriter típus konstruktorának. A módszer segítségével könnyen közbeiktathatunk egy másik „adatfolyamréteget” az adatok kiírása előtt. A BinaryWriter konstruktora bármilyen, Stream-leszármazott típust (pl. FileStream, MemoryStream vagy BufferedStream) kezelhet. Vagyis, ha a memóriába bináris adatot szeretnénk írni, egyszerűen adjunk meg egy érvényes MemoryStream objektumot. A bináris adatok kiolvasására a BinFile.dat fájlból a BinaryReader típus több lehetőséget is ad. Meghívunk több olvasásközpontú tagot, hogy kiolvassanak minden adattöredéket a fájlfolyamból: static void Main(string[] args) { ... FileInfo f = new FileInfo("BinFile.dat"); ... // Kiolvassuk a bináris adatokat az adatfolyamból. using(BinaryReader br = new BinaryReader(f.OpenRead())) { Console.WriteLine(br.ReadDouble()); Console.WriteLine(br.ReadInt32()); Console.WriteLine(br.ReadString()); } Console.ReadLine(); }
Forráskód A BinaryWriterReader projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
33
20. fejezet: Fájlműveletek és elszigetelt tárolás
Fájlok programozott „figyelése” A következőkben nézzük meg a FileSystemWatcher osztály szerepét. Ez a típus meglehetősen hatékonyan segít a rendszerben található fájlok programozott felügyeletében (vagy megfigyelésében). Ez azt jelenti, hogy a FileSystemWatcher típust utasíthatjuk a fájlok felügyeletére a System.IO.NotifyFilters által felsorolt műveletek tekintetében (bár a tagok többsége egyértelmű, célszerű elolvasni a .NET Framework 3.5 SDK dokumentációjának idevágó részeit): public enum NotifyFilters { Attributes, CreationTime, DirectoryName, FileName, LastAccess, LastWrite, Security, Size, }
A FileSystemWatcher használatának első lépéseként be kell állítanunk a Path tulajdonságot a megfigyelendő fájlokat tartalmazó mappa elérési útvonalára, valamint a Filter tulajdonságban meg kell adnunk a felügyelendő fájlok kiterjesztéseit. Itt eldönthetjük, hogy kezeljük-e a Changed, Created és Deleted eseményeket, amelyek mindegyike a FileSystemEventHandler metódusreferenciával dolgozik együtt. Ez a metódusreferencia bármely, a következő mintát követő metódust meghívhatja: // A FileSystemEventHandler metódusreferenciának egy, // a következő szignatúrának megfelelő metódust kell meghívnia. void MyNotificationHandler(object source, FileSystemEventArgs e)
A Renamed eseményt is kezelhetjük a RenamedEventHandler metódusreferenciatípussal, amely a következő szignatúrának megfelelő metódusokat hívhatja meg: // A RenamedEventHandler metódusreferenciának egy, // a következő szignatúrának megfelelő metódust kell meghívnia. void MyNotificationHandler(object source, RenamedEventArgs e)
A fájlok megfigyelésének folyamatát a következő példa szemlélteti; ehhez azt feltételezzük, hogy a C meghajtón létrehoztunk egy MyFolder nevű új könyvtárat, amelyben különböző .txt fájlok találhatók (tetszés szerint elnevezve). Az alábbi (MyDirectoryWatcher nevű) konzolalkalmazás a MyFolder könyvtárban van. A *.txt fájlokat fogja felügyelni, és üzenetet ír ki a konzolra, ha új fájlt hozunk létre, illetőleg meglévő fájlt törlünk, módosítunk vagy átnevezünk: 34
Fájlok programozott „figyelése” static void Main(string[] args) { Console.WriteLine("***** The Amazing File Watcher App *****\n"); // Megadjuk a megfigyelendő mappa elérési útját. FileSystemWatcher watcher = new FileSystemWatcher(); try { watcher.Path = @"C:\MyFolder"; } catch(ArgumentException ex) { Console.WriteLine(ex.Message); return; } // Beállítjuk a monitorozandó eseményeket. watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; // Csak szövegfájlokat figyelünk. watcher.Filter = "*.txt"; // Hozzáadjuk az eseménykezelőket. watcher.Changed += new FileSystemEventHandler(OnChanged); watcher.Created += new FileSystemEventHandler(OnChanged); watcher.Deleted += new FileSystemEventHandler(OnChanged); watcher.Renamed += new RenamedEventHandler(OnRenamed); // Megfigyeljük a könyvtárat. watcher.EnableRaisingEvents = true; // Várunk, amíg a felhasználó ki nem lép. Console.WriteLine(@"Press 'q' to quit app."); while(Console.Read()!='q'); }
A két eseménykezelő egyszerűen kiírja az aktuális fájl módosításait: static void OnChanged(object source, FileSystemEventArgs e) { // Mi történjen, ha a fájl megváltozik, létrehozzák vagy letörlik? Console.WriteLine("File: {0} {1}!", e.FullPath, e.ChangeType); } static void OnRenamed(object source, RenamedEventArgs e) { // Mi történjen, ha a fájlt átnevezik? Console.WriteLine("File: {0} renamed to\n{1}", e.OldFullPath, e.FullPath); }
35
20. fejezet: Fájlműveletek és elszigetelt tárolás
A program kipróbálásához indítsuk el az alkalmazást, és nyissuk meg a Windows Intézőt. Próbáljuk meg átnevezni a fájlokat, hozzunk létre és töröljünk egy *.txt fájlt stb. A MyFolder könyvtárban lévő szövegfájlok állapotával kapcsolatos különböző információk láthatóvá válnak (lásd 20.8. ábrát).
20.8. ábra: Szövegfájlok megfigyelése
Forráskód A MyDirectoryWatcher alkalmazást a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
Aszinkron fájlolvasás és -írás A System.IO névtér áttekintésének befejezéseként nézzük meg, miként hajthatunk végre aszinkron műveleteket a FileStream típussal. Az első kötetben, a többszálú feldolgozás elemzésekor már láthattuk, hogyan támogatja a .NET keretrendszer az aszinkron műveleteket (lásd a 18. fejezetet). Mivel a nagy fájlok írása és olvasása időigényes művelet lehet, a System.IO.Stream osztályból származó típusok több, az aszinkron adatfeldolgozást lehetővé tevő metódussal rendelkeznek. Ezek a metódusok az IAsyncResult típust használják: public abstract class Stream : MarshalByRefObject, IDisposable { ... public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state); public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state); public virtual int EndRead(IAsyncResult asyncResult); public virtual void EndWrite(IAsyncResult asyncResult); }
36
Aszinkron fájlolvasás és -írás
A Stream osztályból származtatott típusok aszinkron viselkedésével végzett munka hasonló ahhoz, mint amikor aszinkron metódusreferenciákkal és aszinkron távoli metódushívásokkal dolgozunk. Bár nem valószínű, hogy az aszinkron viselkedés nagyban meggyorsítja a fájlok elérését, más adatfolyamok (pl. a csatlakozópont-alapúak) aszinkron kezelése során jelentősebb teljesítménynövekedést tapasztalhatunk. A következő (AsyncFileStream nevű) konzolalkalmazás bemutatja a FileStream típus aszinkron kezelésének egyik módját (mindenképpen importáljuk a System.Threading és a System.IO névtereket): class Program { static void Main(string[] args) { Console.WriteLine("***** Fun with Async File I/O *****\n"); Console.WriteLine("Main thread started. ThreadID = {0}", Thread.CurrentThread.GetHashCode()); // Egy FileStream aszinkron olvasási vagy írási hozzáféréséhez // ezt a konstruktort kell használnunk. FileStream fs = new FileStream("logfile.txt", FileMode.Append, FileAccess.Write, FileShare.None, 4096, true); string msg = "this is a test"; byte[] buffer = Encoding.ASCII.GetBytes(msg); // Az aszinkron írás elkezdése. A program a WriteDone-t hívja // meg, ha végzett. Figyeljük meg, hogy a visszahívási metódus // a FileStream objektumot kapja meg állapotinformációként. fs.BeginWrite(buffer, 0, buffer.Length, new AsyncCallback(WriteDone), fs); } private static void WriteDone(IAsyncResult ar) { Console.WriteLine("AsyncCallback method on ThreadID = {0}", Thread.CurrentThread.GetHashCode()); Stream s = (Stream)ar.AsyncState; s.EndWrite(ar); s.Close(); } }
A fenti példa egyetlen érdekessége az, hogy a FileStream típus aszinkron viselkedésének engedélyezéséhez csak a példában szereplő konstruktort használhatjuk. A legutolsó, System.Boolean típusú paraméter (ha igaz értéket kap) arra utasítja a FileStream objektumot, hogy a munkát egy másik szálon végezze. 37
20. fejezet: Fájlműveletek és elszigetelt tárolás
Forráskód A AsyncFileStream alkalmazást a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
Az elszigetelt tároló szerepe Minden előző, fájl I/O-val kapcsolatos példánkban az alkalmazás végrehajtásakor azt feltételeztük, hogy a program a CLR-től mindent megengedő (full trust) biztonsági privilégiumot kap. Egy számítógép merevlemezéről olvasni vagy a lemezre írni potenciális biztonsági fenyegetést jelenthet az alkalmazás eredetétől függően. A .NET platform támogatja, hogy különböző helyekről szerelvényeket töltsünk le, többek között külső webhelyekről, az intranetről, de akár a System.Reflection.Emit névtér típusainak segítségével dinamikusan, a memóriában létrehozott új szerelvénnyel is megtehetjük ezt (lásd az előző kötet 19. fejezetét). Az első kötet 15. fejezetében szó volt arról, hogy az ügyfél *.config fájljának
eleme lehetővé teszi, hogy deklaratív módon tetszőleges elérési utat határozzunk meg, ahonnan a CLR külső szerelvényeket tölthet be, míg az Assembly.LoadFrom() metódussal pedig (lásd a 16. fejezetet) programozottan tölthetünk be külső URI-ról szerelvényt.
Bizalom kérdése Figyelembe véve, hogy .NET-szerelvényeket a számítógép helyi merevlemezén kívül számos különböző helyről tölthetünk be, a bizalom kérdése igen fontossá válik. Tegyük fel például, hogy van egy olyan alkalmazásunk, amely távoli helyről letöltött kódot futtat. Ha ez a kódkönyvtár a helyi számítógépről fájlokat próbál olvasni, vajon hogyan bizonyosodhatunk meg arról, hogy nem akar érzékeny adatokat olvasni? Hasonlóképpen, ha távoli számítógépről töltünk le végrehajtható fájlt, mi garantálja, hogy a szerelvény nem próbál érzékeny adatokat kiolvasni a rendszerleíró adatbázisból, nem intéz (rosszindulatú) hívásokat az operációs rendszer mögöttes API-jához, vagy nem jelent egyéb potenciális biztonsági kockázatot? Erre a .NET platform válasza a kóderedet-alapú biztonság (CAS) elnevezésű, .NET-központú biztonsági mechanizmus. A CAS segítségével a CLR biztonsági kiváltságokat adhat a végrehajtó szerelvénynek; többek között az alábbiakat:
38
Az elszigetelt tároló szerepe
• a számítógép könyvtár- és fájlrendszerének módosítása, • hálózati, internet- és adatbáziskapcsolatok kezelése, • új alkalmazástartományok és dinamikus szerelvények létrehozása, • a .NET reflexiós szolgáltatás használata, • nem felügyelt kód meghívása a PInvoke segítségével. Az I/O-specifikus biztonsági kérdésekre koncentrálva tételezzük fel, hogy olyan alkalmazást fejlesztünk, amelyet egy távoli, webalapú URL-en keresztül telepítünk külső felhasználóknak. Attól függően, hogy a telepítőszkriptet hogyan állítjuk be (és hogy a felhasználó számítógépén milyen biztonsági házirend van érvényben), lehetséges, hogy a program a helyi fájlrendszer elérését tiltó szigorú biztonsági környezetben fut. Ha az alkalmazás felhasználói vagy alkalmazásbeállításokat próbál eltárolni a System.IO névtérrel, a CLR futásidejű biztonsági kivételeket fog dobni. A System.IO.IsolatedStorage névtér típusai használhatók olyan alkalmazás létrehozására, amely a .NET-et futtató számítógép speciális, elszigetelt tárolónak nevezett területén tárolja az alkalmazás adatait. Ez valójában egy biztonságos futtatókörnyezet (angolul: „sandbox”), amelyben a CLR lehetővé teszi az írási/ olvasási műveleteket még akkor is, ha az alkalmazást külső URL-ről töltötték le, vagy a rendszergazda más okból helyezte ebbe a virtuális környezetbe.
Az elszigetelt tárolóhoz tartozó API egyéb felhasználási módjai Az elszigetelt tárolóhoz tartozó API-t nem kizárólag a távoli számítógépekről letöltött kódok olvasási/írási műveleteinek szabályozására használhatjuk. Az API-val azt is könnyen megvalósíthatjuk, hogy a felhasználói adatokat más alkalmazás közvetetten (vagy közvetlenül) ne módosíthassa. Írhatunk például olyan egyszerű alkalmazást, amely az adott munkaállomás összes felhasználójának adatát elkülönített mappákban tárolja. Az elszigetelt tárolóhoz tartozó API használatának másik előnye az, hogy a kódnak nincs szüksége bedrótozott elérési utakra vagy könyvtárnevekre a programban. Ehelyett az elszigetelt tároló használatakor az alkalmazás indirekt módon menti a kód azonosítójával (pl. URL, erős név vagy X509 digitális aláírás) valamilyen módon összefüggésbe hozható egyedi adattároló területre az adatokat (a kódazonosítók ismertetését lásd később). 39
20. fejezet: Fájlműveletek és elszigetelt tárolás
Szerencsére az elszigetelt tároló kezelése igen egyszerű azoknak, akik értik az alapvető fájlműveleteket. Az elszigetelt tároló működésének tanulmányozása előtt nézzük át a .NET kódhozzáférés-szabályozási modelljét. Megjegyzés A CAS teljes bemutatása legalább egy (vagy két) teljes fejezetet igényelne, ezért a CAS működését kizárólag olyan szinten tekintjük át, hogy megérthessük az elszigetelt tároló-API szerepének alapjait. A CAS működésének további részleteit a .NET Framework 3.5 SDK tartalmazza.
Bevezetés a kóderedet-alapú biztonságba A távoli .NET-szerelvények letöltése és futtatása által jelentett biztonsági problémák megelőzésének érdekében a CLR automatikusan azonosítja a szerelvényt, és besorolja az előre meghatározott kódcsoportok egyikébe. Leegyszerűsítve a kódcsoport egy olyan szerelvénygyűjtemény, amelynek tagjai ugyanazon feltételeknek felelnek meg (pl. ugyanaz az eredetük). Azt a feltételt, amely alapján a CLR eldönti egy szerelvényről, hogy az melyik kódcsoportba tartozik, bizonyítéknak nevezzük. A szerelvényeket eredetükön kívül más bizonyíték alapján is besorolhatjuk egy kódcsoportba. Ilyen lehet a szerelvény erős neve, egy beágyazott X509 digitális tanúsítvány vagy bármely, általunk programozottan elkészített egyedi feltétel. Megjegyzés Szigorúan véve kétféle bizonyítékról beszélhetünk: ezek a szerelvényalapú és a hosztalapú bizonyítékok. Míg a szerelvényalapú bizonyítékot lefordítjuk a szerelvénybe, a hosztalapú bizonyítékot programozottan, az AppDomain típus segítségével határozhatjuk meg.
Miután a CLR kiértékelte a szerelvény bizonyítékait, és eldöntötte, hogy melyik kódcsoportba kerüljön, annak érdekében, hogy meghatározhassa, a szerelvény mit tehet, és főként mit nem tehet, lekérdezi a kódcsoporthoz társított engedélykészletet (ez egyszerűen a különálló engedélyek nevesített készlete). A kódcsoportok és a hozzájuk tartozó engedélyek együtt alkotják a biztonsági házirendet, amelyet három nagyobb szintre oszthatunk (vállalati, gép-, illetve felhasználószintű). Ennek a réteges megközelítésnek a használatával a rendszergazdának lehetősége van arra, hogy egyedi házirendet alakítson ki a vállalat, illetve az egyes számítógépek/felhasználók szintjén.
40
Bevezetés a kóderedet-alapú biztonságba
Miután mind a három (vállalati, számítógép- és felhasználói) biztonsági házirend alkalmazása megtörtént, a szerelvény lefut a .NET futásidejű környezetében. Ha a szerelvény a jogosultsági készletébe nem tartozó kódot próbál futtatni, a CLR futásidejű biztonsági kivételt fog dobni. A 20.9. ábra mutatja be, hogy a CAS ezen építőelemei (bizonyíték, kódcsoportok/engedélykészletek és házirendek) hogyan fonódnak össze.
Alkalmazástartomány
Házirend beállítások (Enterprise, Machine, User)
MyAsm.exe Bizonyíték
Kódcsoport Biztosított engedélyek
Bizonyíték
20.9. ábra: A CAS építőelemei
A bizonyíték kiértékelésének, a szerelvény kódcsoportba sorolásának és a jogosultsági készletek hozzárendelésének folyamata minden alkalommal, egy .NET-alkalmazás futtatásakor a háttérben, észrevehetetlenül zajlik le. Legtöbbször az alapértelmezett CAS biztonsági beállítás, valamint a CLR és a CAS közötti interakció a háttérben történik, felhasználói beavatkozás nélkül. Érdemes azonban kicsit részletesebben szemügyre venni a CAS építőelemeit, kezdve a szerelvénybizonyíték fogalmával.
A bizonyítékok szerepe Annak érdekében, hogy a CLR meghatározza, hogy egy szerelvény melyik kódcsoportba kerüljön, először kiolvassa a megadott bizonyítékot. A bizonyíték tehát olyan adat, amelyet a szerelvényből (vagy az azt hosztoló alkalmazástartományból) szerzünk meg, amikor az betöltődik a memóriába. A 20.12. táblázat írja le azokat a főbb bizonyítéktípusokat, amelyet a szerelvény bemutathat a CLR-nek.
41
20. fejezet: Fájlműveletek és elszigetelt tárolás
Hosztbizonyíték-típus
Jelentés
Alkalmazáskönyvtár
A szerelvény telepítési könyvtára.
Szerelvényhashkód
A szerelvény tartalmának hashértéke.
Kibocsátó tanúsítványa
A szerelvényhez rendelt Authenticode X509-es digitális tanúsítvány (ha van).
Hely
Az a forráswebhely, amelyről a szerelvényt betöltötték (nem érvényes a helyi gépről betöltött szerelvények esetében).
A szerelvény erős neve
A szerelvény erős neve (ha van ilyen).
URL
Az az URL, ahonnan a szerelvény betöltődött (HTTP, FTP, fájl stb.).
Zóna
Annak a zónának a neve, ahonnan a szerelvény betöltődött.
20.12. táblázat: Különböző típusú szerelvénybizonyítékok
Bár a bizonyítékok beolvasása automatikusan történik, arra is van lehetőségünk, hogy a bizonyítékokat programozottan olvassuk be a reflexiós API és a System.Security.Policy névtér Evidence típusának a segítségével. A bizonyítékok megértéséhez hozzunk létre egy új, MyEvidenceViewer nevű konzolalkalmazást. Ne felejtsük el importálni a System.Reflection, a System.Collections és a System.Security.Policy névtereket. Ezután egy olyan egyszerű alkalmazást írunk, amely megkéri a felhasználót, hogy adja meg a betöltendő szerelvény nevét. Számba vesszük valamenynyi szerelvénybizonyítékot, majd az eredményt kiírjuk a konzolablakba. Először is, a Program osztály Main() metódusa lehetővé teszi, hogy a felhasználó megadja a kiértékelendő szerelvény teljes elérési útját. Ha az L lehetőséget választja, meghívunk egy segédmetódust, amely megpróbálja betölteni a megadott szerelvényt a memóriába. Ha sikerült, átadjuk az Assembly referenciát egy másik, DisplayAsmEvidence() nevű segédmetódusnak. A megvalósítás a következő: class Program { static void Main(string[] args) { bool isUserDone = false; string userOption = ""; Assembly asm = null;
42
Bevezetés a kóderedet-alapú biztonságba Console.WriteLine("***** Evidence Viewer *****\n"); do { Console.Write("L (load assembly) or Q (quit): "); userOption = Console.ReadLine(); switch (userOption.ToLower()) { case "l": asm = LoadAsm(); if (asm != null) { DisplayAsmEvidence(asm); } break; case "q": isUserDone = true; break; default: Console.WriteLine("I have no idea what you want!"); break; } } while (!isUserDone); } }
A LoadAsm() metódus egyszerűen meghívja az Assembly.LoadForm() metódust, hogy beállítsa a privát Assembly tagváltozót: private static Assembly LoadAsm() { Console.Write("Enter path to assembly: "); try { return Assembly.LoadFrom(Console.ReadLine()); } catch { Console.WriteLine("Load error..."); return null; } }
Végül pedig a DisplayAsmEvidence() metódus olvassa ki a betöltött szerelvényből a bizonyítékot az Assembly típus Evidence tulajdonságának révén. Ezután egy felsoroló segítségével (az Evidence típus GetHostEvidence() metódusával) kiírjuk az összes jelenlévő bizonyítéktípust:
43
20. fejezet: Fájlműveletek és elszigetelt tárolás private static void DisplayAsmEvidence(Assembly asm) { // A bizonyítékgyűjtemény és a mögöttes felsoroló lekérése. Evidence e = asm.Evidence; IEnumerator itfEnum = e.GetHostEnumerator(); // A bizonyíték kiírása. while (itfEnum.MoveNext()) { Console.WriteLine(" **** Press Enter to continue ****"); Console.ReadLine(); Console.WriteLine(itfEnum.Current); } }
Az alkalmazás teszteléséhez érdemes a C meghajtó gyökerében létrehozni egy MyAsms mappát, majd ebbe a mappába belemásolni az erős névvel rendelkező CarLibrary.dll szerelvényt (lásd az első kötet 15. fejezetét), végül elindítani a programot. Feltételezve, hogy az L parancs mellett döntünk, adjuk meg a szerelvény teljes elérési útvonalát, majd nyomjuk meg az Enter billentyűt. Az alkalmazás kiírja a konzolra az összes bizonyítékot, ahogy az a 20.10. ábrán is látható (fontos, hogy az alkalmazás konstrukciója szerint minden egyes bizonyítéktípus megjelenítése után Entert kell ütni).
20.10. ábra: A CarLibrary.dll bizonyítékainak megtekintése
44
Bevezetés a kóderedet-alapú biztonságba
Látható, hogy a CarLibrary.dll a MyComputer zónába került a C:\MyAsms\ CarLibrary.dll URL-ről, és rendelkezik erős névvel. Ha a szerelvényt más helyről töltjük be (mondjuk távoli webhelyről), akkor teljesen más kimenetet fogunk kapni. A lényeg az, hogy a szerelvény memóriába való betöltésekor a CLR megvizsgálja a szerelvény által szolgáltatott bizonyítékokat. Forráskód A MyEvidenceViewer projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
A kódcsoportok szerepe A bizonyítékok használatával a CLR a szerelvényeket kódcsoportokba sorolhatja. Minden kódcsoportot egy biztonsági zónához rendeltek, amelyek gyárilag meghatározott biztonsági beállításokkal rendelkeznek. A CLR ezek segítségével határozza meg, hogy a szerelvény mit tehet meg, és mit nem. A 20.13. táblázat a leggyakoribb kódcsoportokat mutatja be az alapértelmezés szerint hozzájuk rendelt engedélykészlettel együtt (ezekről a következő részben lesz szó). Alapértelmezett kódcsoport
Hozzárendelt engedélyek
My_Computer_Zone
Full Trust
Közvetlenül a helyi merevlemezről betöltött szerelvényt jelképez.
LocalIntranet_Zone
LocalIntranet
A helyi intranet egy megosztási pontjáról letöltött szerelvényt jelez.
Internet_Zone
Internet
A webről letöltött szerelvényt jelez.
Jelentés
20.13. táblázat: Néhány gyakoribb kódcsoport
A .NET keretrendszer 3.5 SDK egy olyan grafikus felügyeleti eszközt biztosít, amellyel a rendszergazdák megnézhetik és módosíthatják a meglévő kódcsoportokat, vagy szükség szerint újat hozhatnak létre. Az eszköz segítségével beállíthatjuk például, hogy a CLR minden, adott címről (pl. http://www. intertech.com) letöltött külső szerelvényt egy testre szabott biztonsági környezetben futtasson. Megjegyzés A .NET Framework 3.5 SDK egy
caspol.exe nevű parancssori programot is tar-
talmaz, amellyel ugyanezeket a beállításokat érhetjük el.
45
20. fejezet: Fájlműveletek és elszigetelt tárolás
Ezt az eszközt a Control Panel Administrative Tools mappájában találjuk Microsoft .NET Framework Configuration néven. Az eszköz elindítása után a CAS-beállításokat három szinten módosíthatjuk: vállalati szinten (pl. minden hálózatba kapcsolt gép), a helyi gép szintjén, illetve felhasználónként. (A CAS-t alkalmazástartományi szinten is beállíthatjuk, de ezt csak programozottan tehetjük meg.) A 20.11. ábra a CAS alapértelmezett „számítógépszintű házirend” beállításainak lenyitott csomópontját mutatja aktuálisan. Az All_Code kódcsoport (amely az összes .NET-szerelvényt jelenti) több olyan zónát is meghatároz, amelybe a szerelvény tartozhat (pl. My_Computer_Zone stb.). Ha jobb gombbal kattintunk az All_Code gyökér alatt található bármelyik csomópontra, megnyithatjuk az adott objektum tulajdonságlapját, amely további részletekkel szolgál. A 20.12. ábra például a My_Computer_Zone kódcsoport (amely a közvetlenül a helyi merevlemezről betöltött szerelvényeket tartalmazza) tulajdonságait mutatja.
20.11. ábra: A CAS-központú számítógépházirend
A Membership Condition fülre kattintva látható, hogy a CLR mi alapján dönti el, hogy egy adott .NET-szerelvénynek ebbe a zónába kell-e tartoznia. A 20.13. ábra mutatja, hogy a tagsági feltétel a nem túl beszédes Zone-érték, amely azt a helyet jelöli, ahonnan a szerelvényt betöltöttük.
46
Bevezetés a kóderedet-alapú biztonságba
20.12. ábra: A My_Computer_Zone kódcsoport részletei
20.13. ábra: A My_Computer_Zone kódcsoport tagsági feltételei
Ha a My_Computer_Zone kódcsoport Permission Set fülére kattintunk, azt látjuk, hogy minden, a helyi merevlemezről betöltött szerelvény a Full Trust elnevezésű biztonsági engedélykészletet kapja meg (lásd a 20.14. ábrát).
47
20. fejezet: Fájlműveletek és elszigetelt tárolás
20.14. ábra: Az alapértelmezés szerint a helyi merevlemezről betöltött szerelvények Full Trust engedélyt kapnak
20.15. ábra: A külső URL-ről betöltött szerelvények az alapértelmezés szerint nem kapják meg a Full Trust engedélykészletet
48
Bevezetés a kóderedet-alapú biztonságba
Ezzel szemben a külső URL-ről (a céges intraneten kívülről) betöltött szerelvények az Internet_Zone kódcsoportba kerülnek, amely jóval szigorúbb jogosultságcsoportba tartozik. Ahogy a 20.15. ábrán is látható, az Internet_Zone csoportba sorolt szerelvények nem a Full Trust engedélyt kapják meg, hanem az Internet nevű engedélykészlettel fognak rendelkezni.
Az engedélykészletek szerepe A CLR különböző feltételek alapján sorolja a szerelvényeket kódcsoportokba (a betöltés eredete, erős név stb. alapján). A kódcsoporthoz pedig egy barátságos névvel rendelkező (Full Trust, Internet stb.) engedélykészlet tartozik. Mindegyik engedélykészlet (mint azt a neve is mutatja) olyan egyedileg beállított engedélyek gyűjteménye, amelyek különböző biztonsági beállításokat vezérelnek (nyomtatóhoz vagy fájlrendszerhez való hozzáférés, reflexiós API használata stb.). A Microsoft .NET Framework Configuration segédprogramban a View Permissions hivatkozásra kattintva nézhetjük meg az alapértelmezett engedélykészletek beállításait. A 20.16. ábra az Internet-engedélykészlet beállításait mutatja a View Permissions hivatkozásra való kattintás után.
20.16. ábra: Internet engedélykészlet
Az itt látható ikonok (File Dialog, Isolated Storage File, Security stb.) mind a készlet egy-egy specifikus engedélyét képviselik, amelyeket még részletesebben beállíthatunk, ha duplán kattintunk rájuk. Ha például kétszer kattintunk a Security engedélyre (amely az általános biztonsági beállítások gyűjtőengedélye), azt láthatjuk, hogy az Internet_Zone alatt futó szerelvény (amelyet 49
20. fejezet: Fájlműveletek és elszigetelt tárolás
ezért az Internet engedélykészlet korlátoz) elindítható, de az alapértelmezés szerint nem hajthat végre számos egyéb alapvető műveletet, nem használhatja például a platformhívási szolgáltatást arra, hogy meghívja az operációs rendszer API-ját (lásd a 20.17. ábrát).
20.17. ábra: Az engedélykészlet egyes engedélyeinek megtekintése
A CAS működése A CAS-műveletek bemutatása érdekében megváltoztatjuk a My_Computer kódcsoport néhány alapértelmezett beállítását, majd megnézzük az eredményt. A kapcsolódó tulajdonságlap megnyitásához kattintsunk jobb gombbal a Microsoft .NET Framework Configuration alkalmazásban a számítógépházirendhez tartozó My_Computer_Zone kódcsoportra. Ezután kattintsunk a Permission Set fülre, majd módosítsuk az engedélykészletet Full Trustról Internetre (lásd a 20.18. ábrát). Az OK gombra kattintva a program módosítja a helyi számítógép biztonsági házirendjét. Ez ebben az esetben azt jelenti, hogy a merevlemezről betöltött .NET végrehajtható fájlok többé nem fogják tudni elérni a helyi merevlemezt a System.IO névtér típusainak felhasználásával. Ellenőrzésképpen indítsuk el valamelyik korábban készített IO-intenzív alkalmazásunkat. Használjuk a parancssort arra, hogy a korábban készített DriveInfoApp.exe alkalmazást tartalmazó könyvtárra váltsunk, és próbáljuk meg elindítani a programot. Ha megjelenik a futásidejű hibát jelző párbeszédpanel, válasszuk a Close the Program lehetőséget, majd figyeljük meg a parancssori kimenetet.
50
Bevezetés a kóderedet-alapú biztonságba
A 20.19. ábrán látszik, hogy az alkalmazás nem tudott hozzáférni a helyi merevlemezhez, és a CLR biztonsági kivételt dobott.
20.18. ábra: A My_Computer_Zone kódcsoport engedélykészletének módosítása
20.19. ábra: Biztonsági veszély. A My_Computer_Zone kódcsoport nem engedélyezi IO-műveletek végrehajtását
51
20. fejezet: Fájlműveletek és elszigetelt tárolás
Full Trust jogosultság visszaállítása a My_Computer_Zone kódcsoportra A Microsoft .NET Framework Configuration alkalmazás segítségével állítsuk vissza a My_Computer_Zone kódcsoportra a Full Trust engedélykészletet. Ezután a DriveInfoApp.exe programnak hiba nélkül le kell futnia. Megjegyzés Ha a számítógépre vonatkozó CAS-beállításokat módosítjuk, előfordulhat, hogy egy véletlen helytelen beállítás következtében nem futnak megfelelően az alkalmazások. Ha vissza kell állítani az alapértelmezett számítógépházirend-beállításokat, kattintsunk jobb gombbal a Runtime Security Policy mappában található Machine ikonra, majd a helyi menüből válasszuk a Reset parancsot.
A fenti rövid leírás nem adhat teljes áttekintést erről a rendkívül sokoldalú API-ról, de ez alapján jobban megérthetjük az elszigetelt tároló szerepének a fontosságát.
Az elszigetelt tároló Az elszigetelt tárolóhoz tartozó API legfontosabb szerepe, hogy egy olyan biztonságos virtuális környezetet hoz létre, amelybe az alkalmazások attól függetlenül írhatnak vagy abból olvashatnak adatokat, hogy melyik kódcsoportban találhatók, anélkül, hogy az alapértelmezett biztonsági házirendet módosítani kellene. Ezenkívül ennek a technológiának a használata az alábbi körülmények között is célszerű lehet. • El kell menteni az alkalmazás felhasználói beállításait vagy egyéb felhasználóhoz kapcsolódó adatokat (DataSet-ek, XML-fájlok stb.). • Egy ClickOnce alkalmazást telepítünk, amely biztonságos futtatókörnyezetben működik, és nincs korlátlan hozzáférése a helyi fájlrendszerhez. • Olyan Windows Forms-vezérlőelemet töltöttünk le az internetről, amely egy webalkalmazásba integrálódik, és az ügyfélgépen kell beállításokat tárolnia. • XAML böngészőalkalmazást (XBAP, XAML Browser Application – lásd a 28. fejezetet) telepítünk, amely a felhasználó számítógépén tárol adatokat. 52
Az elszigetelt tároló
Az elszigetelt tároló használata nem jelenti azt, hogy ne tárolhatnánk adatot a .config fájlban (például kapcsolatsztringeket), vagy helyezhetnénk el különböző alkalmazásbeállításokat a rendszerleíró adatbázisban. Mint minden technológiának, az elszigetelt tárolónak is vannak hátrányai. Első és legfontosabb az, hogy az elszigetelt tárolóba helyezett adat nincs automatikusan titkosítva. Ezért csakúgy, mint a hagyományos IO-műveletek esetén, az érzékeny adatokat (pl. hitelkártya-információkat) manuálisan kell titkosítanunk (és visszafejtenünk). Megjegyzés A .NET titkosító API-jának használata nem témája ennek a könyvnek. Erről részletes információval szolgál a .NET Framework 3.5 SDK dokumentációjának Cryptographic Services című része.
Csakúgy, mint a fájlrendszer más részein tárolt adatokat, a végfelhasználó az elszigetelt tárolóba helyezett fájlokat is átmásolhatja, áthelyezheti vagy letörölheti. A System.IO.IsolatedStorage névtér olyan, dinamikusan generált könyvtárstruktúrában tárolja az adatokat, amely el van rejtve a merevlemez egy adott helyén. Így a legtöbb felhasználó soha sem találkozik közvetlenül a számítógép elszigetelt tárolójával. A rendszergazda korlátozhatja az elszigetelt tároló által elfoglalható lemezterület nagyságát. Ha az alkalmazásunk mellett más alkalmazások is nagy mennyiségű adatot tárolnak, lehetséges, hogy a tároló mérete eléri a megengedett felső korlátot. Tanácsos a strukturált kivételkezelés segítségével felkészülni erre az esetre.
Az elszigetelt tároló hatóköre Természeténél fogva az elszigetelt tároló (legalább) felhasználónként elszigeteli egymástól az adatokat. Ezért, ha a programunk az elszigetelt tárolóba ment adatokat, azokat a .NET az aktuálisan bejelentkezett felhasználó szintjén tárolja. Ha ugyanarra a munkaállomásra egy másik felhasználó is bejelentkezik, és ugyanabból az alkalmazásból adatokat ment el, az eltárolt adatokat egy, az adott felhasználó számára egyedi helyen rögzíti. A felhasználószintű elszigetelésen kívül az elszigetelt tároló szerelvény és/vagy alkalmazástartomány-azonosító szintjén is képes az adatokat izolálni. Ha az elszigetelést felhasználónként és szerelvényenként is engedélyezzük, az alkalmazás ugyanazt a tárolót fogja használni függetlenül attól, hogy a programot melyik alkalmazástartomány hosztolja. 53
20. fejezet: Fájlműveletek és elszigetelt tárolás
Ebben az esetben az elérhető legjellegzetesebb bizonyíték alapján (pl. az erős név) jön létre a tároló neve. Így elérhetjük, hogy ugyanazon a gépen egy időben a program több példánya fusson, amelyek mind ugyanazt a tárolót használják (lásd a 20.20. ábrát). Elszigetelés felhasználó és szerelvény alapján Felhasználó: Homer
A alkalmazástartomány
B alkalmazástartomány
MyAsm.exe
MyAsm.exe
Elszigetelt tároló 1. tár
20.20. ábra: Felhasználó- és szerelvényszintű izoláció
Másrészről, ha felhasználó + szerelvény + alkalmazástartomány elkülönítési szintet állítunk be, akkor a .NET az alkalmazástartományt is figyelembe veszi. A legjellegzetesebb szerelvénybizonyítékokon kívül a legjellegzetesebb alkalmazástartomány-bizonyítékot (jellemzően a Site) is felhasználja a rendszer a tároló létrehozásához. Ekkor, még ha ugyanazt a szerelvényt is használjuk több egyedi alkalmazástartományból, az alkalmazás adatai külön tárolókba kerülnek (lásd a 20.21. ábrát). Az elszigetelt tároló használatával vándorló (roaming) profilok is kialakíthatók. A vándorló hatókör lehetővé teszi, hogy bár a felhasználó különböző számítógépekre jelentkezik be, mégis ugyanazt az alkalmazásadatot kapja meg. Ilyen esetben az adattárolás hálózati helyen történik, és igény szerint letöltődik, ha a felhasználó bejelentkezik egy adott munkaállomásra. Ennek részleteiért olvassuk el a .NET Framework 3.5 SDK dokumentációjának ide vonatkozó részét.
54
Az elszigetelt tároló
Elszigetelés felhasználó, szerelvény és alkalmazástartomány alapján Felhasználó: Homer
A alkalmazástartomány
B alkalmazástartomány
MyAsm.exe
MyAsm.exe
Elszigetelt tároló 1. tár
Elszigetelt tároló 2. tár
20.21. ábra: Felhasználó-, szerelvény- és alkalmazástartomány-szintű izoláció
Az elszigetelt tároló helye Az elszigetelt tároló nem más, mint a .NET-et futtató számítógép fájlrendszerének erre a célra szánt része, vagyis nem különbözik a C:\Windows, C:\Program Files vagy bármely más, a merevlemezen lévő könyvtártól. Mindazonáltal az elszigetelt tároló pontos helye az operációs rendszertől is függ. Egy adott tároló gyökerének a helye egy Vistát futtató számítógépen: C:\Users\\App Data\Local\IsolatedStorage Egy Windows XP számítógépen viszont: C:\Documents and Settings\\Local Settings\Application Data\IsolatedStorage Ez alatt található számtalan (teljesen kimondhatatlan nevű) alkönyvtár, amelyeket az elszigetelt tárolóhoz tartozó API hoz létre és tart karban. A 20.22. ábra a tároló helyét mutatja egy Vistát futtató számítógépen. A könyvtárak neveivel vagy azzal, hogy ezeket programozottan megadjuk a tárolók létrehozása során, nem kell foglalkoznunk. Ezek a nevek automatikusan jönnek létre a felhasználó azonossága és a szükséges szerelvény és/vagy alkalmazástartomány-bizonyítékok alapján.
55
20. fejezet: Fájlműveletek és elszigetelt tárolás
A bizonyítékok gyűjtése során a CLR az alábbi listát ellenőrzi (a legspecifikusabb bizonyítékkal kezdve), ebben a sorrendben: • Kibocsátó tanúsítványa • Erős név • URL • Hely • Zóna
20.22. ábra: Elszigetelt tároló egy Vistát futtató számítógépen
56
Az elszigetelt tároló
Az elszigetelt tároló kezelése a storeadm.exe segítségével A .NET Framework 3.5 SDK tartalmaz egy storeadm.exe nevű parancssori eszközt is, amellyel a számítógép aktuális tárolórendszerét kezelhetjük. Az eszköz segítségével megtekinthetjük az éppen bejelentkezett felhasználó tárolóit (a /list paraméterrel), és törölhetjük az aktuális felhasználó tárolóit (a /remove argumentummal). A 20.23. ábrán a /list kapcsoló kimenete látható. Mint azt az előző ábra is mutatja, ez az eszköz megjeleníti az izoláció szintjét (szerelvény, alkalmazástartomány) és a tároló létrehozásához használt bizonyítékokat is. De nem mutatja meg az egyes tárolókban található fájlok tartalmát. Ehhez meg kell keresnünk a létrehozott tárolóhelyeket a Windows Intézővel, vagy programozottan kell beolvasnunk az adatokat.
20.23. ábra: Az aktuális felhasználó tárolóinak megjelenítése a storeadm.exe programmal
A System.IO.IsolatedStorage típusa Mielőtt megnéznénk, hogy írhatunk (és olvashatunk) adatokat az elszigetelt tárolóba, tekintsük át a 20.14. táblázatot, amely az elszigetelt tároló alapvető típusait ismerteti. Ez a névtér meglehetősen kicsi, mivel típusait a System.IO alapvető típusaival együtt használjuk.
57
20. fejezet: Fájlműveletek és elszigetelt tárolás
System.IO.IsolatedStorage típus
Jelentés
IsolatedStorage
Ez a típus az az absztrakt ősosztály, amelyből valamennyi elszigetelt tárolónak származnia kell.
IsolatedStorageScope
Ennek a felsorolt típusnak a segítségével állíthatjuk be a használandó izoláció szintjét (szerelvény, alkalmazástartomány vagy vándorló).
IsolatedStorageException
Ez a típus határozza meg, hogy milyen kivételt dobjon a program, ha egy művelet nem sikerül az elszigetelt tárolóban.
IsolatedStorageFile
Ez a tag jelöli azt a területet, ahol az elszigetelt tároló a fájlokat és könyvtárakat tartja.
IsolatedStorageFileStream
Ez a típus az elszigetelt tárolóban található fájlok olvasását, írását, létrehozását teszi lehetővé.
20.14. táblázat: A System.IO.IsolatedStorage típusai
Tároló létrehozása az IsolatedStorageFile objektummal Ha az elszigetelt tárolóban szeretnénk alkalmazásadatot tárolni, első lépésként el kell döntenünk, hogy milyen szintű izolációt szeretnénk használni. A .NET a tárolókat legalább felhasználónként mindenképp elkülöníti, de beállíthatunk szerelvényenkénti vagy alkalmazástartományonkénti izolációt is (valamint vándorló profilt is). A megfelelő elkülönítési szint beállításakor egyik lehetőségünk, hogy az IsolatedStorageScope felsorolás egy tagjával beállítjuk az értékeket, majd meghívjuk az IsolatedStorageFile.GetStore() metódust. Az IsolatedStorageFile típus statikus GetUserStoreForDomain() vagy GetUserStoreForAssembly() metódusai is használhatók. Vizsgáljuk meg a következő példákat: static void GetAppDomainStorageForUser() { // Elszigetelt tároló megnyitása szerelvény- és // alkalmazástartomány-azonosító alapján (rövid). IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForDomain();
58
Tároló létrehozása az IsolatedStorageFile objektummal // Vagy egyesítjük a jelzőket, és meghívjuk a GetStore() metódust. IsolatedStorageFile store2 = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Domain, null, null); } static void GetAssemblyStorageForUser() { // Megnyitunk egy tárolót // szerelvényazonosító alapján (rövid). IsolatedStorageFile store2 = IsolatedStorageFile.GetUserStoreForAssembly(); // Vagy egyesítjük a jelzőket, és meghívjuk a GetStore() metódust. IsolatedStorageFile store2 = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null); }
Bármelyik módszert is választjuk, az eredményként egy IsolatedStorageFile objektumot kapunk. Ennek a típusnak a segítségével a tárolóba adatokat írhatunk, abból olvashatunk, vagy létrehozhatunk tetszőleges egyedi könyvtárszerkezetet az aktuális felhasználó tárolóján belül. A 20.15. táblázat az IsolatedStorageFile néhány fontosabb tagját mutatja be. Tag
Jelentés
CurrentSize,
Ezek az írásvédett tulajdonságok visszaadják az elszigetelt tároló méretével kapcsolatos adatokat.
MaximumSize Scope
Megadja az izoláció hatókörét (felhasználó, szerelvény vagy alkalmazástartomány).
CreateDirectory()
Létrehoz egy új könyvtárat a tárolóban.
DeleteDirectory()
Töröl egy könyvtárat a tárolóból.
DeleteFile()
Töröl egy fájlt a megadott könyvtárból.
GetDirectoryNames()
Ez a metódus lehetővé teszi, hogy végigiteráljunk a nevesített könyvtárakon.
GetEnumerator()
Egy hatókör-specifikus IEnumerator típussal tér vissza.
GetFiles()
Visszaadja a tárolóban található fájlok listáját.
GetStore()
Ez a túlterhelt metódus a megadott alkalmazástartomány- és szerelvénybizonyítékoknak és az izolációs hatókörnek megfelelő elszigetelt tárolót adja vissza.
59
20. fejezet: Fájlműveletek és elszigetelt tárolás
Tag
Jelentés
GetUserStoreForAssembly()
Ez a metódus megszerzi a hívó szerelvényazonosítójának megfelelő elszigetelt tárolót.
GetUserStoreForDomain()
Ez a metódus megszerzi az alkalmazástartomány- és szerelvényazonosítónak megfelelő elszigetelt tárolót.
Remove()
Ez a metódus eltávolítja a tárolókat.
20.15. táblázat: Az IsolatedStorageFile tagjai
Adat írása a tárolóba Miután létrehoztuk a tárolót, következő lépésként létre kell hoznunk az IsolatedStorageFileStream típus egy példányát, amely az adattároláshoz használt fájlt jelenti a tárolóban. A többi IO-adatfolyamhoz hasonlóan ez a típus is beállítható a korábban ismertetett System.IO.FileMode felsorolással. Ennek bemutatásához hozzunk létre egy konzolalkalmazást SimpleIsoStorage néven, amelybe importáljuk a System.IO és a System.IO.IsolatedStorage névtereket. Ezután módosítsuk úgy a Main() metódust, hogy a Program osztály alábbi segédmetódusát hívja meg: static void WriteTextToIsoStorage() { // Elszigetelt tároló megnyitása szerelvény- és // felhasználói bizonyítékok alapján. using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly()) { // Létrehozzuk az IsolatedStorageFileStream típust. using (IsolatedStorageFileStream isStream = new IsolatedStorageFileStream("MyData.txt", FileMode.OpenOrCreate, store)) { // Az adatfolyamot ”becsomagoljuk” egy StreamWriter // objektumba, és kiírunk valamilyen szöveget. using (StreamWriter sw = new StreamWriter(isStream)) { sw.WriteLine("This is my data."); sw.WriteLine("Cool, huh?"); } } } }
60
Tároló létrehozása az IsolatedStorageFile objektummal
Először megszerzünk egy tárolót a felhasználó számára a végrehajtó szerelvény (SimpleIsoStorage.exe) azonossága alapján az IsolatedStorageFile.GetUserStoreForAssembly() metódussal. Ezután létrehozunk egy új IsolatedStorageFileStream objektumot, és megadjuk, hogy az újonnan létrehozandó fájl neve MyData.txt legyen a most megszerzett tárolóban (ha a fájl már létezik, akkor megnyitjuk). Végül az IsolatedStorageFileStream objektumot egy System.IO.StreamWriter íróba csomagoljuk, és kiírunk néhány sornyi szöveget. Ha lefuttatjuk az alkalmazást, belenézhetünk a számítógépen lévő elszigetelt tárolóba, és (némi kutakodás után) megtalálhatjuk a MyData.txt fájlt (lásd a 20.24. ábrát). A MyData.txt fájlban, ha jegyzettömbbel megnyitjuk, az abba kiírt két sort látjuk. Természetesen az IsolatedStorageFileStream objektumot más, a Stream osztályból származó típusba is becsomagolhatjuk. Ha például inkább bináris formában szeretnénk kiírni az adatokat, használhatjuk a StreamWriter helyett a BinaryWriter osztályt.
20.24. ábra: Az elszigetelt tárolóba helyezett szövegfájl
Adat olvasása a tárolóból A felhasználó tárolójában elhelyezett adat olvasása is hasonlóan egyszerű. Nézzük meg a következő metódust (amelyet célszerű szintén a Main() függvényből meghívni a WriteTextToIsoStorage() metódus után), amely a MyData.txt fájlban található információt kiolvassa, és megjeleníti a konzolon:
61
20. fejezet: Fájlműveletek és elszigetelt tárolás private static void ReadTextFromIsoStorage() { using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly()) { using (IsolatedStorageFileStream isStream = new IsolatedStorageFileStream("MyData.txt", FileMode.Open, FileAccess.Read, store)) { // ”Becsomagoljuk” StreamReaderbe. using (StreamReader sr = new StreamReader(isStream)) { string allTheData = sr.ReadToEnd(); Console.WriteLine(allTheData); } } } }
Felhasználói adat törlése a tárolóból Az IsolatedStorageFile típus két módszert biztosít a tárolók törlésére. A példányszintű Remove() azt a tárolót törli, amelyik meghívja. A statikus IsolatedStorageFile.Remove() metódus az IsolatedStorageScope.User értéket veszi fel, és törli a kódot futtató felhasználó összes tárolóját. A következő kód például törli az aktív tárolót, és megsemmisíti annak tartalmát: // Töröljük az aktuális felhasználó aktuális tárolójának adatait. IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly(); store.Remove();
Az alábbi kód pedig az aktuális felhasználó összes tárolóját törli (ugyanúgy, mint amikor a storeadm.exe segédprogramot a /remove paraméterrel indítjuk): // Töröljük a felhasználó ÖSSZES tárolóját. IsolatedStorageFile.Remove(IsolatedStorageScope.User);
Egyedi könyvtárstruktúra létrehozása Az előző példákban nem hoztunk létre egyedi hierarchiát a különböző adatfájlok tárolására. Ehelyett a MyData.txt fájlt közvetlenül a tároló gyökerében helyeztük el (a legtöbb esetben pontosan erre is van szükség). Egyedi könyvtárakat a példányszintű CreateDirectory() metódussal hozhatunk létre. Az 62
Tároló létrehozása az IsolatedStorageFile objektummal
elszigetelt tárolóban az alkönyvtárakat nem képezhetjük le objektumokra, hanem olyan sztringeket kell átadnunk, amelyek a létrehozni kívánt könyvtárakat jelzik. Ha ez megvan, a CreateDirectory() metódus egy IsolatedStorageFile típussal tér vissza, amely a legbelső könyvtárhoz nyújt hozzáférését. Nézzük meg a következő kódot: private static void CreateStorageDirectories() { // Hagyományos és fordított perjelet is használhatunk. using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly()) { store.CreateDirectory(@"MyDir\XmlData"); store.CreateDirectory("MyDir\\BinaryData"); store.CreateDirectory("MyDir/TextData"); } }
Ha a fenti metódust meghívjuk a Main() függvényből, megtalálhatjuk a létrehozott könyvtárstruktúrát az elszigetelt tároló területén (lásd a 20.25. ábrát).
20.25. ábra: Könyvtárstruktúra létrehozása a megadott tárolóban
Forráskód A SimpleIsoStorage projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
63
20. fejezet: Fájlműveletek és elszigetelt tárolás
Az elszigetelt tároló működés közben: ClickOnce-telepítés Meglehet, hogy az elszigetelt tároló ez idáig pusztán a felhasználószintű adattárolás egyedi módjának tűnt (ez önmagában is hasznos dolog). Ám az egyik probléma, amit ez az API orvosol, az az, hogy a nem Full Trust jogosultsággal futó alkalmazások is biztonságosan tárolhassanak adatokat. Tegyük fel, hogy van egy Windows Forms-alkalmazásunk (neve: FileOrIsoStorageWinApp), amely egy két gombot tartalmazó űrlapból áll. (A Windows Forms API-ról részletesen a 27. fejezetben lesz szó, de az egyes Button típusok kattintási eseményét már itt is le fogjuk kezelni.) Az egyik gomb a helyi merevlemezre próbál adatot menteni hagyományos IO-megoldásokkal (ne felejtsük el importálni a System.IO és a System. IO.IsolatedStorage névtereket a kódfájlunkba): private void btnFileIO_Click(object sender, EventArgs e) { using (StreamWriter sw = new StreamWriter(@"C:\MyData.txt")) { sw.WriteLine("This is my data."); sw.WriteLine("Cool, huh?"); } }
A második gomb ugyanazt az adatot egy elszigetelt tárolóban lévő fájlba írja ki. A Click eseménykezelő megvalósítása a korábbi projektben már megírt WriteTextToIsoStorage() metódushoz hasonlít: private void btnIsoStorage_Click(object sender, EventArgs e) { // Elszigetelt tároló megnyitása szerelvény- és // felhasználói bizonyítékok alapján. using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly()) { // Létrehozzuk az IsolatedStorageFileStream típust. using (IsolatedStorageFileStream isStream = new IsolatedStorageFileStream("MyData.txt", FileMode.OpenOrCreate, store)) { // Az adatfolyamot ”becsomagoljuk” egy StreamWriter // objektumba, és kiírunk valamilyen szöveget.
64
Az elszigetelt tároló működés közben: ClickOnce-telepítés using (StreamWriter sw = new StreamWriter(isStream)) { sw.WriteLine("This is my data."); sw.WriteLine("Cool, huh?"); } } } }
Az IsolatedStorageFilePermission attribútum Mielőtt kipróbálnánk az alkalmazást, adjuk hozzá az alábbi using direktívát a kezdeti Form-leszármazott osztályt definiáló C#-fájlhoz: using System.Security.Permissions;
Ebben a névtérben számos olyan biztonsággal kapcsolatos, az alkalmazáson használható attribútum található, amelyek segítségével értesíthetjük a biztonsági alrendszert arról, hogy az adott szerelvény megfelelő működéséhez milyen biztonsági beállításokra van szükség (az egyéb részletek mellett). A példában arról értesítjük a CLR-t, hogy az alkalmazásunknak legalább szerelvényszintű, elszigetelt tárolási izolációs jogosultságra van szüksége. Ezt úgy érhetjük el, ha a következő szerelvényszintű attribútumot hozzáadjuk a Formleszármazott típusunk kódfájljához: [assembly: IsolatedStorageFilePermission(SecurityAction.RequestMinimum, UsageAllowed = IsolatedStorageContainment.AssemblyIsolationByUser)]
Ha a programot közvetlenül Visual Studio 2008-ban (a Ctrl + F5 billentyűkombinációval) fordítjuk le, és futtatjuk, azt tapasztaljuk, hogy bármelyik gombra is kattintunk, létrejön a megfelelő szöveges adatot tartalmazó új fájl. Ennek oka az, hogy az alkalmazást a My_Computer_Zone kódcsoportból töltöttük be, amely a Full Trust jogosultságot biztosítja a szerelvény számára.
A biztonsági zóna korlátozása Telepítsük úgy az alkalmazást, hogy az a jóval korlátozóbb Internet_Zone engedélykészlettel fusson. Ennek érdekében az alkalmazásunkat a ClickOnceszal telepítjük. A ClickOnce segítségével telepíthetünk távoli webkiszolgálóról futtatható alkalmazást a végfelhasználó számítógépére. 65
20. fejezet: Fájlműveletek és elszigetelt tárolás
A távoli alkalmazás egy IIS virtuális könyvtárban található, ahonnan egyszerűen az URL-böngészőben történő megadásával letölthetjük, és telepíthetjük a helyi számítógépre. Megjegyzés A ClickOnce teljes ismertetése túlmutat ennek a fejezetnek a keretein. Ha valaki még nem telepített ily módon alkalmazást, csak kövesse az alábbi utasításokat (és igény szerint olvassa el a .NET Framework 3.5 SDK dokumentációját).
Először nyissuk meg a projekt Properties lapját úgy, hogy a Solution Explorerben duplán kattintunk a Properties ikonra. Majd kattintsunk a Security fülre. Az alapértelmezés szerint a ClickOnce alkalmazások Full Trust engedéllyel települnek, ezért ugyanolyan biztonsági jogosultságokkal rendelkeznek, mint a hagyományosan telepített programok. Olyan telepítőszkriptet kell írnunk, amely arra kényszeríti a programot, hogy az Internet zónában fusson, amely nem enged hozzáférést a merevlemezhez hagyományos IO-műveletek felhasználásával.
20.26. ábra: Az alkalmazás biztonsági zónájának korlátozása
66
Az elszigetelt tároló működés közben: ClickOnce-telepítés
Ehhez kattintsunk az Enable ClickOnce Security Settings jelölőnégyzetre, válasszuk a This is a partial trust application rádiógombot, majd válasszuk az Internet lehetőséget a zónákat felsoroló legördülő listamezőből. Végül, de nem utolsósorban kattintsunk a Calculate Permissions gombra a biztonsági beállításokat tartalmazó oldal alján. Ez meghatározza az alkalmazás által igényelt végső engedélykészletet (lásd a 20.26. ábrát).
Az alkalmazás közzététele webkiszolgálón Kattintsunk a tulajdonságszerkesztőben a Publish fülre. Majd kattintsunk az oldal alsó részében található Publish Wizard gombra, és a varázsló első lapján írjuk be az alábbi URL-t, hogy létrehozzuk az alkalmazást tároló új IIS virtuális könyvtárat a helyi gépen: http://localhost/MyIsolatedStorageApp/
Az eszköz további beállításait alapértelmezett értéken hagyhatjuk. Mikor a Finish gombra kattintunk, a webböngésző betölti az automatikusan generált publish.htm fájlt. Ezt fogják látni a végfelhasználók is, amikor a letöltendő és a helyileg telepítendő alkalmazást tároló távoli webkiszolgálóra csatlakoznak (a weboldalt kedvünk szerint testre szabhatjuk; a fájlt és a kapcsolódó tartalmakat a projekt bin\Debug mappájában találjuk). Kattintsunk az Install gombra, majd futtassuk a setup.exe alkalmazást (és fogadjunk el minden biztonsági kérdést). Kis idő múlva az alkalmazás feltelepül a számítógépre, a ClickOnce gyorsítótárnak nevezett területre, és a program elindul.
Az eredmény megtekintése Mivel az alkalmazást úgy állítottuk be, hogy korlátozott környezetben fusson, ha arra a gombra kattintunk, amely a System.IO típusok használatával próbál adatot menteni, akkor a 20.27. ábrán lévő biztonsági kivétel fog megjelenni. Ellenben, ha arra a gombra kattintunk, amely az elszigetelt tároló segítségével ment adatot, az alkalmazás sikeresen lefut.
67
20. fejezet: Fájlműveletek és elszigetelt tárolás
20.27. ábra: Biztonsági veszély. Nem lehet hozzáférni a helyi fájlrendszerhez az Internet zónából
Forráskód A FileOrIsoStorageWinApp projektet a forráskódkönyvtárban a 20. fejezet alkönyvtára tartalmazza. A forráskódkönyvtárról lásd a Bevezetés xlv. oldalát.
Összefoglalás A fejezet elején megismertük a Directory(Info) és File(Info) típusok használatát. Megtudtuk, hogy ezek az osztályok lehetővé teszi a merevlemezen található fizikai fájlok és könyvtárak kezelését. Ezután megismerkedtünk több, az absztrakt Stream osztályból származó típussal, amelyek közül a FileStream típussal bővebben foglalkoztunk. Mivel a Stream-leszármazott típusok nyers byte-folyamokkal végeznek műveleteket, a System.IO névtér számos olyan olvasó/író típust (StreamWriter, StringWriter, BinaryWriter stb.) rendelkezésünkre bocsát, amelyek megkönnyítik ezt a folyamatot. Ezek során a DriveType által nyújtott funkcionalitást is megnéztük, és megvizsgáltuk, hogyan figyelhetünk meg fájlokat a FileSystemWatcher segítségével, és hogyan kezelhetjük az adatfolyamokat aszinkron módon. A fejezet második része az elszigetelt tárolót mutatta be. Ez az API lehetővé teszi a programok számára, hogy egy biztonságos virtuális környezetben végezzenek olvasási és írási műveleteket még akkor is, ha az alkalmazást korlátozott biztonsági környezetben töltötték be. Bár a programozási modell 68
Összefoglalás
meglehetősen egyértelmű (ha megértjük az alapvető fájlműveleteket), a kapcsolódó témakörök valamelyest megbonyolítják. Ennek fényében röviden áttekintettük a kódhozzáférés-szabályzást is. Ebben megtudtuk, hogy a szerelvények alkalmazástartományba való betöltésük során a CLR részére bizonyítékokat szolgáltatnak. Ez alapján kerülnek bele az alapértelmezett engedélykészletekkel rendelkező kódcsoportokba. A CAS fájlműveletek szempontjából fontos tulajdonsága, hogy ha az alkalmazás nem kap Full Trust jogosultságot, a hagyományos IO-műveletek biztonsági kivételt eredményeznek. Ezzel szemben az elszigetelt tároló segítségével a programok felhasználónként elszigetelten és biztonságosan tárolhatnak adatokat.
69