summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-07-07 00:29:22 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-09-14 18:47:16 -0400
commite00fb85ee7822bc7fed2d6bd5a2e4c207a799418 (patch)
tree6ae80165b86d8ce20d4b44a3fa05ceb4d8433301
parentf18ad1210cd813d6ddff665841ac712d62d18b1f (diff)
downloadSMAPI-e00fb85ee7822bc7fed2d6bd5a2e4c207a799418.tar.gz
SMAPI-e00fb85ee7822bc7fed2d6bd5a2e4c207a799418.tar.bz2
SMAPI-e00fb85ee7822bc7fed2d6bd5a2e4c207a799418.zip
migrate compatibility list's wiki data to MongoDB cache (#651)
-rw-r--r--docs/technical/web.md68
-rw-r--r--src/SMAPI.Web/Controllers/ModsController.cs37
-rw-r--r--src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs19
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs43
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs188
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs35
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs73
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs36
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj4
-rw-r--r--src/SMAPI.Web/Startup.cs12
-rw-r--r--src/SMAPI.Web/appsettings.Development.json6
-rw-r--r--src/SMAPI.Web/appsettings.json6
12 files changed, 487 insertions, 40 deletions
diff --git a/docs/technical/web.md b/docs/technical/web.md
index 978114ef..db54a87a 100644
--- a/docs/technical/web.md
+++ b/docs/technical/web.md
@@ -82,25 +82,49 @@ For example:
```
## For SMAPI developers
-### Local development
-`SMAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within
-Visual Studio to run a local version.
-
-There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the
-subdomain portion becomes a route (e.g. `log.smapi.io` &rarr; `localhost:59482/log`).
-
-Before running it locally, you need to enter your credentials in the `appsettings.Development.json`
-file. See the next section for a description of each setting. This file is listed in `.gitignore`
-to prevent accidentally committing credentials.
-
-### Deploying to Amazon Beanstalk
-The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the
-environment, make sure to specify the following environment properties:
-
-property name | description
-------------------------------- | -----------------
-`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API.
-`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously.
-`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`.
-`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info.
-`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info.
+### Local environment
+A local environment lets you run a complete copy of the web project (including cache database) on
+your machine, with no external dependencies aside from the actual mod sites.
+
+Initial setup:
+
+1. [Install MongoDB](https://docs.mongodb.com/manual/administration/install-community/) and add its
+ `bin` folder to the system PATH.
+2. Create a local folder for the MongoDB data (e.g. `C:\dev\smapi-cache`).
+3. Enter your credentials in the `appsettings.Development.json` file. You can leave the MongoDB
+ credentials as-is to use the default local instance; see the next section for the other settings.
+
+To launch the environment:
+1. Launch MongoDB from a terminal (change the data path if applicable):
+ ```sh
+ mongod --dbpath C:\dev\smapi-cache
+ ```
+2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site.
+ <small>(Local URLs will use HTTP instead of HTTPS, and subdomains will become routes, like
+ `log.smapi.io` &rarr; `localhost:59482/log`.)</small>
+
+### Production environment
+A production environment includes the web servers and cache database hosted online for public
+access. This section assumes you're creating a new production environment from scratch (not using
+the official live environment).
+
+Initial setup:
+
+1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)).
+2. Create an AWS Beanstalk .NET environment with these environment properties:
+
+ property name | description
+ ------------------------------- | -----------------
+ `LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API.
+ `LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously.
+ `LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`.
+ `ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info.
+ `ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info.
+ `MongoDB:Host` | The hostname for the MongoDB instance.
+ `MongoDB:Username` | The login username for the MongoDB instance.
+ `MongoDB:Password` | The login password for the MongoDB instance.
+
+To deploy updates:
+1. Deploy the web project using [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/).
+2. If the MongoDB schema changed, delete the collections in the MongoDB database. (They'll be
+ recreated automatically.)
diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs
index ca866a8d..b6040e06 100644
--- a/src/SMAPI.Web/Controllers/ModsController.cs
+++ b/src/SMAPI.Web/Controllers/ModsController.cs
@@ -1,12 +1,10 @@
-using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Toolkit;
-using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+using StardewModdingAPI.Web.Framework.Caching.Wiki;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.ViewModels;
@@ -19,7 +17,7 @@ namespace StardewModdingAPI.Web.Controllers
** Fields
*********/
/// <summary>The cache in which to store mod metadata.</summary>
- private readonly IMemoryCache Cache;
+ private readonly IWikiCacheRepository Cache;
/// <summary>The number of minutes successful update checks should be cached before refetching them.</summary>
private readonly int CacheMinutes;
@@ -31,7 +29,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Construct an instance.</summary>
/// <param name="cache">The cache in which to store mod metadata.</param>
/// <param name="configProvider">The config settings for mod update checks.</param>
- public ModsController(IMemoryCache cache, IOptions<ModCompatibilityListConfig> configProvider)
+ public ModsController(IWikiCacheRepository cache, IOptions<ModCompatibilityListConfig> configProvider)
{
ModCompatibilityListConfig config = configProvider.Value;
@@ -54,21 +52,24 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Asynchronously fetch mod metadata from the wiki.</summary>
public async Task<ModListModel> FetchDataAsync()
{
- return await this.Cache.GetOrCreateAsync($"{nameof(ModsController)}_mod_list", async entry =>
+ // refresh cache
+ CachedWikiMod[] mods;
+ if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata) || this.Cache.IsStale(metadata.LastUpdated, this.CacheMinutes))
{
- WikiModList data = await new ModToolkit().GetWikiCompatibilityListAsync();
- ModListModel model = new ModListModel(
- stableVersion: data.StableVersion,
- betaVersion: data.BetaVersion,
- mods: data
- .Mods
- .Select(mod => new ModModel(mod))
- .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting
- );
+ var wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync();
+ this.Cache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out metadata, out mods);
+ }
+ else
+ mods = this.Cache.GetWikiMods().ToArray();
- entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes);
- return model;
- });
+ // build model
+ return new ModListModel(
+ stableVersion: metadata.StableVersion,
+ betaVersion: metadata.BetaVersion,
+ mods: mods
+ .Select(mod => new ModModel(mod.GetModel()))
+ .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting
+ );
}
}
}
diff --git a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs
new file mode 100644
index 00000000..904455c5
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace StardewModdingAPI.Web.Framework.Caching
+{
+ /// <summary>The base logic for a cache repository.</summary>
+ internal abstract class BaseCacheRepository
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Whether cached data is stale.</summary>
+ /// <param name="lastUpdated">The date when the data was updated.</param>
+ /// <param name="cacheMinutes">The age in minutes before data is considered stale.</param>
+ public bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes)
+ {
+ return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-cacheMinutes);
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs
new file mode 100644
index 00000000..de1ea9db
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using MongoDB.Bson;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Wiki
+{
+ /// <summary>The model for cached wiki metadata.</summary>
+ public class CachedWikiMetadata
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The internal MongoDB ID.</summary>
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")]
+ public ObjectId _id { get; set; }
+
+ /// <summary>When the data was last updated.</summary>
+ public DateTimeOffset LastUpdated { get; set; }
+
+ /// <summary>The current stable Stardew Valley version.</summary>
+ public string StableVersion { get; set; }
+
+ /// <summary>The current beta Stardew Valley version.</summary>
+ public string BetaVersion { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public CachedWikiMetadata() { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="stableVersion">The current stable Stardew Valley version.</param>
+ /// <param name="betaVersion">The current beta Stardew Valley version.</param>
+ public CachedWikiMetadata(string stableVersion, string betaVersion)
+ {
+ this.StableVersion = stableVersion;
+ this.BetaVersion = betaVersion;
+ this.LastUpdated = DateTimeOffset.UtcNow;;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs
new file mode 100644
index 00000000..f4331f8d
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using MongoDB.Bson;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Wiki
+{
+ /// <summary>The model for cached wiki mods.</summary>
+ public class CachedWikiMod
+ {
+ /*********
+ ** Accessors
+ *********/
+ /****
+ ** Tracking
+ ****/
+ /// <summary>The internal MongoDB ID.</summary>
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")]
+ public ObjectId _id { get; set; }
+
+ /// <summary>When the data was last updated.</summary>
+ public DateTimeOffset LastUpdated { get; set; }
+
+ /****
+ ** Mod info
+ ****/
+ /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order.</summary>
+ public string[] ID { get; set; }
+
+ /// <summary>The mod's display name. If the mod has multiple names, the first one is the most canonical name.</summary>
+ public string[] Name { get; set; }
+
+ /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
+ public string[] Author { get; set; }
+
+ /// <summary>The mod ID on Nexus.</summary>
+ public int? NexusID { get; set; }
+
+ /// <summary>The mod ID in the Chucklefish mod repo.</summary>
+ public int? ChucklefishID { get; set; }
+
+ /// <summary>The mod ID in the ModDrop mod repo.</summary>
+ public int? ModDropID { get; set; }
+
+ /// <summary>The GitHub repository in the form 'owner/repo'.</summary>
+ public string GitHubRepo { get; set; }
+
+ /// <summary>The URL to a non-GitHub source repo.</summary>
+ public string CustomSourceUrl { get; set; }
+
+ /// <summary>The custom mod page URL (if applicable).</summary>
+ public string CustomUrl { get; set; }
+
+ /// <summary>The name of the mod which loads this content pack, if applicable.</summary>
+ public string ContentPackFor { get; set; }
+
+ /// <summary>The human-readable warnings for players about this mod.</summary>
+ public string[] Warnings { get; set; }
+
+ /// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
+ public string Anchor { get; set; }
+
+ /****
+ ** Stable compatibility
+ ****/
+ /// <summary>The compatibility status.</summary>
+ public WikiCompatibilityStatus MainStatus { get; set; }
+
+ /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
+ public string MainSummary { get; set; }
+
+ /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
+ public string MainBrokeIn { get; set; }
+
+ /// <summary>The version of the latest unofficial update, if applicable.</summary>
+ public string MainUnofficialVersion { get; set; }
+
+ /// <summary>The URL to the latest unofficial update, if applicable.</summary>
+ public string MainUnofficialUrl { get; set; }
+
+ /****
+ ** Beta compatibility
+ ****/
+ /// <summary>The compatibility status.</summary>
+ public WikiCompatibilityStatus? BetaStatus { get; set; }
+
+ /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
+ public string BetaSummary { get; set; }
+
+ /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
+ public string BetaBrokeIn { get; set; }
+
+ /// <summary>The version of the latest unofficial update, if applicable.</summary>
+ public string BetaUnofficialVersion { get; set; }
+
+ /// <summary>The URL to the latest unofficial update, if applicable.</summary>
+ public string BetaUnofficialUrl { get; set; }
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public CachedWikiMod() { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mod">The mod data.</param>
+ public CachedWikiMod(WikiModEntry mod)
+ {
+ // tracking
+ this.LastUpdated = DateTimeOffset.UtcNow;
+
+ // mod info
+ this.ID = mod.ID;
+ this.Name = mod.Name;
+ this.Author = mod.Author;
+ this.NexusID = mod.NexusID;
+ this.ChucklefishID = mod.ChucklefishID;
+ this.ModDropID = mod.ModDropID;
+ this.GitHubRepo = mod.GitHubRepo;
+ this.CustomSourceUrl = mod.CustomSourceUrl;
+ this.CustomUrl = mod.CustomUrl;
+ this.ContentPackFor = mod.ContentPackFor;
+ this.Warnings = mod.Warnings;
+ this.Anchor = mod.Anchor;
+
+ // stable compatibility
+ this.MainStatus = mod.Compatibility.Status;
+ this.MainSummary = mod.Compatibility.Summary;
+ this.MainBrokeIn = mod.Compatibility.BrokeIn;
+ this.MainUnofficialVersion = mod.Compatibility.UnofficialVersion?.ToString();
+ this.MainUnofficialUrl = mod.Compatibility.UnofficialUrl;
+
+ // beta compatibility
+ this.BetaStatus = mod.BetaCompatibility?.Status;
+ this.BetaSummary = mod.BetaCompatibility?.Summary;
+ this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn;
+ this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString();
+ this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl;
+ }
+
+ /// <summary>Reconstruct the original model.</summary>
+ public WikiModEntry GetModel()
+ {
+ var mod = new WikiModEntry
+ {
+ ID = this.ID,
+ Name = this.Name,
+ Author = this.Author,
+ NexusID = this.NexusID,
+ ChucklefishID = this.ChucklefishID,
+ ModDropID = this.ModDropID,
+ GitHubRepo = this.GitHubRepo,
+ CustomSourceUrl = this.CustomSourceUrl,
+ CustomUrl = this.CustomUrl,
+ ContentPackFor = this.ContentPackFor,
+ Warnings = this.Warnings,
+ Anchor = this.Anchor,
+
+ // stable compatibility
+ Compatibility = new WikiCompatibilityInfo
+ {
+ Status = this.MainStatus,
+ Summary = this.MainSummary,
+ BrokeIn = this.MainBrokeIn,
+ UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null,
+ UnofficialUrl = this.MainUnofficialUrl
+ }
+ };
+
+ // beta compatibility
+ if (this.BetaStatus != null)
+ {
+ mod.BetaCompatibility = new WikiCompatibilityInfo
+ {
+ Status = this.BetaStatus.Value,
+ Summary = this.BetaSummary,
+ BrokeIn = this.BetaBrokeIn,
+ UnofficialVersion = this.BetaUnofficialVersion != null ? new SemanticVersion(this.BetaUnofficialVersion) : null,
+ UnofficialUrl = this.BetaUnofficialUrl
+ };
+ }
+
+ return mod;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
new file mode 100644
index 00000000..d319db69
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Wiki
+{
+ /// <summary>Encapsulates logic for accessing the mod data cache.</summary>
+ internal interface IWikiCacheRepository
+ {
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get the cached wiki metadata.</summary>
+ /// <param name="metadata">The fetched metadata.</param>
+ bool TryGetWikiMetadata(out CachedWikiMetadata metadata);
+
+ /// <summary>Whether cached data is stale.</summary>
+ /// <param name="lastUpdated">The date when the data was updated.</param>
+ /// <param name="cacheMinutes">The age in minutes before data is considered stale.</param>
+ bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes);
+
+ /// <summary>Get the cached wiki mods.</summary>
+ /// <param name="filter">A filter to apply, if any.</param>
+ IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null);
+
+ /// <summary>Save data fetched from the wiki compatibility list.</summary>
+ /// <param name="stableVersion">The current stable Stardew Valley version.</param>
+ /// <param name="betaVersion">The current beta Stardew Valley version.</param>
+ /// <param name="mods">The mod data.</param>
+ /// <param name="cachedMetadata">The stored metadata record.</param>
+ /// <param name="cachedMods">The stored mod records.</param>
+ void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods);
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs
new file mode 100644
index 00000000..5b907462
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using MongoDB.Driver;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Wiki
+{
+ /// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
+ internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The collection for wiki metadata.</summary>
+ private readonly IMongoCollection<CachedWikiMetadata> WikiMetadata;
+
+ /// <summary>The collection for wiki mod data.</summary>
+ private readonly IMongoCollection<CachedWikiMod> WikiMods;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="database">The authenticated MongoDB database.</param>
+ public WikiCacheRepository(IMongoDatabase database)
+ {
+ // get collections
+ this.WikiMetadata = database.GetCollection<CachedWikiMetadata>("wiki-metadata");
+ this.WikiMods = database.GetCollection<CachedWikiMod>("wiki-mods");
+
+ // add indexes if needed
+ this.WikiMods.Indexes.CreateOne(new CreateIndexModel<CachedWikiMod>(Builders<CachedWikiMod>.IndexKeys.Text(p => p.ID)));
+ }
+
+ /// <summary>Get the cached wiki metadata.</summary>
+ /// <param name="metadata">The fetched metadata.</param>
+ public bool TryGetWikiMetadata(out CachedWikiMetadata metadata)
+ {
+ metadata = this.WikiMetadata.Find("{}").FirstOrDefault();
+ return metadata != null;
+ }
+
+ /// <summary>Get the cached wiki mods.</summary>
+ /// <param name="filter">A filter to apply, if any.</param>
+ public IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null)
+ {
+ return filter != null
+ ? this.WikiMods.Find(filter).ToList()
+ : this.WikiMods.Find("{}").ToList();
+ }
+
+ /// <summary>Save data fetched from the wiki compatibility list.</summary>
+ /// <param name="stableVersion">The current stable Stardew Valley version.</param>
+ /// <param name="betaVersion">The current beta Stardew Valley version.</param>
+ /// <param name="mods">The mod data.</param>
+ /// <param name="cachedMetadata">The stored metadata record.</param>
+ /// <param name="cachedMods">The stored mod records.</param>
+ public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods)
+ {
+ cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion);
+ cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray();
+
+ this.WikiMods.DeleteMany("{}");
+ this.WikiMods.InsertMany(cachedMods);
+
+ this.WikiMetadata.DeleteMany("{}");
+ this.WikiMetadata.InsertOne(cachedMetadata);
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs
new file mode 100644
index 00000000..352eb960
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>The config settings for mod compatibility list.</summary>
+ internal class MongoDbConfig
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The MongoDB hostname.</summary>
+ public string Host { get; set; }
+
+ /// <summary>The MongoDB username (if any).</summary>
+ public string Username { get; set; }
+
+ /// <summary>The MongoDB password (if any).</summary>
+ public string Password { get; set; }
+
+
+ /*********
+ ** Public method
+ *********/
+ /// <summary>Get the MongoDB connection string.</summary>
+ /// <param name="authDatabase">The initial database for which to authenticate.</param>
+ public string GetConnectionString(string authDatabase)
+ {
+ bool isLocal = this.Host == "localhost";
+ bool hasLogin = !string.IsNullOrWhiteSpace(this.Username) && !string.IsNullOrWhiteSpace(this.Password);
+
+ return $"mongodb{(isLocal ? "" : "+srv")}://"
+ + (hasLogin ? $"{Uri.EscapeDataString(this.Username)}:{Uri.EscapeDataString(this.Password)}@" : "")
+ + $"{this.Host}/{authDatabase}retryWrites=true&w=majority";
+ }
+ }
+}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index b0aaa8a9..90641611 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -23,6 +23,7 @@
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
+ <PackageReference Include="MongoDB.Driver" Version="2.8.1" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
@@ -30,6 +31,9 @@
</ItemGroup>
<Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
<ItemGroup>
+ <None Include="..\..\docs\technical\web.md" Link="web.md" />
+ </ItemGroup>
+ <ItemGroup>
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index b409a81d..0a8d23a8 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -6,9 +6,11 @@ using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using MongoDB.Driver;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Web.Framework;
+using StardewModdingAPI.Web.Framework.Caching.Wiki;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
@@ -53,6 +55,7 @@ namespace StardewModdingAPI.Web
.Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
.Configure<SiteConfig>(this.Configuration.GetSection("Site"))
+ .Configure<MongoDbConfig>(this.Configuration.GetSection("MongoDB"))
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
.AddMemoryCache()
.AddMvc()
@@ -108,6 +111,15 @@ namespace StardewModdingAPI.Web
devKey: api.PastebinDevKey
));
}
+
+ // init MongoDB
+ {
+ MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get<MongoDbConfig>();
+ string connectionString = mongoConfig.GetConnectionString("smapi");
+
+ services.AddSingleton<IMongoDatabase>(serv => new MongoClient(connectionString).GetDatabase("smapi"));
+ services.AddSingleton<IWikiCacheRepository>(serv => new WikiCacheRepository(serv.GetService<IMongoDatabase>()));
+ }
}
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index 49234a3b..9b0ec535 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -31,5 +31,11 @@
"PastebinUserKey": null,
"PastebinDevKey": null
+ },
+
+ "MongoDB": {
+ "Host": "localhost",
+ "Username": null,
+ "Password": null
}
}
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 9e15aa97..65ccea75 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -47,6 +47,12 @@
"PastebinDevKey": null // see top note
},
+ "MongoDB": {
+ "Host": null, // see top note
+ "Username": null, // see top note
+ "Password": null // see top note
+ },
+
"ModCompatibilityList": {
"CacheMinutes": 10
},