Anytime users save or load something in an application or game, the program typically reads or writes files using the disk as a means of saving or restoring these "states." However, occasionally you cannot use a single file to store information. For instance, you may want to organize the current state into various groups, such as base text data, formatting, metadata, and so on. For that, the appropriate tool is a file package.
In this tutorial, I will explain how to generate and read packages that contain multiple files using C#. Hopefully, by the end of this guide, you will be able to understand the basics of file packages and be able to design customized packages, if need be.
Before we dive into the code I will give an example of a package file so we understand what we are trying to create. Various kinds of software use file packages to save and load states. One such example is Microsoft Office. If you played around or tried to restore an Office file before, you might have found that you can open a file like this:
We perform steps 1 and 2:
Then we open the folder:
As you can see here, a Word file contains files and directories and it is actually acts as a wrapper of the files actually read and written by Office. This all forms a file package.
Now we know what file packages are, we can start writing the code for our packaging utility.
Our packaging utility will have 3 classes.
FilePackage
class to hold information about our file packages.FilePackageWriter
class to write file packages.FilePackageReader
class to read file packages.Let's get started by creating our first class.
1public class FilePackage
2{
3 public string FilePath { get; set; }
4 public IEnumerable<string> ContentFilePathList { get; set; }
5}
This class has two properties; FilePath
to hold the file path of the package and ContentFilePathList
to hold the file paths of the contents of the file package. Now let's create our FilePackageWriter
class to actually write our package.
1public class FilePackageWriter
2{
3 private readonly string _filepath;
4 private readonly IEnumerable<string> _contentFilePathList;
5 private string _tempDirectoryPath;
6
7 public FilePackageWriter(FilePackage filePackage)
8 {
9 _filepath = filePackage.FilePath;
10 _contentFilePathList = filePackage.ContentFilePathList;
11 }
12
13 public void GeneratePackage(bool deleteContents)
14 {
15 try
16 {
17 string parentDirectoryPath = null;
18 string filename = null;
19
20 var fileInfo = new FileInfo(_filepath);
21
22 // Get the parent directory path of the package file and if the package file already exists delete it
23 if (fileInfo.Exists)
24 {
25 filename = fileInfo.Name;
26
27 var parentDirectoryInfo = fileInfo.Directory;
28 if (parentDirectoryInfo != null)
29 {
30 parentDirectoryPath = parentDirectoryInfo.FullName;
31 }
32 else
33 {
34 throw new NullReferenceException("Parent directory info was null!");
35 }
36
37 File.Delete(_filepath);
38 }
39 else
40 {
41 var lastIndexOfFileSeperator = _filepath.LastIndexOf("\\", StringComparison.Ordinal);
42 if (lastIndexOfFileSeperator != -1)
43 {
44 parentDirectoryPath = _filepath.Substring(0, lastIndexOfFileSeperator);
45 filename = _filepath.Substring(lastIndexOfFileSeperator + 1,_filepath.Length - (lastIndexOfFileSeperator + 1));
46 }
47 else
48 {
49 throw new Exception("The input file path '" + _filepath +
50 "' does not contain any file seperators.");
51 }
52 }
53
54 // Create a temp directory for our package
55 _tempDirectoryPath = parentDirectoryPath + "\\" + filename + "_temp";
56 if (Directory.Exists(_tempDirectoryPath))
57 {
58 Directory.Delete(_tempDirectoryPath, true);
59 }
60
61 Directory.CreateDirectory(_tempDirectoryPath);
62 foreach (var filePath in _contentFilePathList)
63 {
64 // Copy every content file into the temp directory we created before
65 var filePathInfo = new FileInfo(filePath);
66 if (filePathInfo.Exists)
67 {
68 File.Copy(filePathInfo.FullName, _tempDirectoryPath + "\\" + filePathInfo.Name);
69 }
70 else
71 {
72 throw new FileNotFoundException("File path " + filePath + " doesn't exist!");
73 }
74 }
75 // Generate the ZIP from the temp directory
76 ZipFile.CreateFromDirectory(_tempDirectoryPath, _filepath);
77 }
78 catch (Exception e)
79 {
80 var errorMessage = "An error occured while generating the package. " + e.Message;
81 throw new Exception(errorMessage);
82 }
83 finally
84 {
85 // Clear the temp directory and the content files
86 if (Directory.Exists(_tempDirectoryPath))
87 {
88 Directory.Delete(_tempDirectoryPath, true);
89 }
90
91 if (deleteContents)
92 {
93 foreach (var filePath in _contentFilePathList)
94 {
95 if (File.Exists(filePath))
96 {
97 File.Delete(filePath);
98 }
99 }
100 }
101 }
102 }
103}
In this class we simply take the FilePackage
and use it to generate our package.
deleteContents
parameter as true
.Now that we can write our packages, let's write the code to read them as well.
1public class FilePackageReader
2{
3 private Dictionary<string, string> _filenameFileContentDictionary;
4 private readonly string _filepath;
5
6 public FilePackageReader(string filepath)
7 {
8 _filepath = filepath;
9 }
10
11 public Dictionary<string, string> GetFilenameFileContentDictionary()
12 {
13 try
14 {
15 _filenameFileContentDictionary = new Dictionary<string, string>();
16
17 // Open the package file
18 using (var fs = new FileStream(_filepath, FileMode.Open))
19 {
20 // Open the package file as a ZIP
21 using (var archive = new ZipArchive(fs))
22 {
23 // Iterate through the content files and add them to a dictionary
24 foreach (var zipArchiveEntry in archive.Entries)
25 {
26 using (var stream = zipArchiveEntry.Open())
27 {
28 using (var zipSr = new StreamReader(stream))
29 {
30 _filenameFileContentDictionary.Add(zipArchiveEntry.Name, zipSr.ReadToEnd());
31 }
32 }
33 }
34 }
35 }
36
37 return _filenameFileContentDictionary;
38 }
39 catch (Exception e)
40 {
41 var errorMessage = "Unable to open/read the package. " + e.Message;
42 throw new Exception(errorMessage);
43 }
44 }
45}
In the reader:
FileStream
and a ZipArchive
.Dictionary
.Our FilePackageWriter and FilePackageReader are finally ready. Now we can test our code and see if it works!
1var test1FilePath = AppDomain.CurrentDomain.BaseDirectory + "PackageTest\\test_1.txt";
2var test2FilePath = AppDomain.CurrentDomain.BaseDirectory + "PackageTest\\test_2.txt";
3
4using (var sw = new StreamWriter(test1FilePath))
5{
6 sw.WriteLine("test1");
7}
8
9using (var sw = new StreamWriter(test2FilePath))
10{
11 sw.WriteLine("test2");
12}
13
14var packageFilePath = AppDomain.CurrentDomain.BaseDirectory + "PackageTest\\test.pkg";
15
16var filePackage = new FilePackage
17{
18 FilePath = packageFilePath,
19 ContentFilePathList = new List<string>
20 {
21 test1FilePath, test2FilePath
22 }
23};
24
25var filePackageWriter = new FilePackageWriter(filePackage);
26filePackageWriter.GeneratePackage(true);
27
28var filePackageReader = new FilePackageReader(packageFilePath);
29var filenameFileContentDictionary = filePackageReader.GetFilenameFileContentDictionary();
30
31foreach (var keyValuePair in filenameFileContentDictionary)
32{
33 Console.WriteLine("Filename: " + keyValuePair.Key);
34 Console.WriteLine("Content: " + keyValuePair.Value);
35}
The output is:
1Filename: test_1.txt
2Content: test1
3
4Filename: test_2.txt
5Content: test2
We can also see it in the file system:
This means that our packaging algorithms worked!
This guide explained how to generate file packages. Packaging is worthwhile when your application needs to read or write to multiple files when saving or restoring states. File packaging can eliminate unnecessary folders for each of your save files. Instead, your files stay neatly tucked in a single file that you can use to load or save states. This approach is also helpful to users because moving saved files into backup drives or sending them via e-mail becomes a streamlined process that no longer entails searching through folders.
I hope this guide will be useful for your projects. Happy coding!