UsmWebConfig.vala 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. using Astralis;
  2. using Json;
  3. namespace UsmWeb {
  4. /** Errors raised by a malformed `"usm-web"` configuration section. */
  5. public errordomain UsmWebError {
  6. /** The section (or its `installer` object) is structurally invalid */
  7. INVALID_CONFIGURATION,
  8. }
  9. /** How the "Install USM" instructions source the installer script. */
  10. public enum InstallerSource {
  11. /** No installer configured; the homepage omits the install block. */
  12. NONE,
  13. /** The installer lives at an external URL; instructions link out. */
  14. URL,
  15. /** The installer is a local file this app serves at /install-usm.sh. */
  16. PATH;
  17. /** The wire/display name used as the homepage's `installer_mode`. */
  18. public string to_string() {
  19. switch (this) {
  20. case InstallerSource.NONE:
  21. return "none";
  22. case InstallerSource.URL:
  23. return "url";
  24. case InstallerSource.PATH:
  25. return "path";
  26. default:
  27. assert_not_reached();
  28. }
  29. }
  30. }
  31. /**
  32. * The `"usm-web"` section of web-config.json.
  33. *
  34. * ```json
  35. * {
  36. * "usm-web": {
  37. * "repo": "/repo",
  38. * "repositories": ["web-stack", "extra"],
  39. * "installer": { "path": "install-usm.sh" },
  40. * "base_url": "https://repo.example.com/",
  41. * "name": "My repositories"
  42. * }
  43. * }
  44. * ```
  45. *
  46. * `repo` is the parent directory holding the repositories this app
  47. * serves: each is a subdirectory containing a `.usmr` and a `public/`
  48. * directory. A relative value resolves against the directory of the
  49. * config file that declared it. It applies only when neither the
  50. * `--repo` argument nor `USM_WEB_REPO_DIR` is given (precedence:
  51. * argument > environment > config > `/repo`).
  52. *
  53. * `repositories` optionally names the subdirectories to serve (in that
  54. * order); when absent (or empty) every subdirectory containing a `.usmr`
  55. * is discovered by scanning. A single-repository layout with the `.usmr`
  56. * directly inside `repo` is still served as one repository.
  57. *
  58. * `installer` carries exactly one of `url` (an external location the
  59. * homepage links to) or `path` (a file the app serves itself at
  60. * {@link InstallerScriptEndpoint}); a relative `path` resolves against
  61. * the directory of the config file that declared it. `base_url` overrides
  62. * the request-derived repository base (normalised to a trailing slash)
  63. * and `name` overrides the overview title.
  64. */
  65. public class UsmWebConfig : GLib.Object {
  66. /** The configured installer source, or {@link InstallerSource.NONE}. */
  67. public InstallerSource installer { get; private set; default = InstallerSource.NONE; }
  68. /** The external installer URL (`url` mode), or null. */
  69. public string? installer_url { get; private set; }
  70. /** The absolute installer script path (`path` mode), or null. */
  71. public string? installer_path { get; private set; }
  72. /** The base-URL override (trailing slash), or null to derive per request. */
  73. public string? base_url { get; private set; }
  74. /** The overview-title override, or null for the default heading. */
  75. public string? display_name { get; private set; }
  76. /**
  77. * The parent directory holding the served repositories (each a
  78. * subdirectory with a `.usmr` and `public/`).
  79. */
  80. public string repo_dir { get; private set; default = "/repo"; }
  81. /**
  82. * The explicitly listed repository subdirectories under
  83. * {@link repo_dir}; empty means scan for every subdirectory holding
  84. * a `.usmr`.
  85. */
  86. public string[] repositories { get; private set; default = new string[0]; }
  87. /**
  88. * Reads the section from the loaded {@link WebConfig}, re-parsing the
  89. * contributing files with json-glib so the nested `installer` object
  90. * and the `repositories` array are readable and a relative
  91. * `path`/`repo` resolves against the config file that declared it
  92. * (later files win, mirroring WebConfig layering). {@link repo_dir}
  93. * wins when given; otherwise the config's `repo` applies, falling
  94. * back to `/repo`.
  95. */
  96. public static UsmWebConfig load(WebConfig config, string? repo_dir = null) throws Error {
  97. var result = new UsmWebConfig();
  98. string? declared_path = null;
  99. string declaring_dir = Environment.get_current_dir();
  100. string? declared_repo = null;
  101. string repo_declaring_dir = declaring_dir;
  102. string[]? declared_repositories = null;
  103. foreach (var file in config.get_loaded_files()) {
  104. var section = read_section(file);
  105. if (section == null) {
  106. continue;
  107. }
  108. var obj = (!)section;
  109. result.display_name = read_string_member(obj, "name") ?? result.display_name;
  110. var base_url = read_string_member(obj, "base_url");
  111. if (base_url != null) {
  112. result.base_url = normalise_base((!)base_url);
  113. }
  114. var repo = read_string_member(obj, "repo");
  115. if (repo != null) {
  116. declared_repo = repo;
  117. repo_declaring_dir = GLib.Path.get_dirname((!)file);
  118. }
  119. var repositories = read_string_array_member(obj, "repositories");
  120. if (repositories != null) {
  121. declared_repositories = repositories;
  122. }
  123. result.installer_url = read_string_member(obj, "installer", "url") ?? result.installer_url;
  124. declared_path = read_string_member(obj, "installer", "path") ?? declared_path;
  125. if (declared_path != null) {
  126. declaring_dir = GLib.Path.get_dirname((!)file);
  127. }
  128. }
  129. if (repo_dir != null) {
  130. result.repo_dir = (!)repo_dir;
  131. }
  132. else if (declared_repo != null) {
  133. var repo = (!)declared_repo;
  134. result.repo_dir = GLib.Path.is_absolute(repo)
  135. ? repo : GLib.Path.build_filename(repo_declaring_dir, repo);
  136. }
  137. if (declared_repositories != null) {
  138. result.repositories = (!)declared_repositories;
  139. }
  140. if (result.installer_url != null && declared_path != null) {
  141. throw new UsmWebError.INVALID_CONFIGURATION(
  142. "usm-web.installer: provide exactly one of \"url\" or \"path\", not both");
  143. }
  144. if (result.installer_url != null) {
  145. result.installer = InstallerSource.URL;
  146. } else if (declared_path != null) {
  147. var path = (!)declared_path;
  148. result.installer_path = GLib.Path.is_absolute(path)
  149. ? path : GLib.Path.build_filename(declaring_dir, path);
  150. result.installer = InstallerSource.PATH;
  151. }
  152. return result;
  153. }
  154. private static string normalise_base(string base_url) {
  155. return base_url.has_suffix("/") ? base_url : base_url + "/";
  156. }
  157. /** The root object of `path`'s `"usm-web"` member, or null when absent. */
  158. private static Json.Object? read_section(string path) {
  159. try {
  160. var parser = new Parser();
  161. parser.load_from_file(path);
  162. var root = parser.get_root().get_object();
  163. return root.has_member("usm-web") ? root.get_member("usm-web").get_object() : null;
  164. } catch (Error e) {
  165. return null;
  166. }
  167. }
  168. private static string? read_string_member(Json.Object obj, string member, string? nested = null) {
  169. if (!obj.has_member(member)) {
  170. return null;
  171. }
  172. var node = obj.get_member(member);
  173. if (nested != null) {
  174. if (node.get_node_type() != NodeType.OBJECT) {
  175. return null;
  176. }
  177. var inner = node.get_object();
  178. if (!inner.has_member(nested)) {
  179. return null;
  180. }
  181. node = inner.get_member(nested);
  182. }
  183. return node.get_node_type() == NodeType.VALUE ? node.get_string() : null;
  184. }
  185. /** The string values of `member` (when it is a JSON array), or null. */
  186. private static string[]? read_string_array_member(Json.Object obj, string member) {
  187. if (!obj.has_member(member)) {
  188. return null;
  189. }
  190. var node = obj.get_member(member);
  191. if (node.get_node_type() != NodeType.ARRAY) {
  192. return null;
  193. }
  194. var array = node.get_array();
  195. var values = new string[0];
  196. for (var i = 0; i < array.get_length(); i++) {
  197. var element = array.get_element(i);
  198. if (element.get_node_type() == NodeType.VALUE) {
  199. values += element.get_string();
  200. }
  201. }
  202. return values;
  203. }
  204. }
  205. }