|
@@ -0,0 +1,162 @@
|
|
|
|
|
+using Invercargill;
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+using InvercargillJson;
|
|
|
|
|
+using Statum;
|
|
|
|
|
+using Usm;
|
|
|
|
|
+
|
|
|
|
|
+namespace UsmWeb {
|
|
|
|
|
+
|
|
|
|
|
+ /** One package row in the homepage table. */
|
|
|
|
|
+ public class PackageRow : Object {
|
|
|
|
|
+ public string name { get; set; default = ""; }
|
|
|
|
|
+ public string version { get; set; default = ""; }
|
|
|
|
|
+ public string summary { get; set; default = ""; }
|
|
|
|
|
+ public string size { get; set; default = ""; }
|
|
|
|
|
+ public string file { get; set; default = ""; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Authors the `packages` PAGE-slot state shared by the homepage
|
|
|
|
|
+ * entrypoint and the search action: repository overview, the
|
|
|
|
|
+ * add-repository / install-USM instruction blocks, and the (possibly
|
|
|
|
|
+ * query-filtered) package table. The list is authored as a JSON array
|
|
|
|
|
+ * because collection-typed GObject properties are write-side only.
|
|
|
|
|
+ */
|
|
|
|
|
+ public class PackagesState : Object {
|
|
|
|
|
+
|
|
|
|
|
+ public const string SLOT_TYPE = "packages";
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Builds the slot state: `repo` fields + instruction blocks + rows
|
|
|
|
|
+ * filtered by the case-insensitive `query` substring over name and
|
|
|
|
|
+ * summary (an empty query keeps every entry).
|
|
|
|
|
+ */
|
|
|
|
|
+ public static State build(RepositoryService repositories, UsmWebConfig config,
|
|
|
|
|
+ ActionRegistry action_registry, string base_uri, string query) throws Error {
|
|
|
|
|
+ var repo = repositories.repo();
|
|
|
|
|
+ var entries = repositories.entries();
|
|
|
|
|
+
|
|
|
|
|
+ var rows = new Vector<PackageRow>();
|
|
|
|
|
+ int64 total_bytes = 0;
|
|
|
|
|
+ uint matched = 0;
|
|
|
|
|
+ foreach (var entry in entries) {
|
|
|
|
|
+ var path = repositories.package_location(Path.get_basename(entry.path));
|
|
|
|
|
+ int64 size = file_size(path);
|
|
|
|
|
+ total_bytes += size;
|
|
|
|
|
+
|
|
|
|
|
+ if (!matches(entry, query)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ matched++;
|
|
|
|
|
+ rows.add(new PackageRow() {
|
|
|
|
|
+ name = entry.manifest.name,
|
|
|
|
|
+ version = entry.manifest.version.to_string(),
|
|
|
|
|
+ summary = entry.manifest.summary,
|
|
|
|
|
+ size = format_size(size),
|
|
|
|
|
+ file = Path.get_basename(entry.path)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var dict = new PropertyDictionary();
|
|
|
|
|
+ dict.set_native<string>("repo_name", config.title_for(repo.name));
|
|
|
|
|
+ dict.set_native<string>("repo_summary", repo.summary);
|
|
|
|
|
+ dict.set_native<string>("key_fingerprint", fingerprint(repo.key.to_hex()));
|
|
|
|
|
+ dict.set_native<int>("package_count", (int) entries.count());
|
|
|
|
|
+ dict.set_native<string>("total_size", format_size(total_bytes));
|
|
|
|
|
+ dict.set_native<string>("base_uri", base_uri);
|
|
|
|
|
+ dict.set_native<string>("repos_d_snippet",
|
|
|
|
|
+ "curl -fsSL -o /etc/usm/repos.d/%s.usmr %srepo.usmr".printf(repo.name, base_uri));
|
|
|
|
|
+ dict.set_native<string>("query", query);
|
|
|
|
|
+ dict.set_native<string>("result_note", result_note(query, matched, entries.count()));
|
|
|
|
|
+ dict.set_native<string>("installer_mode", config.installer.to_string());
|
|
|
|
|
+ dict.set_native<string>("installer_href", installer_href(config, base_uri));
|
|
|
|
|
+ dict.set_native<string>("installer_command", installer_command(config, base_uri));
|
|
|
|
|
+ dict["search"] = action_registry.author<SearchPackagesAction>().to_element();
|
|
|
|
|
+ dict["packages"] = list(rows);
|
|
|
|
|
+
|
|
|
|
|
+ return new State() {
|
|
|
|
|
+ type_name = SLOT_TYPE,
|
|
|
|
|
+ public_data = dict,
|
|
|
|
|
+ private_data = new PropertyDictionary()
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Case-insensitive substring match over name and summary. */
|
|
|
|
|
+ public static bool matches(RepositoryListingEntry entry, string query) {
|
|
|
|
|
+ if (query.length == 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ var needle = query.casefold();
|
|
|
|
|
+ return entry.manifest.name.casefold().contains(needle)
|
|
|
|
|
+ || entry.manifest.summary.casefold().contains(needle);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Builds the rows' JSON array (the {@link StateJson.list} pattern). */
|
|
|
|
|
+ public static JsonElement list(Lot<PackageRow> rows) throws Error {
|
|
|
|
|
+ var arr = new JsonArray();
|
|
|
|
|
+ foreach (var row in rows) {
|
|
|
|
|
+ arr.add(new JsonElement.from_properties(GObjectMapping.to_properties(row)));
|
|
|
|
|
+ }
|
|
|
|
|
+ return new JsonElement.from_elements(arr);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Groups a hex digest into space-separated 16-character chunks. */
|
|
|
|
|
+ public static string fingerprint(string hex) {
|
|
|
|
|
+ var builder = new StringBuilder();
|
|
|
|
|
+ for (int i = 0; i < hex.length; i += 16) {
|
|
|
|
|
+ if (i > 0) {
|
|
|
|
|
+ builder.append_c(' ');
|
|
|
|
|
+ }
|
|
|
|
|
+ builder.append(hex[i:int.min(i + 16, hex.length)]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return builder.str;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Human-readable byte size (KiB/MiB/GiB). */
|
|
|
|
|
+ public static string format_size(int64 bytes) {
|
|
|
|
|
+ if (bytes >= 1073741824) {
|
|
|
|
|
+ return "%.2f GiB".printf(bytes / 1073741824.0);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (bytes >= 1048576) {
|
|
|
|
|
+ return "%.1f MiB".printf(bytes / 1048576.0);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (bytes >= 1024) {
|
|
|
|
|
+ return "%.1f KiB".printf(bytes / 1024.0);
|
|
|
|
|
+ }
|
|
|
|
|
+ return "%lld B".printf(bytes);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** The link the install card points at (external URL or this app). */
|
|
|
|
|
+ private static string installer_href(UsmWebConfig config, string base_uri) {
|
|
|
|
|
+ if (config.installer == InstallerSource.URL) {
|
|
|
|
|
+ return (!)config.installer_url;
|
|
|
|
|
+ }
|
|
|
|
|
+ return base_uri + "install-usm.sh";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** The copy-paste one-liner the install card shows. */
|
|
|
|
|
+ private static string installer_command(UsmWebConfig config, string base_uri) {
|
|
|
|
|
+ return "curl -fsSL %s | sh".printf(installer_href(config, base_uri));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string result_note(string query, uint matched, uint total) {
|
|
|
|
|
+ if (query.length == 0) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (matched == 0) {
|
|
|
|
|
+ return "No packages match \"%s\".".printf(query);
|
|
|
|
|
+ }
|
|
|
|
|
+ return "Showing %u of %u packages.".printf(matched, total);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int64 file_size(string path) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var info = File.new_for_path(path).query_info(
|
|
|
|
|
+ FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE);
|
|
|
|
|
+ return info.get_size();
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|