PackagesState.vala 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using InvercargillJson;
  4. using Statum;
  5. using Usm;
  6. namespace UsmWeb {
  7. /** One package row in the overview and single-repo tables. */
  8. public class PackageRow : Object {
  9. public string name { get; set; default = ""; }
  10. public string version { get; set; default = ""; }
  11. public string summary { get; set; default = ""; }
  12. public string size { get; set; default = ""; }
  13. public string file { get; set; default = ""; }
  14. public string repo { get; set; default = ""; }
  15. }
  16. /**
  17. * One repository summary in the overview: the stats card fields plus the
  18. * per-repository add-repository instruction fields ({@link base_uri},
  19. * {@link usmr_href}, {@link repos_d_snippet}).
  20. *
  21. * Both repository kinds render a card; {@link kind} (the
  22. * {@link RepositoryKind.to_string} wire name) picks which instruction
  23. * block and stats the card shows — a USM card carries its package count
  24. * and byte total, a DCBA card its artifact count and artifact byte
  25. * total — that total sums the listing's `size` fields, so artifacts
  26. * from listings written before the field existed (size 0) contribute
  27. * nothing while still counting toward {@link artifact_count}. The
  28. * `*_href`/`*_snippet` members are named for the USM card;
  29. * a DCBA summary fills them with the `repo.dcbar` / `usm dcba
  30. * add-repo` counterparts so the card markup stays one block with a
  31. * kind branch.
  32. */
  33. public class RepoSummary : Object {
  34. public string name { get; set; default = ""; }
  35. public string summary { get; set; default = ""; }
  36. public string kind { get; set; default = "usm"; }
  37. public int package_count { get; set; }
  38. public string total_size { get; set; default = ""; }
  39. public string href { get; set; default = ""; }
  40. public string key_fingerprint { get; set; default = ""; }
  41. public string base_uri { get; set; default = ""; }
  42. public string usmr_href { get; set; default = ""; }
  43. public string repos_d_snippet { get; set; default = ""; }
  44. public int artifact_count { get; set; }
  45. public string artifact_total_size { get; set; default = ""; }
  46. }
  47. /**
  48. * Authors the `packages` PAGE-slot state shared by the overview and
  49. * single-repo entrypoints and the search action: repository summaries,
  50. * the add-repository / install-USM instruction blocks, and the (possibly
  51. * query-filtered) package table. The lists are authored as JSON arrays
  52. * because collection-typed GObject properties are write-side only.
  53. */
  54. public class PackagesState : Object {
  55. public const string SLOT_TYPE = "packages";
  56. /**
  57. * Builds the slot state: a repo-scoped view when `repo_name` names a
  58. * repository (its overview card, instruction block and table rows),
  59. * otherwise the all-repositories overview (summary cards with
  60. * per-repository add instructions, and one combined table). Rows are
  61. * filtered by the case-insensitive `query` substring over name and
  62. * summary (an empty query keeps every entry).
  63. *
  64. * The overview lists repositories of **both** kinds as summary
  65. * cards, in discovery order, but only the USM source repositories
  66. * feed the package table and the package counts and byte totals —
  67. * a DCBA entry contributes a card (its artifact count and artifact
  68. * byte total from the `ARTIFACTS.usml` sizes) and nothing else, so
  69. * pre-built artifacts surface in the detail views, never in the
  70. * home package table.
  71. *
  72. * A repo-scoped build for a DCBA repository throws instead: the
  73. * package-table state this builder authors does not describe an
  74. * artifact repository, so the browse entrypoint degrades to its
  75. * error card for those names (the dedicated DCBA browse view owns
  76. * that page).
  77. */
  78. public static State build(RepositoryService repositories, UsmWebConfig config,
  79. ActionRegistry action_registry, string base_uri, string query, string? repo_name = null) throws Error {
  80. var repo_rows = new Vector<RepoSummary>();
  81. var package_rows = new Vector<PackageRow>();
  82. int64 total_bytes = 0;
  83. uint total_count = 0;
  84. uint matched = 0;
  85. foreach (var entry in repositories.entries()) {
  86. var repo = entry.repo();
  87. if (repo_name != null && repo.name != repo_name) {
  88. continue;
  89. }
  90. if (entry.kind == RepositoryKind.DCBA) {
  91. if (repo_name != null) {
  92. throw new UsmWebError.INVALID_CONFIGURATION(
  93. @"Repository \"$repo_name\" is a DCBA artifact repository — its pre-built artifacts are not listed as packages");
  94. }
  95. int64 artifact_bytes = 0;
  96. uint artifact_count = 0;
  97. foreach (var artifact_entry in entry.dcba_listing_entries()) {
  98. artifact_bytes += (int64) artifact_entry.size;
  99. artifact_count++;
  100. }
  101. repo_rows.add(new RepoSummary() {
  102. name = repo.name,
  103. summary = repo.summary,
  104. kind = RepositoryKind.DCBA.to_string(),
  105. artifact_count = (int) artifact_count,
  106. artifact_total_size = format_size(artifact_bytes),
  107. href = "/repo/" + repo.name,
  108. key_fingerprint = fingerprint(repo.key.to_hex()),
  109. base_uri = base_uri + "repo/" + repo.name + "/",
  110. usmr_href = base_uri + "repo/" + repo.name + "/repo.dcbar",
  111. repos_d_snippet = "sudo usm dcba add-repo %srepo/%s/repo.dcbar"
  112. .printf(base_uri, repo.name)
  113. });
  114. continue;
  115. }
  116. int64 repo_bytes = 0;
  117. uint repo_count = 0;
  118. var rows = new Vector<PackageRow>();
  119. foreach (var listing_entry in entry.listing_entries()) {
  120. var file = Path.get_basename(listing_entry.path);
  121. int64 size = file_size(entry.package_location(file));
  122. repo_bytes += size;
  123. repo_count++;
  124. if (!matches(listing_entry, query)) {
  125. continue;
  126. }
  127. rows.add(new PackageRow() {
  128. name = listing_entry.manifest.name,
  129. version = listing_entry.manifest.version.to_string(),
  130. summary = listing_entry.manifest.summary,
  131. size = format_size(size),
  132. file = file,
  133. repo = repo.name
  134. });
  135. }
  136. matched += rows.length;
  137. package_rows.add_all(rows);
  138. repo_rows.add(new RepoSummary() {
  139. name = repo.name,
  140. summary = repo.summary,
  141. kind = RepositoryKind.USM.to_string(),
  142. package_count = (int) repo_count,
  143. total_size = format_size(repo_bytes),
  144. href = "/repo/" + repo.name,
  145. key_fingerprint = fingerprint(repo.key.to_hex()),
  146. base_uri = base_uri + "repo/" + repo.name + "/",
  147. usmr_href = base_uri + "repo/" + repo.name + "/repo.usmr",
  148. repos_d_snippet = "sudo usm add-repo %srepo/%s/repo.usmr"
  149. .printf(base_uri, repo.name)
  150. });
  151. total_bytes += repo_bytes;
  152. total_count += repo_count;
  153. }
  154. if (repo_name != null && repo_rows.length == 0) {
  155. throw new UsmWebError.INVALID_CONFIGURATION(@"No repository named \"$repo_name\"");
  156. }
  157. var dict = new PropertyDictionary();
  158. if (repo_name == null) {
  159. dict.set_native<string>("site_name", config.display_name ?? "Repositories");
  160. dict.set_native<int>("repo_count", (int) repo_rows.length);
  161. dict.set_native<int>("package_count", (int) total_count);
  162. dict.set_native<string>("total_size", format_size(total_bytes));
  163. dict["repos"] = repo_list(repo_rows);
  164. } else {
  165. var summary = repo_rows.first();
  166. dict.set_native<string>("repo_name", summary.name);
  167. dict.set_native<string>("repo_summary", summary.summary);
  168. dict.set_native<string>("key_fingerprint", summary.key_fingerprint);
  169. dict.set_native<int>("package_count", summary.package_count);
  170. dict.set_native<string>("total_size", summary.total_size);
  171. dict.set_native<string>("base_uri", summary.base_uri);
  172. dict.set_native<string>("usmr_href", summary.usmr_href);
  173. dict.set_native<string>("repos_d_snippet", summary.repos_d_snippet);
  174. }
  175. dict.set_native<string>("query", query);
  176. dict.set_native<string>("result_note", result_note(query, matched, total_count));
  177. dict.set_native<string>("installer_mode", config.installer.to_string());
  178. dict.set_native<string>("installer_href", installer_href(config, base_uri));
  179. dict.set_native<string>("installer_command", installer_command(config, base_uri));
  180. dict["search"] = action_registry.author<SearchPackagesAction>().to_element();
  181. dict["packages"] = list(package_rows);
  182. return new State() {
  183. type_name = SLOT_TYPE,
  184. public_data = dict,
  185. private_data = new PropertyDictionary()
  186. };
  187. }
  188. /** Case-insensitive substring match over name and summary. */
  189. public static bool matches(RepositoryListingEntry entry, string query) {
  190. if (query.length == 0) {
  191. return true;
  192. }
  193. var needle = query.casefold();
  194. return entry.manifest.name.casefold().contains(needle)
  195. || entry.manifest.summary.casefold().contains(needle);
  196. }
  197. /** Builds the package rows' JSON array (the {@link StateJson.list} pattern). */
  198. public static JsonElement list(Lot<PackageRow> rows) throws Error {
  199. var arr = new JsonArray();
  200. foreach (var row in rows) {
  201. arr.add(new JsonElement.from_properties(GObjectMapping.to_properties(row)));
  202. }
  203. return new JsonElement.from_elements(arr);
  204. }
  205. /** Builds the repository summaries' JSON array. */
  206. public static JsonElement repo_list(Lot<RepoSummary> rows) throws Error {
  207. var arr = new JsonArray();
  208. foreach (var row in rows) {
  209. arr.add(new JsonElement.from_properties(GObjectMapping.to_properties(row)));
  210. }
  211. return new JsonElement.from_elements(arr);
  212. }
  213. /** Groups a hex digest into space-separated 16-character chunks. */
  214. public static string fingerprint(string hex) {
  215. var builder = new StringBuilder();
  216. for (int i = 0; i < hex.length; i += 16) {
  217. if (i > 0) {
  218. builder.append_c(' ');
  219. }
  220. builder.append(hex[i:int.min(i + 16, hex.length)]);
  221. }
  222. return builder.str;
  223. }
  224. /** Human-readable byte size (KiB/MiB/GiB). */
  225. public static string format_size(int64 bytes) {
  226. if (bytes >= 1073741824) {
  227. return "%.2f GiB".printf(bytes / 1073741824.0);
  228. }
  229. if (bytes >= 1048576) {
  230. return "%.1f MiB".printf(bytes / 1048576.0);
  231. }
  232. if (bytes >= 1024) {
  233. return "%.1f KiB".printf(bytes / 1024.0);
  234. }
  235. return "%lld B".printf(bytes);
  236. }
  237. /** The link the install card points at (external URL or this app). */
  238. private static string installer_href(UsmWebConfig config, string base_uri) {
  239. if (config.installer == InstallerSource.URL) {
  240. return (!)config.installer_url;
  241. }
  242. return base_uri + "install-usm.sh";
  243. }
  244. /** The copy-paste one-liner the install card shows. */
  245. private static string installer_command(UsmWebConfig config, string base_uri) {
  246. return "curl -fsSL %s | sh".printf(installer_href(config, base_uri));
  247. }
  248. private static string result_note(string query, uint matched, uint total) {
  249. if (query.length == 0) {
  250. return "";
  251. }
  252. if (matched == 0) {
  253. return "No packages match \"%s\".".printf(query);
  254. }
  255. return "Showing %u of %u packages.".printf(matched, total);
  256. }
  257. private static int64 file_size(string path) {
  258. try {
  259. var info = File.new_for_path(path).query_info(
  260. FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE);
  261. return info.get_size();
  262. } catch (Error e) {
  263. return 0;
  264. }
  265. }
  266. }
  267. }