using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.ModBuildConfig.Framework
{
/// Manages the files that are part of a mod package.
internal class ModFileManager
{
/*********
** Properties
*********/
/// The name of the manifest file.
private readonly string ManifestFileName = "manifest.json";
/// The files that are part of the package.
private readonly IDictionary Files;
/*********
** Public methods
*********/
/// Construct an instance.
/// The folder containing the project files.
/// The folder containing the build output.
/// Custom regex patterns matching files to ignore when deploying or zipping the mod.
/// Whether to validate that required mod files like the manifest are present.
/// The mod package isn't valid.
public ModFileManager(string projectDir, string targetDir, Regex[] ignoreFilePatterns, bool validateRequiredModFiles)
{
this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
// validate paths
if (!Directory.Exists(projectDir))
throw new UserErrorException("Could not create mod package because the project folder wasn't found.");
if (!Directory.Exists(targetDir))
throw new UserErrorException("Could not create mod package because no build output was found.");
// project manifest
bool hasProjectManifest = false;
{
FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json"));
if (manifest.Exists)
{
this.Files[this.ManifestFileName] = manifest;
hasProjectManifest = true;
}
}
// project i18n files
bool hasProjectTranslations = false;
DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
if (translationsFolder.Exists)
{
foreach (FileInfo file in translationsFolder.EnumerateFiles())
this.Files[Path.Combine("i18n", file.Name)] = file;
hasProjectTranslations = true;
}
// build output
DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
// get relative paths
string relativePath = file.FullName.Replace(buildFolder.FullName, "");
string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, "");
// prefer project manifest/i18n files
if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName))
continue;
if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n"))
continue;
// handle ignored files
if (this.ShouldIgnore(file, relativePath, ignoreFilePatterns))
continue;
// add file
this.Files[relativePath] = file;
}
// check for required files
if (validateRequiredModFiles)
{
// manifest
if (!this.Files.ContainsKey(this.ManifestFileName))
throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");
// DLL
// ReSharper disable once SimplifyLinqExpression
if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
}
}
/// Get the files in the mod package.
public IDictionary GetFiles()
{
return new Dictionary(this.Files, StringComparer.InvariantCultureIgnoreCase);
}
/// Get a semantic version from the mod manifest.
/// The manifest is missing or invalid.
public string GetManifestVersion()
{
if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile) || !new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest manifest))
throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor
return manifest.Version.ToString();
}
/*********
** Private methods
*********/
/// Get whether a build output file should be ignored.
/// The file to check.
/// The file's relative path in the package.
/// Custom regex patterns matching files to ignore when deploying or zipping the mod.
private bool ShouldIgnore(FileInfo file, string relativePath, Regex[] ignoreFilePatterns)
{
return
// release zips
this.EqualsInvariant(file.Extension, ".zip")
// Json.NET (bundled into SMAPI)
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll")
|| this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")
// code analysis files
|| file.Name.EndsWith(".CodeAnalysisLog.xml", StringComparison.InvariantCultureIgnoreCase)
|| file.Name.EndsWith(".lastcodeanalysissucceeded", StringComparison.InvariantCultureIgnoreCase)
// OS metadata files
|| this.EqualsInvariant(file.Name, ".DS_Store")
|| this.EqualsInvariant(file.Name, "Thumbs.db")
// custom ignore patterns
|| ignoreFilePatterns.Any(p => p.IsMatch(relativePath));
}
/// Get whether a string is equal to another case-insensitively.
/// The string value.
/// The string to compare with.
private bool EqualsInvariant(string str, string other)
{
return str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
}
}
}