Repository navigation
Import: copy a raw repository's files (Nexus, another Forge, a web server) into a hosted raw repository - #719
Merged
Merged
Conversation
…rver) into a hosted raw repository Raw has no protocol beyond a GET of a path and no standard listing, so a raw import reads one of three sources (crates/server/src/import/raw.rs): - Nexus' asset listing (/service/rest/v1/assets, paged with continuationToken) of <nexus>/repository/<name>: path, size, content type, SHA-1/256/512, blobCreated, lastDownloaded and uploader; - another Forge's JSON listing, walked a directory at a time with `after`; the cursor is the directory and the entry the walk got to; - the paths the import names, for a server without a listing: a HEAD for size, type and Last-Modified, and the .sha256 sidecar where served. The preflight picks the listing (`listing`: directories or assets) or refuses a source without one unless the import names its paths. A file is downloaded from the import's URL, checked against its size and every checksum listed, and stored through the file store's new import path (files::store::import): path rules, the administrator's selectors, the read-only policy, CAS dedup and an audit line with the uploader. A file Forge holds with the listed SHA-256 is identical without a download; one with another SHA-256 follows on_conflict without a download. The source's content type is kept as an upload's declared type is; blobCreated / last_modified / Last-Modified becomes the upload time and lastDownloaded / last_downloaded the last download (the import time where none). A path Forge refuses fails as its own item without counting towards the failures in a row that stop an import. Migration 0010 lets scan_listing take `assets` and `directories` and an item be a `file` (its lists also hold the npm import's values, so 0009 of #643 and 0010 apply in either order). The web UI offers Import on raw hosted repositories; docs/raw.md drops its "No import" limitation. Fixes #656 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJc3No8xu6rxoMc3pFJ
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 31, 2026
Resolves the conflict in docs/nexus-import.md with #720: the alias host cutover paragraph stays in the introduction, the new "Raw" section follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJc3No8xu6rxoMc3pFJ
#718 added npm to the same import framework this branch adds raw to. The merge keeps both formats: - FORMATS / IMPORT_FORMATS: nuget, npm, docker, raw. - Listing: npm's All beside raw's Assets and Directories (as_str, parse, their round-trip test). - Protocol (from #718) gets a Raw variant: a Nexus server plus repository resolves to <url>/repository/<name>, as for npm; the admin API and the worker pick raw's RawSource / Target::Files through it. - Found.kind: one field for component, tag (npm) and file (raw); npm's versions and dist-tags are built with raw's Found::component. - ItemSource: npm's integrity, shasum, tarball, deprecated and target beside raw's sha256, sha1, sidecar, content_type, error and missing. - copy(): kinds component, manifest, tag, file; copy_item: raw's Target::Files arm beside npm's dist-tag and deprecation arms, with raw's item labels in the failure messages; the expand_one method (#718) takes raw's named files. npm's verify/PackageSource and raw's verify_file both stay. - ImportView.listing: the one field both added; its values documented. - Import.tsx: the one format prop both introduced, with npm and raw wording side by side; both web tests kept. - Docs (import.md, nexus-import.md, README.md, docs/README.md) and the CI e2e pre-pulls (Verdaccio and nginx) carry both. Migration 0010 already lists 0009's values (all, tag) beside raw's (assets, directories, file), so it applies after 0009 unchanged. No query text changed: .sqlx keeps both sides' files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJc3No8xu6rxoMc3pFJ
schmitch
added a commit
that referenced
this pull request
Oct 31, 2026
…e apt branch - The apt migration is 0012 now, after main's 0010_raw_import and 0011_retention_unlisted_at. - Retention asks the plugin's `version_class` (#721): apt answers `Release` for every version, and the host's `Format::is_prerelease` of this branch is dropped. `Descriptor.prereleases` stays for the search filter, which still reads the `-` (browse lists every apt version). - The raw import's file store write sets `immutable: false`. - docs: the Signed indexes, Format options and Retention sections side by side; apt's package ids already count keep-N per architecture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012e6DJbxCkG53eCuDWqGGFy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #656
An import now copies a raw repository's files into a hosted raw repository. Raw has no protocol beyond a
GETof a path and no standard listing, so the import reads one of three sources, through a newcrates/server/src/import/raw.rs(RawSource), and stores each file through a new import path of the file store (files::store::import). It builds on #642 (#712)'s source interface: the leased worker, the scan cursor, resume and retry, the conflict policies and the dry run are the existing ones.Sources
GET <nexus>/service/rest/v1/assets?repository=<name>, paged withcontinuationToken, for aurlof a Nexus repository's shape (<nexus>/repository/<name>, a context path allowed).blobCreated(elselastModified),lastDownloadedand the uploader."blobCreated": null, "blobCreated": "<time>"is read with the lenient object reader the pre-Import through the format's own protocol (a NuGet feed, a registry's catalog) instead of Nexus' REST API, named neutrally #642 Nexus client had./of an old Nexus' raw path goes.GET <url>/<dir>/?format=json&limit=100&after=…withAccept: application/json; each file's size, content type, SHA-256,last_modifiedandlast_downloaded.{"dir", "after"}: a page's files up to its first subdirectory are recorded, the walk goes down into it, and at a directory's end it goes on in the parent after it. No stack, so a stopped walk resumes in the directory it was in.after) stops the scan with an error.HEADof each gives size,Content-TypeandLast-Modified(a server answering405/501gives nothing, the download tells).<path>.sha256, where served, gives the SHA-256: the first word, 64 hex digits (<hex>as Nexus writes it, or<hex> <name>assha256sumand Forge do). A sidecar that holds no SHA-256 counts as none.404/410) is an item that fails. It is looked for again when the failed items are retried.Preflight. Without
names, the preflight tries another Forge's listing ofurl(?format=json&limit=1), then Nexus' asset listing, and records which inscan_listing(directoriesorassets). A source with neither is refused with 400: name the paths. Withnames, the first named file is asked for (HEAD), so wrong credentials or an unreachable server are reported before anything is stored. A read-only raw repository is refused. The request form of a Nexus server plusrepositorybecomes<url>/repository/<name>for raw.Copy
Paths Forge refuses. An empty,
.or..segment, a backslash, a control character, a segment over 255 bytes, or more than 1,024 bytes in all.Files Forge already holds. Compared through the SHA-256 the source lists:
identical, without a download;on_conflictwithout a download:skipreports a conflict,overwritereplaces,failfails the import.Download and checks. A file is downloaded from the import's URL and its path (segments percent-encoded), never from Nexus'
downloadUrl. It is staged and checked against the listed size and every listed checksum (SHA-512, SHA-256, SHA-1; the message names a mismatching.sha256sidecar as such).Store.
files::store::import, the upload path's rules:forge::auditlineaction = "import"with the path, SHA-256, outcome, administrator, source and uploader.overwritereplaces even where the repository refuses redeploys, as for NuGet imports. The upload's staging is split intostore::stage(staged and hashed) and the commit, so a file is checked before it reaches the object store.Content type. The source's (Nexus'
contentType, Forge'scontent_type, else the response'sContent-Type) is kept as an upload's declared type is (store::content_type): "bytes only" types give way to the file name's.Times.
blobCreated/last_modified/Last-Modifiedbecomes the file'screated_atandupdated_at(the upload timeLast-Modified,older_than_daysandkeep_newest_per_directoryread).lastDownloaded/last_downloadedbecomeslast_downloaded_at; without one, the import time, as Import through the format's own protocol (a NuGet feed, a registry's catalog) instead of Nexus' REST API, named neutrally #642 has it for packages.raw_dao::set_imported_timesandraw_dao::lock_updated_at(runtime queries, no new.sqlxfiles).Uploader. Nexus' uploader goes into the item's
uploaderand the audit line.Schema
Migration
0010_raw_import.sql. It only replaces two CHECK constraints:repository_import.scan_listingalso takesassetsanddirectories;repository_import_item.kindalso takesfile(itsnameis the path, itsversionempty).Import: copy an npm registry's packages (Nexus, Verdaccio, another Forge) into a hosted npm repository #718's
0009_npm_import.sql, onmainnow, replaces the same two constraints;0010runs after it, and its lists keep0009's values (all,tag) beside raw's. No upgrade note is needed.API / UI
POST /admin/repositories/{name}/imports:FORMATSis nownuget, npm, docker, raw);urlis the raw repository,namesare file paths below it (normalized; a path Forge refuses or a directory is a 400; duplicates go).listingfield (Import: copy an npm registry's packages (Nexus, Verdaccio, another Forge) into a hosted npm repository #718 adds it too) also takesassetsanddirectories. Items of a raw import have kindfile.IMPORT_FORMATSincludes raw, so raw hosted repositories link to the import screen;Docs
docs/import.md:url/names/on_conflictfor raw;docs/nexus-import.md: a "Raw" section: the asset listing (REST API), 10 assets a page,downloadUrlnot used, and the privilegesnx-repository-view-raw-<name>-browseand-read.docs/raw.md: a new section "Importing another server's repository", and the "No import" limitation goes.docs/changes/2.2.0/656.feature.md.mirror.gcr.io/library/nginx:1.27.5-alpine.Tests
crates/server/tests/raw_import.rs(new, 4 tests) against a mock source (axum, Basic auth) that serves Nexus' asset listing (2 assets a page,blobCreatedwritten as Nexus 3.70 does), a Forge's JSON listing (2 entries a page) and a plain web server with sidecars, with download and listing counters:raw_import_from_nexus_keeps_bytes_types_times_and_uploaders:listing: assets,source_serverNexus';.sha256sidecar;created_at/updated_at=blobCreated,last_downloaded_at=lastDownloadedor the import time; uploader on the item;//,.., a control character, a 300-byte segment) each fail with the reason, and the import completes;not_downloaded_for_days: 30selects only the file Nexus last served in 2022, not the ones downloaded "at import";overwritein the previous request form (Nexus server +repository) replaces it with Nexus' upload time;failfails the import, naming the path.raw_dry_run_reports_files_sizes_and_conflicts_and_a_walk_resumes:planned_size, 1 identical and 1 conflict, writes nothing and downloads nothing;overwriteit plans the replacement;dir2/(its listing refuses the credentials) resumes there without listingdir1/again;raw_import_of_named_paths_checks_sha256_sidecars:/names collapse;Last-Modifiedis the upload time;raw_import_preflight_refusals: wrong credentials (400), an unreachable source (502), paths Forge refuses and a directory innames(400), a named import whose first file refuses the credentials, a read-only repository, a proxy.import::raw: Nexus assets with the repeatedblobCreated, upper-case digests and an old leading/; the Forge listing; sidecar parsing; the walk down, across pages and back up, with a cursor round trip; refused directory names; URL building (percent-encoding, Nexus behind a context path, what is not a Nexus URL).import: a listed file's item (times, the epoch, SHA-512 hex → base64, the stored source), every refused-path reason, the printable item name, and the new listings' round trip.crates/e2e/tests/raw_import.rs(new, 3 tests), run here against real containers:raw_import_from_a_real_nexus(mirror.gcr.io/sonatype/nexus3:3.70.1):docs/guides/a b ü.txtand 25 bulk files: 30 assets, which Nexus' listing gives in 4 pages;lastDownloaded.planned_size= their sum, 1 failed, nothing written.adminon every item. Each file's bytes, content type (where Nexus' is notapplication/octet-stream),.sha256sidecar = Nexus' SHA-256, upload time =blobCreated, last download =lastDownloadedor the import time.writePolicy: allow): 1 imported, 1 conflict, 28 identical,bytes_copied= the new file's 3 bytes.overwritein the previous request form: 1 replaced.raw_import_from_another_forge: 109 files on the same server (105 in one directory, more than the 100 a listing page holds, plus nested directories and a non-ASCII name with a space). Upload times, content types and last downloads equal the source's as the import found them; a second run is all identical with 0 bytes copied.raw_import_of_named_paths_from_a_web_server(mirror.gcr.io/library/nginx:1.27.5-alpine, files copied in andtouched to a fixed time):Last-Modified;imports.test.tsx): "offers the import on a raw hosted repository, with paths one per line": the link from the raw repository, the listing text, a failed refused-path item with its reason, the "Repository URL" prefilled from the last import, and paths with spaces and commas posted whole.Checks run
Environment: the shared build machine,
CARGO_PROFILE_{DEV,TEST}_DEBUG=line-tables-only,SQLX_OFFLINE=true,CARGO_BUILD_JOBS=2(the last e2e and clippy runs withCARGO_INCREMENTAL=0to save disk).cargo fmt --all -- --check: clean.cargo clippy -p forge-db -p forge-server -p forge-e2e --all-targets --features forge-e2e/forge-e2e -- -D warnings: clean (re-run after the last change).cargo test -p forge-server --lib -- import:: files::: 17 passed.cargo test -p forge-server --test raw_import: 4 passed (re-run on the final code).cargo test -p forge-server --test import --test raw: 12 + 9 passed (the existing NuGet/Docker import and raw suites; run before the last refactor, which only renamed a raw-only helper of the worker).cargo test -p forge-db --test migrations --test raw: 3 + 2 passed (migrations through 0010 on Postgres 18).cargo test -p forge-e2e --features forge-e2e --test raw_import -- --nocapture: 3 passed, on the final code ("Nexus' asset listing: 30 assets in 4 pages").cargo xtask changelog --check: "32 changelog fragment(s) under docs/changes, all for milestones after 2.1.0."cargo xtask docs --check: "16 page(s) of the site, every link resolves."pnpm install --frozen-lockfile,pnpm run tsc(clean),pnpm run test(40 files, 535 tests passed),pnpm run build(ok)..sqlxfiles: the two new raw_dao queries use the runtime API.Notes
urlhas a Nexus repository's shape, and only when the source does not answer as another Forge. If Decide whether an import from a Nexus also reads Nexus' REST API for checksums, last downloads and uploaders #645 decides "no", the Nexus source here goes; the named-paths source still reaches a Nexus raw repository, which serves the files (but not their times).on_conflict. As for NuGet imports,overwritereplaces a file even where the repository refuses redeploys (the import is the administrator's decision), but it needs the delete level on the path (selectors). A read-only repository refuses the import in the preflight, and each file in the worker.If-Modified-Sincenever answers 304 for new content. A created file takes the source's time as it is.<= 1970-01-02, files unpacked without times, e.g. nginx serving a tar's mtime 0) count as none, so retention's age rules do not see such a file as 56 years old right after the import.Last-Modifiedbecomes the upload time (the issue names no source time for them). A directory innamesis refused; naming a subtree of a listing source is not supported. An unparsable sidecar counts as no sidecar, and so does one the credentials may not read.mainis merged into this branch (3aa261e), keeping both formats side by side:FORMATS/IMPORT_FORMATS,Listing(npm'sAllbesideAssets/Directories), oneFound.kindforcomponent,tagandfile, both sides'ItemSourcefields, the copy kindscomponent,manifest,tag,file, raw'sFilestarget beside npm's arms incopy_itemand Import: copy an npm registry's packages (Nexus, Verdaccio, another Forge) into a hosted npm repository #718'sexpand_onemethod, and aRawvariant of Import: copy an npm registry's packages (Nexus, Verdaccio, another Forge) into a hosted npm repository #718'sProtocol(<url>/repository/<name>, as for npm).ImportView.listingandImport.tsx'sformatprop are one each; the docs, the CI pre-pulls (Verdaccio, nginx) and the web tests carry both. After the merge, fmt, clippy (withforge-format-npm),--lib import::(13),--test import --test raw_import(16 + 4),forge-db --test migrations(3), the changelog and docs checks, the web checks (41 files, 550 tests) and the raw e2e (3) pass.HEADis refused while itsGETis served (the named-path scan treats a405/501as "nothing known", any other refusal as an error).Generated by Claude Code