Introduction
I have a web site built on .NET Core 2.1 and I want it to be GDPR compliant, so I need to provide a way to download the user data. In this site, each user can have files in a subdirectory inside the root, in addition to the information saved in the database. I wanted to be able to provide a zip containing all his files and a dictionary with all his personal data.
Background
This link provides an example of dynamically generating a zip file with minimal threading and memory usage.
.NET Core 2.1 provides you with templates generated code to comply with GDPR, you can read more here.
Using the Code
First, I created the required fileCallbackResult
and WriteOnlyStreamWrapper
classes following the first link of the background.
In the DownloadPersonalData
Razor Page generated by .NET Core, the personal information from the ApplicationUser
is added to a dictionary and download as a JSON. I wanted to be able to download a zip file containing any information and existing file belonging to the user.
In order to do that, I added the following methods:
private async Task<Dictionary<string, string>> ReadClient(ApplicationUser user, string UserId)
{
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(ApplicationUser).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var p in personalDataProps)
{
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
}
User client = await _context.User.AsNoTracking().FirstOrDefaultAsync(b => b.UserID == UserId);
personalData.Add("ClientName", client.Name);
personalData.Add("ClientEmail", client.Email);
......
return personalData;
}
This method builds the dictionary containing all data from the database.
private Dictionary<string, string> ListClientFiles(string UserId)
{
var clientpath = GetClientPath(UserId);
var filenamesAndUrls = new Dictionary<string, string>();
if (Directory.Exists(Path.GetDirectoryName(clientpath)))
{
DirectoryInfo d = new DirectoryInfo(clientpath);
FileInfo[] Files = d.GetFiles("*.*", SearchOption.AllDirectories);
foreach (FileInfo file in Files)
{
string fileName = Path.GetFileNameWithoutExtension(file.Name);
string fileExtension = file.Extension;
if (filenamesAndUrls.ContainsKey(fileName + fileExtension)) {
int copy = 0;
do {
copy++;
} while (filenamesAndUrls.ContainsKey(fileNname + "(" + copy + ")" + fileExtension));
fileName = fileName +"("+copy+")";
}
fileName = fileName + fileExtension;
filenamesAndUrls.Add(fileName, file.FullName);
}
}
return filenamesAndUrls;
}
This method creates a dictionary with the keys and full path of the files to be zip.
Finally, I modify the OnPostAsync
method in DownloadPersonalData
Razor Page to do the following:
Dictionary<string, string> personalData = await ReadClient(user, UserId);
var filenamesAndUrls = ListClientFiles(UserId);
return new FileCallbackResult(new MediaTypeHeaderValue("application/octet-stream"),
async (outputStream, _) =>
{
using (var zipArchive = new ZipArchive(new WriteOnlyStreamWrapper(outputStream),
ZipArchiveMode.Create))
{
var userZipEntry = zipArchive.CreateEntry("personaldata.json");
using (var userZipStream = userZipEntry.Open())
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes
(JsonConvert.SerializeObject(personalData))) )
await stream.CopyToAsync(userZipStream);
foreach (var kvp in filenamesAndUrls)
{
var zipEntry = zipArchive.CreateEntry(kvp.Key);
using (var zipStream = zipEntry.Open())
using (var stream = System.IO.File.OpenRead(kvp.Value))
await stream.CopyToAsync(zipStream);
}
}
})
{
FileDownloadName = "DownloadPersonalData.zip"
};
So, the dictionary containing the personal data - or any other pertinent data coming from the database - is inserted to the zip as a zip entry through a memory stream without needing to save it in a temporary file. If you rather, you could create many entries in the zip for the different information in different tables in the database.
The other personal files are read and inserted to the zip as other zip entries.
History
- 24th September, 2018: Initial version