Inheritance: AssetMetadata, IModMetadata
        private void MigrateModJsonPreClientVersion2()
        {
            foreach (var mod in _parkitect.Assets[AssetType.Mod])
            {
                var metaPath = Path.Combine(mod.InstallationPath, "modinfo.meta");
                if (!File.Exists(metaPath))
                {
                    var oldData =
                        JsonConvert.DeserializeObject<OldModJsonFile>(
                            File.ReadAllText(Path.Combine(mod.InstallationPath, "mod.json")));

                    var meta = new ModMetadata
                    {
                        Tag = oldData.Tag,
                        Repository = oldData.Repository,
                        Id = null,
                        InstalledVersion = DateTime.Now
                    };

                    File.WriteAllText(metaPath, JsonConvert.SerializeObject(meta));
                }
            }
        }
        public async Task<IAsset> StoreAsset(IDownloadedAsset downloadedAsset)
        {
            switch (downloadedAsset.ApiAsset.Type)
            {
                case AssetType.Blueprint:
                case AssetType.Savegame:
                {
                    // Create the directory where the asset should be stored and create a path to where the asset should be stored.
                    var storagePath = _parkitect.Paths.GetAssetPath(downloadedAsset.ApiAsset.Type);
                    var assetPath = Path.Combine(storagePath, downloadedAsset.FileName);
                    Directory.CreateDirectory(storagePath);

                    _log.WriteLine($"Storing asset to {assetPath}.");

                    // If the file already exists, add a number behind the file name.
                    if (File.Exists(assetPath))
                    {
                        _log.WriteLine("Asset already exists, comparing hashes.");

                        // Compute hash of downloaded asset to match with installed hash.
                        var validHash = downloadedAsset.Stream.CreateMD5Checksum();

                        if (validHash.SequenceEqual(File.OpenRead(assetPath).CreateMD5Checksum()))
                        {
                            _log.WriteLine("Asset hashes match, aborting.");
                            return null;
                        }

                        _log.WriteLine("Asset hashes mismatch, computing new file name.");
                        // Separate the filename and the extension.
                        var attempt = 1;
                        var fileName = Path.GetFileNameWithoutExtension(downloadedAsset.FileName);
                        var fileExtension = Path.GetExtension(downloadedAsset.FileName);

                        // Update the path to where the the asset should be stored by adding a number behind the name until an available filename has been found.
                        do
                        {
                            assetPath = Path.Combine(storagePath, $"{fileName} ({++attempt}){fileExtension}");

                            if (File.Exists(assetPath) &&
                                validHash.SequenceEqual(File.OpenRead(assetPath).CreateMD5Checksum()))
                                return null;
                        } while (File.Exists(assetPath));

                        _log.WriteLine($"Newly computed path is {assetPath}.");
                    }

                    _log.WriteLine("Writing asset to file.");
                    // Write the stream to a file at the asset path.
                    using (var fileStream = File.Create(assetPath))
                    {
                        downloadedAsset.Stream.Seek(0, SeekOrigin.Begin);
                        await downloadedAsset.Stream.CopyToAsync(fileStream);
                    }

                    var meta = new AssetMetadata
                    {
                        Id = downloadedAsset.ApiAsset.Id
//                        InstalledVersion = downloadedAsset.ApiAsset.UpdatedAt
                    };// TODO: Re-add installed version

                    _assetMetadataStorage.StoreMetadata(downloadedAsset.ApiAsset.Type, assetPath, meta);
                    var cachedData =
                        await _assetCachedDataStorage.GetData(downloadedAsset.ApiAsset.Type, meta, assetPath);

                    Asset createdAsset = null;
                    switch (downloadedAsset.ApiAsset.Type)
                    {
                        case AssetType.Blueprint:
                            createdAsset = new BlueprintAsset(assetPath, meta, cachedData);
                            break;
                        case AssetType.Savegame:
                            createdAsset = new SavegameAsset(assetPath, meta, cachedData as AssetWithImageCachedData);
                            break;
                    }

                    OnAssetAdded(new AssetEventArgs(createdAsset));
                    return createdAsset;
                }
                case AssetType.Mod:
                {
                    _log.WriteLine("Attempting to open mod stream.");
                    using (var zip = new ZipArchive(downloadedAsset.Stream, ZipArchiveMode.Read))
                    {
                        // Compute name of main directory inside archive.
                        var mainFolder = zip.Entries.FirstOrDefault()?.FullName;
                        if (mainFolder == null)
                            throw new Exception("invalid archive");

                        _log.WriteLine($"Mod archive main folder is {mainFolder}.");

                        // Find the mod.json file. Yay for / \ path divider compatibility.
                        var modJsonPath = Path.Combine(mainFolder, "mod.json").Replace('/', '\\');
                        var modJson = zip.Entries.FirstOrDefault(e => e.FullName.Replace('/', '\\') == modJsonPath);

                        // Read mod.json.
                        if (modJson == null) throw new Exception("mod is missing mod.json file");
                        using (var streamReader = new StreamReader(modJson.Open()))
                        {
                            var modInformationString = await streamReader.ReadToEndAsync();
                            var modInformation = JsonConvert.DeserializeObject<ModInformation>(modInformationString);

                            var meta = new ModMetadata
                            {
                                Id = downloadedAsset.ApiAsset.Id,
//                                InstalledVersion = downloadedAsset.ApiAsset.UpdatedAt,
                                Tag = downloadedAsset.Info.Tag,
                                Repository = downloadedAsset.Info.Repository
                            }; // TODO: Re-add installed version

                            var installationPath = Path.Combine(_parkitect.Paths.GetAssetPath(AssetType.Mod),
                                downloadedAsset.Info.Repository.Replace('/', '@'));

                            // TODO: Should actually try and look if the mod has been updated since and delete the whole folder.
                            if (Directory.Exists(installationPath))
                            {
                                if (File.Exists(Path.Combine(installationPath, "modinfo.meta")))
                                    File.Delete(Path.Combine(installationPath, "modinfo.meta"));
                                if (File.Exists(Path.Combine(installationPath, "moddata.cache")))
                                    File.Delete(Path.Combine(installationPath, "moddata.cache"));
                            }

                            _log.WriteLine($"mod.json was deserialized to mod object '{modInformation}'.");

                            // Set default mod properties.
                            modInformation.IsEnabled = true;
                            modInformation.IsDevelopment = false;

                            // Find previous version of mod.
                            // TODO uninstall previous versions

                            // Install mod.
                            _log.WriteLine("Copying mod to mods folder.");
                            foreach (var entry in zip.Entries)
                            {
                                if (!entry.FullName.StartsWith(mainFolder))
                                    continue;

                                // Compute path.
                                var partDir = entry.FullName.Substring(mainFolder.Length);
                                var path = Path.Combine(installationPath, partDir);

                                if (partDir == "moddata.cache" || partDir == "modinfo.meta")
                                    continue;

                                if (string.IsNullOrEmpty(entry.Name))
                                {
                                    _log.WriteLine($"Creating directory '{path}'.");
                                    Directory.CreateDirectory(path);
                                }
                                else
                                {
                                    _log.WriteLine($"Storing mod file '{path}'.");
                                    using (var openStream = entry.Open())
                                    using (var fileStream = File.OpenWrite(path))
                                        await openStream.CopyToAsync(fileStream);
                                }
                            }

                            _log.WriteLine("Register installation to API.");
                            _website.API.RegisterDownload(downloadedAsset.ApiAsset.Id);

                            _assetMetadataStorage.StoreMetadata(downloadedAsset.ApiAsset.Type, installationPath, meta);
                            var cachedData = await _assetCachedDataStorage.GetData(downloadedAsset.ApiAsset.Type, meta,
                                installationPath);

                            modInformationString = JsonConvert.SerializeObject(modInformation);
                            File.WriteAllText(Path.Combine(installationPath, "mod.json"), modInformationString);

                            var createdAsset = new ModAsset(installationPath, meta,
                                cachedData as AssetWithImageCachedData, modInformation);
                            OnAssetAdded(new AssetEventArgs(createdAsset));

                            return createdAsset;
                        }
                    }
                }
                default:
                    throw new Exception("unknown asset type");
            }
        }