| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Usm;
- namespace UsmWeb {
- /** The kind of a discovered repository directory. */
- public enum RepositoryKind {
- /** A USM source repository: `.usmr` + `public/PACKAGES.usml` + `.usmc` packages. */
- USM,
- /** A DCBA artifact repository: `.dcbar` + `public/ARTIFACTS.usml` + `.dcba` artifacts. */
- DCBA;
- /** The wire/display name used on generated pages. */
- public string to_string() {
- switch (this) {
- case RepositoryKind.USM:
- return "usm";
- case RepositoryKind.DCBA:
- return "dcba";
- default:
- assert_not_reached();
- }
- }
- }
- /**
- * One on-disk repository discovered under the configured parent
- * directory, built on libusm via {@link Usm.Repository.from_file}.
- *
- * A USM entry is a subdirectory holding a `.usmr` beside a `public/`
- * directory with `PACKAGES.usml`; a DCBA entry is the artifact-repo
- * counterpart holding a `.dcbar` (the same JSON shape) beside a
- * `public/` directory with `ARTIFACTS.usml` and `.dcba` files. The
- * two kinds share the loading machinery but expose disjoint members;
- * reaching for a member of the other kind throws instead of
- * silently answering nonsense.
- *
- * Each entry tracks its descriptor (`.usmr`/`.dcbar`) and listing
- * (`PACKAGES.usml`/`ARTIFACTS.usml`) mtimes independently, so one
- * repository can be rebuilt and re-read without disturbing the
- * others, and both file roles reload when their own mtime moves.
- */
- public class RepositoryEntry : Object {
- /** The repository's directory (absolute). */
- public string dir_path { get; private set; }
- /**
- * The absolute path of the `.usmr` (USM entries) or `.dcbar`
- * (DCBA entries) inside {@link dir_path}.
- */
- public string descriptor_path { get; private set; }
- /** This repository's kind (source or artifact). */
- public RepositoryKind kind { get; private set; }
- /**
- * Compatibility alias for {@link descriptor_path} — the endpoint
- * serving `repo.usmr` reads the raw descriptor through this name.
- */
- public string usmr_path {
- get { return descriptor_path; }
- }
- private Repository? repository;
- private RepositoryListing? listing;
- private DcbaListing? dcba_listing;
- private int64 descriptor_mtime;
- private int64 listing_mtime;
- public RepositoryEntry.usm(string dir_path, string usmr_path) {
- this.dir_path = dir_path;
- this.kind = RepositoryKind.USM;
- this.descriptor_path = usmr_path;
- }
- public RepositoryEntry.dcba(string dir_path, string dcbar_path) {
- this.dir_path = dir_path;
- this.kind = RepositoryKind.DCBA;
- this.descriptor_path = dcbar_path;
- }
- /** Whether the descriptor and this kind's listing are currently loaded. */
- public bool is_loaded {
- get {
- if (kind == RepositoryKind.DCBA) {
- return repository != null && dcba_listing != null;
- }
- return repository != null && listing != null;
- }
- }
- /**
- * Reloads the descriptor and listing when either file changed on
- * disk (or was never loaded); a failure throws while keeping the
- * previously loaded snapshot, so an in-flight rebuild cannot lose
- * what is already held.
- */
- public void ensure_loaded() throws Error {
- int64 current_descriptor_mtime = file_mtime(descriptor_path);
- int64 current_listing_mtime = file_mtime(current_listing_path());
- if (is_loaded
- && current_descriptor_mtime == descriptor_mtime
- && current_listing_mtime == listing_mtime) {
- return;
- }
- if (repository == null || current_descriptor_mtime != descriptor_mtime) {
- repository = new Repository.from_file(descriptor_path);
- descriptor_mtime = current_descriptor_mtime;
- }
- if (kind == RepositoryKind.DCBA) {
- if (dcba_listing == null || current_listing_mtime != listing_mtime) {
- var file = File.new_for_path(current_listing_path());
- dcba_listing = new DcbaListing.from_stream(new DataInputStream(file.read()));
- listing_mtime = current_listing_mtime;
- }
- } else if (listing == null || current_listing_mtime != listing_mtime) {
- var file = File.new_for_path(current_listing_path());
- listing = new RepositoryListing.from_stream(new DataInputStream(file.read()));
- listing_mtime = current_listing_mtime;
- }
- }
- /** The loaded repository metadata (name, summary, signing key). */
- public Repository repo() throws Error {
- ensure_loaded();
- return (!)repository;
- }
- /**
- * The loaded `PACKAGES.usml` entries, in listing order. USM
- * entries only — DCBA entries answer with
- * {@link dcba_listing_entries}.
- */
- public Enumerable<RepositoryListingEntry> listing_entries() throws Error {
- if (kind == RepositoryKind.DCBA) {
- throw_wrong_kind("PACKAGES.usml entries");
- }
- ensure_loaded();
- return ((!)listing).entries;
- }
- /**
- * The loaded `ARTIFACTS.usml` entries, in listing order. DCBA
- * entries only — USM entries answer with {@link listing_entries}.
- */
- public Enumerable<DcbaListingEntry> dcba_listing_entries() throws Error {
- if (kind != RepositoryKind.DCBA) {
- throw_wrong_kind("ARTIFACTS.usml entries");
- }
- ensure_loaded();
- return ((!)dcba_listing).entries;
- }
- /**
- * The listing entry whose package file is `file`, or null. USM
- * entries only.
- */
- public RepositoryListingEntry? find_entry(string file) throws Error {
- foreach (var entry in listing_entries()) {
- if (Path.get_basename(entry.path) == file) {
- return entry;
- }
- }
- return null;
- }
- /**
- * The artifact entry whose package name and version match `name`
- * and `version` exactly (a plain string comparison on both, per
- * the DCBA specification — listing strings never mutate through
- * a {@link Usm.Version} round trip), or null when this repository
- * carries no artifact for that package. DCBA entries only.
- */
- public DcbaListingEntry? find_dcba_artifact(string name, string version) throws Error {
- foreach (var entry in dcba_listing_entries()) {
- if (entry.name == name && entry.version == version) {
- return entry;
- }
- }
- return null;
- }
- /**
- * The absolute path of the verbatim-served `PACKAGES.usml`. USM
- * entries only — DCBA entries answer with
- * {@link artifacts_listing_location}.
- */
- public string listing_location() throws Error {
- if (kind == RepositoryKind.DCBA) {
- throw_wrong_kind("PACKAGES.usml location");
- }
- return Path.build_filename(dir_path, "public",
- Usm.RepositoryClient.LISTING_NAME);
- }
- /**
- * The absolute path of the verbatim-served `ARTIFACTS.usml`. DCBA
- * entries only — USM entries answer with {@link listing_location}.
- */
- public string artifacts_listing_location() throws Error {
- if (kind != RepositoryKind.DCBA) {
- throw_wrong_kind("ARTIFACTS.usml location");
- }
- return Path.build_filename(dir_path, "public",
- Usm.RepositoryClient.DCBA_LISTING_NAME);
- }
- /**
- * The path of this kind's listing file, unguarded: both kinds
- * keep their listing under `public/`, so {@link ensure_loaded}
- * watches one location per kind.
- */
- private string current_listing_path() {
- var listing_name = kind == RepositoryKind.DCBA
- ? Usm.RepositoryClient.DCBA_LISTING_NAME
- : Usm.RepositoryClient.LISTING_NAME;
- return Path.build_filename(dir_path, "public", listing_name);
- }
- /**
- * The actual descriptor filename inside the repository
- * directory (`name.usmr` or `name.dcbar`), for messages and
- * sibling-route naming.
- */
- public string descriptor_basename() {
- return Path.get_basename(descriptor_path);
- }
- /**
- * The absolute path of a package archive inside `public/`. USM
- * entries only — DCBA entries answer with
- * {@link artifact_location}.
- */
- public string package_location(string file) throws Error {
- if (kind == RepositoryKind.DCBA) {
- throw_wrong_kind("package location");
- }
- return Path.build_filename(dir_path, "public", file);
- }
- /**
- * The absolute path of a `.dcba` artifact inside `public/`. DCBA
- * entries only — USM entries answer with {@link package_location}.
- */
- public string artifact_location(string file) throws Error {
- if (kind != RepositoryKind.DCBA) {
- throw_wrong_kind("artifact location");
- }
- return Path.build_filename(dir_path, "public", file);
- }
- /**
- * Raises the cross-kind misuse error: every member documented as
- * kind-specific guards against being reached through the wrong
- * kind, so a DCBA entry never answers for a `PACKAGES.usml` and
- * vice versa.
- */
- private void throw_wrong_kind(string member) throws Error {
- throw new UsmWebError.INVALID_CONFIGURATION(
- @"Repository at \"$dir_path\" is a $kind repository — $member is only available for $(kind == RepositoryKind.DCBA ? RepositoryKind.USM : RepositoryKind.DCBA) repositories");
- }
- private static int64 file_mtime(string path) {
- try {
- var info = File.new_for_path(path).query_info(
- FileAttribute.TIME_MODIFIED, FileQueryInfoFlags.NONE);
- return (int64) info.get_attribute_uint64(FileAttribute.TIME_MODIFIED);
- } catch (Error e) {
- return -1;
- }
- }
- }
- /**
- * Read-side access to the on-disk repositories this app browses and
- * serves, in a mixed layout.
- *
- * The repositories live under {@link UsmWebConfig.repo_dir}: every
- * subdirectory containing a `.usmr` is a USM source repository
- * (`.usmc` packages listed in `public/PACKAGES.usml`) and every
- * subdirectory containing a `.dcbar` is a DCBA artifact repository
- * (`.dcba` files listed in `public/ARTIFACTS.usml`); both are
- * discovered by the same scan, or only those named by
- * {@link UsmWebConfig.repositories} are used when that list is
- * configured — the list applies to both kinds. A repository is
- * addressed by the name in its descriptor, which is also its routing
- * segment (`/repo/<name>/…`). A directory holding both a `.usmr`
- * and a `.dcbar` is a configuration error. A legacy
- * single-repository layout with the `.usmr` directly inside the
- * configured directory is still served as one USM repository.
- *
- * {@link entries} returns every loadable repository of either kind,
- * in discovery order; {@link usm_entries} and {@link dcba_entries}
- * filter it by kind for callers that iterate one world at a time
- * (the home package table and search only see source repositories).
- *
- * Discovery re-runs on every {@link ensure_loaded}, so repositories
- * added or removed on disk are picked up without a restart; each
- * repository's descriptor and listing reload independently when
- * their mtimes change. A repository that fails to (re)load is
- * skipped with a warning rather than taking the whole app down.
- */
- public class RepositoryService : Object {
- private UsmWebConfig config = Inversion.inject<UsmWebConfig>();
- private Vector<RepositoryEntry> discovered = new Vector<RepositoryEntry>();
- private Vector<RepositoryEntry> loaded = new Vector<RepositoryEntry>();
- private Dictionary<string, RepositoryEntry> entries_by_dir = new Dictionary<string, RepositoryEntry>();
- private string? legacy_dir;
- /**
- * Discovers the repositories under the configured parent directory
- * and (re)loads each one. Throws when nothing is configured or
- * discovered, or when nothing discovered can be loaded; individual
- * failures only warn and drop that repository until it loads again.
- */
- public void ensure_loaded() throws Error {
- var current = discover();
- var still_current = new Vector<RepositoryEntry>();
- Error? first_failure = null;
- foreach (var entry in current) {
- try {
- entry.ensure_loaded();
- still_current.add(entry);
- } catch (Error e) {
- warning("[usm-web] Repository at %s failed to load: %s\n", entry.dir_path, e.message);
- if (first_failure == null) {
- first_failure = e;
- }
- }
- }
- var unique = new Vector<RepositoryEntry>();
- var seen_names = new Dictionary<string, RepositoryEntry>();
- foreach (var entry in still_current) {
- var name = entry.repo().name;
- RepositoryEntry? seen = null;
- seen_names.try_get(name, out seen);
- if (seen != null) {
- warning("[usm-web] \"%s\" is also served by %s — keeping %s\n",
- name, entry.dir_path, ((!)seen).dir_path);
- continue;
- }
- seen_names.set(name, entry);
- unique.add(entry);
- }
- discovered.clear();
- discovered.add_all(current);
- loaded.clear();
- loaded.add_all(unique);
- if (loaded.length == 0) {
- if (first_failure != null) {
- throw first_failure;
- }
- throw new UsmWebError.INVALID_CONFIGURATION(
- @"No repositories found under \"$(config.repo_dir)\" — is the parent directory mounted?");
- }
- }
- /** The currently loadable repositories, in discovery order. */
- public Enumerable<RepositoryEntry> entries() throws Error {
- ensure_loaded();
- return loaded;
- }
- /**
- * The currently loadable USM source repositories, in discovery
- * order — the home package table, search and package detail
- * flows iterate these. Same load/throw contract as
- * {@link entries}.
- */
- public Enumerable<RepositoryEntry> usm_entries() throws Error {
- var result = new Vector<RepositoryEntry>();
- foreach (var entry in entries()) {
- if (entry.kind == RepositoryKind.USM) {
- result.add(entry);
- }
- }
- return result;
- }
- /**
- * The currently loadable DCBA artifact repositories, in discovery
- * order. Same load/throw contract as {@link entries}.
- */
- public Enumerable<RepositoryEntry> dcba_entries() throws Error {
- var result = new Vector<RepositoryEntry>();
- foreach (var entry in entries()) {
- if (entry.kind == RepositoryKind.DCBA) {
- result.add(entry);
- }
- }
- return result;
- }
- /**
- * The repository whose descriptor is named `name`, or null when
- * no loaded repository carries that name. A discovered repository
- * that cannot be read surfaces its error (so callers can 500
- * rather than 404) unless another repository matches cleanly
- * first.
- */
- public RepositoryEntry? find(string name) throws Error {
- ensure_loaded();
- Error? first_failure = null;
- foreach (var entry in discovered) {
- string entry_name;
- try {
- entry_name = entry.repo().name;
- } catch (Error e) {
- if (first_failure == null) {
- first_failure = e;
- }
- continue;
- }
- if (entry_name == name) {
- return entry;
- }
- }
- if (first_failure != null) {
- throw first_failure;
- }
- return null;
- }
- /**
- * Discovers repository directories: the configured subdirectories
- * when {@link UsmWebConfig.repositories} names them, otherwise every
- * subdirectory holding a `.usmr` or a `.dcbar`, falling back to the
- * legacy single-repository layout (`.usmr` only). Already-known
- * directories keep their entry (and loaded snapshot).
- */
- private Vector<RepositoryEntry> discover() throws Error {
- var result = new Vector<RepositoryEntry>();
- var dir_names = new Vector<string>();
- if (config.repositories.length > 0) {
- foreach (var name in config.repositories) {
- dir_names.add(name);
- }
- } else {
- try {
- var dir = Dir.open(config.repo_dir);
- string? child;
- while ((child = dir.read_name()) != null) {
- dir_names.add((!)child);
- }
- } catch (Error e) {
- throw new UsmWebError.INVALID_CONFIGURATION(
- @"Cannot read repository parent \"$(config.repo_dir)\": $(e.message)");
- }
- }
- foreach (var name in dir_names.order_by<string>(n => n, Operators.comparison<string>())) {
- var child_path = Path.build_filename(config.repo_dir, name);
- if (!FileUtils.test(child_path, FileTest.IS_DIR)) {
- if (config.repositories.length > 0) {
- throw new UsmWebError.INVALID_CONFIGURATION(
- @"\"$name\" from the configured repositories list is not a directory under \"$(config.repo_dir)\"");
- }
- continue;
- }
- var descriptor = find_descriptor(child_path);
- if (descriptor == null) {
- if (config.repositories.length > 0) {
- throw new UsmWebError.INVALID_CONFIGURATION(
- @"\"$name\" from the configured repositories list has no *.usmr or *.dcbar under \"$(config.repo_dir)\"");
- }
- continue;
- }
- result.add(entry_for(child_path, (!)descriptor));
- }
- if (result.length == 0) {
- var legacy_usmr = find_usmr(config.repo_dir);
- if (legacy_usmr != null) {
- if (legacy_dir != config.repo_dir) {
- legacy_dir = config.repo_dir;
- warning("[usm-web] \"$(config.repo_dir)\" holds a .usmr directly — serving it as a single repository (move it into a subdirectory for the multi-repository layout)\n");
- }
- result.add(entry_for(config.repo_dir, (!)legacy_usmr));
- }
- }
- drop_stale(result);
- return result;
- }
- /**
- * Reuses the entry for `dir_path` when discovery finds it again.
- * A directory whose descriptor kind changed on disk (a `.dcbar`
- * replaced by a `.usmr` between discoveries) gets a fresh entry
- * so no stale snapshot of the old kind lingers.
- */
- private RepositoryEntry entry_for(string dir_path, string descriptor_path) {
- RepositoryEntry? existing = null;
- entries_by_dir.try_get(dir_path, out existing);
- if (existing != null && ((!)existing).kind == kind_of((!)descriptor_path)) {
- return (!)existing;
- }
- var created = kind_of((!)descriptor_path) == RepositoryKind.DCBA
- ? new RepositoryEntry.dcba(dir_path, descriptor_path)
- : new RepositoryEntry.usm(dir_path, descriptor_path);
- entries_by_dir.set(dir_path, created);
- return created;
- }
- /** Forgets entries whose directory is no longer discovered. */
- private void drop_stale(Vector<RepositoryEntry> current) {
- var current_dirs = new Dictionary<string, RepositoryEntry>();
- foreach (var entry in current) {
- current_dirs.set(entry.dir_path, entry);
- }
- foreach (var dir_path in entries_by_dir.keys.to_array()) {
- if (!current_dirs.has(dir_path)) {
- entries_by_dir.remove(dir_path);
- }
- }
- }
- /**
- * The first `*.usmr` or `*.dcbar` directly inside `dir`, or null
- * when there is none. A directory holding both is a
- * configuration error.
- */
- private static string? find_descriptor(string dir) throws Error {
- string? usmr = find_suffix(dir, ".usmr");
- string? dcbar = find_suffix(dir, ".dcbar");
- if (usmr != null && dcbar != null) {
- throw new UsmWebError.INVALID_CONFIGURATION(
- @"\"$dir\" holds both a .usmr and a .dcbar — a repository directory must be either a USM source repository or a DCBA artifact repository, not both");
- }
- return usmr ?? dcbar;
- }
- /** The first `*.usmr` directly inside `dir`, or null when there is none. */
- private static string? find_usmr(string dir) {
- return find_suffix(dir, ".usmr");
- }
- /** The first file ending in `suffix` directly inside `dir`, or null. */
- private static string? find_suffix(string dir, string suffix) {
- try {
- var handle = Dir.open(dir);
- string? name = null;
- string? found = null;
- while ((name = handle.read_name()) != null) {
- if (name.has_suffix(suffix)) {
- found = name;
- break;
- }
- }
- return found == null ? null : Path.build_filename(dir, (!)found);
- } catch (Error e) {
- return null;
- }
- }
- /** The kind implied by a descriptor path's extension. */
- private static RepositoryKind kind_of(string descriptor_path) {
- return descriptor_path.has_suffix(".dcbar") ? RepositoryKind.DCBA : RepositoryKind.USM;
- }
- }
- }
|