Преглед изворни кода

RepositoryService: discover and load DCBA artifact repositories

Repository directories under the configured parent are discovered by
looking for a .usmr (USM source repository) or a .dcbar (DCBA artifact
repository, same JSON shape) with the same scan loop, explicit
repositories list handling, dedup warnings and entries_by_dir snapshot
reuse. A directory holding both descriptors is a configuration error
with a clear message; the legacy single-repository fallback stays
.usmr-only.

RepositoryEntry gains a RepositoryKind (USM/DCBA) with kind-guarded
members: the .usmr/.dcbar descriptor is mtime-watched the same way,
DCBA entries load Usm.DcbaListing from public/ARTIFACTS.usml with its
own mtime watch, and find_dcba_artifact() matches name+version with a
plain string comparison per the DCBA specification. Cross-kind access
(listing_entries, dcba_listing_entries, find_entry, find_dcba_artifact,
listing_location, artifacts_listing_location, package_location,
artifact_location) throws a descriptive error instead of answering
nonsense.

RepositoryService.entries() keeps returning every loadable repository
of either kind in discovery order (what the homepage iterates), with
new usm_entries()/dcba_entries() filters for callers that iterate one
kind at a time. ensure_loaded semantics preserved: failing repositories
warn and drop, and nothing loading still throws.
saddle-Clanker-Saddle пре 1 месец
родитељ
комит
8c14622b92
1 измењених фајлова са 300 додато и 58 уклоњено
  1. 300 58
      src/RepositoryService.vala

+ 300 - 58
src/RepositoryService.vala

@@ -4,61 +4,121 @@ 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 USM repository discovered under the configured parent
-     * directory: a subdirectory holding a `.usmr` beside a `public/`
-     * directory, built on libusm via {@link Usm.Repository.from_file} and
-     * {@link Usm.RepositoryListing.from_stream}.
+     * 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 `.usmr` and `public/PACKAGES.usml` mtimes
-     * independently, so one repository can be rebuilt and re-read without
-     * disturbing the others.
+     * 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` inside {@link dir_path}. */
-        public string usmr_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 int64 usmr_mtime;
+        private DcbaListing? dcba_listing;
+        private int64 descriptor_mtime;
         private int64 listing_mtime;
 
-        public RepositoryEntry(string dir_path, string usmr_path) {
+        public RepositoryEntry.usm(string dir_path, string usmr_path) {
             this.dir_path = dir_path;
-            this.usmr_path = usmr_path;
+            this.kind = RepositoryKind.USM;
+            this.descriptor_path = usmr_path;
         }
 
-        /** Whether both the `.usmr` and the listing are currently loaded. */
+        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 { return repository != null && listing != null; }
+            get {
+                if (kind == RepositoryKind.DCBA) {
+                    return repository != null && dcba_listing != null;
+                }
+                return repository != null && listing != null;
+            }
         }
 
         /**
-         * Reloads the `.usmr` and listing when either file changed on disk
-         * (or was never loaded); a failure throws while keeping the
+         * 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_usmr_mtime = file_mtime(usmr_path);
-            int64 current_listing_mtime = file_mtime(listing_location());
+            int64 current_descriptor_mtime = file_mtime(descriptor_path);
+            int64 current_listing_mtime = file_mtime(current_listing_path());
             if (is_loaded
-                    && current_usmr_mtime == usmr_mtime
+                    && current_descriptor_mtime == descriptor_mtime
                     && current_listing_mtime == listing_mtime) {
                 return;
             }
 
-            if (repository == null || current_usmr_mtime != usmr_mtime) {
-                repository = new Repository.from_file(usmr_path);
-                usmr_mtime = current_usmr_mtime;
+            if (repository == null || current_descriptor_mtime != descriptor_mtime) {
+                repository = new Repository.from_file(descriptor_path);
+                descriptor_mtime = current_descriptor_mtime;
             }
 
-            if (listing == null || current_listing_mtime != listing_mtime) {
-                var file = File.new_for_path(listing_location());
+            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;
             }
@@ -70,13 +130,35 @@ namespace UsmWeb {
             return (!)repository;
         }
 
-        /** The loaded `PACKAGES.usml` entries, in listing order. */
+        /**
+         * 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 listing entry whose package file is `file`, or null. */
+        /**
+         * 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) {
@@ -86,16 +168,102 @@ namespace UsmWeb {
             return null;
         }
 
-        /** The absolute path of the verbatim-served `PACKAGES.usml`. */
-        public string listing_location() {
-            return Path.build_filename(dir_path, "public", "PACKAGES.usml");
+        /**
+         * 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 absolute path of a package archive inside `public/`. */
-        public string package_location(string file) {
+        /**
+         * 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(
@@ -108,22 +276,33 @@ namespace UsmWeb {
     }
 
     /**
-     * Read-side access to the on-disk USM repositories this app browses and
-     * serves.
+     * 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 discovered by scanning, or only
-     * those named by {@link UsmWebConfig.repositories} are used when that
-     * list is configured. A repository is addressed by the name in its
-     * `.usmr`, which is also its routing segment (`/repo/<name>/…`). A
-     * legacy single-repository layout with the `.usmr` directly inside the
-     * configured directory is still served as one repository.
+     * 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 `.usmr` and `PACKAGES.usml` 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.
+     * 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 {
 
@@ -193,10 +372,41 @@ namespace UsmWeb {
         }
 
         /**
-         * The repository whose `.usmr` 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.
+         * 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();
@@ -226,9 +436,9 @@ namespace UsmWeb {
         /**
          * Discovers repository directories: the configured subdirectories
          * when {@link UsmWebConfig.repositories} names them, otherwise every
-         * subdirectory holding a `.usmr`, falling back to the legacy
-         * single-repository layout. Already-known directories keep their
-         * entry (and loaded snapshot).
+         * 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>();
@@ -260,15 +470,15 @@ namespace UsmWeb {
                     }
                     continue;
                 }
-                var usmr_path = find_usmr(child_path);
-                if (usmr_path == null) {
+                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 under \"$(config.repo_dir)\"");
+                            @"\"$name\" from the configured repositories list has no *.usmr or *.dcbar under \"$(config.repo_dir)\"");
                     }
                     continue;
                 }
-                result.add(entry_for(child_path, (!)usmr_path));
+                result.add(entry_for(child_path, (!)descriptor));
             }
 
             if (result.length == 0) {
@@ -286,14 +496,21 @@ namespace UsmWeb {
             return result;
         }
 
-        /** Reuses the entry for `dir_path` when discovery finds it again. */
-        private RepositoryEntry entry_for(string dir_path, string usmr_path) {
+        /**
+         * 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) {
+            if (existing != null && ((!)existing).kind == kind_of((!)descriptor_path)) {
                 return (!)existing;
             }
-            var created = new RepositoryEntry(dir_path, usmr_path);
+            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;
         }
@@ -311,14 +528,34 @@ namespace UsmWeb {
             }
         }
 
+        /**
+         * 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(".usmr")) {
+                    if (name.has_suffix(suffix)) {
                         found = name;
                         break;
                     }
@@ -328,5 +565,10 @@ namespace UsmWeb {
                 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;
+        }
     }
 }