PackagesState.vala 16 KB

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