| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- using Astralis;
- using Json;
- namespace UsmWeb {
- /** Errors raised by a malformed `"usm-web"` configuration section. */
- public errordomain UsmWebError {
- /** The section (or its `installer` object) is structurally invalid */
- INVALID_CONFIGURATION,
- }
- /** How the "Install USM" instructions source the installer script. */
- public enum InstallerSource {
- /** No installer configured; the homepage omits the install block. */
- NONE,
- /** The installer lives at an external URL; instructions link out. */
- URL,
- /** The installer is a local file this app serves at /install-usm.sh. */
- PATH;
- /** The wire/display name used as the homepage's `installer_mode`. */
- public string to_string() {
- switch (this) {
- case InstallerSource.NONE:
- return "none";
- case InstallerSource.URL:
- return "url";
- case InstallerSource.PATH:
- return "path";
- default:
- assert_not_reached();
- }
- }
- }
- /**
- * The `"usm-web"` section of web-config.json.
- *
- * ```json
- * {
- * "usm-web": {
- * "repo": "/repo",
- * "repositories": ["web-stack", "extra"],
- * "installer": { "path": "install-usm.sh" },
- * "base_url": "https://repo.example.com/",
- * "name": "My repositories"
- * }
- * }
- * ```
- *
- * `repo` is the parent directory holding the repositories this app
- * serves: each is a subdirectory containing a `.usmr` and a `public/`
- * directory. A relative value resolves against the directory of the
- * config file that declared it. It applies only when neither the
- * `--repo` argument nor `USM_WEB_REPO_DIR` is given (precedence:
- * argument > environment > config > `/repo`).
- *
- * `repositories` optionally names the subdirectories to serve (in that
- * order); when absent (or empty) every subdirectory containing a `.usmr`
- * is discovered by scanning. A single-repository layout with the `.usmr`
- * directly inside `repo` is still served as one repository.
- *
- * `installer` carries exactly one of `url` (an external location the
- * homepage links to) or `path` (a file the app serves itself at
- * {@link InstallerScriptEndpoint}); a relative `path` resolves against
- * the directory of the config file that declared it. `base_url` overrides
- * the request-derived repository base (normalised to a trailing slash)
- * and `name` overrides the overview title.
- */
- public class UsmWebConfig : GLib.Object {
- /** The configured installer source, or {@link InstallerSource.NONE}. */
- public InstallerSource installer { get; private set; default = InstallerSource.NONE; }
- /** The external installer URL (`url` mode), or null. */
- public string? installer_url { get; private set; }
- /** The absolute installer script path (`path` mode), or null. */
- public string? installer_path { get; private set; }
- /** The base-URL override (trailing slash), or null to derive per request. */
- public string? base_url { get; private set; }
- /** The overview-title override, or null for the default heading. */
- public string? display_name { get; private set; }
- /**
- * The parent directory holding the served repositories (each a
- * subdirectory with a `.usmr` and `public/`).
- */
- public string repo_dir { get; private set; default = "/repo"; }
- /**
- * The explicitly listed repository subdirectories under
- * {@link repo_dir}; empty means scan for every subdirectory holding
- * a `.usmr`.
- */
- public string[] repositories { get; private set; default = new string[0]; }
- /**
- * Reads the section from the loaded {@link WebConfig}, re-parsing the
- * contributing files with json-glib so the nested `installer` object
- * and the `repositories` array are readable and a relative
- * `path`/`repo` resolves against the config file that declared it
- * (later files win, mirroring WebConfig layering). {@link repo_dir}
- * wins when given; otherwise the config's `repo` applies, falling
- * back to `/repo`.
- */
- public static UsmWebConfig load(WebConfig config, string? repo_dir = null) throws Error {
- var result = new UsmWebConfig();
- string? declared_path = null;
- string declaring_dir = Environment.get_current_dir();
- string? declared_repo = null;
- string repo_declaring_dir = declaring_dir;
- string[]? declared_repositories = null;
- foreach (var file in config.get_loaded_files()) {
- var section = read_section(file);
- if (section == null) {
- continue;
- }
- var obj = (!)section;
- result.display_name = read_string_member(obj, "name") ?? result.display_name;
- var base_url = read_string_member(obj, "base_url");
- if (base_url != null) {
- result.base_url = normalise_base((!)base_url);
- }
- var repo = read_string_member(obj, "repo");
- if (repo != null) {
- declared_repo = repo;
- repo_declaring_dir = GLib.Path.get_dirname((!)file);
- }
- var repositories = read_string_array_member(obj, "repositories");
- if (repositories != null) {
- declared_repositories = repositories;
- }
- result.installer_url = read_string_member(obj, "installer", "url") ?? result.installer_url;
- declared_path = read_string_member(obj, "installer", "path") ?? declared_path;
- if (declared_path != null) {
- declaring_dir = GLib.Path.get_dirname((!)file);
- }
- }
- if (repo_dir != null) {
- result.repo_dir = (!)repo_dir;
- }
- else if (declared_repo != null) {
- var repo = (!)declared_repo;
- result.repo_dir = GLib.Path.is_absolute(repo)
- ? repo : GLib.Path.build_filename(repo_declaring_dir, repo);
- }
- if (declared_repositories != null) {
- result.repositories = (!)declared_repositories;
- }
- if (result.installer_url != null && declared_path != null) {
- throw new UsmWebError.INVALID_CONFIGURATION(
- "usm-web.installer: provide exactly one of \"url\" or \"path\", not both");
- }
- if (result.installer_url != null) {
- result.installer = InstallerSource.URL;
- } else if (declared_path != null) {
- var path = (!)declared_path;
- result.installer_path = GLib.Path.is_absolute(path)
- ? path : GLib.Path.build_filename(declaring_dir, path);
- result.installer = InstallerSource.PATH;
- }
- return result;
- }
- private static string normalise_base(string base_url) {
- return base_url.has_suffix("/") ? base_url : base_url + "/";
- }
- /** The root object of `path`'s `"usm-web"` member, or null when absent. */
- private static Json.Object? read_section(string path) {
- try {
- var parser = new Parser();
- parser.load_from_file(path);
- var root = parser.get_root().get_object();
- return root.has_member("usm-web") ? root.get_member("usm-web").get_object() : null;
- } catch (Error e) {
- return null;
- }
- }
- private static string? read_string_member(Json.Object obj, string member, string? nested = null) {
- if (!obj.has_member(member)) {
- return null;
- }
- var node = obj.get_member(member);
- if (nested != null) {
- if (node.get_node_type() != NodeType.OBJECT) {
- return null;
- }
- var inner = node.get_object();
- if (!inner.has_member(nested)) {
- return null;
- }
- node = inner.get_member(nested);
- }
- return node.get_node_type() == NodeType.VALUE ? node.get_string() : null;
- }
- /** The string values of `member` (when it is a JSON array), or null. */
- private static string[]? read_string_array_member(Json.Object obj, string member) {
- if (!obj.has_member(member)) {
- return null;
- }
- var node = obj.get_member(member);
- if (node.get_node_type() != NodeType.ARRAY) {
- return null;
- }
- var array = node.get_array();
- var values = new string[0];
- for (var i = 0; i < array.get_length(); i++) {
- var element = array.get_element(i);
- if (element.get_node_type() == NodeType.VALUE) {
- values += element.get_string();
- }
- }
- return values;
- }
- }
- }
|