mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-06-30 09:27:57 +00:00
Compare commits
204 Commits
release/1.6.2
...
v1.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 908f7348d9 | |||
| 65b34cfec5 | |||
| 48907fc915 | |||
| 6f92c51134 | |||
| c63324756b | |||
| cfa79a0a32 | |||
| 1b177bbf68 | |||
| c0c4bc3e35 | |||
| 64e298e737 | |||
| 423fb6e813 | |||
| 6c4d7a75f9 | |||
| a743f03242 | |||
| d9bedfa478 | |||
| e8e3f944ea | |||
| 3c98aa237a | |||
| be68b15b74 | |||
| 4a19238df8 | |||
| dd85493b1a | |||
| 29e643cdf6 | |||
| 9bb765d048 | |||
| b9d4aed7d8 | |||
| 160d16d96c | |||
| 4655f10048 | |||
| 3333a643cb | |||
| acc79d2cf6 | |||
| 9502f3833f | |||
| 7a2a82c60e | |||
| 7c47b8662f | |||
| ce070ec010 | |||
| fbad25e2b5 | |||
| 041eeff67d | |||
| 6244747912 | |||
| b28daa8417 | |||
| 214c151194 | |||
| 4b8f0c42ac | |||
| 8ebe80f066 | |||
| b0c65112cb | |||
| 46e9ac65fa | |||
| a2fb925a94 | |||
| b4a171b3ea | |||
| 83b1c1b6cb | |||
| 7dd5c34254 | |||
| e04df59065 | |||
| c02c469fa6 | |||
| d5fbf0f795 | |||
| ed4af9a0f6 | |||
| 9bb8de68d0 | |||
| 00e75ebf3c | |||
| c2b0379e91 | |||
| 84a5c20abe | |||
| d3a613c335 | |||
| 4e4ca0f38e | |||
| 12390f102e | |||
| 1224708283 | |||
| 1c1de6564b | |||
| 9c121c8111 | |||
| 2605dd24f5 | |||
| 781eaff397 | |||
| 33c06b1b4a | |||
| caed9c234d | |||
| af1df410a2 | |||
| dfa34157a0 | |||
| 722064363c | |||
| 9771f228bc | |||
| 6ee51b6454 | |||
| ab150890cb | |||
| 2974558aa7 | |||
| 00773f9840 | |||
| 8dc61cf362 | |||
| 4a9ddbdc34 | |||
| c316ea9b73 | |||
| d027a251ba | |||
| 16b6348710 | |||
| 1c1abe6b10 | |||
| c4bfbe52ca | |||
| c723fea807 | |||
| 0d31298f37 | |||
| bba6bd7db5 | |||
| faeaad0378 | |||
| a20eb6866a | |||
| 809ab47042 | |||
| 0b84009b4a | |||
| 92d7561d49 | |||
| e908531bef | |||
| f8620d10b2 | |||
| 8be72b48a1 | |||
| 5655480e00 | |||
| 3c8defa304 | |||
| 1ed50697ec | |||
| 3b432d42b5 | |||
| 89e3bdfa07 | |||
| f8d2d3cb8d | |||
| 01004e19c0 | |||
| 92bb28149c | |||
| 652210acfa | |||
| 45f3da256b | |||
| 3c5e83366a | |||
| a7a4bb7001 | |||
| 2f7f726d4c | |||
| 43d7284657 | |||
| 02423af931 | |||
| 2a228625e2 | |||
| 16e0302f30 | |||
| 6ecbc9ba90 | |||
| 7276b9621f | |||
| fb7734b5b0 | |||
| 29c37293b9 | |||
| f25ba2e6b0 | |||
| 6a5b9ddacf | |||
| 48355f65ed | |||
| d616977904 | |||
| 3c068febde | |||
| 76adbe49e0 | |||
| f6221a2413 | |||
| 4f46cb04f5 | |||
| 66e814c086 | |||
| b3f5d96490 | |||
| 144265122a | |||
| 4f95d75c37 | |||
| 8db1d2e7f1 | |||
| 4088a811cd | |||
| 2ac4c75fad | |||
| e2ebcbb02a | |||
| 9defe70190 | |||
| 23943d47e9 | |||
| 49f342878a | |||
| 1f29c65a95 | |||
| a65f79eb79 | |||
| c6a9f82358 | |||
| 1702537979 | |||
| 12604b9379 | |||
| 9b523e6bd5 | |||
| aa030e3f36 | |||
| 8e739524b0 | |||
| 4ba4c0cba6 | |||
| 48d02918c1 | |||
| d672bfa317 | |||
| 9a90038dd2 | |||
| 7d23196f76 | |||
| 2c6812934e | |||
| 60f3eb151b | |||
| 0db9797c4e | |||
| a2ffffedc1 | |||
| 19b98c62c1 | |||
| 06fea598e1 | |||
| 9a6f06d23e | |||
| 67f6a0e458 | |||
| a57e2aecd7 | |||
| 1e7c15b69b | |||
| a70f572efd | |||
| 8c183b45c6 | |||
| a8f7f58dab | |||
| 31fe26de5e | |||
| eb1b770dc2 | |||
| abb2ad635f | |||
| a75df0a697 | |||
| ea797f8ebe | |||
| a4cc9211d6 | |||
| 836d9f3b8b | |||
| 61650e5b3b | |||
| 4e457aa570 | |||
| fe70da9c08 | |||
| 4b57e65658 | |||
| bcd81eeae4 | |||
| af483d1165 | |||
| 6b8651fda2 | |||
| de699aebe5 | |||
| 0021cf876b | |||
| 32b601bde6 | |||
| b464e7f80b | |||
| e0f282aca9 | |||
| ba65daf6cb | |||
| b8455f6de9 | |||
| 132c923f25 | |||
| b6d83a4f61 | |||
| 4526d6d831 | |||
| 02d2ba255c | |||
| d94792dd65 | |||
| 66eb75f492 | |||
| 33a2f70d07 | |||
| 10f942c8e0 | |||
| 568a9ce4d5 | |||
| ddf415a359 | |||
| 29ac9c1919 | |||
| d3bed7830c | |||
| c2d5f47643 | |||
| 731e92c8e4 | |||
| 94a600c0c1 | |||
| e1d8ae8a35 | |||
| d3b7186dea | |||
| 3608c137a0 | |||
| 15a3efe758 | |||
| 4b73ae462f | |||
| b49a631e0b | |||
| 12b6b04055 | |||
| a1f659bea0 | |||
| 8ca4cb8dcb | |||
| 8ce8f250d5 | |||
| 3672f6f92f | |||
| 888a6b2caa | |||
| 231039e86c | |||
| dc884e6052 | |||
| 4675589cf6 | |||
| 32f03bfd62 |
@@ -4,6 +4,10 @@ Fixes #
|
||||
|
||||
All new code should be covered with tests, documentation should be updated. CI should pass.
|
||||
|
||||
Also, to speed up things, if you could kindly "Allow edits and access to secrets by maintainers" in the
|
||||
PR settings, as this allows us to rebase the PR on master, fix conflicts, run coverage and help with
|
||||
implementing code and tests.
|
||||
|
||||
## Description of the Change
|
||||
|
||||
<!--
|
||||
@@ -14,6 +18,7 @@ Why this change is important?
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] allow Maintainers to edit PR (rebase, run coverage, help with tests, ...)
|
||||
- [ ] unit-test added (if change is algorithm)
|
||||
- [ ] functional test added/updated (if change is functional)
|
||||
- [ ] man page updated (if applicable)
|
||||
|
||||
+78
-27
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: CI
|
||||
|
||||
on:
|
||||
@@ -10,15 +11,38 @@ on:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# see: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell
|
||||
shell: bash --noprofile --norc -eo pipefail {0}
|
||||
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: "Unit Tests"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
- name: "Docker Image"
|
||||
run: |
|
||||
make docker-image
|
||||
- name: "Unit Tests"
|
||||
run: |
|
||||
make docker-unit-test
|
||||
mkdir -p out/coverage
|
||||
mv unit.out out/coverage/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-tests-coverage
|
||||
path: out/
|
||||
|
||||
test:
|
||||
name: "Test (Ubuntu 22.04)"
|
||||
name: "System Test"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
@@ -33,7 +57,7 @@ jobs:
|
||||
- name: "Install Test Packages"
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends graphviz gnupg2 gpgv2 git gcc make devscripts python3 python3-requests-unixsocket python3-termcolor python3-swiftclient python3-boto python3-azure-storage python3-etcd3 python3-plyvel flake8 faketime
|
||||
sudo apt-get install -y --no-install-recommends graphviz gnupg2 gpgv2 git gcc make devscripts python3 python3-requests-unixsocket python3-termcolor python3-swiftclient python3-boto python3-azure-storage python3-etcd3 python3-plyvel flake8 faketime dput-ng
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,21 +87,10 @@ jobs:
|
||||
with:
|
||||
directory: ${{ runner.temp }}
|
||||
|
||||
- name: "Run Unit Tests"
|
||||
env:
|
||||
RUN_LONG_TESTS: 'yes'
|
||||
AZURE_STORAGE_ENDPOINT: "http://127.0.0.1:10000/devstoreaccount1"
|
||||
AZURE_STORAGE_ACCOUNT: "devstoreaccount1"
|
||||
AZURE_STORAGE_ACCESS_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
sudo mkdir -p /srv ; sudo chown runner /srv
|
||||
COVERAGE_DIR=${{ runner.temp }} make test
|
||||
|
||||
- name: "Run Benchmark"
|
||||
run: |
|
||||
COVERAGE_DIR=${{ runner.temp }} make bench
|
||||
mkdir -p out/coverage
|
||||
COVERAGE_DIR=$PWD/out/coverage make bench
|
||||
|
||||
- name: "Run System Tests"
|
||||
env:
|
||||
@@ -87,32 +100,68 @@ jobs:
|
||||
AZURE_STORAGE_ACCESS_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
JFROG_URL: ${{ secrets.JFROG_URL }}
|
||||
JFROG_USERNAME: ${{ secrets.JFROG_USERNAME }}
|
||||
JFROG_PASSWORD: ${{ secrets.JFROG_PASSWORD }}
|
||||
run: |
|
||||
sudo mkdir -p /srv ; sudo chown runner /srv
|
||||
COVERAGE_DIR=${{ runner.temp }} make system-test
|
||||
mkdir -p out/coverage
|
||||
COVERAGE_DIR=$PWD/out/coverage make system-test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: system-tests-coverage
|
||||
path: out/
|
||||
|
||||
coverage:
|
||||
name: "Upload Coverage"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- unit-test
|
||||
- test
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Download Unit Test Coverage"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unit-tests-coverage
|
||||
|
||||
- name: "Download System Test Coverage"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: system-tests-coverage
|
||||
|
||||
- name: "Merge Code Coverage"
|
||||
run: |
|
||||
go install github.com/wadey/gocovmerge@v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
~/go/bin/gocovmerge unit.out ${{ runner.temp }}/*.out > coverage.txt
|
||||
# go install github.com/wadey/gocovmerge@v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
# ~/go/bin/gocovmerge coverage/*.out > coverage.txt
|
||||
awk 'FNR==1 && NR!=1 {next} {print}' coverage/*.out > coverage.txt
|
||||
|
||||
- name: "Upload Code Coverage"
|
||||
uses: codecov/codecov-action@v2
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
uses: codecov/codecov-action@v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: coverage.txt
|
||||
fail_ci_if_error: true
|
||||
|
||||
|
||||
ci-debian-build:
|
||||
name: "Build"
|
||||
needs: test
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: ["Debian 13/testing", "Debian 12/bookworm", "Debian 11/bullseye", "Debian 10/buster", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
|
||||
name: ["Debian 13/trixie", "Debian 12/bookworm", "Debian 11/bullseye", "Ubuntu 26.04", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
|
||||
arch: ["amd64", "i386" , "arm64" , "armhf"]
|
||||
include:
|
||||
- name: "Debian 13/testing"
|
||||
- name: "Debian 13/trixie"
|
||||
suite: trixie
|
||||
image: debian:trixie-slim
|
||||
- name: "Debian 12/bookworm"
|
||||
@@ -121,9 +170,9 @@ jobs:
|
||||
- name: "Debian 11/bullseye"
|
||||
suite: bullseye
|
||||
image: debian:bullseye-slim
|
||||
- name: "Debian 10/buster"
|
||||
suite: buster
|
||||
image: debian:buster-slim
|
||||
- name: "Ubuntu 26.04"
|
||||
suite: resolute
|
||||
image: ubuntu:26.04
|
||||
- name: "Ubuntu 24.04"
|
||||
suite: noble
|
||||
image: ubuntu:24.04
|
||||
@@ -135,6 +184,7 @@ jobs:
|
||||
image: ubuntu:20.04
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
env:
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
@@ -226,7 +276,8 @@ jobs:
|
||||
|
||||
ci-binary-build:
|
||||
name: "Build"
|
||||
needs: test
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -35,16 +35,17 @@ jobs:
|
||||
- name: Install and initialize swagger
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init -q --markdownFiles docs
|
||||
swag init -q --propertyStrategy pascalcase --markdownFiles docs
|
||||
shell: sh
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
# Require: The version of golangci-lint to use.
|
||||
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
|
||||
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
|
||||
version: v1.64.5
|
||||
version: v2.12.2
|
||||
args: --timeout=10m
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
@@ -26,6 +26,8 @@ _testmain.go
|
||||
*.test
|
||||
|
||||
coverage.txt
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
*.pyc
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
version: "2"
|
||||
run:
|
||||
timeout: 5m
|
||||
linters:
|
||||
settings:
|
||||
staticcheck:
|
||||
|
||||
@@ -69,3 +69,18 @@ List of contributors, in chronological order:
|
||||
* Leigh London (https://github.com/leighlondon)
|
||||
* Gordian Schoenherr (https://github.com/schoenherrg)
|
||||
* Silke Hofstra (https://github.com/silkeh)
|
||||
* Itay Porezky (https://github.com/itayporezky)
|
||||
* Alejandro Guijarro Monerris (https://github.com/alguimodd)
|
||||
* JupiterRider (https://github.com/JupiterRider)
|
||||
* Agustin Henze (https://github.com/agustinhenze)
|
||||
* Tobias Assarsson (https://github.com/daedaluz)
|
||||
* Yaksh Bariya (https://github.com/thunder-coding)
|
||||
* Juan Calderon-Perez (https://github.com/gaby)
|
||||
* Ato Araki (https://github.com/atotto)
|
||||
* Roman Lebedev (https://github.com/LebedevRI)
|
||||
* Brian Witt (https://github.com/bwitt)
|
||||
* Ales Bregar (https://github.com/abregar)
|
||||
* Tim Foerster (https://github.com/tonobo)
|
||||
* Zhang Xiao (https://github.com/xzhang1)
|
||||
* Tom Nguyen (https://github.com/lecafard)
|
||||
* Philip Cramer (https://github.com/PhilipCramer)
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ Please report unacceptable behavior on [https://github.com/aptly-dev/aptly/discu
|
||||
### List of Repositories
|
||||
|
||||
* [aptly-dev/aptly](https://github.com/aptly-dev/aptly) - aptly source code, functional tests, man page
|
||||
* [apty-dev/aptly-dev.github.io](https://github.com/aptly-dev/aptly-dev.github.io) - aptly website (https://www.aptly.info/)
|
||||
* [aptly-dev/aptly-dev.github.io](https://github.com/aptly-dev/aptly-dev.github.io) - aptly website (https://www.aptly.info/)
|
||||
* [aptly-dev/aptly-fixture-db](https://github.com/aptly-dev/aptly-fixture-db) & [aptly-dev/aptly-fixture-pool](https://github.com/aptly-dev/aptly-fixture-pool) provide
|
||||
fixtures for aptly functional tests
|
||||
|
||||
@@ -130,14 +130,14 @@ aptly version: 1.5.0+189+g0fc90dff
|
||||
|
||||
In order to run aptly unit tests, enter the following:
|
||||
```
|
||||
make docker-unit-tests
|
||||
make docker-unit-test
|
||||
```
|
||||
|
||||
#### Running system tests
|
||||
|
||||
In order to run aptly system tests, enter the following:
|
||||
```
|
||||
make docker-system-tests
|
||||
make docker-system-test
|
||||
```
|
||||
|
||||
#### Running golangci-lint
|
||||
|
||||
@@ -2,14 +2,28 @@ GOPATH=$(shell go env GOPATH)
|
||||
VERSION=$(shell make -s version)
|
||||
PYTHON?=python3
|
||||
BINPATH?=$(GOPATH)/bin
|
||||
GOLANGCI_LINT_VERSION=v2.0.2 # version supporting go 1.24
|
||||
GOLANGCI_LINT_VERSION=v2.12.2 # version supporting go 1.25
|
||||
COVERAGE_DIR?=$(shell mktemp -d)
|
||||
GOOS=$(shell go env GOHOSTOS)
|
||||
GOARCH=$(shell go env GOHOSTARCH)
|
||||
|
||||
export PODMAN_USERNS = keep-id
|
||||
DOCKER_RUN = docker run --security-opt label=disable --user 0:0 --rm -v ${PWD}:/work/src
|
||||
|
||||
# Setting TZ for certificates
|
||||
export TZ=UTC
|
||||
# Unit Tests and some sysmte tests rely on expired certificates, turn back the time
|
||||
export TEST_FAKETIME := 2025-01-02 03:04:05
|
||||
|
||||
# run with 'COVERAGE_SKIP=1' to skip coverage checks during system tests
|
||||
ifeq ($(COVERAGE_SKIP),1)
|
||||
COVERAGE_ARG_BUILD :=
|
||||
COVERAGE_ARG_TEST := --coverage-skip
|
||||
else
|
||||
COVERAGE_ARG_BUILD := -coverpkg="./..."
|
||||
COVERAGE_ARG_TEST := --coverage-dir $(COVERAGE_DIR)
|
||||
endif
|
||||
|
||||
# export CAPUTRE=1 for regenrating test gold files
|
||||
ifeq ($(CAPTURE),1)
|
||||
CAPTURE_ARG := --capture
|
||||
@@ -63,7 +77,7 @@ azurite-stop:
|
||||
|
||||
swagger: swagger-install
|
||||
# Generate swagger docs
|
||||
@PATH=$(BINPATH)/:$(PATH) swag init --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
@PATH=$(BINPATH)/:$(PATH) swag init --propertyStrategy pascalcase --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
|
||||
etcd-install:
|
||||
# Install etcd
|
||||
@@ -101,13 +115,13 @@ test: prepare swagger etcd-install ## Run unit tests (add TEST=regex to specify
|
||||
|
||||
system-test: prepare swagger etcd-install ## Run system tests
|
||||
# build coverage binary
|
||||
go test -v -coverpkg="./..." -c -tags testruncli
|
||||
go test -v $(COVERAGE_ARG_BUILD) -c -tags testruncli
|
||||
# Download fixture-db, fixture-pool, etcd.db
|
||||
if [ ! -e ~/aptly-fixture-db ]; then git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; fi
|
||||
if [ ! -e ~/aptly-fixture-pool ]; then git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; fi
|
||||
test -f ~/etcd.db || (curl -o ~/etcd.db.xz http://repo.aptly.info/system-tests/etcd.db.xz && xz -d ~/etcd.db.xz)
|
||||
# Run system tests
|
||||
PATH=$(BINPATH)/:$(PATH) FORCE_COLOR=1 $(PYTHON) system/run.py --long --coverage-dir $(COVERAGE_DIR) $(CAPTURE_ARG) $(TEST)
|
||||
PATH=$(BINPATH)/:$(PATH) FORCE_COLOR=1 $(PYTHON) system/run.py --long $(COVERAGE_ARG_TEST) $(CAPTURE_ARG) $(TEST)
|
||||
|
||||
bench:
|
||||
@echo "\e[33m\e[1mRunning benchmark ...\e[0m"
|
||||
@@ -117,7 +131,8 @@ serve: prepare swagger-install ## Run development server (auto recompiling)
|
||||
test -f $(BINPATH)/air || go install github.com/air-verse/air@v1.52.3
|
||||
cp debian/aptly.conf ~/.aptly.conf
|
||||
sed -i /enable_swagger_endpoint/s/false/true/ ~/.aptly.conf
|
||||
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
|
||||
sed -i /enable_metrics_endpoint/s/false/true/ ~/.aptly.conf
|
||||
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --propertyStrategy pascalcase --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
|
||||
|
||||
dpkg: prepare swagger ## Build debian packages
|
||||
@test -n "$(DEBARCH)" || (echo "please define DEBARCH"; exit 1)
|
||||
@@ -171,16 +186,16 @@ docker-image-no-cache: ## Build aptly-dev docker image (no cache)
|
||||
@docker build --no-cache -f system/Dockerfile . -t aptly-dev
|
||||
|
||||
docker-build: ## Build aptly in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper build
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper build
|
||||
|
||||
docker-shell: ## Run aptly and other commands in docker container
|
||||
@docker run -it --rm -p 3142:3142 -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper || true
|
||||
@$(DOCKER_RUN) -it -p 3142:3142 aptly-dev /work/src/system/docker-wrapper || true
|
||||
|
||||
docker-deb: ## Build debian packages in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper dpkg DEBARCH=amd64
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper dpkg DEBARCH=amd64
|
||||
|
||||
docker-unit-test: ## Run unit tests in docker container (add TEST=regex to specify which tests to run)
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
$(DOCKER_RUN) -t --tmpfs /smallfs:rw,size=1m aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
@@ -189,27 +204,27 @@ docker-unit-test: ## Run unit tests in docker container (add TEST=regex to spec
|
||||
azurite-stop
|
||||
|
||||
docker-system-test: ## Run system tests in docker container (add TEST=t04_mirror or TEST=UpdateMirror26Test to run only specific tests)
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) \
|
||||
AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \
|
||||
system-test TEST=$(TEST) \
|
||||
system-test TEST=$(TEST) CAPTURE=$(CAPTURE) COVERAGE_SKIP=$(COVERAGE_SKIP) \
|
||||
azurite-stop
|
||||
|
||||
docker-serve: ## Run development server (auto recompiling) on http://localhost:3142
|
||||
@docker run -it --rm -p 3142:3142 -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper serve || true
|
||||
@$(DOCKER_RUN) -it -p 3142:3142 -v /tmp/cache-go-aptly:/var/lib/aptly/.cache/go-build aptly-dev /work/src/system/docker-wrapper serve || true
|
||||
|
||||
docker-lint: ## Run golangci-lint in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper lint
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper lint
|
||||
|
||||
docker-binaries: ## Build binary releases (FreeBSD, macOS, Linux generic) in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper binaries
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper binaries
|
||||
|
||||
docker-man: ## Create man page in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper man
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper man
|
||||
|
||||
mem.png: mem.dat mem.gp
|
||||
gnuplot mem.gp
|
||||
|
||||
+10
-6
@@ -63,7 +63,7 @@ Define Release APT sources in ``/etc/apt/sources.list.d/aptly.list``::
|
||||
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/release DIST main
|
||||
|
||||
Where DIST is one of: ``buster``, ``bullseye``, ``bookworm``, ``focal``, ``jammy``, ``noble``
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Install aptly packages::
|
||||
|
||||
@@ -80,7 +80,7 @@ Define CI APT sources in ``/etc/apt/sources.list.d/aptly-ci.list``::
|
||||
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/ci DIST main
|
||||
|
||||
Where DIST is one of: ``buster``, ``bullseye``, ``bookworm``, ``focal``, ``jammy``, ``noble``
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Note: same gpg key is used as for the Upstream Debian Packages.
|
||||
|
||||
@@ -111,10 +111,8 @@ With configuration management systems:
|
||||
|
||||
- `Chef cookbook <https://github.com/hw-cookbooks/aptly>`_ by Aaron Baer
|
||||
(Heavy Water Operations, LLC)
|
||||
- `Puppet module <https://github.com/alphagov/puppet-aptly>`_ by
|
||||
Government Digital Services
|
||||
- `Puppet module <https://github.com/tubemogul/puppet-aptly>`_ by
|
||||
TubeMogul
|
||||
- `Puppet module <https://github.com/voxpupuli/puppet-aptly>`_ by
|
||||
Vox Pupuli
|
||||
- `SaltStack Formula <https://github.com/saltstack-formulas/aptly-formula>`_ by
|
||||
Forrest Alvarez and Brian Jackson
|
||||
- `Ansible role <https://github.com/aioue/ansible-role-aptly>`_ by Tom Paine
|
||||
@@ -135,3 +133,9 @@ Scala sbt:
|
||||
Molior:
|
||||
|
||||
- `Molior Debian Build System <https://github.com/molior-dbs/molior>`_ by André Roth
|
||||
|
||||
Jenny:
|
||||
|
||||
- `Jenny, an APT repository manager, a tool to manage Debian package repositories for a Debian-like distribution <https://github.com/groupe-edf/Jenny>`_ by EDF Group
|
||||
It handles incoming packages either built locally or mirrored from external sources, follows them through phases of development (called "environments"), and publishes them as repositories usable by apt and related tools.
|
||||
This tool is currently only in French language.
|
||||
|
||||
@@ -13,5 +13,6 @@ git push origin v$version master
|
||||
- run swagger locally (`make docker-serve`)
|
||||
- copy generated docs/swagger.json to https://github.com/aptly-dev/www.aptly.info/tree/master/static/swagger/aptly_1.x.y.json
|
||||
- add new version to select tag in content/doc/api/swagger.md line 48
|
||||
- update version in content/download.md
|
||||
- push commit to master
|
||||
- create release announcement on https://github.com/aptly-dev/aptly/discussions
|
||||
|
||||
+2
-2
@@ -70,7 +70,7 @@ func apiReady(isReady *atomic.Value) func(*gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
c.JSON(200, status)
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func truthy(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(v) {
|
||||
case "n", "no", "f", "false", "0", "off":
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -41,6 +42,22 @@ func createTestConfig() *os.File {
|
||||
jsonString, err := json.Marshal(gin.H{
|
||||
"architectures": []string{},
|
||||
"enableMetricsEndpoint": true,
|
||||
"S3PublishEndpoints": map[string]map[string]string{
|
||||
"test-s3": {
|
||||
"region": "us-east-1",
|
||||
"bucket": "bucket-s3",
|
||||
},
|
||||
},
|
||||
"GcsPublishEndpoints": map[string]map[string]string{
|
||||
"test-gcs": {
|
||||
"bucket": "bucket-gcs",
|
||||
},
|
||||
},
|
||||
"JFrogPublishEndpoints": map[string]map[string]string{
|
||||
"test-jfrog": {
|
||||
"url": "http://jfrog.example.com",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -173,3 +190,39 @@ func (s *APISuite) TestTruthy(c *C) {
|
||||
c.Check(truthy(-1), Equals, true)
|
||||
c.Check(truthy(gin.H{}), Equals, true)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetJFrogEndpoints(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/jfrog", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var endpoints []string
|
||||
err = json.Unmarshal(response.Body.Bytes(), &endpoints)
|
||||
c.Assert(err, IsNil)
|
||||
sort.Strings(endpoints)
|
||||
c.Check(endpoints, DeepEquals, []string{"test-jfrog"})
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetS3Endpoints(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/s3", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var endpoints []string
|
||||
err = json.Unmarshal(response.Body.Bytes(), &endpoints)
|
||||
c.Assert(err, IsNil)
|
||||
sort.Strings(endpoints)
|
||||
c.Check(endpoints, DeepEquals, []string{"test-s3"})
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetGCSEndpoints(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gcs", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var endpoints []string
|
||||
err = json.Unmarshal(response.Body.Bytes(), &endpoints)
|
||||
c.Assert(err, IsNil)
|
||||
sort.Strings(endpoints)
|
||||
c.Check(endpoints, DeepEquals, []string{"test-gcs"})
|
||||
}
|
||||
|
||||
+104
-2
@@ -13,6 +13,10 @@ import (
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// syncFile is a seam to allow tests to force fsync failures (e.g. ENOSPC).
|
||||
// In production it calls (*os.File).Sync().
|
||||
var syncFile = func(f *os.File) error { return f.Sync() }
|
||||
|
||||
func verifyPath(path string) bool {
|
||||
path = filepath.Clean(path)
|
||||
for _, part := range strings.Split(path, string(filepath.Separator)) {
|
||||
@@ -114,34 +118,132 @@ func apiFilesUpload(c *gin.Context) {
|
||||
}
|
||||
|
||||
stored := []string{}
|
||||
openFiles := []*os.File{}
|
||||
|
||||
// Write all files first
|
||||
for _, files := range c.Request.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
destPath := filepath.Join(path, filepath.Base(file.Filename))
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
_ = src.Close()
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
_ = src.Close()
|
||||
if err != nil {
|
||||
_ = dst.Close()
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Keep file open for batch sync
|
||||
openFiles = append(openFiles, dst)
|
||||
stored = append(stored, filepath.Join(c.Params.ByName("dir"), filepath.Base(file.Filename)))
|
||||
}
|
||||
}
|
||||
|
||||
// Sync all files at once to catch ENOSPC errors
|
||||
for i, dst := range openFiles {
|
||||
err := syncFile(dst)
|
||||
if err != nil {
|
||||
// Close all files
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("error syncing file %s: %s", stored[i], err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Close all files
|
||||
for _, dst := range openFiles {
|
||||
_ = dst.Close()
|
||||
}
|
||||
|
||||
apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
|
||||
c.JSON(200, stored)
|
||||
}
|
||||
|
||||
// @Summary Upload One File
|
||||
// @Description **Upload one file to a directory**
|
||||
// @Description
|
||||
// @Description - file is uploaded
|
||||
// @Description - existing uploaded are overwritten
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ dput aptly aptly_0.9~dev+217+ge5d646c_i386.changes
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Param dir path string true "Directory to upload files to. Created if does not exist"
|
||||
// @Param file path string true "File to upload"
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "Name of uploaded file"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir}/{file} [put]
|
||||
func apiFilesUploadOne(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
fileName := c.Params.ByName("file")
|
||||
if !verifyPath(fileName) {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong file"))
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))
|
||||
err := os.MkdirAll(path, 0777)
|
||||
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
stored := []string{}
|
||||
|
||||
destPath := filepath.Join(path, fileName)
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
if _, err = io.Copy(dst, c.Request.Body); err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = syncFile(dst); err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("error syncing file %s: %s", fileName, err))
|
||||
return
|
||||
}
|
||||
|
||||
stored = append(stored, filepath.Join(c.Params.ByName("dir"), fileName))
|
||||
|
||||
apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
|
||||
c.JSON(200, stored)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type FilesUploadDiskFullSuite struct {
|
||||
aptlyContext *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&FilesUploadDiskFullSuite{})
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) SetUpTest(c *C) {
|
||||
aptly.Version = "testVersion"
|
||||
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
c.Assert(err, IsNil)
|
||||
s.configFile = file
|
||||
|
||||
jsonString, err := json.Marshal(gin.H{
|
||||
"architectures": []string{},
|
||||
"rootDir": c.MkDir(),
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
_, err = file.Write(jsonString)
|
||||
c.Assert(err, IsNil)
|
||||
_ = file.Close()
|
||||
|
||||
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
|
||||
flags.Bool("no-lock", false, "dummy")
|
||||
flags.Int("db-open-attempts", 3, "dummy")
|
||||
flags.String("config", s.configFile.Name(), "dummy")
|
||||
flags.String("architectures", "", "dummy")
|
||||
s.flags = flags
|
||||
|
||||
aptlyContext, err := ctx.NewContext(s.flags)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.aptlyContext = aptlyContext
|
||||
s.router = Router(aptlyContext)
|
||||
context = aptlyContext
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TearDownTest(c *C) {
|
||||
if s.configFile != nil {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
}
|
||||
if s.aptlyContext != nil {
|
||||
s.aptlyContext.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadSuccessWithSync(c *C) {
|
||||
testContent := []byte("test file content for upload")
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "testfile.txt")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = part.Write(testContent)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir", "testfile.txt")
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(content, DeepEquals, testContent)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadVerifiesFileIntegrity(c *C) {
|
||||
testContent := bytes.Repeat([]byte("A"), 10000)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "largefile.bin")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = io.Copy(part, bytes.NewReader(testContent))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir2", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir2", "largefile.bin")
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(content), Equals, len(testContent))
|
||||
c.Check(content, DeepEquals, testContent)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadMultipleFilesWithBatchSync(c *C) {
|
||||
testFiles := map[string][]byte{
|
||||
"file1.txt": []byte("content of file 1"),
|
||||
"file2.txt": bytes.Repeat([]byte("B"), 5000),
|
||||
"file3.deb": []byte("debian package content"),
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
for filename, content := range testFiles {
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write(content)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
err := writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/multitest", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadDir := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "multitest")
|
||||
for filename, expectedContent := range testFiles {
|
||||
uploadedFile := filepath.Join(uploadDir, filename)
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil, Commentf("Failed to read %s", filename))
|
||||
c.Check(content, DeepEquals, expectedContent, Commentf("Content mismatch for %s", filename))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturnsErrorOnSyncFailure(c *C) {
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(f *os.File) error {
|
||||
if filepath.Base(f.Name()) == "syncfail.txt" {
|
||||
return syscall.ENOSPC
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part1, err := writer.CreateFormFile("file", "ok.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part1.Write([]byte("ok"))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
part2, err := writer.CreateFormFile("file", "syncfail.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part2.Write([]byte("will fail on sync"))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/syncfaildir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
c.Check(bytes.Contains(w.Body.Bytes(), []byte("error syncing file")), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestVerifyPath(c *C) {
|
||||
c.Check(verifyPath("a/b/c"), Equals, true)
|
||||
c.Check(verifyPath("../x"), Equals, false)
|
||||
c.Check(verifyPath("./x"), Equals, true)
|
||||
c.Check(verifyPath(".."), Equals, false)
|
||||
c.Check(verifyPath("."), Equals, false)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsEmptyWhenUploadMissing(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
c.Check(strings.TrimSpace(w.Body.String()), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsReturnsDirectories(c *C) {
|
||||
uploadRoot := s.aptlyContext.UploadPath()
|
||||
c.Assert(os.MkdirAll(filepath.Join(uploadRoot, "d1"), 0777), IsNil)
|
||||
c.Assert(os.MkdirAll(filepath.Join(uploadRoot, "d2"), 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(uploadRoot, "rootfile"), []byte("x"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
body := w.Body.String()
|
||||
c.Check(strings.Contains(body, "d1"), Equals, true)
|
||||
c.Check(strings.Contains(body, "d2"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesNotFound(c *C) {
|
||||
req, err := http.NewRequest("GET", "/api/files/does-not-exist", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesReturnsFiles(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dir")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "b.txt"), []byte("b"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files/dir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
body := w.Body.String()
|
||||
c.Check(strings.Contains(body, "a.txt"), Equals, true)
|
||||
c.Check(strings.Contains(body, "b.txt"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteDirRemovesDirectory(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
_, statErr := os.Stat(base)
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileRemovesFile(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel2")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel2/a.txt", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
_, statErr := os.Stat(filepath.Join(base, "a.txt"))
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileNotFoundStillOk(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel3")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel3/nope.txt", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestRejectsInvalidDir(c *C) {
|
||||
req, err := http.NewRequest("DELETE", "/api/files/..", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestRejectsInvalidFileName(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dirx")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/dirx/..", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsEmptyIfUploadPathIsNotDir(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
c.Assert(os.WriteFile(s.aptlyContext.UploadPath(), []byte("not a dir"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
c.Check(strings.TrimSpace(w.Body.String()), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesReturns500OnPermissionError(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "noperms")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
c.Assert(os.Chmod(base, 0), IsNil)
|
||||
defer func() { _ = os.Chmod(base, 0777) }()
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files/noperms", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileReturns500OnNonNotExistError(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dirisfile")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
subdir := filepath.Join(base, "subdir")
|
||||
c.Assert(os.MkdirAll(subdir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(subdir, "x"), []byte("x"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/dirisfile/subdir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadBadMultipartReturns400(c *C) {
|
||||
req, err := http.NewRequest("POST", "/api/files/badmultipart", bytes.NewBufferString("not multipart"))
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary=missing")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadRejectsInvalidDir(c *C) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/..", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500IfUploadRootIsNotDir(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
c.Assert(os.WriteFile(s.aptlyContext.UploadPath(), []byte("not a dir"), 0644), IsNil)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500OnFileOpenFailure(c *C) {
|
||||
// Pre-populate MultipartForm to inject a FileHeader that fails on Open().
|
||||
form := &multipart.Form{
|
||||
File: map[string][]*multipart.FileHeader{
|
||||
"file": {{Filename: "broken.bin"}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/openfaildir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
req.MultipartForm = form
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500OnCreateFailure(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "readonly")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.Chmod(base, 0555), IsNil)
|
||||
defer func() { _ = os.Chmod(base, 0777) }()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/readonly", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteDirReturns500OnRemoveFailure(c *C) {
|
||||
parent := s.aptlyContext.UploadPath()
|
||||
base := filepath.Join(parent, "cantremove")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
c.Assert(os.Chmod(parent, 0555), IsNil)
|
||||
defer func() { _ = os.Chmod(parent, 0777) }()
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/cantremove", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary GCS buckets
|
||||
// @Description **Get list of GCS buckets**
|
||||
// @Description
|
||||
// @Description List configured GCS buckets.
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "List of GCS buckets"
|
||||
// @Router /api/gcs [get]
|
||||
func apiGCSList(c *gin.Context) {
|
||||
keys := []string{}
|
||||
for k := range context.Config().GCSPublishRoots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
c.JSON(200, keys)
|
||||
}
|
||||
+202
-1
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -12,6 +13,23 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type gpgKeyInfo struct {
|
||||
// 16-character key ID (short form)
|
||||
KeyID string `json:"KeyID" example:"8B48AD6246925553"`
|
||||
// Full fingerprint
|
||||
Fingerprint string `json:"Fingerprint" example:"D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0"`
|
||||
// Key validity (u=unknown, f=fulltrust, m=marginal, n=never)
|
||||
Validity string `json:"Validity" example:"u"`
|
||||
// User ID(s) associated with this key
|
||||
UserIDs []string `json:"UserIDs" example:"John Doe <john@example.com>"`
|
||||
// Creation date (Unix timestamp format from gpg)
|
||||
CreatedAt string `json:"CreatedAt" example:"2023-01-15"`
|
||||
}
|
||||
|
||||
type gpgKeyListResponse struct {
|
||||
Keys []gpgKeyInfo `json:"Keys"`
|
||||
}
|
||||
|
||||
type gpgAddKeyParams struct {
|
||||
// Keyring for adding the keys (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
@@ -25,10 +43,18 @@ type gpgAddKeyParams struct {
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500 8B48AD6246925553"`
|
||||
}
|
||||
|
||||
type gpgDeleteKeyParams struct {
|
||||
// Keyring to delete keys from (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Key ID or fingerprint to delete
|
||||
GpgKeyID string `json:"GpgKeyID" example:"8B48AD6246925553"`
|
||||
}
|
||||
|
||||
// @Summary Add GPG Keys
|
||||
// @Description **Adds GPG keys to aptly keyring**
|
||||
// @Description
|
||||
// @Description Add GPG public keys for veryfing remote repositories for mirroring.
|
||||
// @Description Add GPG public keys for verifying remote repositories for mirroring.
|
||||
// @Description
|
||||
// @Description Keys can be added in two ways:
|
||||
// @Description * By providing the ASCII armord key in `GpgKeyArmor` (leave Keyserver and GpgKeyID empty)
|
||||
@@ -108,3 +134,178 @@ func apiGPGAddKey(c *gin.Context) {
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
|
||||
// @Summary List GPG Keys
|
||||
// @Description **Lists all GPG keys in aptly keyring**
|
||||
// @Description
|
||||
// @Description Returns all public keys currently installed in the aptly GPG keyring.
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Param keyring query string false "Keyring file to list keys from (default: trustedkeys.gpg)" example(trustedkeys.gpg)
|
||||
// @Produce json
|
||||
// @Success 200 {object} gpgKeyListResponse "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/keys [get]
|
||||
func apiGPGListKeys(c *gin.Context) {
|
||||
keyring := c.DefaultQuery("keyring", "trustedkeys.gpg")
|
||||
keyring = utils.SanitizePath(keyring)
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-default-keyring",
|
||||
"--with-colons",
|
||||
"--keyring", keyring,
|
||||
"--list-keys",
|
||||
}
|
||||
|
||||
cmd := exec.Command(gpg, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("failed to list keys: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
keys := parseGPGOutput(string(out))
|
||||
c.JSON(200, gpgKeyListResponse{Keys: keys})
|
||||
}
|
||||
|
||||
// @Summary Delete GPG Key
|
||||
// @Description **Deletes a GPG key from aptly keyring**
|
||||
// @Description
|
||||
// @Description Removes a public key from the aptly GPG keyring. This is useful for removing
|
||||
// @Description compromised keys or cleaning up obsolete keys.
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgDeleteKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/key [delete]
|
||||
func apiGPGDeleteKey(c *gin.Context) {
|
||||
b := gpgDeleteKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(b.GpgKeyID)) == 0 {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("GpgKeyID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
b.GpgKeyID = utils.SanitizePath(b.GpgKeyID)
|
||||
// b.Keyring can be an absolute path
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-default-keyring",
|
||||
"--allow-non-selfsigned-uid",
|
||||
"--batch",
|
||||
"--yes",
|
||||
}
|
||||
|
||||
keyring := "trustedkeys.gpg"
|
||||
if len(b.Keyring) > 0 {
|
||||
keyring = b.Keyring
|
||||
}
|
||||
|
||||
args = append(args, "--keyring", keyring)
|
||||
args = append(args, "--delete-keys", b.GpgKeyID)
|
||||
|
||||
cmd := exec.Command(gpg, args...)
|
||||
fmt.Printf("running %s %s\n", gpg, strings.Join(args, " "))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("failed to delete key: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
|
||||
// parseGPGOutput parses the output of `gpg --with-colons --list-keys`
|
||||
// and returns a structured list of keys
|
||||
func parseGPGOutput(output string) []gpgKeyInfo {
|
||||
var keys []gpgKeyInfo
|
||||
var currentKey *gpgKeyInfo
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
recordType := parts[0]
|
||||
|
||||
// pub: public key record
|
||||
if recordType == "pub" {
|
||||
// Save previous key if it exists
|
||||
if currentKey != nil && currentKey.KeyID != "" {
|
||||
keys = append(keys, *currentKey)
|
||||
}
|
||||
|
||||
// Create new key entry
|
||||
// Format: pub:trust:length:algo:keyid:created:expires:uidhash:...
|
||||
keyID := parts[4]
|
||||
if len(keyID) >= 16 {
|
||||
keyID = keyID[len(keyID)-16:] // Last 16 chars = short key ID
|
||||
}
|
||||
validity := parts[1]
|
||||
createdAt := parts[5]
|
||||
|
||||
currentKey = &gpgKeyInfo{
|
||||
KeyID: keyID,
|
||||
Validity: validity,
|
||||
CreatedAt: createdAt,
|
||||
UserIDs: []string{},
|
||||
Fingerprint: "",
|
||||
}
|
||||
}
|
||||
|
||||
// uid: user ID record
|
||||
if recordType == "uid" && currentKey != nil {
|
||||
// Format: uid:trust:created:expires:keyid:uidhash:uidtype:validity:userID:...
|
||||
if len(parts) >= 10 {
|
||||
userID := parts[9]
|
||||
if userID != "" {
|
||||
currentKey.UserIDs = append(currentKey.UserIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fpr: fingerprint record
|
||||
if recordType == "fpr" && currentKey != nil {
|
||||
// Format: fpr:::::::::fingerprint:
|
||||
if len(parts) >= 10 {
|
||||
fingerprint := parts[9]
|
||||
currentKey.Fingerprint = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last key
|
||||
if currentKey != nil && currentKey.KeyID != "" {
|
||||
keys = append(keys, *currentKey)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GPGSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&GPGSuite{})
|
||||
|
||||
func (s *GPGSuite) withFakeGPG(c *C, scriptBody string, test func(scriptPath string)) {
|
||||
tempDir, err := os.MkdirTemp("", "aptly-fake-gpg")
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
scriptPath := filepath.Join(tempDir, "gpg")
|
||||
err = os.WriteFile(scriptPath, []byte(scriptBody), 0o755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldPath := os.Getenv("PATH")
|
||||
err = os.Setenv("PATH", tempDir+string(os.PathListSeparator)+oldPath)
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.Setenv("PATH", oldPath) }()
|
||||
|
||||
test(scriptPath)
|
||||
}
|
||||
|
||||
func (s *GPGSuite) fakeGPGScript(c *C, listOutput string, deleteOutput string, deleteError string) string {
|
||||
return "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then\n" +
|
||||
" echo 'gpg (GnuPG) 2.2.27'\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"args=\"$*\"\n" +
|
||||
"if printf '%s' \"$args\" | grep -q -- '--list-keys'; then\n" +
|
||||
" cat <<'EOF'\n" + listOutput + "\nEOF\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"if printf '%s' \"$args\" | grep -q -- '--delete-keys'; then\n" +
|
||||
" if [ -n \"" + strings.ReplaceAll(deleteError, "\n", "") + "\" ]; then\n" +
|
||||
" echo '" + strings.ReplaceAll(deleteError, "'", "'\\''") + "'\n" +
|
||||
" exit 1\n" +
|
||||
" fi\n" +
|
||||
" cat <<'EOF'\n" + deleteOutput + "\nEOF\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"echo 'unexpected invocation' >&2\n" +
|
||||
"exit 1\n"
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEmpty tests parsing of empty GPG output
|
||||
func (s *GPGSuite) TestParseGPGOutputEmpty(c *C) {
|
||||
output := ""
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 0)
|
||||
}
|
||||
|
||||
// TestParseGPGOutputSingleKeyMinimal tests parsing a single key with minimal fields
|
||||
func (s *GPGSuite) TestParseGPGOutputSingleKeyMinimal(c *C) {
|
||||
// Minimal valid GPG output with one key
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(key.Validity, Equals, "u")
|
||||
c.Check(key.CreatedAt, Equals, "1611864000")
|
||||
c.Check(key.Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
c.Check(key.UserIDs, DeepEquals, []string{"John Doe <john@example.com>"})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMultipleKeys tests parsing multiple keys
|
||||
func (s *GPGSuite) TestParseGPGOutputMultipleKeys(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
pub:f:2048:1:A1B2C3D4E5F67890:1580592000:1612128000:uidhash:::scESC:::::::23::0:
|
||||
uid:f::::1580592000::0987654321::Jane Smith <jane@example.com>::::::::::0:
|
||||
fpr:::::::::E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 2)
|
||||
|
||||
// First key
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(keys[0].Validity, Equals, "u")
|
||||
c.Check(keys[0].UserIDs, DeepEquals, []string{"John Doe <john@example.com>"})
|
||||
|
||||
// Second key
|
||||
c.Check(keys[1].KeyID, Equals, "A1B2C3D4E5F67890")
|
||||
c.Check(keys[1].Validity, Equals, "f")
|
||||
c.Check(keys[1].UserIDs, DeepEquals, []string{"Jane Smith <jane@example.com>"})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMultipleUIDs tests a key with multiple user IDs
|
||||
func (s *GPGSuite) TestParseGPGOutputMultipleUIDs(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
uid:u::::1611864000::1234567891::John Doe <john.doe@company.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.UserIDs, HasLen, 2)
|
||||
c.Check(key.UserIDs, DeepEquals, []string{
|
||||
"John Doe <john@example.com>",
|
||||
"John Doe <john.doe@company.com>",
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMalformedLines tests that malformed lines are skipped
|
||||
func (s *GPGSuite) TestParseGPGOutputMalformedLines(c *C) {
|
||||
// Mix of valid and invalid lines (too few fields)
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
invalid:line:with:only:three:fields
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEmptyLines tests that empty lines are skipped
|
||||
func (s *GPGSuite) TestParseGPGOutputEmptyLines(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputKeyWithoutUID tests a public key without user ID
|
||||
func (s *GPGSuite) TestParseGPGOutputKeyWithoutUID(c *C) {
|
||||
// Key without uid record (should still be included)
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(key.UserIDs, HasLen, 0)
|
||||
c.Check(key.Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputVariousValidity tests different validity values
|
||||
func (s *GPGSuite) TestParseGPGOutputVariousValidity(c *C) {
|
||||
output := `pub:u:4096:1:KEY1111111111111:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::Key1::::::::::0:
|
||||
fpr:::::::::1111111111111111111111111111111111111111:
|
||||
pub:f:4096:1:KEY2222222222222:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:f::::1611864000::1234567891::Key2::::::::::0:
|
||||
fpr:::::::::2222222222222222222222222222222222222222:
|
||||
pub:m:4096:1:KEY3333333333333:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:m::::1611864000::1234567892::Key3::::::::::0:
|
||||
fpr:::::::::3333333333333333333333333333333333333333:
|
||||
pub:n:4096:1:KEY4444444444444:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:n::::1611864000::1234567893::Key4::::::::::0:
|
||||
fpr:::::::::4444444444444444444444444444444444444444:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 4)
|
||||
|
||||
validities := []string{"u", "f", "m", "n"}
|
||||
for i, validity := range validities {
|
||||
c.Check(keys[i].Validity, Equals, validity)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseGPGOutputShortKeyID tests that key IDs are shortened to 16 chars
|
||||
func (s *GPGSuite) TestParseGPGOutputShortKeyID(c *C) {
|
||||
// 40-character key ID that should be shortened to last 16 chars
|
||||
longKeyID := "0123456789ABCDEF0123456789ABCDEF8B48AD62"
|
||||
output := `pub:u:4096:1:` + longKeyID + `:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should extract the last 16 characters: 89ABCDEF8B48AD62
|
||||
c.Check(keys[0].KeyID, Equals, "89ABCDEF8B48AD62")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputSpecialCharactersInUID tests user IDs with special characters
|
||||
func (s *GPGSuite) TestParseGPGOutputSpecialCharactersInUID(c *C) {
|
||||
// UID with Unicode characters and special formatting
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::J\xc3\xb6hn D\xc3\xb6\xc3\xa9 (D\xc3\xbcss) <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should preserve the encoded special characters
|
||||
c.Check(keys[0].UserIDs, HasLen, 1)
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysDefaultKeyring tests the HTTP endpoint with default keyring
|
||||
func (s *GPGSuite) TestAPIGPGListKeysDefaultKeyring(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, `pub:u:4096:1:8B48AD6246925553:1611864000:::::
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`, "", ""), func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result gpgKeyListResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(result.Keys, HasLen, 1)
|
||||
c.Check(result.Keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysWithKeyringParam tests the HTTP endpoint with custom keyring parameter
|
||||
func (s *GPGSuite) TestAPIGPGListKeysWithKeyringParam(c *C) {
|
||||
argFile, err := os.CreateTemp("", "aptly-gpg-args")
|
||||
c.Assert(err, IsNil)
|
||||
_ = argFile.Close()
|
||||
defer func() { _ = os.Remove(argFile.Name()) }()
|
||||
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"printf '%s\n' \"$@\" > '" + argFile.Name() + "'\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--list-keys'; then\n" +
|
||||
"cat <<'EOF'\n" +
|
||||
"pub:u:4096:1:8B48AD6246925553:1611864000:::::\n" +
|
||||
"fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:\n" +
|
||||
"EOF\n" +
|
||||
"exit 0\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
response, reqErr := s.HTTPRequest("GET", "/api/gpg/keys?keyring=/custom.gpg", nil)
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
argBytes, readErr := os.ReadFile(argFile.Name())
|
||||
c.Assert(readErr, IsNil)
|
||||
c.Check(string(argBytes), Matches, `(?s).*--keyring\ncustom\.gpg\n.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysResponseFormat tests that the response has the correct structure
|
||||
func (s *GPGSuite) TestAPIGPGListKeysResponseFormat(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, `pub:f:4096:1:A1B2C3D4E5F67890:1611864000:::::
|
||||
uid:f::::1611864000::1234567890::Jane Smith <jane@example.com>::::::::::0:
|
||||
fpr:::::::::E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4:`, "", ""), func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result gpgKeyListResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(result.Keys, HasLen, 1)
|
||||
c.Check(result.Keys[0].KeyID, Equals, "A1B2C3D4E5F67890")
|
||||
c.Check(result.Keys[0].Validity, Equals, "f")
|
||||
c.Check(result.Keys[0].CreatedAt, Equals, "1611864000")
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEdgeCaseUIDWithoutFields tests UID record with missing fields
|
||||
func (s *GPGSuite) TestParseGPGOutputEdgeCaseUIDWithoutFields(c *C) {
|
||||
// UID record with fewer than 10 fields
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should not have user ID since it's in field 9 and this record is too short
|
||||
c.Check(keys[0].UserIDs, HasLen, 0)
|
||||
}
|
||||
|
||||
// TestParseGPGOutputFingerprintWithoutCurrentKey tests FPR record appearing before any PUB
|
||||
func (s *GPGSuite) TestParseGPGOutputFingerprintWithoutCurrentKey(c *C) {
|
||||
// FPR record without a preceding PUB (should be ignored)
|
||||
output := `fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should only have one key with the correct fingerprint
|
||||
c.Check(keys[0].Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputComplexRealWorldExample tests real-world-like GPG output
|
||||
func (s *GPGSuite) TestParseGPGOutputComplexRealWorldExample(c *C) {
|
||||
// Real-world GPG output with multiple keys, UIDs, and other record types (sig, sub)
|
||||
// Note: sub and sig records are skipped as we only care about pub/uid/fpr
|
||||
realWorldOutput := `tru::1:1611864000:0:3:1:5
|
||||
pub:u:4096:1:8B48AD6246925553:1611864000:2023-01-15T00:00:00:::::scESC:::::::23::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
uid:u::::1611864100::1234567891::John Doe <john@work.com>::::::::::0:
|
||||
pub:f:2048:1:1234567890123456:1580592000:2022-12-31T00:00:00::u:::scESC:::::::23::0:
|
||||
fpr:::::::::F4E3D2C1B0A9F8E7D6C5B4A3F2E1D0C9:
|
||||
uid:f::::1580592000::0987654321::Maintainer Key <maint@example.com>::::::::::0:`
|
||||
|
||||
keys := parseGPGOutput(realWorldOutput)
|
||||
c.Check(keys, HasLen, 2)
|
||||
|
||||
// First key should have 2 UIDs
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(keys[0].UserIDs, HasLen, 2)
|
||||
c.Check(keys[0].Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
|
||||
// Second key should have 1 UID
|
||||
c.Check(keys[1].KeyID, Equals, "1234567890123456")
|
||||
c.Check(keys[1].UserIDs, HasLen, 1)
|
||||
c.Check(keys[1].Fingerprint, Equals, "F4E3D2C1B0A9F8E7D6C5B4A3F2E1D0C9")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputConsecutiveEmptyUIDs tests handling of consecutive empty user ID fields
|
||||
func (s *GPGSuite) TestParseGPGOutputConsecutiveEmptyUIDs(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890:::::::::::0:
|
||||
uid:u::::1611864000::1234567891::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should skip empty UID but include the non-empty one
|
||||
c.Check(keys[0].UserIDs, HasLen, 1)
|
||||
c.Check(keys[0].UserIDs[0], Equals, "John Doe <john@example.com>")
|
||||
}
|
||||
|
||||
// TestGPGDeleteKeyParamsValidation tests gpgDeleteKeyParams validation
|
||||
func (s *GPGSuite) TestGPGDeleteKeyParamsValidation(c *C) {
|
||||
// This is a unit test that validates parameter structure (no HTTP needed)
|
||||
params := gpgDeleteKeyParams{
|
||||
Keyring: "custom.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
}
|
||||
c.Check(params.Keyring, Equals, "custom.gpg")
|
||||
c.Check(params.GpgKeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyMissingKeyID tests delete with missing key ID parameter
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyMissingKeyID(c *C) {
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
// GpgKeyID is missing
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyInvalidJSON tests delete with invalid JSON request
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyInvalidJSON(c *C) {
|
||||
response, err := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader([]byte("invalid json")))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeySuccess tests successful key deletion
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeySuccess(c *C) {
|
||||
argFile, err := os.CreateTemp("", "aptly-gpg-delete-args")
|
||||
c.Assert(err, IsNil)
|
||||
_ = argFile.Close()
|
||||
defer func() { _ = os.Remove(argFile.Name()) }()
|
||||
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"printf '%s\n' \"$@\" > '" + argFile.Name() + "'\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--delete-keys'; then\n" +
|
||||
"echo 'deleted'\n" +
|
||||
"exit 0\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
body, marshalErr := json.Marshal(gpgDeleteKeyParams{
|
||||
Keyring: "/trustedkeys.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
})
|
||||
c.Assert(marshalErr, IsNil)
|
||||
|
||||
response, reqErr := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, `"deleted\\n"`)
|
||||
|
||||
argBytes, readErr := os.ReadFile(argFile.Name())
|
||||
c.Assert(readErr, IsNil)
|
||||
argText := string(argBytes)
|
||||
c.Check(argText, Matches, `(?s).*--batch\n--yes\n.*`)
|
||||
c.Check(argText, Matches, `(?s).*--keyring\n/trustedkeys\.gpg\n.*`)
|
||||
c.Check(argText, Matches, `(?s).*--delete-keys\n8B48AD6246925553\n.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysCommandFailure tests list error propagation from gpg
|
||||
func (s *GPGSuite) TestAPIGPGListKeysCommandFailure(c *C) {
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--list-keys'; then\n" +
|
||||
"echo 'keyring missing'\n" +
|
||||
"exit 1\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, `(?s).*failed to list keys: keyring missing.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyCommandFailure tests delete error propagation from gpg
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyCommandFailure(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, "", "", "delete failed"), func(_ string) {
|
||||
body, err := json.Marshal(gpgDeleteKeyParams{
|
||||
Keyring: "trustedkeys.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, reqErr := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, `(?s).*failed to delete key: delete failed.*`)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary JFrog repositories
|
||||
// @Description **Get list of JFrog repositories**
|
||||
// @Description
|
||||
// @Description List configured JFrog publish endpoints.
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "List of JFrog publish endpoints"
|
||||
// @Router /api/jfrog [get]
|
||||
func apiJFrogList(c *gin.Context) {
|
||||
keys := []string{}
|
||||
for k := range context.Config().JFrogPublishRoots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
c.JSON(200, keys)
|
||||
}
|
||||
+226
-67
@@ -31,22 +31,61 @@ func getVerifier(keyRings []string) (pgp.Verifier, error) {
|
||||
return verifier, nil
|
||||
}
|
||||
|
||||
// stringSlicesEqual compares two string slices for equality (order matters)
|
||||
func stringSlicesEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// uniqueStrings returns a new slice with only unique strings from the input, sorted
|
||||
func uniqueStrings(input []string) []string {
|
||||
if len(input) == 0 {
|
||||
return input
|
||||
}
|
||||
seen := make(map[string]struct{}, len(input))
|
||||
result := make([]string, 0, len(input))
|
||||
for _, s := range input {
|
||||
if _, ok := seen[s]; !ok {
|
||||
seen[s] = struct{}{}
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// @Summary List Mirrors
|
||||
// @Description **Show list of currently available mirrors**
|
||||
// @Description Each mirror is returned as in “show” API.
|
||||
// @Tags Mirrors
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.RemoteRepo
|
||||
// @Success 200 {array} remoteRepoResponse
|
||||
// @Router /api/mirrors [get]
|
||||
func apiMirrorsList(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
result := []*deb.RemoteRepo{}
|
||||
_ = collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
result = append(result, repo)
|
||||
result := []remoteRepoResponse{}
|
||||
err := collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err := collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newRemoteRepoResponse(repo))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -72,6 +111,8 @@ type mirrorCreateParams struct {
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller bool ` json:"DownloadInstaller"`
|
||||
// Set "true" to mirror AppStream (DEP-11) metadata
|
||||
DownloadAppStream bool ` json:"DownloadAppStream"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to skip if the given components are in the Release file
|
||||
@@ -123,7 +164,7 @@ func apiMirrorsCreate(c *gin.Context) {
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(b.Name, b.ArchiveURL, b.Distribution, b.Components, b.Architectures,
|
||||
b.DownloadSources, b.DownloadUdebs, b.DownloadInstaller)
|
||||
b.DownloadSources, b.DownloadUdebs, b.DownloadInstaller, b.DownloadAppStream)
|
||||
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to create mirror: %s", err))
|
||||
@@ -175,9 +216,9 @@ func apiMirrorsDrop(c *gin.Context) {
|
||||
name := c.Params.ByName("name")
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
|
||||
// Phase 1: Pre-task validation (shallow load for 404 check only)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
mirrorCollection := collectionFactory.RemoteRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
repo, err := mirrorCollection.ByName(name)
|
||||
if err != nil {
|
||||
@@ -187,21 +228,34 @@ func apiMirrorsDrop(c *gin.Context) {
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Delete mirror %s", name)
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := repo.CheckLock()
|
||||
// Phase 2: Inside task lock - create fresh collections
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskMirrorCollection := taskCollectionFactory.RemoteRepoCollection()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
|
||||
// Fresh load after lock acquired
|
||||
repo, err := taskMirrorCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
|
||||
err = repo.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.ByRemoteRepoSource(repo)
|
||||
// Fresh checks with current collections
|
||||
snapshots := taskSnapshotCollection.ByRemoteRepoSource(repo)
|
||||
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusForbidden, Value: nil}, fmt.Errorf("won't delete mirror with snapshots, use 'force=1' to override")
|
||||
}
|
||||
}
|
||||
|
||||
err = mirrorCollection.Drop(repo)
|
||||
err = taskMirrorCollection.Drop(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
@@ -232,6 +286,7 @@ func apiMirrorsShow(c *gin.Context) {
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
@@ -330,29 +385,133 @@ func apiMirrorsPackages(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
type mirrorEditParams struct {
|
||||
// Package query that is applied to mirror packages
|
||||
Filter *string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps *bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller *bool `json:"DownloadInstaller"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources *bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs *bool ` json:"DownloadUdebs"`
|
||||
// URL of the archive to mirror
|
||||
ArchiveURL *string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Comma separated list of architectures
|
||||
Architectures *[]string `json:"Architectures" example:"amd64"`
|
||||
// Gpg keyring(s) for verifying Release file if a mirror update is required.
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures *bool ` json:"IgnoreSignatures"`
|
||||
}
|
||||
|
||||
// @Summary Edit Mirror
|
||||
// @Description **Edit mirror config**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name to edit"
|
||||
// @Consume json
|
||||
// @Param request body mirrorEditParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo "Mirror was edited successfully"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 409 {object} Error "Aptly db locked"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [post]
|
||||
func apiMirrorsEdit(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
b mirrorEditParams
|
||||
repo *deb.RemoteRepo
|
||||
)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = repo.CheckLock()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fetchMirror := false
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
|
||||
if b.Filter != nil {
|
||||
repo.Filter = *b.Filter
|
||||
}
|
||||
if b.FilterWithDeps != nil {
|
||||
repo.FilterWithDeps = *b.FilterWithDeps
|
||||
}
|
||||
if b.DownloadInstaller != nil {
|
||||
repo.DownloadInstaller = *b.DownloadInstaller
|
||||
}
|
||||
if b.DownloadSources != nil {
|
||||
repo.DownloadSources = *b.DownloadSources
|
||||
}
|
||||
if b.DownloadUdebs != nil {
|
||||
repo.DownloadUdebs = *b.DownloadUdebs
|
||||
}
|
||||
if b.ArchiveURL != nil && *b.ArchiveURL != repo.ArchiveRoot {
|
||||
repo.SetArchiveRoot(*b.ArchiveURL)
|
||||
fetchMirror = true
|
||||
}
|
||||
if b.Architectures != nil {
|
||||
uniqueArchitectures := uniqueStrings(*b.Architectures)
|
||||
if !stringSlicesEqual(uniqueArchitectures, uniqueStrings(repo.Architectures)) {
|
||||
repo.Architectures = uniqueArchitectures
|
||||
fetchMirror = true
|
||||
}
|
||||
}
|
||||
if b.IgnoreSignatures != nil {
|
||||
ignoreSignatures = *b.IgnoreSignatures
|
||||
}
|
||||
|
||||
if repo.IsFlat() && repo.DownloadUdebs {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to edit: flat mirrors don't support udebs"))
|
||||
return
|
||||
}
|
||||
|
||||
if fetchMirror {
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = collection.Update(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
type mirrorUpdateParams struct {
|
||||
// Change mirror name to `Name`
|
||||
Name string ` json:"Name" example:"mirror1"`
|
||||
// Url of the archive to mirror
|
||||
ArchiveURL string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Package query that is applied to mirror packages
|
||||
Filter string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Limit mirror to those architectures, if not specified aptly would fetch all architectures
|
||||
Architectures []string ` json:"Architectures" example:"amd64"`
|
||||
// Components to mirror, if not specified aptly would fetch all components
|
||||
Components []string ` json:"Components" example:"main"`
|
||||
// Gpg keyring(s) for verifing Release file
|
||||
// Gpg keyring(s) for verifying Release file
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to skip checking if the given components are in the Release file
|
||||
SkipComponentCheck bool ` json:"SkipComponentCheck"`
|
||||
// Set "true" to skip checking if the given architectures are in the Release file
|
||||
SkipArchitectureCheck bool ` json:"SkipArchitectureCheck"`
|
||||
// Set "true" to ignore checksum errors
|
||||
IgnoreChecksums bool ` json:"IgnoreChecksums"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
@@ -361,6 +520,8 @@ type mirrorUpdateParams struct {
|
||||
ForceUpdate bool ` json:"ForceUpdate"`
|
||||
// Set "true" to skip downloading already downloaded packages
|
||||
SkipExistingPackages bool ` json:"SkipExistingPackages"`
|
||||
// Set "true" to download only the latest version per package/architecture
|
||||
LatestOnly bool ` json:"LatestOnly"`
|
||||
}
|
||||
|
||||
// @Summary Update Mirror
|
||||
@@ -387,21 +548,14 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
remote, err = collection.ByName(c.Params.ByName("name"))
|
||||
name := c.Params.ByName("name")
|
||||
remote, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
b.Name = remote.Name
|
||||
b.DownloadUdebs = remote.DownloadUdebs
|
||||
b.DownloadSources = remote.DownloadSources
|
||||
b.SkipComponentCheck = remote.SkipComponentCheck
|
||||
b.SkipArchitectureCheck = remote.SkipArchitectureCheck
|
||||
b.FilterWithDeps = remote.FilterWithDeps
|
||||
b.Filter = remote.Filter
|
||||
b.Architectures = remote.Architectures
|
||||
b.Components = remote.Components
|
||||
b.IgnoreSignatures = context.Config().GpgDisableVerify
|
||||
|
||||
log.Info().Msgf("%s: Starting mirror update", b.Name)
|
||||
@@ -410,6 +564,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-task validation of new name if provided
|
||||
if b.Name != remote.Name {
|
||||
_, err = collection.ByName(b.Name)
|
||||
if err == nil {
|
||||
@@ -418,27 +573,6 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if b.DownloadUdebs != remote.DownloadUdebs {
|
||||
if remote.IsFlat() && b.DownloadUdebs {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to update: flat mirrors don't support udebs"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.ArchiveURL != "" {
|
||||
remote.SetArchiveRoot(b.ArchiveURL)
|
||||
}
|
||||
|
||||
remote.Name = b.Name
|
||||
remote.DownloadUdebs = b.DownloadUdebs
|
||||
remote.DownloadSources = b.DownloadSources
|
||||
remote.SkipComponentCheck = b.SkipComponentCheck
|
||||
remote.SkipArchitectureCheck = b.SkipArchitectureCheck
|
||||
remote.FilterWithDeps = b.FilterWithDeps
|
||||
remote.Filter = b.Filter
|
||||
remote.Architectures = b.Architectures
|
||||
remote.Components = b.Components
|
||||
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
@@ -447,9 +581,26 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
|
||||
resources := []string{string(remote.Key())}
|
||||
maybeRunTaskInBackground(c, "Update mirror "+b.Name, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
// Phase 2: Inside task lock - create fresh factory
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.RemoteRepoCollection()
|
||||
|
||||
// Fresh load after lock acquired (use captured `name` variable, not gin context)
|
||||
remote, err := taskCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
// Fresh rename check inside lock (if renaming)
|
||||
if b.Name != remote.Name {
|
||||
_, err := taskCollection.ByName(b.Name)
|
||||
if err == nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to rename: mirror %s already exists", b.Name)
|
||||
}
|
||||
}
|
||||
|
||||
downloader := context.NewDownloader(out)
|
||||
err := remote.Fetch(downloader, verifier, b.IgnoreSignatures)
|
||||
err = remote.Fetch(downloader, verifier, b.IgnoreSignatures)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -461,11 +612,19 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, b.IgnoreSignatures, b.SkipComponentCheck)
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, taskCollectionFactory, b.IgnoreSignatures, remote.SkipComponentCheck)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if remote.DownloadAppStream && !remote.IsFlat() {
|
||||
err = remote.DownloadAppStreamFiles(out, downloader,
|
||||
context.PackagePool(), taskCollectionFactory.ChecksumCollection(nil), b.IgnoreChecksums)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if remote.Filter != "" {
|
||||
var filterQuery deb.PackageQuery
|
||||
|
||||
@@ -480,8 +639,8 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), b.SkipExistingPackages)
|
||||
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), taskCollectionFactory.PackageCollection(),
|
||||
taskCollectionFactory.ChecksumCollection(nil), b.SkipExistingPackages, b.LatestOnly)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -491,12 +650,12 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
e := context.ReOpenDatabase()
|
||||
if e == nil {
|
||||
remote.MarkAsIdle()
|
||||
_ = collection.Update(remote)
|
||||
_ = taskCollection.Update(remote)
|
||||
}
|
||||
}()
|
||||
|
||||
remote.MarkAsUpdating()
|
||||
err = collection.Update(remote)
|
||||
err = taskCollection.Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -600,7 +759,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// and import it back to the pool
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, collectionFactory.ChecksumCollection(nil))
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, taskCollectionFactory.ChecksumCollection(nil))
|
||||
if err != nil {
|
||||
//return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to import file: %s", err)
|
||||
pushError(err)
|
||||
@@ -653,8 +812,8 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Finalizing download...", b.Name)
|
||||
_ = remote.FinalizeDownload(collectionFactory, out)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(remote)
|
||||
_ = remote.FinalizeDownload(taskCollectionFactory, out)
|
||||
err = taskCollection.Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
+66
-1
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
@@ -17,7 +18,10 @@ var _ = Suite(&MirrorSuite{})
|
||||
func (s *MirrorSuite) TestGetMirrors(c *C) {
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Equals, "[]")
|
||||
|
||||
var mirrors []map[string]interface{}
|
||||
err := json.Unmarshal(response.Body.Bytes(), &mirrors)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) {
|
||||
@@ -26,6 +30,21 @@ func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) {
|
||||
c.Check(response.Body.String(), Equals, "{\"error\":\"unable to drop: mirror with name does-not-exist not found\"}")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirrorFlatWithAppStream(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "test-flat-appstream",
|
||||
"ArchiveURL": "http://example.com/repo/",
|
||||
"Distribution": "./",
|
||||
"DownloadAppStream": true,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("POST", "/api/mirrors", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, ".*AppStream.*flat.*")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.ExpectFailure("Need to mock downloads")
|
||||
body, err := json.Marshal(gin.H{
|
||||
@@ -38,3 +57,49 @@ func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Equals, "")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().RemoteRepoCollection()
|
||||
|
||||
repo, err := deb.NewRemoteRepo("count-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = collection.Add(repo)
|
||||
c.Assert(err, IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), makePackageRefList(c).Encode())
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var mirrors []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &mirrors)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, mirror := range mirrors {
|
||||
if mirror["Name"] == "count-mirror" {
|
||||
found = true
|
||||
value, ok := mirror["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrorsReturns500OnCorruptRefList(c *C) {
|
||||
collection := s.context.NewCollectionFactory().RemoteRepoCollection()
|
||||
|
||||
repo, err := deb.NewRemoteRepo("broken-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(collection.Add(repo), IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*unable to show:.*")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import "github.com/aptly-dev/aptly/deb"
|
||||
|
||||
type remoteRepoResponse struct {
|
||||
*deb.RemoteRepo
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
type localRepoResponse struct {
|
||||
*deb.LocalRepo
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
type snapshotResponse struct {
|
||||
*deb.Snapshot
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
func newRemoteRepoResponse(repo *deb.RemoteRepo) remoteRepoResponse {
|
||||
return remoteRepoResponse{RemoteRepo: repo, NumPackages: repo.NumPackages()}
|
||||
}
|
||||
|
||||
func newLocalRepoResponse(repo *deb.LocalRepo) localRepoResponse {
|
||||
return localRepoResponse{LocalRepo: repo, NumPackages: repo.NumPackages()}
|
||||
}
|
||||
|
||||
func newSnapshotResponse(snapshot *deb.Snapshot) snapshotResponse {
|
||||
return snapshotResponse{Snapshot: snapshot, NumPackages: snapshot.NumPackages()}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func makePackageRefList(c *C) *deb.PackageRefList {
|
||||
list := deb.NewPackageList()
|
||||
c.Assert(list.Add(&deb.Package{Name: "libcount", Version: "1.0", Architecture: "amd64"}), IsNil)
|
||||
c.Assert(list.Add(&deb.Package{Name: "appcount", Version: "2.0", Architecture: "all"}), IsNil)
|
||||
return deb.NewPackageRefListFromPackageList(list)
|
||||
}
|
||||
|
||||
func putRawDBValue(c *C, s *APISuite, key []byte, value []byte) {
|
||||
db, err := s.context.Database()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(db.Put(key, value), IsNil)
|
||||
}
|
||||
+390
-172
@@ -16,8 +16,8 @@ import (
|
||||
type signingParams struct {
|
||||
// Don't sign published repository
|
||||
Skip bool ` json:"Skip" example:"false"`
|
||||
// GPG key ID to use when signing the release, if not specified default key is used
|
||||
GpgKey string ` json:"GpgKey" example:"A0546A43624A8331"`
|
||||
// GPG key ID(s) to use when signing the release, separated by comma, and if not specified, default configured key(s) are used
|
||||
GpgKey string ` json:"GpgKey" example:"KEY_ID_a, KEY_ID_b"`
|
||||
// GPG keyring to use (instead of default)
|
||||
Keyring string ` json:"Keyring" example:"trustedkeys.gpg"`
|
||||
// GPG secret keyring to use (instead of default) Note: depreciated with gpg2
|
||||
@@ -41,7 +41,21 @@ func getSigner(options *signingParams) (pgp.Signer, error) {
|
||||
}
|
||||
|
||||
signer := context.GetSigner()
|
||||
signer.SetKey(options.GpgKey)
|
||||
|
||||
var multiGpgKeys []string
|
||||
// REST params have priority over config
|
||||
if options.GpgKey != "" {
|
||||
for _, p := range strings.Split(options.GpgKey, ",") {
|
||||
if t := strings.TrimSpace(p); t != "" {
|
||||
multiGpgKeys = append(multiGpgKeys, t)
|
||||
}
|
||||
}
|
||||
} else if len(context.Config().GpgKeys) > 0 {
|
||||
multiGpgKeys = context.Config().GpgKeys
|
||||
}
|
||||
for _, gpgKey := range multiGpgKeys {
|
||||
signer.SetKey(gpgKey)
|
||||
}
|
||||
signer.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||
signer.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||
|
||||
@@ -110,7 +124,7 @@ func apiPublishList(c *gin.Context) {
|
||||
// @Description See also: `aptly publish show`
|
||||
// @Tags Publish
|
||||
// @Produce json
|
||||
// @Param prefix path string true "publishing prefix, use `:.` instead of `.` because it is ambigious in URLs"
|
||||
// @Param prefix path string true "publishing prefix, use `:.` instead of `.` because it is ambiguous in URLs"
|
||||
// @Param distribution path string true "distribution name"
|
||||
// @Success 200 {object} deb.PublishedRepo
|
||||
// @Failure 404 {object} Error "Published repository not found"
|
||||
@@ -168,8 +182,12 @@ type publishedRepoCreateParams struct {
|
||||
SkipBz2 *bool ` json:"SkipBz2" example:"false"`
|
||||
// Provide index files by hash
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file.
|
||||
SignedBy *string ` json:"SignedBy" example:""`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Version of the release
|
||||
Version string ` json:"Version" example:""`
|
||||
}
|
||||
|
||||
// @Summary Create Published Repository
|
||||
@@ -249,7 +267,7 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, string(snapshot.ResourceKey()))
|
||||
resources = append(resources, string(snapshot.Key()))
|
||||
sources = append(sources, snapshot)
|
||||
}
|
||||
} else if b.SourceKind == deb.SourceLocalRepo {
|
||||
@@ -280,11 +298,24 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
multiDist = *b.MultiDist
|
||||
}
|
||||
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
// Non-MultiDist publishes share a single pool/ directory under the
|
||||
// prefix. Lock at the prefix level so that concurrent publish/drop
|
||||
// operations on sibling distributions cannot race during cleanup.
|
||||
if !multiDist {
|
||||
storagePrefix := prefix
|
||||
if storage != "" {
|
||||
storagePrefix = storage + ":" + prefix
|
||||
}
|
||||
|
||||
resources = append(resources, deb.PrefixPoolLockKey(storagePrefix))
|
||||
}
|
||||
|
||||
taskName := fmt.Sprintf("Publish %s repository %s/%s with components \"%s\" and sources \"%s\"",
|
||||
b.SourceKind, param, b.Distribution, strings.Join(components, `", "`), strings.Join(names, `", "`))
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
taskDetail := task.PublishDetail{
|
||||
Detail: detail,
|
||||
}
|
||||
@@ -296,10 +327,10 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
for _, source := range sources {
|
||||
switch s := source.(type) {
|
||||
case *deb.Snapshot:
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
snapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
err = snapshotCollection.LoadComplete(s)
|
||||
case *deb.LocalRepo:
|
||||
localCollection := collectionFactory.LocalRepoCollection()
|
||||
localCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
err = localCollection.LoadComplete(s)
|
||||
default:
|
||||
err = fmt.Errorf("unexpected type for source: %T", source)
|
||||
@@ -309,13 +340,11 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
published, err := deb.NewPublishedRepo(storage, prefix, b.Distribution, b.Architectures, components, sources, collectionFactory, multiDist)
|
||||
published, err := deb.NewPublishedRepo(storage, prefix, b.Distribution, b.Architectures, components, sources, taskCollectionFactory, multiDist)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
resources = append(resources, string(published.Key()))
|
||||
|
||||
if b.Origin != "" {
|
||||
published.Origin = b.Origin
|
||||
}
|
||||
@@ -341,18 +370,26 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
duplicate := collection.CheckDuplicate(published)
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
|
||||
if b.Version != "" {
|
||||
published.Version = b.Version
|
||||
}
|
||||
|
||||
duplicate := taskCollection.CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
_ = taskCollectionFactory.PublishedRepoCollection().LoadComplete(duplicate, taskCollectionFactory)
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, publishOutput, b.ForceOverwrite, context.SkelPath())
|
||||
err = published.Publish(context.PackagePool(), context, taskCollectionFactory, signer, publishOutput, b.ForceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = collection.Add(published)
|
||||
err = taskCollection.Add(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -376,8 +413,16 @@ type publishedRepoUpdateSwitchParams struct {
|
||||
Snapshots []sourceParams ` json:"Snapshots"`
|
||||
// Provide index files by hash
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file
|
||||
SignedBy *string ` json:"SignedBy" example:""`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Value of Label: field in published repository stanza
|
||||
Label *string ` json:"Label" example:"Debian"`
|
||||
// Value of Origin: field in published repository stanza
|
||||
Origin *string ` json:"Origin" example:"Debian"`
|
||||
// Version of the release: Optional
|
||||
Version *string ` json:"Version" example:"13.3"`
|
||||
}
|
||||
|
||||
// @Summary Update Published Repository
|
||||
@@ -393,7 +438,6 @@ type publishedRepoUpdateSwitchParams struct {
|
||||
// @Description
|
||||
// @Description See also: `aptly publish update` / `aptly publish switch`
|
||||
// @Tags Publish
|
||||
// @Produce json
|
||||
// @Param prefix path string true "publishing prefix"
|
||||
// @Param distribution path string true "distribution name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
@@ -425,6 +469,7 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
localRepoCollection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
@@ -432,48 +477,88 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
|
||||
if published.SourceKind == deb.SourceLocalRepo {
|
||||
if len(b.Snapshots) > 0 {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("snapshots shouldn't be given when updating local repo"))
|
||||
return
|
||||
}
|
||||
} else if published.SourceKind == deb.SourceSnapshot {
|
||||
for _, snapshotInfo := range b.Snapshots {
|
||||
_, err2 := snapshotCollection.ByName(snapshotInfo.Name)
|
||||
for _, uuid := range published.Sources {
|
||||
repo, err2 := localRepoCollection.ByUUID(uuid)
|
||||
if err2 != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err2)
|
||||
return
|
||||
}
|
||||
resources = append(resources, string(repo.Key()))
|
||||
}
|
||||
} else if published.SourceKind == deb.SourceSnapshot {
|
||||
for _, snapshotInfo := range b.Snapshots {
|
||||
snapshot, err2 := snapshotCollection.ByName(snapshotInfo.Name)
|
||||
if err2 != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err2)
|
||||
return
|
||||
}
|
||||
resources = append(resources, string(snapshot.Key()))
|
||||
}
|
||||
} else {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unknown published repository type"))
|
||||
return
|
||||
}
|
||||
|
||||
if b.SkipContents != nil {
|
||||
published.SkipContents = *b.SkipContents
|
||||
// Non-MultiDist distributions share a single pool/ directory under the
|
||||
// prefix. Acquire the prefix-level pool lock so that concurrent updates
|
||||
// on sibling distributions are serialised and cannot race during cleanup.
|
||||
if !published.MultiDist {
|
||||
resources = append(resources, deb.PrefixPoolLockKey(published.StoragePrefix()))
|
||||
}
|
||||
|
||||
if b.SkipBz2 != nil {
|
||||
published.SkipBz2 = *b.SkipBz2
|
||||
}
|
||||
|
||||
if b.AcquireByHash != nil {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
if b.MultiDist != nil {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
// Field mutations and fresh DB load are deferred to inside the task so
|
||||
// they always operate on a consistent state after the lock is held.
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
// Capture MultiDist before mutations to detect a false→true transition.
|
||||
prevMultiDist := published.MultiDist
|
||||
|
||||
// Apply field mutations on the freshly loaded object.
|
||||
if b.SkipContents != nil {
|
||||
published.SkipContents = *b.SkipContents
|
||||
}
|
||||
if b.SkipBz2 != nil {
|
||||
published.SkipBz2 = *b.SkipBz2
|
||||
}
|
||||
if b.AcquireByHash != nil {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
if b.MultiDist != nil {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
if b.Label != nil {
|
||||
published.Label = *b.Label
|
||||
}
|
||||
if b.Origin != nil {
|
||||
published.Origin = *b.Origin
|
||||
}
|
||||
if b.Version != nil {
|
||||
published.Version = *b.Version
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
@@ -485,17 +570,17 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
result, err := published.Update(collectionFactory, out)
|
||||
result, err := published.Update(taskCollectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
|
||||
err = published.Publish(context.PackagePool(), context, taskCollectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = collection.Update(published)
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -503,10 +588,19 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
if b.SkipCleanup == nil || !*b.SkipCleanup {
|
||||
cleanComponents := make([]string, 0, len(result.UpdatedSources)+len(result.RemovedSources))
|
||||
cleanComponents = append(append(cleanComponents, result.UpdatedComponents()...), result.RemovedComponents()...)
|
||||
err = collection.CleanupPrefixComponentFiles(context, published, cleanComponents, collectionFactory, out)
|
||||
err = taskCollection.CleanupPrefixComponentFiles(context, published, cleanComponents, taskCollectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
// When MultiDist is toggled, the old pool layout still has files that
|
||||
// CleanupPrefixComponentFiles won't touch (it only scans the new layout).
|
||||
// Run a second pass over the previous layout to remove stale files.
|
||||
if prevMultiDist != published.MultiDist {
|
||||
err = taskCollection.CleanupAfterMultiDistToggle(context, published, prevMultiDist, cleanComponents, taskCollectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to clean legacy pool: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: published}, nil
|
||||
@@ -551,10 +645,19 @@ func apiPublishDrop(c *gin.Context) {
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
// Non-MultiDist distributions share a single pool/ directory under the
|
||||
// prefix. Acquire the prefix-level pool lock so that a drop cannot race
|
||||
// with a concurrent update or drop of a sibling distribution during cleanup.
|
||||
if !published.MultiDist {
|
||||
resources = append(resources, deb.PrefixPoolLockKey(published.StoragePrefix()))
|
||||
}
|
||||
taskName := fmt.Sprintf("Delete published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := collection.Remove(context, storage, prefix, distribution,
|
||||
collectionFactory, out, force, skipCleanup)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
err := taskCollection.Remove(context, storage, prefix, distribution,
|
||||
taskCollectionFactory, out, force, skipCleanup)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
@@ -590,43 +693,52 @@ func apiPublishAddSource(c *gin.Context) {
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
distribution := slashEscape(c.Params.ByName("distribution"))
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Load shallowly (no LoadComplete) to verify existence and obtain the
|
||||
// resource key and task name. The actual mutation is performed inside
|
||||
// the task on a freshly loaded copy to prevent lost-update races.
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to create: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to create: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
component := b.Component
|
||||
name := b.Name
|
||||
|
||||
_, exists := sources[component]
|
||||
if exists {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("unable to create: Component '%s' already exists", component))
|
||||
return
|
||||
}
|
||||
|
||||
sources[component] = name
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to create: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to create: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
component := b.Component
|
||||
name := b.Name
|
||||
|
||||
_, exists := sources[component]
|
||||
if exists {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("unable to create: Component '%s' already exists", component)
|
||||
}
|
||||
|
||||
sources[component] = name
|
||||
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -708,39 +820,48 @@ func apiPublishSetSources(c *gin.Context) {
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
distribution := slashEscape(c.Params.ByName("distribution"))
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Load shallowly for 404 check, resource key, and task name.
|
||||
// Full load and mutation happen inside the task.
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to update: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := make(map[string]string, len(b))
|
||||
revision.Sources = sources
|
||||
|
||||
for _, source := range b {
|
||||
component := source.Component
|
||||
name := source.Name
|
||||
sources[component] = name
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := make(map[string]string, len(b))
|
||||
revision.Sources = sources
|
||||
|
||||
for _, source := range b {
|
||||
component := source.Component
|
||||
name := source.Name
|
||||
sources[component] = name
|
||||
}
|
||||
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -773,24 +894,33 @@ func apiPublishDropChanges(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Load shallowly for 404 check, resource key, and task name.
|
||||
// Full load and DropRevision happen inside the task.
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to delete: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to delete: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
published.DropRevision()
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to delete: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to delete: %s", err)
|
||||
}
|
||||
|
||||
published.DropRevision()
|
||||
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -826,51 +956,58 @@ func apiPublishUpdateSource(c *gin.Context) {
|
||||
param := slashEscape(c.Params.ByName("prefix"))
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
distribution := slashEscape(c.Params.ByName("distribution"))
|
||||
component := slashEscape(c.Params.ByName("component"))
|
||||
urlComponent := slashEscape(c.Params.ByName("component"))
|
||||
|
||||
// Default component to the URL path segment; the body may rename it.
|
||||
b.Component = urlComponent
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Load shallowly for 404 check, resource key, and task name.
|
||||
// Full load and mutation happen inside the task.
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to update: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
_, exists := sources[component]
|
||||
if !exists {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: Component '%s' does not exist", component))
|
||||
return
|
||||
}
|
||||
|
||||
b.Component = component
|
||||
b.Name = revision.Sources[component]
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.Component != component {
|
||||
delete(sources, component)
|
||||
}
|
||||
|
||||
component = b.Component
|
||||
name := b.Name
|
||||
sources[component] = name
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
_, exists := sources[urlComponent]
|
||||
if !exists {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to update: Component '%s' does not exist", urlComponent)
|
||||
}
|
||||
|
||||
if b.Component != urlComponent {
|
||||
delete(sources, urlComponent)
|
||||
}
|
||||
|
||||
newComponent := b.Component
|
||||
name := b.Name
|
||||
sources[newComponent] = name
|
||||
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -907,33 +1044,41 @@ func apiPublishRemoveSource(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Load shallowly for 404 check, resource key, and task name.
|
||||
// Full load and mutation happen inside the task.
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to delete: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to delete: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
_, exists := sources[component]
|
||||
if !exists {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to delete: Component '%s' does not exist", component))
|
||||
return
|
||||
}
|
||||
|
||||
delete(sources, component)
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to delete: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to delete: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
_, exists := sources[component]
|
||||
if !exists {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("unable to delete: Component '%s' does not exist", component)
|
||||
}
|
||||
|
||||
delete(sources, component)
|
||||
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -955,8 +1100,16 @@ type publishedRepoUpdateParams struct {
|
||||
SkipCleanup *bool ` json:"SkipCleanup" example:"false"`
|
||||
// Provide index files by hash
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file
|
||||
SignedBy *string ` json:"SignedBy" example:""`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Value of Label: field in published repository stanza
|
||||
Label *string ` json:"Label" example:"Debian"`
|
||||
// Value of Origin: field in published repository stanza
|
||||
Origin *string ` json:"Origin" example:"Debian"`
|
||||
// Version of the release: Optional
|
||||
Version *string ` json:"Version" example:"13.3"`
|
||||
}
|
||||
|
||||
// @Summary Update Published Repository
|
||||
@@ -997,48 +1150,104 @@ func apiPublishUpdate(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Load shallowly for 404 check, resource key, and task name.
|
||||
// Full load and field mutations happen inside the task.
|
||||
published, err := collection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("unable to update: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to update: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if b.SkipContents != nil {
|
||||
published.SkipContents = *b.SkipContents
|
||||
}
|
||||
|
||||
if b.SkipBz2 != nil {
|
||||
published.SkipBz2 = *b.SkipBz2
|
||||
}
|
||||
|
||||
if b.AcquireByHash != nil {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
if b.MultiDist != nil {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
|
||||
// Non-MultiDist distributions share a single pool/ directory under the
|
||||
// prefix. Acquire the prefix-level pool lock so that concurrent updates
|
||||
// on sibling distributions are serialised and cannot race during cleanup.
|
||||
if !published.MultiDist {
|
||||
resources = append(resources, deb.PrefixPoolLockKey(published.StoragePrefix()))
|
||||
}
|
||||
|
||||
// Lock source repos / snapshots the same way apiPublishUpdateSwitch does,
|
||||
// because published.Update() reads from them and concurrent modification
|
||||
// would produce an inconsistent view.
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
localRepoCollection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
if published.SourceKind == deb.SourceLocalRepo {
|
||||
for _, uuid := range published.Sources {
|
||||
repo, err2 := localRepoCollection.ByUUID(uuid)
|
||||
if err2 != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err2)
|
||||
return
|
||||
}
|
||||
resources = append(resources, string(repo.Key()))
|
||||
}
|
||||
} else if published.SourceKind == deb.SourceSnapshot {
|
||||
for _, uuid := range published.Sources {
|
||||
snapshot, err2 := snapshotCollection.ByUUID(uuid)
|
||||
if err2 != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err2)
|
||||
return
|
||||
}
|
||||
resources = append(resources, string(snapshot.Key()))
|
||||
}
|
||||
}
|
||||
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
result, err := published.Update(collectionFactory, out)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
published, err := taskCollection.ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
|
||||
err = taskCollection.LoadComplete(published, taskCollectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = collection.Update(published)
|
||||
// Capture MultiDist before mutations to detect a false→true transition.
|
||||
prevMultiDist := published.MultiDist
|
||||
|
||||
// Apply field mutations on the freshly loaded object.
|
||||
if b.SkipContents != nil {
|
||||
published.SkipContents = *b.SkipContents
|
||||
}
|
||||
if b.SkipBz2 != nil {
|
||||
published.SkipBz2 = *b.SkipBz2
|
||||
}
|
||||
if b.AcquireByHash != nil {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
if b.MultiDist != nil {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
if b.Label != nil {
|
||||
published.Label = *b.Label
|
||||
}
|
||||
if b.Origin != nil {
|
||||
published.Origin = *b.Origin
|
||||
}
|
||||
if b.Version != nil {
|
||||
published.Version = *b.Version
|
||||
}
|
||||
|
||||
result, err := published.Update(taskCollectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, taskCollectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -1046,10 +1255,19 @@ func apiPublishUpdate(c *gin.Context) {
|
||||
if b.SkipCleanup == nil || !*b.SkipCleanup {
|
||||
cleanComponents := make([]string, 0, len(result.UpdatedSources)+len(result.RemovedSources))
|
||||
cleanComponents = append(append(cleanComponents, result.UpdatedComponents()...), result.RemovedComponents()...)
|
||||
err = collection.CleanupPrefixComponentFiles(context, published, cleanComponents, collectionFactory, out)
|
||||
err = taskCollection.CleanupPrefixComponentFiles(context, published, cleanComponents, taskCollectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
// When MultiDist is toggled, the old pool layout still has files that
|
||||
// CleanupPrefixComponentFiles won't touch (it only scans the new layout).
|
||||
// Run a second pass over the previous layout to remove stale files.
|
||||
if prevMultiDist != published.MultiDist {
|
||||
err = taskCollection.CleanupAfterMultiDistToggle(context, published, prevMultiDist, cleanComponents, taskCollectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to clean legacy pool: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: published}, nil
|
||||
|
||||
@@ -0,0 +1,737 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// PublishedFileMissingSuite reproduces the exact bug where:
|
||||
// - Package import succeeds
|
||||
// - Metadata is updated (Packages.gz shows the package)
|
||||
// - Publish reports success
|
||||
// - BUT the .deb file is missing from the published pool directory
|
||||
// - Result: apt-get returns 404 when trying to download the package
|
||||
type PublishedFileMissingSuite struct {
|
||||
context *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
tempDir string
|
||||
poolPath string
|
||||
publicPath string
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishedFileMissingSuite{})
|
||||
|
||||
func (s *PublishedFileMissingSuite) SetUpSuite(c *C) {
|
||||
aptly.Version = "publishedFileMissingTest"
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "aptly-published-missing-test")
|
||||
c.Assert(err, IsNil)
|
||||
s.tempDir = tempDir
|
||||
s.poolPath = filepath.Join(tempDir, "pool")
|
||||
s.publicPath = filepath.Join(tempDir, "public")
|
||||
|
||||
file, err := os.CreateTemp("", "aptly-published-missing-config")
|
||||
c.Assert(err, IsNil)
|
||||
s.configFile = file
|
||||
|
||||
config := gin.H{
|
||||
"rootDir": tempDir,
|
||||
"downloadDir": filepath.Join(tempDir, "download"),
|
||||
"architectures": []string{"amd64"},
|
||||
"dependencyFollowSuggests": false,
|
||||
"dependencyFollowRecommends": false,
|
||||
"gpgDisableSign": true,
|
||||
"gpgDisableVerify": true,
|
||||
"gpgProvider": "internal",
|
||||
"skipLegacyPool": true,
|
||||
"enableMetricsEndpoint": false,
|
||||
}
|
||||
|
||||
jsonString, err := json.Marshal(config)
|
||||
c.Assert(err, IsNil)
|
||||
_, err = file.Write(jsonString)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
flags := flag.NewFlagSet("publishedFileMissingTestFlags", flag.ContinueOnError)
|
||||
flags.Bool("no-lock", true, "disable database locking for test")
|
||||
flags.Int("db-open-attempts", 3, "dummy")
|
||||
flags.String("config", s.configFile.Name(), "config file")
|
||||
flags.String("architectures", "", "dummy")
|
||||
s.flags = flags
|
||||
|
||||
context, err := ctx.NewContext(s.flags)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.context = context
|
||||
s.router = Router(context)
|
||||
}
|
||||
|
||||
func (s *PublishedFileMissingSuite) TearDownSuite(c *C) {
|
||||
if s.configFile != nil {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
}
|
||||
if s.context != nil {
|
||||
s.context.Shutdown()
|
||||
}
|
||||
if s.tempDir != "" {
|
||||
_ = os.RemoveAll(s.tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishedFileMissingSuite) SetUpTest(c *C) {
|
||||
collectionFactory := s.context.NewCollectionFactory()
|
||||
|
||||
localRepoCollection := collectionFactory.LocalRepoCollection()
|
||||
_ = localRepoCollection.ForEach(func(repo *deb.LocalRepo) error {
|
||||
_ = localRepoCollection.Drop(repo)
|
||||
return nil
|
||||
})
|
||||
|
||||
publishedCollection := collectionFactory.PublishedRepoCollection()
|
||||
_ = publishedCollection.ForEach(func(published *deb.PublishedRepo) error {
|
||||
_ = publishedCollection.Remove(s.context, published.Storage, published.Prefix,
|
||||
published.Distribution, collectionFactory, nil, true, true)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PublishedFileMissingSuite) TearDownTest(c *C) {
|
||||
s.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *PublishedFileMissingSuite) httpRequest(c *C, method string, url string, body []byte) *httptest.ResponseRecorder {
|
||||
w := httptest.NewRecorder()
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if body != nil {
|
||||
req, err = http.NewRequest(method, url, bytes.NewReader(body))
|
||||
} else {
|
||||
req, err = http.NewRequest(method, url, nil)
|
||||
}
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func (s *PublishedFileMissingSuite) createDebPackage(c *C, uploadID, packageName, version string) {
|
||||
uploadPath := s.context.UploadPath()
|
||||
uploadDir := filepath.Join(uploadPath, uploadID)
|
||||
err := os.MkdirAll(uploadDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "deb-build")
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
debianDir := filepath.Join(tempDir, "DEBIAN")
|
||||
err = os.MkdirAll(debianDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
controlContent := fmt.Sprintf(`Package: %s
|
||||
Version: %s
|
||||
Section: libs
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: Test <test@example.com>
|
||||
Description: Test package
|
||||
Test package for published file missing bug.
|
||||
`, packageName, version)
|
||||
|
||||
err = os.WriteFile(filepath.Join(debianDir, "control"), []byte(controlContent), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
usrDir := filepath.Join(tempDir, "usr", "lib")
|
||||
err = os.MkdirAll(usrDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
err = os.WriteFile(filepath.Join(usrDir, "lib.so"), []byte("library"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
debFile := filepath.Join(uploadDir, fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
cmd := exec.Command("dpkg-deb", "--build", tempDir, debFile)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
// TestPublishedFileGoMissing reproduces the exact production bug
|
||||
func (s *PublishedFileMissingSuite) TestPublishedFileGoMissing(c *C) {
|
||||
c.Log("=== Reproducing: Package in metadata but 404 on download ===")
|
||||
|
||||
// Create and publish a repository
|
||||
repoName := "test-repo"
|
||||
distribution := "bullseye"
|
||||
|
||||
createBody, _ := json.Marshal(gin.H{
|
||||
"Name": repoName,
|
||||
"DefaultDistribution": distribution,
|
||||
"DefaultComponent": "main",
|
||||
})
|
||||
resp := s.httpRequest(c, "POST", "/api/repos", createBody)
|
||||
c.Assert(resp.Code, Equals, 201, Commentf("Failed to create repo: %s", resp.Body.String()))
|
||||
|
||||
publishBody, _ := json.Marshal(gin.H{
|
||||
"SourceKind": "local",
|
||||
"Distribution": distribution,
|
||||
"Architectures": []string{"amd64"},
|
||||
"Sources": []gin.H{
|
||||
{"Component": "main", "Name": repoName},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
})
|
||||
resp = s.httpRequest(c, "POST", "/api/publish/hrt", publishBody)
|
||||
c.Assert(resp.Code, Equals, 201, Commentf("Failed to publish: %s", resp.Body.String()))
|
||||
|
||||
// Create package
|
||||
packageName := "hrt-libblobbyclient1"
|
||||
version := "20250926.152427+hrtdeb11"
|
||||
uploadID := "test-upload-1"
|
||||
|
||||
s.createDebPackage(c, uploadID, packageName, version)
|
||||
|
||||
// Add package
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/file/%s?noRemove=0", repoName, uploadID), nil)
|
||||
c.Assert(resp.Code, Equals, 200, Commentf("Failed to add package: %s", resp.Body.String()))
|
||||
|
||||
// Update publish
|
||||
updateBody, _ := json.Marshal(gin.H{
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"ForceOverwrite": true,
|
||||
})
|
||||
resp = s.httpRequest(c, "PUT", fmt.Sprintf("/api/publish/hrt/%s", distribution), updateBody)
|
||||
c.Assert(resp.Code, Equals, 200, Commentf("Failed to update publish: %s", resp.Body.String()))
|
||||
|
||||
// Now check if the file is actually accessible in the published location
|
||||
publishedStorage, err := s.context.GetPublishedStorage("")
|
||||
c.Assert(err, IsNil)
|
||||
publicPath := publishedStorage.(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
|
||||
// Expected file path: hrt/pool/main/h/hrt-libblobbyclient1/hrt-libblobbyclient1_20250926.152427+hrtdeb11_amd64.deb
|
||||
expectedPath := filepath.Join(publicPath, "hrt", "pool", "main", "h", packageName,
|
||||
fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
|
||||
c.Logf("Checking for published file at: %s", expectedPath)
|
||||
|
||||
fileInfo, err := os.Stat(expectedPath)
|
||||
fileExists := err == nil
|
||||
|
||||
c.Logf("File exists: %v", fileExists)
|
||||
if fileExists {
|
||||
c.Logf("File size: %d bytes", fileInfo.Size())
|
||||
}
|
||||
|
||||
// Check metadata
|
||||
resp = s.httpRequest(c, "GET", fmt.Sprintf("/api/repos/%s/packages", repoName), nil)
|
||||
var packages []string
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &packages)
|
||||
c.Assert(err, IsNil)
|
||||
c.Logf("Packages in metadata: %d", len(packages))
|
||||
|
||||
// THE BUG: Metadata says package exists, but file is missing from published location
|
||||
if len(packages) > 0 && !fileExists {
|
||||
c.Logf("★★★ BUG REPRODUCED! ★★★")
|
||||
c.Logf("Metadata shows %d package(s) but file is missing at: %s", len(packages), expectedPath)
|
||||
c.Logf("This is exactly what causes: 404 Not Found [IP: 10.20.72.62 3142]")
|
||||
|
||||
c.Fatal("BUG CONFIRMED: Package in metadata but missing from published directory!")
|
||||
}
|
||||
|
||||
c.Assert(fileExists, Equals, true, Commentf(
|
||||
"Published file should exist at %s when package is in metadata", expectedPath))
|
||||
}
|
||||
|
||||
// TestConcurrentPublishRace tries to trigger the race with concurrent publishes
|
||||
func (s *PublishedFileMissingSuite) TestConcurrentPublishRace(c *C) {
|
||||
c.Log("=== Testing concurrent publish race condition ===")
|
||||
|
||||
const numIterations = 4
|
||||
|
||||
for iteration := 0; iteration < numIterations; iteration++ {
|
||||
c.Logf("--- Iteration %d/%d ---", iteration+1, numIterations)
|
||||
|
||||
// Create repo
|
||||
repoName := fmt.Sprintf("race-repo-%d", iteration)
|
||||
distribution := fmt.Sprintf("dist-%d", iteration)
|
||||
|
||||
createBody, _ := json.Marshal(gin.H{
|
||||
"Name": repoName,
|
||||
"DefaultDistribution": distribution,
|
||||
"DefaultComponent": "main",
|
||||
})
|
||||
resp := s.httpRequest(c, "POST", "/api/repos", createBody)
|
||||
c.Assert(resp.Code, Equals, 201)
|
||||
|
||||
publishBody, _ := json.Marshal(gin.H{
|
||||
"SourceKind": "local",
|
||||
"Distribution": distribution,
|
||||
"Architectures": []string{"amd64"},
|
||||
"Sources": []gin.H{
|
||||
{"Component": "main", "Name": repoName},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
})
|
||||
resp = s.httpRequest(c, "POST", "/api/publish/concurrent", publishBody)
|
||||
c.Assert(resp.Code, Equals, 201)
|
||||
|
||||
// Create multiple packages
|
||||
var wg sync.WaitGroup
|
||||
numPackages := 5
|
||||
|
||||
for i := 0; i < numPackages; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
|
||||
packageName := fmt.Sprintf("pkg-%d-%d", iteration, idx)
|
||||
version := "1.0.0"
|
||||
uploadID := fmt.Sprintf("upload-%d-%d", iteration, idx)
|
||||
|
||||
s.createDebPackage(c, uploadID, packageName, version)
|
||||
|
||||
// Add package
|
||||
resp := s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/file/%s?noRemove=0", repoName, uploadID), nil)
|
||||
c.Logf("Package %d add: %d", idx, resp.Code)
|
||||
|
||||
// Small delay
|
||||
time.Sleep(time.Duration(5+idx*2) * time.Millisecond)
|
||||
|
||||
// Publish
|
||||
updateBody, _ := json.Marshal(gin.H{
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"ForceOverwrite": true,
|
||||
})
|
||||
resp = s.httpRequest(c, "PUT", fmt.Sprintf("/api/publish/concurrent/%s", distribution), updateBody)
|
||||
c.Logf("Publish %d: %d", idx, resp.Code)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Check all packages
|
||||
resp = s.httpRequest(c, "GET", fmt.Sprintf("/api/repos/%s/packages", repoName), nil)
|
||||
var packages []string
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &packages)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Check published files
|
||||
publishedStorage, err := s.context.GetPublishedStorage("")
|
||||
c.Assert(err, IsNil)
|
||||
publicPath := publishedStorage.(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
|
||||
missingFiles := []string{}
|
||||
for i := 0; i < numPackages; i++ {
|
||||
packageName := fmt.Sprintf("pkg-%d-%d", iteration, i)
|
||||
version := "1.0.0"
|
||||
|
||||
// Calculate pool path
|
||||
poolSubdir := string(packageName[0])
|
||||
expectedPath := filepath.Join(publicPath, "concurrent", "pool", "main", poolSubdir, packageName,
|
||||
fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
|
||||
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||||
missingFiles = append(missingFiles, expectedPath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingFiles) > 0 {
|
||||
c.Logf("★★★ BUG DETECTED in iteration %d/%d! ★★★", iteration+1, numIterations)
|
||||
c.Logf("Metadata shows %d packages, but %d files are MISSING:", len(packages), len(missingFiles))
|
||||
for i, f := range missingFiles {
|
||||
c.Logf(" [iter %d] File MISSING %d/%d: %s", iteration+1, i+1, len(missingFiles), f)
|
||||
}
|
||||
|
||||
c.Fatalf("BUG REPRODUCED in iteration %d/%d: %d published files missing", iteration+1, numIterations, len(missingFiles))
|
||||
} else {
|
||||
c.Logf("[iter %d/%d] All %d files present - OK", iteration+1, numIterations, numPackages)
|
||||
}
|
||||
}
|
||||
|
||||
c.Logf("All %d iterations passed - bug not reproduced with current timing", numIterations)
|
||||
}
|
||||
|
||||
// TestIdenticalPackageRace tests the specific case of identical SHA256 packages
|
||||
func (s *PublishedFileMissingSuite) TestIdenticalPackageRace(c *C) {
|
||||
c.Log("=== AGGRESSIVE test: identical package (same SHA256) race ===")
|
||||
|
||||
const numIterations = 4
|
||||
packageName := "shared-package"
|
||||
|
||||
for iter := 0; iter < numIterations; iter++ {
|
||||
c.Logf("Iteration %d/%d", iter+1, numIterations)
|
||||
|
||||
// Create two repos that will get the SAME package (unique per iteration)
|
||||
repos := []string{fmt.Sprintf("identical-a-%d", iter), fmt.Sprintf("identical-b-%d", iter)}
|
||||
dists := []string{fmt.Sprintf("dist-a-%d", iter), fmt.Sprintf("dist-b-%d", iter)}
|
||||
|
||||
for i := range repos {
|
||||
createBody, _ := json.Marshal(gin.H{
|
||||
"Name": repos[i],
|
||||
"DefaultDistribution": dists[i],
|
||||
"DefaultComponent": "main",
|
||||
})
|
||||
resp := s.httpRequest(c, "POST", "/api/repos", createBody)
|
||||
c.Assert(resp.Code, Equals, 201)
|
||||
|
||||
publishBody, _ := json.Marshal(gin.H{
|
||||
"SourceKind": "local",
|
||||
"Distribution": dists[i],
|
||||
"Architectures": []string{"amd64"},
|
||||
"Sources": []gin.H{
|
||||
{"Component": "main", "Name": repos[i]},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"SkipBz2": true,
|
||||
})
|
||||
resp = s.httpRequest(c, "POST", "/api/publish/identical", publishBody)
|
||||
c.Assert(resp.Code, Equals, 201)
|
||||
}
|
||||
|
||||
// Create IDENTICAL package file with UNIQUE VERSION per iteration
|
||||
version := fmt.Sprintf("1.0.%d", iter)
|
||||
uploadID1 := fmt.Sprintf("identical-upload-1-%d", iter)
|
||||
uploadID2 := fmt.Sprintf("identical-upload-2-%d", iter)
|
||||
|
||||
s.createDebPackage(c, uploadID1, packageName, version)
|
||||
|
||||
// Copy to second upload (same SHA256)
|
||||
uploadPath := s.context.UploadPath()
|
||||
src := filepath.Join(uploadPath, uploadID1, fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
destDir := filepath.Join(uploadPath, uploadID2)
|
||||
err := os.MkdirAll(destDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
dest := filepath.Join(destDir, fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
|
||||
srcData, readErr := os.ReadFile(src)
|
||||
c.Assert(readErr, IsNil)
|
||||
err = os.WriteFile(dest, srcData, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Race: add and publish both simultaneously
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/file/%s?noRemove=0", repos[0], uploadID1), nil)
|
||||
updateBody, _ := json.Marshal(gin.H{"Signing": gin.H{"Skip": true}, "ForceOverwrite": true, "SkipBz2": true})
|
||||
s.httpRequest(c, "PUT", fmt.Sprintf("/api/publish/identical/%s", dists[0]), updateBody)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/file/%s?noRemove=0", repos[1], uploadID2), nil)
|
||||
updateBody, _ := json.Marshal(gin.H{"Signing": gin.H{"Skip": true}, "ForceOverwrite": true, "SkipBz2": true})
|
||||
s.httpRequest(c, "PUT", fmt.Sprintf("/api/publish/identical/%s", dists[1]), updateBody)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
c.Logf("[iter %d] All operations complete", iter)
|
||||
|
||||
// Check the shared pool location
|
||||
publishedStorage, err := s.context.GetPublishedStorage("")
|
||||
c.Assert(err, IsNil)
|
||||
publicPath := publishedStorage.(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
|
||||
poolSubdir := string(packageName[0])
|
||||
sharedPoolPath := filepath.Join(publicPath, "identical", "pool", "main", poolSubdir, packageName,
|
||||
fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
|
||||
fileInfo, err := os.Stat(sharedPoolPath)
|
||||
fileExists := err == nil
|
||||
|
||||
if fileExists {
|
||||
c.Logf("[iter %d] File EXISTS at %s (size: %d)", iter, sharedPoolPath, fileInfo.Size())
|
||||
} else {
|
||||
c.Logf("[iter %d] File MISSING at %s (error: %v)", iter, sharedPoolPath, err)
|
||||
}
|
||||
|
||||
// Check metadata
|
||||
var packagesA, packagesB []string
|
||||
resp := s.httpRequest(c, "GET", fmt.Sprintf("/api/repos/%s/packages", repos[0]), nil)
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &packagesA)
|
||||
c.Assert(err, IsNil)
|
||||
resp = s.httpRequest(c, "GET", fmt.Sprintf("/api/repos/%s/packages", repos[1]), nil)
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &packagesB)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Logf("[iter %d] Packages in metadata: A=%d, B=%d", iter, len(packagesA), len(packagesB))
|
||||
|
||||
// THE BUG: Both repos show packages in metadata, but the shared pool file is missing
|
||||
if (len(packagesA) > 0 || len(packagesB) > 0) && !fileExists {
|
||||
c.Logf("★★★ BUG REPRODUCED in iteration %d! ★★★", iter+1)
|
||||
c.Logf("Packages in metadata A: %d, B: %d", len(packagesA), len(packagesB))
|
||||
c.Logf("Shared pool file exists: %v", fileExists)
|
||||
c.Logf("Pool path: %s", sharedPoolPath)
|
||||
|
||||
// List what files ARE in the pool directory
|
||||
poolDir := filepath.Dir(sharedPoolPath)
|
||||
if entries, err := os.ReadDir(poolDir); err == nil {
|
||||
c.Logf("Files in pool directory %s:", poolDir)
|
||||
for _, entry := range entries {
|
||||
c.Logf(" - %s", entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
c.Fatalf("Metadata shows packages but shared pool file is missing (iteration %d)", iter+1)
|
||||
}
|
||||
}
|
||||
|
||||
c.Logf("All %d iterations passed - bug not reproduced", numIterations)
|
||||
}
|
||||
|
||||
// TestConcurrentSnapshotPublishToSamePrefix reproduces the EXACT production bug:
|
||||
// Multiple snapshots are published concurrently to the SAME prefix but different distributions.
|
||||
// Example from production logs:
|
||||
// - trixie-pgdg published to "external/postgres-auto/trixie"
|
||||
// - bullseye-pgdg published to "external/postgres-auto/bullseye"
|
||||
// Both share the same pool directory, causing cleanup race conditions.
|
||||
func (s *PublishedFileMissingSuite) TestConcurrentSnapshotPublishToSamePrefix(c *C) {
|
||||
const numIterations = 4
|
||||
|
||||
for iter := 0; iter < numIterations; iter++ {
|
||||
c.Logf("--- Iteration %d/%d ---", iter+1, numIterations)
|
||||
|
||||
// Create two repos with different packages (simulating trixie-pgdg and bullseye-pgdg)
|
||||
repoTrixie := fmt.Sprintf("trixie-pgdg-%d", iter)
|
||||
repoBullseye := fmt.Sprintf("bullseye-pgdg-%d", iter)
|
||||
|
||||
// Create trixie repo
|
||||
createBody, _ := json.Marshal(gin.H{
|
||||
"Name": repoTrixie,
|
||||
"DefaultDistribution": "trixie",
|
||||
"DefaultComponent": "main",
|
||||
})
|
||||
resp := s.httpRequest(c, "POST", "/api/repos", createBody)
|
||||
c.Assert(resp.Code, Equals, 201, Commentf("Failed to create trixie repo"))
|
||||
|
||||
// Create bullseye repo
|
||||
createBody, _ = json.Marshal(gin.H{
|
||||
"Name": repoBullseye,
|
||||
"DefaultDistribution": "bullseye",
|
||||
"DefaultComponent": "main",
|
||||
})
|
||||
resp = s.httpRequest(c, "POST", "/api/repos", createBody)
|
||||
c.Assert(resp.Code, Equals, 201, Commentf("Failed to create bullseye repo"))
|
||||
|
||||
// Add packages to both repos
|
||||
numPackages := 3
|
||||
|
||||
// Add packages to trixie repo
|
||||
for i := 0; i < numPackages; i++ {
|
||||
packageName := fmt.Sprintf("postgresql-17-trixie-pkg%d", i)
|
||||
version := fmt.Sprintf("17.0.%d", iter)
|
||||
uploadID := fmt.Sprintf("trixie-upload-%d-%d", iter, i)
|
||||
|
||||
s.createDebPackage(c, uploadID, packageName, version)
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/file/%s?noRemove=0", repoTrixie, uploadID), nil)
|
||||
c.Assert(resp.Code, Equals, 200, Commentf("Failed to add package to trixie"))
|
||||
}
|
||||
|
||||
// Add packages to bullseye repo
|
||||
for i := 0; i < numPackages; i++ {
|
||||
packageName := fmt.Sprintf("postgresql-17-bullseye-pkg%d", i)
|
||||
version := fmt.Sprintf("17.0.%d", iter)
|
||||
uploadID := fmt.Sprintf("bullseye-upload-%d-%d", iter, i)
|
||||
|
||||
s.createDebPackage(c, uploadID, packageName, version)
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/file/%s?noRemove=0", repoBullseye, uploadID), nil)
|
||||
c.Assert(resp.Code, Equals, 200, Commentf("Failed to add package to bullseye"))
|
||||
}
|
||||
|
||||
// Create snapshots from both repos
|
||||
snapshotTrixie := fmt.Sprintf("%s-snap", repoTrixie)
|
||||
snapshotBullseye := fmt.Sprintf("%s-snap", repoBullseye)
|
||||
|
||||
createSnapshotBody, _ := json.Marshal(gin.H{"Name": snapshotTrixie})
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/snapshots", repoTrixie), createSnapshotBody)
|
||||
c.Assert(resp.Code, Equals, 201, Commentf("Failed to create trixie snapshot"))
|
||||
|
||||
createSnapshotBody, _ = json.Marshal(gin.H{"Name": snapshotBullseye})
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/repos/%s/snapshots", repoBullseye), createSnapshotBody)
|
||||
c.Assert(resp.Code, Equals, 201, Commentf("Failed to create bullseye snapshot"))
|
||||
|
||||
// Publish both snapshots CONCURRENTLY to the SAME prefix
|
||||
// This mimics production where both are published to "external/postgres-auto"
|
||||
// Use the SAME prefix across all iterations to trigger the race more aggressively
|
||||
sharedPrefix := "postgres-auto"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var trixiePublishCode, bullseyePublishCode int
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
// Publish or update trixie snapshot
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
var resp *httptest.ResponseRecorder
|
||||
if iter == 0 {
|
||||
// First iteration: CREATE
|
||||
publishBody, _ := json.Marshal(gin.H{
|
||||
"SourceKind": "snapshot",
|
||||
"Distribution": "trixie",
|
||||
"Architectures": []string{"amd64"},
|
||||
"Sources": []gin.H{
|
||||
{"Name": snapshotTrixie},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"SkipBz2": true,
|
||||
"ForceOverwrite": true,
|
||||
"SkipCleanup": false, // Force cleanup to run
|
||||
})
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/publish/%s", sharedPrefix), publishBody)
|
||||
} else {
|
||||
// Subsequent iterations: UPDATE (this is what happens in production)
|
||||
updateBody, _ := json.Marshal(gin.H{
|
||||
"Snapshots": []gin.H{
|
||||
{"Component": "main", "Name": snapshotTrixie},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"SkipBz2": true,
|
||||
"ForceOverwrite": true,
|
||||
"SkipCleanup": false,
|
||||
})
|
||||
resp = s.httpRequest(c, "PUT", fmt.Sprintf("/api/publish/%s/trixie", sharedPrefix), updateBody)
|
||||
}
|
||||
trixiePublishCode = resp.Code
|
||||
c.Logf("[iter %d] Trixie publish/update completed: %d", iter, resp.Code)
|
||||
}()
|
||||
|
||||
// Publish or update bullseye snapshot
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
var resp *httptest.ResponseRecorder
|
||||
if iter == 0 {
|
||||
// First iteration: CREATE
|
||||
publishBody, _ := json.Marshal(gin.H{
|
||||
"SourceKind": "snapshot",
|
||||
"Distribution": "bullseye",
|
||||
"Architectures": []string{"amd64"},
|
||||
"Sources": []gin.H{
|
||||
{"Name": snapshotBullseye},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"SkipBz2": true,
|
||||
"ForceOverwrite": true,
|
||||
"SkipCleanup": false,
|
||||
})
|
||||
resp = s.httpRequest(c, "POST", fmt.Sprintf("/api/publish/%s", sharedPrefix), publishBody)
|
||||
} else {
|
||||
// Subsequent iterations: UPDATE
|
||||
updateBody, _ := json.Marshal(gin.H{
|
||||
"Snapshots": []gin.H{
|
||||
{"Component": "main", "Name": snapshotBullseye},
|
||||
},
|
||||
"Signing": gin.H{"Skip": true},
|
||||
"SkipBz2": true,
|
||||
"ForceOverwrite": true,
|
||||
"SkipCleanup": false,
|
||||
})
|
||||
resp = s.httpRequest(c, "PUT", fmt.Sprintf("/api/publish/%s/bullseye", sharedPrefix), updateBody)
|
||||
}
|
||||
bullseyePublishCode = resp.Code
|
||||
c.Logf("[iter %d] Bullseye publish/update completed: %d", iter, resp.Code)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Verify publishes succeeded (201 for create, 200 for update)
|
||||
expectedCode := 201
|
||||
if iter > 0 {
|
||||
expectedCode = 200
|
||||
}
|
||||
c.Assert(trixiePublishCode, Equals, expectedCode, Commentf("Trixie publish/update should succeed"))
|
||||
c.Assert(bullseyePublishCode, Equals, expectedCode, Commentf("Bullseye publish/update should succeed"))
|
||||
|
||||
// Verify ALL package files exist in the published pool
|
||||
publishedStorage, err := s.context.GetPublishedStorage("")
|
||||
c.Assert(err, IsNil)
|
||||
publicPath := publishedStorage.(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
|
||||
missingFiles := []string{}
|
||||
expectedFiles := []string{}
|
||||
|
||||
// Check trixie packages
|
||||
for i := 0; i < numPackages; i++ {
|
||||
packageName := fmt.Sprintf("postgresql-17-trixie-pkg%d", i)
|
||||
version := fmt.Sprintf("17.0.%d", iter)
|
||||
|
||||
poolSubdir := string(packageName[0])
|
||||
expectedPath := filepath.Join(publicPath, sharedPrefix, "pool", "main", poolSubdir, packageName,
|
||||
fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
|
||||
expectedFiles = append(expectedFiles, expectedPath)
|
||||
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||||
missingFiles = append(missingFiles, fmt.Sprintf("TRIXIE: %s", filepath.Base(expectedPath)))
|
||||
}
|
||||
}
|
||||
|
||||
// Check bullseye packages
|
||||
for i := 0; i < numPackages; i++ {
|
||||
packageName := fmt.Sprintf("postgresql-17-bullseye-pkg%d", i)
|
||||
version := fmt.Sprintf("17.0.%d", iter)
|
||||
|
||||
poolSubdir := string(packageName[0])
|
||||
expectedPath := filepath.Join(publicPath, sharedPrefix, "pool", "main", poolSubdir, packageName,
|
||||
fmt.Sprintf("%s_%s_amd64.deb", packageName, version))
|
||||
|
||||
expectedFiles = append(expectedFiles, expectedPath)
|
||||
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||||
missingFiles = append(missingFiles, fmt.Sprintf("BULLSEYE: %s", filepath.Base(expectedPath)))
|
||||
}
|
||||
}
|
||||
|
||||
// BUG: Files from one distribution are deleted by the other's cleanup
|
||||
if len(missingFiles) > 0 {
|
||||
c.Logf("★★★ BUG REPRODUCED in iteration %d/%d! ★★★", iter+1, numIterations)
|
||||
c.Logf("Both publishes to prefix '%s' succeeded, but %d files are MISSING:", sharedPrefix, len(missingFiles))
|
||||
for i, f := range missingFiles {
|
||||
c.Logf(" Missing file %d/%d: %s", i+1, len(missingFiles), f)
|
||||
}
|
||||
|
||||
c.Logf("\nThis reproduces the exact production bug where:")
|
||||
c.Logf(" 1. Mirror updates complete successfully")
|
||||
c.Logf(" 2. Snapshots are created")
|
||||
c.Logf(" 3. Both snapshots publish to same prefix (different distributions)")
|
||||
c.Logf(" 4. Cleanup from one publish DELETES files from the other")
|
||||
c.Logf(" 5. Result: apt-get returns 404 when downloading packages")
|
||||
|
||||
// List what's actually in the pool
|
||||
poolDir := filepath.Join(publicPath, sharedPrefix, "pool", "main")
|
||||
if entries, err := os.ReadDir(poolDir); err == nil {
|
||||
c.Logf("\nActual pool directory contents (%s):", poolDir)
|
||||
for _, entry := range entries {
|
||||
c.Logf(" - %s/", entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
c.Fatalf("BUG CONFIRMED (iteration %d/%d): %d files missing from shared pool",
|
||||
iter+1, numIterations, len(missingFiles))
|
||||
} else {
|
||||
c.Logf("[iter %d/%d] All %d files present - OK", iter+1, numIterations, len(expectedFiles))
|
||||
}
|
||||
}
|
||||
c.Logf("✓ All %d iterations passed - no files missing", numIterations)
|
||||
}
|
||||
+197
-85
@@ -24,7 +24,7 @@ import (
|
||||
// @Tags Repos
|
||||
// @Produce html
|
||||
// @Success 200 {object} string "HTML"
|
||||
// @Router /api/repos [get]
|
||||
// @Router /repos [get]
|
||||
func reposListInAPIMode(localRepos map[string]utils.FileSystemPublishRoot) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -49,7 +49,7 @@ func reposListInAPIMode(localRepos map[string]utils.FileSystemPublishRoot) gin.H
|
||||
// @Param pkgPath path string true "Package Path" allowReserved=true
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/{storage}/{pkgPath} [get]
|
||||
// @Router /repos/{storage}/{pkgPath} [get]
|
||||
func reposServeInAPIMode(c *gin.Context) {
|
||||
pkgpath := c.Param("pkgPath")
|
||||
|
||||
@@ -60,7 +60,12 @@ func reposServeInAPIMode(c *gin.Context) {
|
||||
storage = "filesystem:" + storage
|
||||
}
|
||||
|
||||
publicPath := context.GetPublishedStorage(storage).(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
ps, err := context.GetPublishedStorage(storage)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
publicPath := ps.(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
c.FileFromFS(pkgpath, http.Dir(publicPath))
|
||||
}
|
||||
|
||||
@@ -69,17 +74,26 @@ func reposServeInAPIMode(c *gin.Context) {
|
||||
// @Description Each repo is returned as in “show” API.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.LocalRepo
|
||||
// @Success 200 {array} localRepoResponse
|
||||
// @Router /api/repos [get]
|
||||
func apiReposList(c *gin.Context) {
|
||||
result := []*deb.LocalRepo{}
|
||||
result := []localRepoResponse{}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
_ = collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
result = append(result, r)
|
||||
err := collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
err := collection.LoadComplete(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newLocalRepoResponse(r))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -93,7 +107,7 @@ type repoCreateParams struct {
|
||||
DefaultDistribution string ` json:"DefaultDistribution" example:"stable"`
|
||||
// Default component when publishing from this local repo
|
||||
DefaultComponent string ` json:"DefaultComponent" example:"main"`
|
||||
// Snapshot name to create repoitory from (optional)
|
||||
// Snapshot name to create repository from (optional)
|
||||
FromSnapshot string ` json:"FromSnapshot" example:""`
|
||||
}
|
||||
|
||||
@@ -122,63 +136,79 @@ func apiReposCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
repo := deb.NewLocalRepo(b.Name, b.Comment)
|
||||
repo.DefaultComponent = b.DefaultComponent
|
||||
repo.DefaultDistribution = b.DefaultDistribution
|
||||
|
||||
// Handler: Pre-task validations (shallow)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
var resources []string
|
||||
if b.FromSnapshot != "" {
|
||||
var snapshot *deb.Snapshot
|
||||
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
snapshot, err := snapshotCollection.ByName(b.FromSnapshot)
|
||||
snapshot, err := collectionFactory.SnapshotCollection().ByName(b.FromSnapshot)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("source snapshot not found: %s", err))
|
||||
return
|
||||
}
|
||||
resources = append(resources, string(snapshot.Key()))
|
||||
}
|
||||
|
||||
err = snapshotCollection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to load source snapshot: %s", err))
|
||||
return
|
||||
taskName := fmt.Sprintf("Create repository %s", b.Name)
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
// Task: Create fresh collection and check/create ATOMIC inside task
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
|
||||
// Check duplicate inside lock
|
||||
if _, err := taskCollection.ByName(b.Name); err == nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil},
|
||||
fmt.Errorf("local repo with name %s already exists", b.Name)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(snapshot.RefList())
|
||||
}
|
||||
// Create repo
|
||||
repo := deb.NewLocalRepo(b.Name, b.Comment)
|
||||
repo.DefaultComponent = b.DefaultComponent
|
||||
repo.DefaultDistribution = b.DefaultDistribution
|
||||
|
||||
localRepoCollection := collectionFactory.LocalRepoCollection()
|
||||
if b.FromSnapshot != "" {
|
||||
snapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
|
||||
if _, err := localRepoCollection.ByName(b.Name); err == nil {
|
||||
AbortWithJSONError(c, http.StatusConflict, fmt.Errorf("local repo with name %s already exists", b.Name))
|
||||
return
|
||||
}
|
||||
snapshot, err := snapshotCollection.ByName(b.FromSnapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil},
|
||||
fmt.Errorf("source snapshot not found: %s", err)
|
||||
}
|
||||
|
||||
err := localRepoCollection.Add(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
err = snapshotCollection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil},
|
||||
fmt.Errorf("unable to load source snapshot: %s", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, repo)
|
||||
repo.UpdateRefList(snapshot.RefList())
|
||||
}
|
||||
|
||||
err := taskCollection.Add(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusCreated, Value: repo}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type reposEditParams struct {
|
||||
// Name of repository to modify
|
||||
Name *string `binding:"required" json:"Name" example:"repo1"`
|
||||
Name *string ` json:"Name" example:"new-repo-name"`
|
||||
// Change Comment of repository
|
||||
Comment *string ` json:"Comment" example:"example repo"`
|
||||
// Change Default Distribution for publishing
|
||||
DefaultDistribution *string ` json:"DefaultDistribution" example:""`
|
||||
// Change Devault Component for publishing
|
||||
// Change Default Component for publishing
|
||||
DefaultComponent *string ` json:"DefaultComponent" example:""`
|
||||
}
|
||||
|
||||
// @Summary Update Repository
|
||||
// @Description **Update local repository meta information**
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param name path string true "Repository name to modify"
|
||||
// @Consume json
|
||||
// @Param request body reposEditParams true "Parameters"
|
||||
// @Produce json
|
||||
@@ -191,42 +221,66 @@ func apiReposEdit(c *gin.Context) {
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Load shallowly for 404 check and resource key.
|
||||
// Mutation and duplicate check happen inside the task for atomicity.
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
if b.Name != nil {
|
||||
_, err := collection.ByName(*b.Name)
|
||||
if err == nil {
|
||||
// already exists
|
||||
AbortWithJSONError(c, 404, err)
|
||||
if b.Name != nil && *b.Name != name {
|
||||
if _, err = collection.ByName(*b.Name); err == nil {
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to rename: local repo %q already exists", *b.Name))
|
||||
return
|
||||
}
|
||||
repo.Name = *b.Name
|
||||
}
|
||||
if b.Comment != nil {
|
||||
repo.Comment = *b.Comment
|
||||
}
|
||||
if b.DefaultDistribution != nil {
|
||||
repo.DefaultDistribution = *b.DefaultDistribution
|
||||
}
|
||||
if b.DefaultComponent != nil {
|
||||
repo.DefaultComponent = *b.DefaultComponent
|
||||
}
|
||||
|
||||
err = collection.Update(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Edit repository %s", name)
|
||||
|
||||
c.JSON(200, repo)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
// Task: Create fresh collection inside task after lock
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
|
||||
// Fresh load after lock acquired
|
||||
repo, err := taskCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, err
|
||||
}
|
||||
|
||||
// Check and update ATOMIC (inside lock)
|
||||
if b.Name != nil && *b.Name != name {
|
||||
_, err := taskCollection.ByName(*b.Name)
|
||||
if err == nil {
|
||||
// already exists
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil},
|
||||
fmt.Errorf("local repo with name %q already exists", *b.Name)
|
||||
}
|
||||
repo.Name = *b.Name
|
||||
}
|
||||
if b.Comment != nil {
|
||||
repo.Comment = *b.Comment
|
||||
}
|
||||
if b.DefaultDistribution != nil {
|
||||
repo.DefaultDistribution = *b.DefaultDistribution
|
||||
}
|
||||
if b.DefaultComponent != nil {
|
||||
repo.DefaultComponent = *b.DefaultComponent
|
||||
}
|
||||
|
||||
err = taskCollection.Update(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: repo}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/repos/:name
|
||||
@@ -268,10 +322,10 @@ func apiReposDrop(c *gin.Context) {
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
// Load shallowly for 404 check, resource key, and task name.
|
||||
// Full checks (published/snapshots) happen inside the task.
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
publishedCollection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
@@ -282,19 +336,32 @@ func apiReposDrop(c *gin.Context) {
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Delete repo %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
published := publishedCollection.ByLocalRepo(repo)
|
||||
// Task: Create fresh collections inside task after lock acquired
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
taskPublishedCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Re-read repo with fresh collection after lock
|
||||
repo, err := taskCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
// Check with fresh collections
|
||||
published := taskPublishedCollection.ByLocalRepo(repo)
|
||||
if len(published) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop, local repo is published")
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.ByLocalRepoSource(repo)
|
||||
snapshots := taskSnapshotCollection.ByLocalRepoSource(repo)
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop, local repo has snapshots, use ?force=1 to override")
|
||||
}
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, collection.Drop(repo)
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, taskCollection.Drop(repo)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -351,10 +418,13 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
return
|
||||
}
|
||||
|
||||
// Load shallowly for 404 check and resource key.
|
||||
// Full load and mutations happen inside the task.
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
@@ -363,13 +433,23 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
resources := []string{string(repo.Key())}
|
||||
|
||||
maybeRunTaskInBackground(c, taskNamePrefix+repo.Name, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(repo)
|
||||
// Task: Create fresh factory and collection inside task after lock
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
|
||||
// Fresh load after lock acquired (use captured `name` variable, not gin context)
|
||||
repo, err := taskCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, err
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
out.Printf("Loading packages...\n")
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), nil)
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), taskCollectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -378,7 +458,7 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
for _, ref := range b.PackageRefs {
|
||||
var p *deb.Package
|
||||
|
||||
p, err = collectionFactory.PackageCollection().ByKey([]byte(ref))
|
||||
p, err = taskCollectionFactory.PackageCollection().ByKey([]byte(ref))
|
||||
if err != nil {
|
||||
if err == database.ErrNotFound {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("packages %s: %s", ref, err)
|
||||
@@ -394,7 +474,7 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
err = taskCollection.Update(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -410,6 +490,7 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
// @Description API verifies that packages actually exist in aptly database and checks constraint that conflicting packages can’t be part of the same local repository.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
@@ -455,7 +536,7 @@ func apiReposPackagesDelete(c *gin.Context) {
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param file path string false "Filename (optional)"
|
||||
// @Param file path string true "Filename"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "OK"
|
||||
@@ -500,6 +581,8 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Load shallowly for 404 check and resource key.
|
||||
// Full load and mutations happen inside the task.
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
@@ -523,7 +606,17 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
resources := []string{string(repo.Key())}
|
||||
resources = append(resources, sources...)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(repo)
|
||||
// Task: Create fresh factory and collection inside task after lock
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
|
||||
// Fresh load after lock acquired
|
||||
repo, err := taskCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
err = taskCollection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -544,13 +637,13 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
|
||||
packageFiles, otherFiles, failedFiles = deb.CollectPackageFiles(sources, reporter)
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), nil)
|
||||
list, err = deb.NewPackageListFromRefList(repo.RefList(), taskCollectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
|
||||
processedFiles, failedFiles2, err = deb.ImportPackageFiles(list, packageFiles, forceReplace, verifier, context.PackagePool(),
|
||||
collectionFactory.PackageCollection(), reporter, nil, collectionFactory.ChecksumCollection)
|
||||
taskCollectionFactory.PackageCollection(), reporter, nil, taskCollectionFactory.ChecksumCollection)
|
||||
failedFiles = append(failedFiles, failedFiles2...)
|
||||
processedFiles = append(processedFiles, otherFiles...)
|
||||
|
||||
@@ -560,7 +653,7 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
err = taskCollection.Update(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -613,11 +706,11 @@ type reposCopyPackageParams struct {
|
||||
// @Summary Copy Package
|
||||
// @Description Copies a package from a source to destination repository
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Destination repo"
|
||||
// @Param src path string true "Source repo"
|
||||
// @Param file path string true "File/packages to copy"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
@@ -639,6 +732,8 @@ func apiReposCopyPackage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Load shallowly for 404 check and resource keys.
|
||||
// Full load and mutations happen inside the task.
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
dstRepo, err := collectionFactory.LocalRepoCollection().ByName(dstRepoName)
|
||||
if err != nil {
|
||||
@@ -662,12 +757,26 @@ func apiReposCopyPackage(c *gin.Context) {
|
||||
resources := []string{string(dstRepo.Key()), string(srcRepo.Key())}
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(dstRepo)
|
||||
// Task: Create fresh factory and collections inside task after lock
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// Fresh load of both repos after lock acquired
|
||||
dstRepo, err := taskCollectionFactory.LocalRepoCollection().ByName(dstRepoName)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("dest repo error: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(srcRepo)
|
||||
srcRepo, err := taskCollectionFactory.LocalRepoCollection().ByName(srcRepoName)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("src repo error: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollectionFactory.LocalRepoCollection().LoadComplete(dstRepo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("dest repo error: %s", err)
|
||||
}
|
||||
|
||||
err = taskCollectionFactory.LocalRepoCollection().LoadComplete(srcRepo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("src repo error: %s", err)
|
||||
}
|
||||
@@ -680,12 +789,12 @@ func apiReposCopyPackage(c *gin.Context) {
|
||||
RemovedLines: []string{},
|
||||
}
|
||||
|
||||
dstList, err := deb.NewPackageListFromRefList(dstRepo.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
dstList, err := deb.NewPackageListFromRefList(dstRepo.RefList(), taskCollectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages in dest: %s", err)
|
||||
}
|
||||
|
||||
srcList, err := deb.NewPackageListFromRefList(srcRefList, collectionFactory.PackageCollection(), context.Progress())
|
||||
srcList, err := deb.NewPackageListFromRefList(srcRefList, taskCollectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages in src: %s", err)
|
||||
}
|
||||
@@ -753,7 +862,7 @@ func apiReposCopyPackage(c *gin.Context) {
|
||||
} else {
|
||||
dstRepo.UpdateRefList(deb.NewPackageRefListFromPackageList(dstList))
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().Update(dstRepo)
|
||||
err = taskCollectionFactory.LocalRepoCollection().Update(dstRepo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
@@ -856,6 +965,9 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
resources = append(resources, sources...)
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
// Task: Create fresh factory and collection inside task after lock
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
|
||||
var (
|
||||
err error
|
||||
verifier = context.GetVerifier()
|
||||
@@ -871,8 +983,8 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
changesFiles, failedFiles = deb.CollectChangesFiles(sources, reporter)
|
||||
_, failedFiles2, err = deb.ImportChangesFiles(
|
||||
changesFiles, reporter, acceptUnsigned, ignoreSignature, forceReplace, noRemoveFiles, verifier,
|
||||
repoTemplate, context.Progress(), collectionFactory.LocalRepoCollection(), collectionFactory.PackageCollection(),
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection, nil, query.Parse)
|
||||
repoTemplate, context.Progress(), taskCollectionFactory.LocalRepoCollection(), taskCollectionFactory.PackageCollection(),
|
||||
context.PackagePool(), taskCollectionFactory.ChecksumCollection, nil, query.Parse)
|
||||
failedFiles = append(failedFiles, failedFiles2...)
|
||||
|
||||
if err != nil {
|
||||
@@ -901,10 +1013,10 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
out.Printf("Failed files: %s\n", strings.Join(failedFiles, ", "))
|
||||
}
|
||||
|
||||
ret := reposIncludePackageFromDirResponse{
|
||||
ret := reposIncludePackageFromDirResponse{
|
||||
Report: reporter,
|
||||
FailedFiles: failedFiles,
|
||||
}
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: ret}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ReposSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ReposSuite{})
|
||||
|
||||
func (s *ReposSuite) TestGetReposIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().LocalRepoCollection()
|
||||
repo := deb.NewLocalRepo("count-repo-list", "")
|
||||
repo.UpdateRefList(makePackageRefList(c))
|
||||
c.Assert(collection.Add(repo), IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/repos", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var repos []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &repos)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, repo := range repos {
|
||||
if repo["Name"] == "count-repo-list" {
|
||||
found = true
|
||||
value, ok := repo["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *ReposSuite) TestGetReposReturns500OnCorruptRefList(c *C) {
|
||||
body, err := json.Marshal(gin.H{"Name": "broken-repo-list"})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 201)
|
||||
|
||||
collection := s.context.NewCollectionFactory().LocalRepoCollection()
|
||||
repo, err := collection.ByName("broken-repo-list")
|
||||
c.Assert(err, IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err = s.HTTPRequest("GET", "/api/repos", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
|
||||
}
|
||||
@@ -18,6 +18,12 @@ import (
|
||||
|
||||
var context *ctx.AptlyContext
|
||||
|
||||
// @Summary Get Metrics
|
||||
// @Description **Get Prometheus Metrics**
|
||||
// @Tags Status
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string Metrics
|
||||
// @Router /api/metrics [get]
|
||||
func apiMetricsGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
countPackagesByRepos()
|
||||
@@ -158,21 +164,27 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
api.GET("/mirrors/:name", apiMirrorsShow)
|
||||
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
|
||||
api.POST("/mirrors", apiMirrorsCreate)
|
||||
api.POST("/mirrors/:name", apiMirrorsEdit)
|
||||
api.PUT("/mirrors/:name", apiMirrorsUpdate)
|
||||
api.DELETE("/mirrors/:name", apiMirrorsDrop)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/gpg/keys", apiGPGListKeys)
|
||||
api.POST("/gpg/key", apiGPGAddKey)
|
||||
api.DELETE("/gpg/key", apiGPGDeleteKey)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/s3", apiS3List)
|
||||
api.GET("/gcs", apiGCSList)
|
||||
api.GET("/jfrog", apiJFrogList)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/files", apiFilesListDirs)
|
||||
api.POST("/files/:dir", apiFilesUpload)
|
||||
api.PUT("/files/:dir/:file", apiFilesUploadOne)
|
||||
api.GET("/files/:dir", apiFilesListFiles)
|
||||
api.DELETE("/files/:dir", apiFilesDeleteDir)
|
||||
api.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||
|
||||
@@ -14,7 +14,8 @@ import (
|
||||
// @Router /api/s3 [get]
|
||||
func apiS3List(c *gin.Context) {
|
||||
keys := []string{}
|
||||
for k := range context.Config().S3PublishRoots {
|
||||
s3Roots := context.Config().S3PublishRoots
|
||||
for k := range s3Roots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
c.JSON(200, keys)
|
||||
|
||||
+175
-68
@@ -20,7 +20,7 @@ import (
|
||||
// @Description Each snapshot is returned as in “show” API.
|
||||
// @Tags Snapshots
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.Snapshot
|
||||
// @Success 200 {array} snapshotResponse
|
||||
// @Router /api/snapshots [get]
|
||||
func apiSnapshotsList(c *gin.Context) {
|
||||
SortMethodString := c.Request.URL.Query().Get("sort")
|
||||
@@ -32,11 +32,20 @@ func apiSnapshotsList(c *gin.Context) {
|
||||
SortMethodString = "name"
|
||||
}
|
||||
|
||||
result := []*deb.Snapshot{}
|
||||
_ = collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
result = append(result, snapshot)
|
||||
result := []snapshotResponse{}
|
||||
err := collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
err := collection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newSnapshotResponse(snapshot))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -74,26 +83,33 @@ func apiSnapshotsCreateFromMirror(c *gin.Context) {
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
repo, err = collection.ByName(name)
|
||||
repo, err = collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
// including snapshot resource key
|
||||
resources := []string{string(repo.Key()), "S" + b.Name}
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Create snapshot of mirror %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := repo.CheckLock()
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskMirrorCollection := taskCollectionFactory.RemoteRepoCollection()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
|
||||
repo, err := taskMirrorCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
err = repo.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, err
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
err = taskMirrorCollection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -107,7 +123,7 @@ func apiSnapshotsCreateFromMirror(c *gin.Context) {
|
||||
snapshot.Description = b.Description
|
||||
}
|
||||
|
||||
err = snapshotCollection.Add(snapshot)
|
||||
err = taskSnapshotCollection.Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
@@ -156,6 +172,7 @@ func apiSnapshotsCreate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 1: Pre-task validation (shallow load for 404 checks only)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
var resources []string
|
||||
@@ -169,37 +186,62 @@ func apiSnapshotsCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, string(sources[i].ResourceKey()))
|
||||
resources = append(resources, string(sources[i].Key()))
|
||||
}
|
||||
|
||||
maybeRunTaskInBackground(c, "Create snapshot "+b.Name, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
for i := range sources {
|
||||
err = snapshotCollection.LoadComplete(sources[i])
|
||||
// Phase 2: Inside task lock - create fresh factory
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
taskPackageCollection := taskCollectionFactory.PackageCollection()
|
||||
|
||||
// Fresh load of all sources after lock acquired
|
||||
freshSources := make([]*deb.Snapshot, len(b.SourceSnapshots))
|
||||
for i := range b.SourceSnapshots {
|
||||
freshSources[i], err = taskSnapshotCollection.ByName(b.SourceSnapshots[i])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
// LoadComplete on fresh copy
|
||||
err = taskSnapshotCollection.LoadComplete(freshSources[i])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
}
|
||||
|
||||
list := deb.NewPackageList()
|
||||
// Merge packages from all source snapshots
|
||||
var refList *deb.PackageRefList
|
||||
if len(freshSources) > 0 {
|
||||
refList = freshSources[0].RefList()
|
||||
for i := 1; i < len(freshSources); i++ {
|
||||
refList = refList.Merge(freshSources[i].RefList(), true, false)
|
||||
}
|
||||
} else {
|
||||
refList = deb.NewPackageRefList()
|
||||
}
|
||||
|
||||
// verify package refs and build package list
|
||||
for _, ref := range b.PackageRefs {
|
||||
p, err := collectionFactory.PackageCollection().ByKey([]byte(ref))
|
||||
if err != nil {
|
||||
if err == database.ErrNotFound {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("package %s: %s", ref, err)
|
||||
// Add any explicitly specified package refs on top
|
||||
if len(b.PackageRefs) > 0 {
|
||||
list := deb.NewPackageList()
|
||||
for _, ref := range b.PackageRefs {
|
||||
p, err := taskPackageCollection.ByKey([]byte(ref))
|
||||
if err != nil {
|
||||
if err == database.ErrNotFound {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("package %s: %s", ref, err)
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = list.Add(p)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = list.Add(p)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
refList = refList.Merge(deb.NewPackageRefListFromPackageList(list), true, false)
|
||||
}
|
||||
|
||||
snapshot = deb.NewSnapshotFromRefList(b.Name, sources, deb.NewPackageRefListFromPackageList(list), b.Description)
|
||||
snapshot = deb.NewSnapshotFromRefList(b.Name, freshSources, refList, b.Description)
|
||||
|
||||
err = snapshotCollection.Add(snapshot)
|
||||
err = taskSnapshotCollection.Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
@@ -217,10 +259,9 @@ type snapshotsCreateFromRepositoryParams struct {
|
||||
// @Summary Snapshot Repository
|
||||
// @Description **Create a snapshot of a repository by name**
|
||||
// @Tags Snapshots
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body snapshotsCreateFromRepositoryParams true "Parameters"
|
||||
// @Param name path string true "Name of the snapshot"
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.Snapshot "Created snapshot object"
|
||||
@@ -241,21 +282,28 @@ func apiSnapshotsCreateFromRepository(c *gin.Context) {
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
repo, err = collection.ByName(name)
|
||||
repo, err = collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
// including snapshot resource key
|
||||
resources := []string{string(repo.Key()), "S" + b.Name}
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Create snapshot of repo %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := collection.LoadComplete(repo)
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskRepoCollection := taskCollectionFactory.LocalRepoCollection()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
|
||||
repo, err := taskRepoCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
err = taskRepoCollection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -269,7 +317,7 @@ func apiSnapshotsCreateFromRepository(c *gin.Context) {
|
||||
snapshot.Description = b.Description
|
||||
}
|
||||
|
||||
err = snapshotCollection.Add(snapshot)
|
||||
err = taskSnapshotCollection.Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
@@ -307,6 +355,7 @@ func apiSnapshotsUpdate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Phase 1: Pre-task validation (shallow load for 404 check only)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
name := c.Params.ByName("name")
|
||||
@@ -317,14 +366,38 @@ func apiSnapshotsUpdate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(snapshot.ResourceKey()), "S" + b.Name}
|
||||
taskName := fmt.Sprintf("Update snapshot %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
_, err := collection.ByName(b.Name)
|
||||
// Pre-task validation of new name if provided (skip if renaming to same name)
|
||||
if b.Name != "" && b.Name != name {
|
||||
_, err = collection.ByName(b.Name)
|
||||
if err == nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to rename: snapshot %s already exists", b.Name)
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to rename: snapshot %s already exists", b.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resources := []string{string(snapshot.Key())}
|
||||
taskName := fmt.Sprintf("Update snapshot %s", name)
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
// Phase 2: Inside task lock - create fresh factory
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskCollection := taskCollectionFactory.SnapshotCollection()
|
||||
|
||||
// Fresh load after lock acquired
|
||||
snapshot, err = taskCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
// Fresh duplicate check inside lock
|
||||
if b.Name != "" {
|
||||
_, err := taskCollection.ByName(b.Name)
|
||||
if err == nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to rename: snapshot %s already exists", b.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Update fresh copy
|
||||
if b.Name != "" {
|
||||
snapshot.Name = b.Name
|
||||
}
|
||||
@@ -333,7 +406,7 @@ func apiSnapshotsUpdate(c *gin.Context) {
|
||||
snapshot.Description = b.Description
|
||||
}
|
||||
|
||||
err = collectionFactory.SnapshotCollection().Update(snapshot)
|
||||
err = taskCollection.Update(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -387,9 +460,9 @@ func apiSnapshotsDrop(c *gin.Context) {
|
||||
name := c.Params.ByName("name")
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
|
||||
// Phase 1: Pre-task validation (shallow load for 404 check only)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
publishedCollection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
snapshot, err := snapshotCollection.ByName(name)
|
||||
if err != nil {
|
||||
@@ -397,23 +470,37 @@ func apiSnapshotsDrop(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(snapshot.ResourceKey())}
|
||||
resources := []string{string(snapshot.Key())}
|
||||
taskName := fmt.Sprintf("Delete snapshot %s", name)
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
published := publishedCollection.BySnapshot(snapshot)
|
||||
// Phase 2: Inside task lock - create fresh collections
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
taskPublishedCollection := taskCollectionFactory.PublishedRepoCollection()
|
||||
|
||||
// Fresh load after lock acquired
|
||||
snapshot, err := taskSnapshotCollection.ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
// Fresh checks with current collections
|
||||
published := taskPublishedCollection.BySnapshot(snapshot)
|
||||
|
||||
if len(published) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop: snapshot is published")
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.BySnapshotSource(snapshot)
|
||||
// Using fresh collection for dependency check
|
||||
snapshots := taskSnapshotCollection.BySnapshotSource(snapshot)
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("won't delete snapshot that was used as source for other snapshots, use ?force=1 to override")
|
||||
}
|
||||
}
|
||||
|
||||
err = snapshotCollection.Drop(snapshot)
|
||||
err = taskSnapshotCollection.Drop(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -568,6 +655,7 @@ func apiSnapshotsMerge(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Phase 1: Pre-task validation (shallow load for 404 checks only)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
@@ -580,36 +668,47 @@ func apiSnapshotsMerge(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources[i] = string(sources[i].ResourceKey())
|
||||
resources[i] = string(sources[i].Key())
|
||||
}
|
||||
|
||||
maybeRunTaskInBackground(c, "Merge snapshot "+name, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = snapshotCollection.LoadComplete(sources[0])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
result := sources[0].RefList()
|
||||
for i := 1; i < len(sources); i++ {
|
||||
err = snapshotCollection.LoadComplete(sources[i])
|
||||
// Phase 2: Inside task lock - create fresh factory
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
taskSnapshotCollection := taskCollectionFactory.SnapshotCollection()
|
||||
|
||||
// Fresh load of all sources inside task
|
||||
freshSources := make([]*deb.Snapshot, len(body.Sources))
|
||||
for i := range body.Sources {
|
||||
freshSources[i], err = taskSnapshotCollection.ByName(body.Sources[i])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
result = result.Merge(sources[i].RefList(), overrideMatching, false)
|
||||
// LoadComplete on fresh copy
|
||||
err = taskSnapshotCollection.LoadComplete(freshSources[i])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Merge using fresh sources
|
||||
result := freshSources[0].RefList()
|
||||
for i := 1; i < len(freshSources); i++ {
|
||||
result = result.Merge(freshSources[i].RefList(), overrideMatching, false)
|
||||
}
|
||||
|
||||
if latest {
|
||||
result.FilterLatestRefs()
|
||||
}
|
||||
|
||||
sourceDescription := make([]string, len(sources))
|
||||
for i, s := range sources {
|
||||
sourceDescription := make([]string, len(freshSources))
|
||||
for i, s := range freshSources {
|
||||
sourceDescription[i] = fmt.Sprintf("'%s'", s.Name)
|
||||
}
|
||||
|
||||
snapshot = deb.NewSnapshotFromRefList(name, sources, result,
|
||||
snapshot = deb.NewSnapshotFromRefList(name, freshSources, result,
|
||||
fmt.Sprintf("Merged from sources: %s", strings.Join(sourceDescription, ", ")))
|
||||
|
||||
err = collectionFactory.SnapshotCollection().Add(snapshot)
|
||||
err = taskCollectionFactory.SnapshotCollection().Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
@@ -690,24 +789,32 @@ func apiSnapshotsPull(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(sourceSnapshot.ResourceKey()), string(toSnapshot.ResourceKey())}
|
||||
resources := []string{string(sourceSnapshot.Key()), string(toSnapshot.Key())}
|
||||
taskName := fmt.Sprintf("Pull snapshot %s into %s and save as %s", body.Source, name, body.Destination)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(toSnapshot)
|
||||
// Phase 2: Inside task lock - create fresh factory
|
||||
taskCollectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// Fresh load of snapshots after lock acquired
|
||||
freshToSnapshot, err := taskCollectionFactory.SnapshotCollection().ByName(name)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(sourceSnapshot)
|
||||
freshSourceSnapshot, err := taskCollectionFactory.SnapshotCollection().ByName(body.Source)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = taskCollectionFactory.SnapshotCollection().LoadComplete(freshSourceSnapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
// convert snapshots to package list
|
||||
toPackageList, err := deb.NewPackageListFromRefList(toSnapshot.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
toPackageList, err := deb.NewPackageListFromRefList(freshToSnapshot.RefList(), taskCollectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
sourcePackageList, err := deb.NewPackageListFromRefList(sourceSnapshot.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
sourcePackageList, err := deb.NewPackageListFromRefList(freshSourceSnapshot.RefList(), taskCollectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
@@ -804,10 +911,10 @@ func apiSnapshotsPull(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create <destination> snapshot
|
||||
destinationSnapshot = deb.NewSnapshotFromPackageList(body.Destination, []*deb.Snapshot{toSnapshot, sourceSnapshot}, toPackageList,
|
||||
fmt.Sprintf("Pulled into '%s' with '%s' as source, pull request was: '%s'", toSnapshot.Name, sourceSnapshot.Name, strings.Join(body.Queries, ", ")))
|
||||
destinationSnapshot = deb.NewSnapshotFromPackageList(body.Destination, []*deb.Snapshot{freshToSnapshot, freshSourceSnapshot}, toPackageList,
|
||||
fmt.Sprintf("Pulled into '%s' with '%s' as source, pull request was: '%s'", freshToSnapshot.Name, freshSourceSnapshot.Name, strings.Join(body.Queries, ", ")))
|
||||
|
||||
err = collectionFactory.SnapshotCollection().Add(destinationSnapshot)
|
||||
err = taskCollectionFactory.SnapshotCollection().Add(destinationSnapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type SnapshotsSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&SnapshotsSuite{})
|
||||
|
||||
func (s *SnapshotsSuite) TestGetSnapshotsIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().SnapshotCollection()
|
||||
snapshot := deb.NewSnapshotFromRefList("count-snapshot-list", nil, makePackageRefList(c), "")
|
||||
c.Assert(collection.Add(snapshot), IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var snapshots []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &snapshots)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, snapshot := range snapshots {
|
||||
if snapshot["Name"] == "count-snapshot-list" {
|
||||
found = true
|
||||
value, ok := snapshot["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *SnapshotsSuite) TestGetSnapshotsReturns500OnCorruptRefList(c *C) {
|
||||
collection := s.context.NewCollectionFactory().SnapshotCollection()
|
||||
snapshot := deb.NewSnapshotFromRefList("broken-snapshot-list", nil, makePackageRefList(c), "")
|
||||
c.Assert(collection.Add(snapshot), IsNil)
|
||||
putRawDBValue(c, &s.APISuite, snapshot.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
|
||||
}
|
||||
+2
-2
@@ -95,8 +95,8 @@ type FileSystemPublishedStorage interface {
|
||||
|
||||
// PublishedStorageProvider is a thing that returns PublishedStorage by name
|
||||
type PublishedStorageProvider interface {
|
||||
// GetPublishedStorage returns PublishedStorage by name
|
||||
GetPublishedStorage(name string) PublishedStorage
|
||||
// GetPublishedStorage returns PublishedStorage by name, or an error if the storage is not configured
|
||||
GetPublishedStorage(name string) (PublishedStorage, error)
|
||||
}
|
||||
|
||||
// BarType used to differentiate between different progress bars
|
||||
|
||||
@@ -104,7 +104,7 @@ func (pool *PackagePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating tempfile for %s", path)
|
||||
}
|
||||
defer func () { _ = os.Remove(temp.Name()) }()
|
||||
defer func() { _ = os.Remove(temp.Name()) }()
|
||||
|
||||
_, err = pool.az.client.DownloadFile(context.TODO(), pool.az.container, path, temp, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
@@ -50,10 +50,10 @@ func (s *PackagePoolSuite) SetUpTest(c *C) {
|
||||
|
||||
s.pool, err = NewPackagePool(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.pool.az.client.CreateContainer(context.TODO(), s.pool.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.pool.az.client.CreateContainer(context.TODO(), s.pool.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedPool, err = NewPackagePool(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
|
||||
+22
-22
@@ -1,17 +1,17 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"bytes"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -69,10 +69,10 @@ func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
|
||||
s.storage, err = NewPublishedStorage(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.storage.az.client.CreateContainer(context.Background(), s.storage.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.storage.az.client.CreateContainer(context.Background(), s.storage.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedStorage, err = NewPublishedStorage(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
@@ -80,12 +80,12 @@ func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TearDownTest(c *C) {
|
||||
_, err := s.storage.az.client.DeleteContainer(context.Background(), s.storage.az.container, nil)
|
||||
_, err := s.storage.az.client.DeleteContainer(context.Background(), s.storage.az.container, nil)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
resp, err := s.storage.az.client.DownloadStream(context.Background(), s.storage.az.container, path, nil)
|
||||
resp, err := s.storage.az.client.DownloadStream(context.Background(), s.storage.az.container, path, nil)
|
||||
c.Assert(err, IsNil)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
c.Assert(err, IsNil)
|
||||
@@ -93,26 +93,26 @@ func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) AssertNoFile(c *C, path string) {
|
||||
serviceClient := s.storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(s.storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
serviceClient := s.storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(s.storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
c.Assert(err, NotNil)
|
||||
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(storageError.StatusCode, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) PutFile(c *C, path string, data []byte) {
|
||||
hash := md5.Sum(data)
|
||||
uploadOptions := &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobContentMD5: hash[:],
|
||||
},
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
_, err := s.storage.az.client.UploadStream(context.Background(), s.storage.az.container, path, reader, uploadOptions)
|
||||
uploadOptions := &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobContentMD5: hash[:],
|
||||
},
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
_, err := s.storage.az.client.UploadStream(context.Background(), s.storage.az.container, path, reader, uploadOptions)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
// collect information about references packages...
|
||||
existingPackageRefs := deb.NewPackageRefList()
|
||||
referencedAppStreamFiles := []string{}
|
||||
|
||||
// used only in verbose mode to report package use source
|
||||
packageRefSources := map[string][]string{}
|
||||
@@ -55,6 +56,10 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, poolPath := range repo.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -118,6 +123,11 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for _, poolPath := range snapshot.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -236,6 +246,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
referencedFiles = append(referencedFiles, referencedAppStreamFiles...)
|
||||
sort.Strings(referencedFiles)
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
downloadSources := LookupOption(context.Config().DownloadSourcePackages, context.Flags(), "with-sources")
|
||||
downloadUdebs := context.Flags().Lookup("with-udebs").Value.Get().(bool)
|
||||
downloadInstaller := context.Flags().Lookup("with-installer").Value.Get().(bool)
|
||||
downloadAppStream := context.Flags().Lookup("with-appstream").Value.Get().(bool)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
@@ -41,7 +42,7 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(mirrorName, archiveURL, distribution, components, context.ArchitecturesList(),
|
||||
downloadSources, downloadUdebs, downloadInstaller)
|
||||
downloadSources, downloadUdebs, downloadInstaller, downloadAppStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create mirror: %s", err)
|
||||
}
|
||||
@@ -100,6 +101,7 @@ Example:
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-appstream", false, "download AppStream (DEP-11) metadata")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
|
||||
@@ -35,6 +35,8 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
repo.Filter = flag.Value.String() // allows file/stdin with @
|
||||
case "filter-with-deps":
|
||||
repo.FilterWithDeps = flag.Value.Get().(bool)
|
||||
case "with-appstream":
|
||||
repo.DownloadAppStream = flag.Value.Get().(bool)
|
||||
case "with-installer":
|
||||
repo.DownloadInstaller = flag.Value.Get().(bool)
|
||||
case "with-sources":
|
||||
@@ -53,6 +55,10 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to edit: flat mirrors don't support udebs")
|
||||
}
|
||||
|
||||
if repo.IsFlat() && repo.DownloadAppStream {
|
||||
return fmt.Errorf("unable to edit: flat mirrors don't support AppStream (DEP-11) metadata")
|
||||
}
|
||||
|
||||
if repo.Filter != "" {
|
||||
_, err = query.Parse(repo.Filter)
|
||||
if err != nil {
|
||||
@@ -107,6 +113,7 @@ Example:
|
||||
AddStringOrFileFlag(&cmd.Flag, "filter", "", "filter packages in mirror, use '@file' to read filter from file or '@-' for stdin")
|
||||
cmd.Flag.Bool("filter-with-deps", false, "when filtering, include dependencies of matching packages as well")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-appstream", false, "download AppStream (DEP-11) metadata")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
|
||||
@@ -61,6 +61,11 @@ func aptlyMirrorShowTxt(_ *commander.Command, args []string) error {
|
||||
downloadUdebs = Yes
|
||||
}
|
||||
fmt.Printf("Download .udebs: %s\n", downloadUdebs)
|
||||
downloadAppStream := No
|
||||
if repo.DownloadAppStream {
|
||||
downloadAppStream = Yes
|
||||
}
|
||||
fmt.Printf("Download AppStream: %s\n", downloadAppStream)
|
||||
if repo.Filter != "" {
|
||||
fmt.Printf("Filter: %s\n", repo.Filter)
|
||||
filterWithDeps := No
|
||||
|
||||
+12
-1
@@ -64,6 +64,15 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if repo.DownloadAppStream && !repo.IsFlat() {
|
||||
context.Progress().Printf("Downloading AppStream metadata...\n")
|
||||
err = repo.DownloadAppStreamFiles(context.Progress(), context.Downloader(),
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection(nil), ignoreChecksums)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if repo.Filter != "" {
|
||||
context.Progress().Printf("Applying filter...\n")
|
||||
var filterQuery deb.PackageQuery
|
||||
@@ -87,10 +96,11 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
skipExistingPackages := context.Flags().Lookup("skip-existing-packages").Value.Get().(bool)
|
||||
latestOnly := context.Flags().Lookup("latest").Value.Get().(bool)
|
||||
|
||||
context.Progress().Printf("Building download queue...\n")
|
||||
queue, downloadSize, err = repo.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), skipExistingPackages)
|
||||
collectionFactory.ChecksumCollection(nil), skipExistingPackages, latestOnly)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
@@ -292,6 +302,7 @@ Example:
|
||||
cmd.Flag.Bool("ignore-checksums", false, "ignore checksum mismatches while downloading package files and metadata")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("skip-existing-packages", false, "do not check file existence for packages listed in the internal database of the mirror")
|
||||
cmd.Flag.Bool("latest", false, "download only latest version of each package (per architecture)")
|
||||
cmd.Flag.Int64("download-limit", 0, "limit download speed (kbytes/sec)")
|
||||
cmd.Flag.String("downloader", "default", "downloader to use (e.g. grab)")
|
||||
cmd.Flag.Int("max-tries", 1, "max download tries till process fails with download error")
|
||||
|
||||
+33
-1
@@ -1,6 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
@@ -12,7 +14,20 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
}
|
||||
|
||||
signer := context.GetSigner()
|
||||
signer.SetKey(flags.Lookup("gpg-key").Value.String())
|
||||
|
||||
var gpgKeys []string
|
||||
|
||||
// CLI args have priority over config
|
||||
cliKeys := flags.Lookup("gpg-key").Value.Get().([]string)
|
||||
if len(cliKeys) > 0 {
|
||||
gpgKeys = cliKeys
|
||||
} else if len(context.Config().GpgKeys) > 0 {
|
||||
gpgKeys = context.Config().GpgKeys
|
||||
}
|
||||
|
||||
for _, gpgKey := range gpgKeys {
|
||||
signer.SetKey(gpgKey)
|
||||
}
|
||||
signer.SetKeyRing(flags.Lookup("keyring").Value.String(), flags.Lookup("secret-keyring").Value.String())
|
||||
signer.SetPassphrase(flags.Lookup("passphrase").Value.String(), flags.Lookup("passphrase-file").Value.String())
|
||||
signer.SetBatch(flags.Lookup("batch").Value.Get().(bool))
|
||||
@@ -26,6 +41,23 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
|
||||
}
|
||||
|
||||
type gpgKeyFlag struct {
|
||||
gpgKeys []string
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) Set(value string) error {
|
||||
k.gpgKeys = append(k.gpgKeys, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) Get() interface{} {
|
||||
return k.gpgKeys
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) String() string {
|
||||
return strings.Join(k.gpgKeys, ",")
|
||||
}
|
||||
|
||||
func makeCmdPublish() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "publish",
|
||||
|
||||
+3
-1
@@ -34,7 +34,7 @@ Example:
|
||||
}
|
||||
cmd.Flag.String("distribution", "", "distribution name to publish")
|
||||
cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -51,7 +51,9 @@ Example:
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+16
-4
@@ -150,10 +150,18 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
published.AcquireByHash = context.Flags().Lookup("acquire-by-hash").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
duplicate := collectionFactory.PublishedRepoCollection().CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
@@ -190,9 +198,11 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
|
||||
context.Progress().Printf("\n%s been successfully published.\n", message)
|
||||
|
||||
if localStorage, ok := context.GetPublishedStorage(storage).(aptly.FileSystemPublishedStorage); ok {
|
||||
context.Progress().Printf("Please setup your webserver to serve directory '%s' with autoindexing.\n",
|
||||
localStorage.PublicPath())
|
||||
if ps, err := context.GetPublishedStorage(storage); err == nil {
|
||||
if localStorage, ok := ps.(aptly.FileSystemPublishedStorage); ok {
|
||||
context.Progress().Printf("Please setup your webserver to serve directory '%s' with autoindexing.\n",
|
||||
localStorage.PublicPath())
|
||||
}
|
||||
}
|
||||
|
||||
context.Progress().Printf("Now you can add following line to apt sources:\n")
|
||||
@@ -230,7 +240,7 @@ Example:
|
||||
}
|
||||
cmd.Flag.String("distribution", "", "distribution name to publish")
|
||||
cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -247,6 +257,8 @@ Example:
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
return cmd
|
||||
|
||||
+11
-1
@@ -99,6 +99,14 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
@@ -151,7 +159,7 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-switch", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -162,6 +170,8 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("component", "", "component names to update (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
|
||||
+21
-1
@@ -60,10 +60,26 @@ func aptlyPublishUpdate(cmd *commander.Command, args []string) error {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("origin") {
|
||||
published.Origin = context.Flags().Lookup("origin").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("label") {
|
||||
published.Label = context.Flags().Lookup("label").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
@@ -115,7 +131,7 @@ Example:
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-update", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -125,8 +141,12 @@ Example:
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
cmd.Flag.String("origin", "", "overwrite origin name to publish")
|
||||
cmd.Flag.String("label", "", "overwrite label to publish")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+5
-1
@@ -97,7 +97,11 @@ func aptlyServe(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
publicPath := context.GetPublishedStorage("").(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
ps, err := context.GetPublishedStorage("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicPath := ps.(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
ShutdownContext()
|
||||
|
||||
fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen)
|
||||
|
||||
@@ -185,6 +185,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
$keyring \
|
||||
"-with-sources=[download source packages in addition to binary packages]:$bool" \
|
||||
"-with-udebs=[download .udeb packages (Debian installer support)]:$bool" \
|
||||
"-with-appstream=[download AppStream (DEP-11) metadata]:$bool" \
|
||||
"(-)2:new mirror name: " ":archive url:_urls" ":distribution:($dists)" "*:components:_values -s ' ' components $components"
|
||||
;;
|
||||
list)
|
||||
@@ -211,6 +212,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
$keyring \
|
||||
"-max-tries=[max download tries till process fails with download error]:number: " \
|
||||
"-skip-existing-packages=[do not check file existence for packages listed in the internal database of the mirror]:$bool" \
|
||||
"-latest=[download only latest version of each package (per architecture)]:$bool" \
|
||||
"(-)2:mirror name:$mirrors"
|
||||
;;
|
||||
rename)
|
||||
@@ -223,6 +225,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
"-filter-with-deps=[when filtering, include dependencies of matching packages as well]:$bool" \
|
||||
"-with-sources=[download source packages in addition to binary packages]:$bool" \
|
||||
"-with-udebs=[download .udeb packages (Debian installer support)]:$bool" \
|
||||
"-with-appstream=[download AppStream (DEP-11) metadata]:$bool" \
|
||||
"(-)2:mirror name:$mirrors"
|
||||
;;
|
||||
search)
|
||||
|
||||
+3
-3
@@ -203,7 +203,7 @@ _aptly()
|
||||
"create")
|
||||
if [[ $numargs -eq 0 ]]; then
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-filter= -filter-with-deps -force-components -ignore-signatures -keyring= -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
COMPREPLY=($(compgen -W "-filter= -filter-with-deps -force-components -ignore-signatures -keyring= -with-appstream -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -211,7 +211,7 @@ _aptly()
|
||||
"edit")
|
||||
if [[ $numargs -eq 0 ]]; then
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-archive-url= -filter= -filter-with-deps -ignore-signatures -keyring= -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
COMPREPLY=($(compgen -W "-archive-url= -filter= -filter-with-deps -ignore-signatures -keyring= -with-appstream -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
else
|
||||
COMPREPLY=($(compgen -W "$(__aptly_mirror_list)" -- ${cur}))
|
||||
fi
|
||||
@@ -263,7 +263,7 @@ _aptly()
|
||||
"update")
|
||||
if [[ $numargs -eq 0 ]]; then
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-force -download-limit= -downloader= -ignore-checksums -ignore-signatures -keyring= -skip-existing-packages" -- ${cur}))
|
||||
COMPREPLY=($(compgen -W "-force -download-limit= -downloader= -ignore-checksums -ignore-signatures -keyring= -skip-existing-packages -latest" -- ${cur}))
|
||||
else
|
||||
COMPREPLY=($(compgen -W "$(__aptly_mirror_list)" -- ${cur}))
|
||||
fi
|
||||
|
||||
@@ -11,7 +11,7 @@ func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type ProgressSuite struct {}
|
||||
type ProgressSuite struct{}
|
||||
|
||||
var _ = Suite(&ProgressSuite{})
|
||||
|
||||
|
||||
+45
-12
@@ -23,7 +23,9 @@ import (
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/gcs"
|
||||
"github.com/aptly-dev/aptly/http"
|
||||
"github.com/aptly-dev/aptly/jfrog"
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/aptly-dev/aptly/s3"
|
||||
"github.com/aptly-dev/aptly/swift"
|
||||
@@ -115,7 +117,12 @@ func (context *AptlyContext) config() *utils.ConfigStructure {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Config file not found, creating default config at %s\n\n", homeLocation)
|
||||
|
||||
_ = utils.SaveConfigRaw(homeLocation, aptly.AptlyConf)
|
||||
defaultConfig := aptly.AptlyConf
|
||||
if len(defaultConfig) == 0 {
|
||||
defaultConfig = []byte("root_dir: \"\"")
|
||||
}
|
||||
|
||||
_ = utils.SaveConfigRaw(homeLocation, defaultConfig)
|
||||
err = utils.LoadConfig(homeLocation, &utils.Config)
|
||||
if err != nil {
|
||||
Fatal(fmt.Errorf("error loading config file %s: %s", homeLocation, err))
|
||||
@@ -406,8 +413,8 @@ func (context *AptlyContext) PackagePool() aptly.PackagePool {
|
||||
return context.packagePool
|
||||
}
|
||||
|
||||
// GetPublishedStorage returns instance of PublishedStorage
|
||||
func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedStorage {
|
||||
// GetPublishedStorage returns instance of PublishedStorage, or an error if the storage is not configured
|
||||
func (context *AptlyContext) GetPublishedStorage(name string) (aptly.PublishedStorage, error) {
|
||||
context.Lock()
|
||||
defer context.Unlock()
|
||||
|
||||
@@ -418,14 +425,14 @@ func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedSto
|
||||
} else if strings.HasPrefix(name, "filesystem:") {
|
||||
params, ok := context.config().FileSystemPublishRoots[name[11:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published local storage %v not configured", name[11:]))
|
||||
return nil, fmt.Errorf("published local storage %v not configured", name[11:])
|
||||
}
|
||||
|
||||
publishedStorage = files.NewPublishedStorage(params.RootDir, params.LinkMethod, params.VerifyMethod)
|
||||
} else if strings.HasPrefix(name, "s3:") {
|
||||
params, ok := context.config().S3PublishRoots[name[3:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published S3 storage %v not configured", name[3:]))
|
||||
return nil, fmt.Errorf("published S3 storage %v not configured", name[3:])
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -435,39 +442,65 @@ func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedSto
|
||||
params.EncryptionMethod, params.PlusWorkaround, params.DisableMultiDel,
|
||||
params.ForceSigV2, params.ForceVirtualHostedStyle, params.Debug)
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(name, "gcs:") {
|
||||
params, ok := context.config().GCSPublishRoots[name[4:]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("published GCS storage %v not configured", name[4:])
|
||||
}
|
||||
|
||||
var err error
|
||||
publishedStorage, err = gcs.NewPublishedStorage(
|
||||
params.Bucket, params.Prefix, params.CredentialsFile, params.ServiceAccountJSON,
|
||||
params.Project, params.Endpoint, params.ACL, params.StorageClass, params.EncryptionKey,
|
||||
params.DisableMultiDel, params.Debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(name, "swift:") {
|
||||
params, ok := context.config().SwiftPublishRoots[name[6:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published Swift storage %v not configured", name[6:]))
|
||||
return nil, fmt.Errorf("published Swift storage %v not configured", name[6:])
|
||||
}
|
||||
|
||||
var err error
|
||||
publishedStorage, err = swift.NewPublishedStorage(params.UserName, params.Password,
|
||||
params.AuthURL, params.Tenant, params.TenantID, params.Domain, params.DomainID, params.TenantDomain, params.TenantDomainID, params.Container, params.Prefix)
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(name, "azure:") {
|
||||
params, ok := context.config().AzurePublishRoots[name[6:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published Azure storage %v not configured", name[6:]))
|
||||
return nil, fmt.Errorf("published Azure storage %v not configured", name[6:])
|
||||
}
|
||||
|
||||
var err error
|
||||
publishedStorage, err = azure.NewPublishedStorage(
|
||||
params.AccountName, params.AccountKey, params.Container, params.Prefix, params.Endpoint)
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.HasPrefix(name, "jfrog:") {
|
||||
params, ok := context.config().JFrogPublishRoots[name[6:]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("published JFrog storage %v not configured", name[6:])
|
||||
}
|
||||
|
||||
var err error
|
||||
publishedStorage, err = jfrog.NewPublishedStorage(
|
||||
name[6:], params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating jfrog manager: %w", err)
|
||||
}
|
||||
} else {
|
||||
Fatal(fmt.Errorf("unknown published storage format: %v", name))
|
||||
return nil, fmt.Errorf("unknown published storage format: %v", name)
|
||||
}
|
||||
context.publishedStorages[name] = publishedStorage
|
||||
}
|
||||
|
||||
return publishedStorage
|
||||
return publishedStorage, nil
|
||||
}
|
||||
|
||||
// UploadPath builds path to upload storage
|
||||
|
||||
+61
-7
@@ -2,10 +2,10 @@ package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -80,10 +80,64 @@ func (s *AptlyContextSuite) SetUpTest(c *C) {
|
||||
|
||||
func (s *AptlyContextSuite) TestGetPublishedStorageBadFS(c *C) {
|
||||
// https://github.com/aptly-dev/aptly/issues/711
|
||||
// This will fail on account of us not having a config, so the
|
||||
// storage never exists.
|
||||
c.Assert(func() { s.context.GetPublishedStorage("filesystem:fuji") },
|
||||
FatalErrorPanicMatches,
|
||||
&FatalError{ReturnCode: 1, Message: fmt.Sprintf("error loading config file %s/.aptly.conf: invalid yaml (EOF) or json (EOF)",
|
||||
os.Getenv("HOME"))})
|
||||
// https://github.com/aptly-dev/aptly/issues/1477
|
||||
// GetPublishedStorage must return an error (not panic) when the
|
||||
// requested storage is not configured.
|
||||
_, err := s.context.GetPublishedStorage("filesystem:fuji")
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *AptlyContextSuite) TestGetPublishedStorageJFrogConfigured(c *C) {
|
||||
prevConfig := utils.Config
|
||||
defer func() { utils.Config = prevConfig }()
|
||||
|
||||
s.context.configLoaded = true
|
||||
utils.Config.RootDir = c.MkDir()
|
||||
utils.Config.JFrogPublishRoots = map[string]utils.JFrogPublishRoot{
|
||||
"test": {
|
||||
Repository: "aptly-repo",
|
||||
URL: "https://example.jfrog.local/artifactory",
|
||||
AccessToken: "token",
|
||||
Prefix: "public",
|
||||
},
|
||||
}
|
||||
|
||||
storage, err := s.context.GetPublishedStorage("jfrog:test")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(storage, NotNil)
|
||||
c.Assert(fmt.Sprintf("%v", storage), Equals, "jfrog:aptly-repo:public")
|
||||
|
||||
// Ensure we get the cached object on repeated lookups.
|
||||
storageAgain, err := s.context.GetPublishedStorage("jfrog:test")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(storageAgain, Equals, storage)
|
||||
}
|
||||
|
||||
func (s *AptlyContextSuite) TestGetPublishedStorageJFrogMissing(c *C) {
|
||||
prevConfig := utils.Config
|
||||
defer func() { utils.Config = prevConfig }()
|
||||
|
||||
s.context.configLoaded = true
|
||||
utils.Config.JFrogPublishRoots = map[string]utils.JFrogPublishRoot{}
|
||||
|
||||
_, err := s.context.GetPublishedStorage("jfrog:missing")
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "published JFrog storage missing not configured")
|
||||
}
|
||||
|
||||
func (s *AptlyContextSuite) TestGetPublishedStorageJFrogInitError(c *C) {
|
||||
prevConfig := utils.Config
|
||||
defer func() { utils.Config = prevConfig }()
|
||||
|
||||
s.context.configLoaded = true
|
||||
utils.Config.JFrogPublishRoots = map[string]utils.JFrogPublishRoot{
|
||||
"broken": {
|
||||
Repository: "aptly-repo",
|
||||
URL: "ssh://example.local/artifactory",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := s.context.GetPublishedStorage("jfrog:broken")
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err.Error(), Matches, `error creating jfrog manager: .*`)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
type EtcDDBSuite struct {
|
||||
db database.Storage
|
||||
db database.Storage
|
||||
}
|
||||
|
||||
var _ = Suite(&EtcDDBSuite{})
|
||||
@@ -133,7 +133,7 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
v, err := s.db.Get(key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(v, DeepEquals, value)
|
||||
err = transaction.Delete(key)
|
||||
err = transaction.Delete(key)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = transaction.Get(key2)
|
||||
@@ -156,4 +156,3 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
_, err = transaction.Get(key)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -26,8 +26,11 @@ var (
|
||||
"Version",
|
||||
"Codename",
|
||||
"Date",
|
||||
"Valid-Until",
|
||||
"NotAutomatic",
|
||||
"ButAutomaticUpgrades",
|
||||
"Acquire-By-Hash",
|
||||
"Signed-By",
|
||||
"Architectures",
|
||||
"Architecture",
|
||||
"Components",
|
||||
@@ -288,8 +291,11 @@ func (c *ControlFileReader) ReadStanza() (Stanza, error) {
|
||||
lastField = canonicalCase(parts[0])
|
||||
lastFieldMultiline = isMultilineField(lastField, c.isRelease)
|
||||
if lastFieldMultiline {
|
||||
stanza[lastField] = parts[1]
|
||||
if parts[1] != "" {
|
||||
// Trim trailing whitespace from the inline value so that
|
||||
// "Package-List: " does not add empty line
|
||||
inlineVal := strings.TrimRight(parts[1], " \t")
|
||||
stanza[lastField] = inlineVal
|
||||
if inlineVal != "" {
|
||||
stanza[lastField] += "\n"
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -128,6 +128,35 @@ func (s *ControlFileSuite) TestReadWriteStanza(c *C) {
|
||||
c.Assert(strings.HasPrefix(str, "Package: "), Equals, true)
|
||||
}
|
||||
|
||||
// Sources may contain "Package-List: " with a trailing space.
|
||||
// That trailing space must not be preserved and re-emitted
|
||||
// as a spurious blank continuation line when the stanza is written back out.
|
||||
func (s *ControlFileSuite) TestPackageListTrailingSpace(c *C) {
|
||||
input := "Package-List: \n" +
|
||||
" bash deb shells required arch=any\n" +
|
||||
" bash-doc deb doc optional arch=all\n"
|
||||
|
||||
r := NewControlFileReader(bytes.NewBufferString(input), false, false)
|
||||
stanza, err := r.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Check(stanza["Package-List"], Equals,
|
||||
" bash deb shells required arch=any\n"+
|
||||
" bash-doc deb doc optional arch=all\n")
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w := bufio.NewWriter(buf)
|
||||
err = stanza.Copy().WriteTo(w, true, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(w.Flush(), IsNil)
|
||||
|
||||
written := buf.String()
|
||||
c.Assert(strings.Contains(written, "Package-List:\n \n"), Equals, false,
|
||||
Commentf("spurious blank continuation line found in written output:\n%s", written))
|
||||
c.Assert(strings.Contains(written, "Package-List:\n bash"), Equals, true,
|
||||
Commentf("expected Package-List entries not found in written output:\n%s", written))
|
||||
}
|
||||
|
||||
func (s *ControlFileSuite) TestReadWriteInstallerStanza(c *C) {
|
||||
s.reader = bytes.NewBufferString(installerFile)
|
||||
r := NewControlFileReader(s.reader, false, true)
|
||||
|
||||
+33
@@ -172,6 +172,39 @@ func (l *PackageList) ForEach(handler func(*Package) error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FilterLatest creates a copy of the package list containing only the
|
||||
// latest version for each package name/architecture pair.
|
||||
func (l *PackageList) FilterLatest() (*PackageList, error) {
|
||||
if l == nil {
|
||||
return nil, fmt.Errorf("package list is nil")
|
||||
}
|
||||
|
||||
filtered := make(map[string]*Package, l.Len())
|
||||
|
||||
err := l.ForEach(func(p *Package) error {
|
||||
key := p.Architecture + "|" + p.Name
|
||||
|
||||
if existing, found := filtered[key]; !found || CompareVersions(p.Version, existing.Version) > 0 {
|
||||
filtered[key] = p
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := NewPackageListWithDuplicates(l.duplicatesAllowed, len(filtered))
|
||||
|
||||
for _, pkg := range filtered {
|
||||
if err = result.Add(pkg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ForEachIndexed calls handler for each package in list in indexed order
|
||||
func (l *PackageList) ForEachIndexed(handler func(*Package) error) error {
|
||||
if !l.indexed {
|
||||
|
||||
@@ -503,3 +503,52 @@ func (s *PackageListSuite) TestArchitectures(c *C) {
|
||||
sort.Strings(archs)
|
||||
c.Check(archs, DeepEquals, []string{"amd64", "arm", "i386", "s390"})
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestFilterLatest(c *C) {
|
||||
list := NewPackageList()
|
||||
|
||||
older := packageStanza.Copy()
|
||||
older["Version"] = "1.0"
|
||||
olderPkg := NewPackageFromControlFile(older)
|
||||
_ = list.Add(olderPkg)
|
||||
|
||||
newer := packageStanza.Copy()
|
||||
newer["Version"] = "2.0"
|
||||
newerPkg := NewPackageFromControlFile(newer)
|
||||
_ = list.Add(newerPkg)
|
||||
|
||||
shared := packageStanza.Copy()
|
||||
shared["Architecture"] = ArchitectureAll
|
||||
shared["Version"] = "3.0"
|
||||
shared["Package"] = "shared"
|
||||
sharedPkg := NewPackageFromControlFile(shared)
|
||||
_ = list.Add(sharedPkg)
|
||||
|
||||
filtered, err := list.FilterLatest()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(filtered.Len(), Equals, 2)
|
||||
c.Check(filtered.Has(newerPkg), Equals, true)
|
||||
c.Check(filtered.Has(sharedPkg), Equals, true)
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestFilterLatestPreservesDuplicatesFlag(c *C) {
|
||||
list := NewPackageListWithDuplicates(true, 2)
|
||||
|
||||
_ = list.Add(NewPackageFromControlFile(packageStanza.Copy()))
|
||||
|
||||
another := packageStanza.Copy()
|
||||
another["Version"] = "7.41-1"
|
||||
_ = list.Add(NewPackageFromControlFile(another))
|
||||
|
||||
filtered, err := list.FilterLatest()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(filtered.duplicatesAllowed, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestFilterLatestNil(c *C) {
|
||||
var list *PackageList
|
||||
|
||||
filtered, err := list.FilterLatest()
|
||||
c.Assert(err, ErrorMatches, "package list is nil")
|
||||
c.Assert(filtered, IsNil)
|
||||
}
|
||||
|
||||
+2
-1
@@ -168,6 +168,8 @@ func (collection *LocalRepoCollection) Update(repo *LocalRepo) error {
|
||||
|
||||
// LoadComplete loads additional information for local repo
|
||||
func (collection *LocalRepoCollection) LoadComplete(repo *LocalRepo) error {
|
||||
repo.packageRefs = &PackageRefList{}
|
||||
|
||||
encoded, err := collection.db.Get(repo.RefKey())
|
||||
if err == database.ErrNotFound {
|
||||
return nil
|
||||
@@ -176,7 +178,6 @@ func (collection *LocalRepoCollection) LoadComplete(repo *LocalRepo) error {
|
||||
return err
|
||||
}
|
||||
|
||||
repo.packageRefs = &PackageRefList{}
|
||||
return repo.packageRefs.Decode(encoded)
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,18 @@ func (s *LocalRepoCollectionSuite) TestByUUID(c *C) {
|
||||
c.Assert(r.String(), Equals, repo.String())
|
||||
}
|
||||
|
||||
func (s *LocalRepoCollectionSuite) TestLoadCompleteNoRefKey(c *C) {
|
||||
repo := NewLocalRepo("local1", "Comment 1")
|
||||
c.Assert(s.collection.Update(repo), IsNil)
|
||||
|
||||
r, err := s.collection.ByName("local1")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Assert(s.collection.LoadComplete(r), IsNil)
|
||||
c.Assert(r.packageRefs, NotNil)
|
||||
c.Assert(r.NumPackages(), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *LocalRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||
repo := NewLocalRepo("local1", "Comment 1")
|
||||
c.Assert(s.collection.Update(repo), IsNil)
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ func (s *PackageSuite) TestNewUdebFromPara(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackageSuite) TestNewInstallerFromPara(c *C) {
|
||||
repo, _ := NewRemoteRepo("yandex", "http://example.com/debian", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://example.com/debian", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
downloader := http.NewFakeDownloader()
|
||||
downloader.ExpectResponse("http://example.com/debian/dists/squeeze/main/installer-i386/current/images/MANIFEST.udebs", "MANIFEST.udebs")
|
||||
downloader.ExpectResponse("http://example.com/debian/dists/squeeze/main/installer-i386/current/images/udeb.list", "udeb.list")
|
||||
|
||||
+6
-1
@@ -28,6 +28,11 @@ func ParsePPA(ppaURL string, config *utils.ConfigStructure) (url string, distrib
|
||||
}
|
||||
}
|
||||
|
||||
baseurl := config.PpaBaseURL
|
||||
if baseurl == "" {
|
||||
baseurl = "http://ppa.launchpad.net"
|
||||
}
|
||||
|
||||
codename := config.PpaCodename
|
||||
if codename == "" {
|
||||
codename, err = getCodename()
|
||||
@@ -39,7 +44,7 @@ func ParsePPA(ppaURL string, config *utils.ConfigStructure) (url string, distrib
|
||||
|
||||
distribution = codename
|
||||
components = []string{"main"}
|
||||
url = fmt.Sprintf("http://ppa.launchpad.net/%s/%s/%s", matches[1], matches[2], distributorID)
|
||||
url = fmt.Sprintf("%s/%s/%s/%s", baseurl, matches[1], matches[2], distributorID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+140
-6
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,6 +55,7 @@ type PublishedRepo struct {
|
||||
Label string
|
||||
Suite string
|
||||
Codename string
|
||||
Version string
|
||||
// Architectures is a list of all architectures published
|
||||
Architectures []string
|
||||
// SourceKind is "local"/"repo"
|
||||
@@ -81,6 +83,11 @@ type PublishedRepo struct {
|
||||
// Provide index files per hash also
|
||||
AcquireByHash bool
|
||||
|
||||
// An optional field containing a comma separated list
|
||||
// of OpenPGP key fingerprints to be used
|
||||
// for validating the next Release file
|
||||
SignedBy string
|
||||
|
||||
// Support multiple distributions
|
||||
MultiDist bool
|
||||
|
||||
@@ -520,6 +527,7 @@ func (p *PublishedRepo) MarshalJSON() ([]byte, error) {
|
||||
"Origin": p.Origin,
|
||||
"Suite": p.Suite,
|
||||
"Codename": p.Codename,
|
||||
"Version": p.Version,
|
||||
"NotAutomatic": p.NotAutomatic,
|
||||
"ButAutomaticUpgrades": p.ButAutomaticUpgrades,
|
||||
"Prefix": p.Prefix,
|
||||
@@ -529,6 +537,7 @@ func (p *PublishedRepo) MarshalJSON() ([]byte, error) {
|
||||
"Storage": p.Storage,
|
||||
"SkipContents": p.SkipContents,
|
||||
"AcquireByHash": p.AcquireByHash,
|
||||
"SignedBy": p.SignedBy,
|
||||
"MultiDist": p.MultiDist,
|
||||
})
|
||||
}
|
||||
@@ -603,6 +612,15 @@ func (p *PublishedRepo) Key() []byte {
|
||||
return []byte("U" + p.StoragePrefix() + ">>" + p.Distribution)
|
||||
}
|
||||
|
||||
// PrefixPoolLockKey returns the task-queue resource key that serialises all
|
||||
// publish operations sharing the same pool directory under storagePrefix.
|
||||
// It must be held whenever a non-MultiDist publish may read or clean the
|
||||
// shared pool, to prevent concurrent cleanup runs from deleting each other's
|
||||
// files. See docs/Resource-Locking.md for the full key-namespace table.
|
||||
func PrefixPoolLockKey(storagePrefix string) string {
|
||||
return "P" + storagePrefix
|
||||
}
|
||||
|
||||
// RefKey is a unique id for package reference list
|
||||
func (p *PublishedRepo) RefKey(component string) []byte {
|
||||
return []byte("E" + p.UUID + component)
|
||||
@@ -814,9 +832,12 @@ func (p *PublishedRepo) GetSkelFiles(skelDir string, component string) (map[stri
|
||||
// Publish publishes snapshot (repository) contents, links package files, generates Packages & Release files, signs them
|
||||
func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageProvider aptly.PublishedStorageProvider,
|
||||
collectionFactory *CollectionFactory, signer pgp.Signer, progress aptly.Progress, forceOverwrite bool, skelDir string) error {
|
||||
publishedStorage := publishedStorageProvider.GetPublishedStorage(p.Storage)
|
||||
publishedStorage, err := publishedStorageProvider.GetPublishedStorage(p.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := publishedStorage.MkDir(filepath.Join(p.Prefix, "pool"))
|
||||
err = publishedStorage.MkDir(filepath.Join(p.Prefix, "pool"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1046,6 +1067,38 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
}
|
||||
}
|
||||
|
||||
// Pass-through AppStream (DEP-11) files from snapshots
|
||||
for component, item := range p.sourceItems {
|
||||
if item.snapshot == nil || len(item.snapshot.AppStreamFiles) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
prefix := component + "/"
|
||||
for relPath, poolPath := range item.snapshot.AppStreamFiles {
|
||||
if !strings.HasPrefix(relPath, prefix) {
|
||||
continue
|
||||
}
|
||||
withinComponent := strings.TrimPrefix(relPath, prefix)
|
||||
|
||||
poolFile, err := packagePool.Open(poolPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open AppStream file from pool: %v", err)
|
||||
}
|
||||
|
||||
bufWriter, err := indexes.SkelIndex(component, withinComponent).BufWriter()
|
||||
if err != nil {
|
||||
_ = poolFile.Close()
|
||||
return fmt.Errorf("unable to generate AppStream index: %v", err)
|
||||
}
|
||||
|
||||
_, err = bufio.NewReader(poolFile).WriteTo(bufWriter)
|
||||
_ = poolFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write AppStream file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
udebs := []bool{false}
|
||||
if hadUdebs {
|
||||
udebs = append(udebs, true)
|
||||
@@ -1070,6 +1123,12 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
if p.AcquireByHash {
|
||||
release["Acquire-By-Hash"] = "yes"
|
||||
}
|
||||
if p.SignedBy != "" {
|
||||
release["Signed-By"] = p.SignedBy
|
||||
}
|
||||
if p.Version != "" {
|
||||
release["Version"] = p.Version
|
||||
}
|
||||
|
||||
var bufWriter *bufio.Writer
|
||||
bufWriter, err = indexes.ReleaseIndex(component, arch, udeb).BufWriter()
|
||||
@@ -1126,11 +1185,31 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
release["Label"] = p.GetLabel()
|
||||
release["Suite"] = p.GetSuite()
|
||||
release["Codename"] = p.GetCodename()
|
||||
release["Date"] = time.Now().UTC().Format("Mon, 2 Jan 2006 15:04:05 MST")
|
||||
datetimeformat := "Mon, 2 Jan 2006 15:04:05 MST"
|
||||
|
||||
publishDate := time.Now().UTC()
|
||||
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
||||
if sec, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
||||
publishDate = time.Unix(sec, 0).UTC()
|
||||
}
|
||||
}
|
||||
release["Date"] = publishDate.Format(datetimeformat)
|
||||
release["Architectures"] = strings.Join(utils.StrSlicesSubstract(p.Architectures, []string{ArchitectureSource}), " ")
|
||||
if p.AcquireByHash {
|
||||
release["Acquire-By-Hash"] = "yes"
|
||||
}
|
||||
if p.SignedBy != "" {
|
||||
// "If the field is present, a client should only accept future updates
|
||||
// to the repository that are signed with keys listed in the field.
|
||||
// The field should be ignored if the Valid-Until field
|
||||
// is not present or if it is expired."
|
||||
release["Signed-By"] = p.SignedBy
|
||||
// Let's use a century as a "forever" value.
|
||||
release["Valid-Until"] = publishDate.AddDate(100, 0, 0).Format(datetimeformat)
|
||||
}
|
||||
if p.Version != "" {
|
||||
release["Version"] = p.Version
|
||||
}
|
||||
release["Description"] = " Generated by aptly\n"
|
||||
release["MD5Sum"] = ""
|
||||
release["SHA1"] = ""
|
||||
@@ -1182,7 +1261,10 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
// It can remove prefix fully, and part of pool (for specific component)
|
||||
func (p *PublishedRepo) RemoveFiles(publishedStorageProvider aptly.PublishedStorageProvider, removePrefix bool,
|
||||
removePoolComponents []string, progress aptly.Progress) error {
|
||||
publishedStorage := publishedStorageProvider.GetPublishedStorage(p.Storage)
|
||||
publishedStorage, err := publishedStorageProvider.GetPublishedStorage(p.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// I. Easy: remove whole prefix (meta+packages)
|
||||
if removePrefix {
|
||||
@@ -1195,7 +1277,7 @@ func (p *PublishedRepo) RemoveFiles(publishedStorageProvider aptly.PublishedStor
|
||||
}
|
||||
|
||||
// II. Medium: remove metadata, it can't be shared as prefix/distribution as unique
|
||||
err := publishedStorage.RemoveDirs(filepath.Join(p.Prefix, "dists", p.Distribution), progress)
|
||||
err = publishedStorage.RemoveDirs(filepath.Join(p.Prefix, "dists", p.Distribution), progress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1522,6 +1604,55 @@ func (collection *PublishedRepoCollection) listReferencedFilesByComponent(prefix
|
||||
return referencedFiles, nil
|
||||
}
|
||||
|
||||
// CleanupAfterMultiDistToggle cleans up stale pool files left behind when the
|
||||
// MultiDist flag is toggled on a published repository.
|
||||
//
|
||||
// - false→true: Publish() wrote packages into pool/<distribution>/<component>/
|
||||
// but the old flat pool/<component>/ files were not removed because
|
||||
// CleanupPrefixComponentFiles only scans the new MultiDist tree.
|
||||
// A second pass with MultiDist=false cleans the legacy flat layout by
|
||||
// reusing the existing orphan-detection logic (the repo is now MultiDist=true
|
||||
// so it is excluded from the referenced-files scan, making its old pool
|
||||
// entries appear orphaned).
|
||||
//
|
||||
// - true→false: Publish() wrote packages into pool/<component>/ but the old
|
||||
// per-distribution pool/<distribution>/<component>/ directories were not
|
||||
// removed. The orphan-detection approach cannot be used here because the
|
||||
// repo's RefList still contains all packages (they just moved locations).
|
||||
// Instead we directly remove each pool/<distribution>/<component>/ directory.
|
||||
// This is safe because per-distribution pool dirs are exclusive to a single
|
||||
// prefix+distribution combination — no other published repo can share them.
|
||||
func (collection *PublishedRepoCollection) CleanupAfterMultiDistToggle(publishedStorageProvider aptly.PublishedStorageProvider,
|
||||
published *PublishedRepo, prevMultiDist bool, cleanComponents []string, collectionFactory *CollectionFactory, progress aptly.Progress) error {
|
||||
if prevMultiDist == published.MultiDist {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !prevMultiDist && published.MultiDist {
|
||||
// false→true: use orphan-detection via the existing cleanup, but with
|
||||
// MultiDist temporarily set to false so it scans the flat pool layout.
|
||||
legacy := *published
|
||||
legacy.MultiDist = false
|
||||
return collection.CleanupPrefixComponentFiles(publishedStorageProvider, &legacy, cleanComponents, collectionFactory, progress)
|
||||
}
|
||||
|
||||
// true→false: directly remove the per-distribution pool directories.
|
||||
publishedStorage, err := publishedStorageProvider.GetPublishedStorage(published.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, component := range cleanComponents {
|
||||
poolDir := filepath.Join(published.Prefix, "pool", published.Distribution, component)
|
||||
if err := publishedStorage.RemoveDirs(poolDir, progress); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Remove the distribution-level pool dir if it is now empty.
|
||||
distPoolDir := filepath.Join(published.Prefix, "pool", published.Distribution)
|
||||
_ = publishedStorage.RemoveDirs(distPoolDir, progress)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupPrefixComponentFiles removes all unreferenced files in published storage under prefix/component pair
|
||||
func (collection *PublishedRepoCollection) CleanupPrefixComponentFiles(publishedStorageProvider aptly.PublishedStorageProvider,
|
||||
published *PublishedRepo, cleanComponents []string, collectionFactory *CollectionFactory, progress aptly.Progress) error {
|
||||
@@ -1535,7 +1666,10 @@ func (collection *PublishedRepoCollection) CleanupPrefixComponentFiles(published
|
||||
distribution := published.Distribution
|
||||
|
||||
rootPath := filepath.Join(prefix, "dists", distribution)
|
||||
publishedStorage := publishedStorageProvider.GetPublishedStorage(published.Storage)
|
||||
publishedStorage, err := publishedStorageProvider.GetPublishedStorage(published.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(cleanComponents)
|
||||
publishedComponents := published.Components()
|
||||
|
||||
+128
-6
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/ugorji/go/codec"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -61,12 +62,12 @@ type FakeStorageProvider struct {
|
||||
storages map[string]aptly.PublishedStorage
|
||||
}
|
||||
|
||||
func (p *FakeStorageProvider) GetPublishedStorage(name string) aptly.PublishedStorage {
|
||||
func (p *FakeStorageProvider) GetPublishedStorage(name string) (aptly.PublishedStorage, error) {
|
||||
storage, ok := p.storages[name]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown storage: %#v", name))
|
||||
return nil, fmt.Errorf("unknown storage: %#v", name)
|
||||
}
|
||||
return storage
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
type PublishedRepoSuite struct {
|
||||
@@ -115,7 +116,7 @@ func (s *PublishedRepoSuite) SetUpTest(c *C) {
|
||||
|
||||
s.reflist = NewPackageRefListFromPackageList(s.list)
|
||||
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
repo.packageRefs = s.reflist
|
||||
_ = s.factory.RemoteRepoCollection().Add(repo)
|
||||
|
||||
@@ -425,6 +426,81 @@ func (s *PublishedRepoSuite) TestPublish(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishAppStream(c *C) {
|
||||
// Components + icons
|
||||
content1 := []byte("DEP-11 test content for Components-amd64.yml.gz")
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "Components-amd64.yml.gz")
|
||||
c.Assert(os.WriteFile(tmpFile1, content1, 0644), IsNil)
|
||||
|
||||
checksums1 := utils.ChecksumInfo{Size: int64(len(content1))}
|
||||
poolPath1, err := s.packagePool.Import(tmpFile1, "Components-amd64.yml.gz", &checksums1, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
content2 := []byte("DEP-11 icons tar data")
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "icons-48x48.tar.gz")
|
||||
c.Assert(os.WriteFile(tmpFile2, content2, 0644), IsNil)
|
||||
|
||||
checksums2 := utils.ChecksumInfo{Size: int64(len(content2))}
|
||||
poolPath2, err := s.packagePool.Import(tmpFile2, "icons-48x48.tar.gz", &checksums2, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Include contrib file that should be skipped
|
||||
contribContent := []byte("DEP-11 contrib content")
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "Components-contrib.yml.gz")
|
||||
c.Assert(os.WriteFile(tmpFile3, contribContent, 0644), IsNil)
|
||||
|
||||
checksums3 := utils.ChecksumInfo{Size: int64(len(contribContent))}
|
||||
poolPath3, err := s.packagePool.Import(tmpFile3, "Components-contrib.yml.gz", &checksums3, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.snapshot.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": poolPath1,
|
||||
"main/dep11/icons-48x48.tar.gz": poolPath2,
|
||||
"contrib/dep11/Components-amd64.yml.gz": poolPath3,
|
||||
}
|
||||
|
||||
err = s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Both main files should exist
|
||||
appstreamPath1 := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main/dep11/Components-amd64.yml.gz")
|
||||
c.Check(appstreamPath1, PathExists)
|
||||
actual1, err := os.ReadFile(appstreamPath1)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(actual1, DeepEquals, content1)
|
||||
|
||||
appstreamPath2 := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main/dep11/icons-48x48.tar.gz")
|
||||
c.Check(appstreamPath2, PathExists)
|
||||
actual2, err := os.ReadFile(appstreamPath2)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(actual2, DeepEquals, content2)
|
||||
|
||||
// Contrib file should not appear
|
||||
contribPath := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/contrib/dep11/Components-amd64.yml.gz")
|
||||
_, statErr := os.Stat(contribPath)
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
|
||||
// Release file should reference AppStream files
|
||||
rf, err := os.Open(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/Release"))
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = rf.Close() }()
|
||||
|
||||
cfr := NewControlFileReader(rf, true, false)
|
||||
st, err := cfr.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Check(st["MD5Sum"], Matches, "(?s).*main/dep11/Components-amd64\\.yml\\.gz.*")
|
||||
c.Check(st["SHA256"], Matches, "(?s).*main/dep11/Components-amd64\\.yml\\.gz.*")
|
||||
|
||||
// Pool open error
|
||||
s.snapshot.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "nonexistent/pool/path",
|
||||
}
|
||||
|
||||
err = s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
|
||||
c.Assert(err, ErrorMatches, "unable to open AppStream file from pool.*")
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishNoSigner(c *C) {
|
||||
err := s.repo.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -433,6 +509,47 @@ func (s *PublishedRepoSuite) TestPublishNoSigner(c *C) {
|
||||
c.Check(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main/binary-i386/Release"), PathExists)
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishSourceDateEpoch(c *C) {
|
||||
// Test with SOURCE_DATE_EPOCH set
|
||||
_ = os.Setenv("SOURCE_DATE_EPOCH", "1234567890")
|
||||
defer func() { _ = os.Unsetenv("SOURCE_DATE_EPOCH") }()
|
||||
|
||||
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rf, err := os.Open(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/Release"))
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = rf.Close() }()
|
||||
|
||||
cfr := NewControlFileReader(rf, true, false)
|
||||
st, err := cfr.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Expected date for Unix timestamp 1234567890: Fri, 13 Feb 2009 23:31:30 UTC
|
||||
c.Check(st["Date"], Equals, "Fri, 13 Feb 2009 23:31:30 UTC")
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishSourceDateEpochInvalid(c *C) {
|
||||
// Test with invalid SOURCE_DATE_EPOCH (should fallback to current time)
|
||||
_ = os.Setenv("SOURCE_DATE_EPOCH", "invalid")
|
||||
defer func() { _ = os.Unsetenv("SOURCE_DATE_EPOCH") }()
|
||||
|
||||
err := s.repo2.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rf, err := os.Open(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/maverick/Release"))
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = rf.Close() }()
|
||||
|
||||
cfr := NewControlFileReader(rf, true, false)
|
||||
st, err := cfr.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Should have a valid Date field (not empty, not the fixed date from SOURCE_DATE_EPOCH)
|
||||
c.Check(st["Date"], Not(Equals), "")
|
||||
c.Check(st["Date"], Not(Equals), "Fri, 13 Feb 2009 23:31:30 UTC")
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishLocalRepo(c *C) {
|
||||
err := s.repo2.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -756,7 +873,10 @@ func (s *PublishedRepoCollectionSuite) TestListReferencedFiles(c *C) {
|
||||
snap3 := NewSnapshotFromRefList("snap3", []*Snapshot{}, s.snap2.RefList(), "desc3")
|
||||
_ = s.snapshotCollection.Add(snap3)
|
||||
|
||||
// Ensure that adding a second publish point with matching files doesn't give duplicate results.
|
||||
// When a second publish point references the same package (snap3 is a clone of snap2,
|
||||
// both containing p3/lonely-strangers), listReferencedFilesByComponent deduplicates by
|
||||
// package ref so the file appears only once. StrSlicesSubstract handles a single entry
|
||||
// correctly, so no duplicate is needed for cleanup safety.
|
||||
repo3, err := NewPublishedRepo("", "", "anaconda-2", []string{}, []string{"main"}, []interface{}{snap3}, s.factory, false)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.collection.Add(repo3), IsNil)
|
||||
@@ -771,7 +891,9 @@ func (s *PublishedRepoCollectionSuite) TestListReferencedFiles(c *C) {
|
||||
"a/alien-arena/alien-arena-common_7.40-2_i386.deb",
|
||||
"a/alien-arena/mars-invaders_7.40-2_i386.deb",
|
||||
},
|
||||
"main": {"a/alien-arena/lonely-strangers_7.40-2_i386.deb"},
|
||||
"main": {
|
||||
"a/alien-arena/lonely-strangers_7.40-2_i386.deb",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ func (l *PackageRefList) Decode(input []byte) error {
|
||||
|
||||
// ForEach calls handler for each package ref in list
|
||||
func (l *PackageRefList) ForEach(handler func([]byte) error) error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
for _, p := range l.Refs {
|
||||
err = handler(p)
|
||||
|
||||
+12
-1
@@ -65,7 +65,7 @@ func (s *PackageRefListSuite) TestNewPackageListFromRefList(c *C) {
|
||||
list, err := NewPackageListFromRefList(reflist, coll, nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(list.Len(), Equals, 4)
|
||||
c.Check(list.Add(s.p4), ErrorMatches, "package already exists and is different: .*")
|
||||
c.Check(list.Add(s.p4), ErrorMatches, "package already exists and is different: .*")
|
||||
|
||||
list, err = NewPackageListFromRefList(nil, coll, nil)
|
||||
c.Assert(err, IsNil)
|
||||
@@ -130,6 +130,17 @@ func (s *PackageRefListSuite) TestPackageRefListForeach(c *C) {
|
||||
c.Check(err, Equals, e)
|
||||
}
|
||||
|
||||
func (s *PackageRefListSuite) TestForEachNilList(c *C) {
|
||||
var l *PackageRefList
|
||||
called := false
|
||||
err := l.ForEach(func([]byte) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(called, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PackageRefListSuite) TestHas(c *C) {
|
||||
_ = s.list.Add(s.p1)
|
||||
_ = s.list.Add(s.p3)
|
||||
|
||||
+102
-3
@@ -70,6 +70,10 @@ type RemoteRepo struct {
|
||||
DownloadUdebs bool
|
||||
// Should we download installer files?
|
||||
DownloadInstaller bool
|
||||
// Should we download AppStream (DEP-11) metadata?
|
||||
DownloadAppStream bool
|
||||
// AppStream files: relative path (e.g. "main/dep11/Components-amd64.yml.gz") → pool path
|
||||
AppStreamFiles map[string]string `codec:"AppStreamFiles" json:"-"`
|
||||
// Packages for json output
|
||||
Packages []string `codec:"-" json:",omitempty"`
|
||||
// "Snapshot" of current list of packages
|
||||
@@ -82,7 +86,7 @@ type RemoteRepo struct {
|
||||
|
||||
// NewRemoteRepo creates new instance of Debian remote repository with specified params
|
||||
func NewRemoteRepo(name string, archiveRoot string, distribution string, components []string,
|
||||
architectures []string, downloadSources bool, downloadUdebs bool, downloadInstaller bool) (*RemoteRepo, error) {
|
||||
architectures []string, downloadSources bool, downloadUdebs bool, downloadInstaller bool, downloadAppStream bool) (*RemoteRepo, error) {
|
||||
result := &RemoteRepo{
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
@@ -93,6 +97,7 @@ func NewRemoteRepo(name string, archiveRoot string, distribution string, compone
|
||||
DownloadSources: downloadSources,
|
||||
DownloadUdebs: downloadUdebs,
|
||||
DownloadInstaller: downloadInstaller,
|
||||
DownloadAppStream: downloadAppStream,
|
||||
}
|
||||
|
||||
err := result.prepare()
|
||||
@@ -111,6 +116,9 @@ func NewRemoteRepo(name string, archiveRoot string, distribution string, compone
|
||||
if result.DownloadUdebs {
|
||||
return nil, fmt.Errorf("debian-installer udebs aren't supported for flat repos")
|
||||
}
|
||||
if result.DownloadAppStream {
|
||||
return nil, fmt.Errorf("AppStream (DEP-11) metadata isn't supported for flat repos")
|
||||
}
|
||||
result.Components = nil
|
||||
}
|
||||
|
||||
@@ -147,6 +155,9 @@ func (repo *RemoteRepo) String() string {
|
||||
if repo.DownloadInstaller {
|
||||
srcFlag += " [installer]"
|
||||
}
|
||||
if repo.DownloadAppStream {
|
||||
srcFlag += " [appstream]"
|
||||
}
|
||||
distribution := repo.Distribution
|
||||
if distribution == "" {
|
||||
distribution = "./"
|
||||
@@ -264,6 +275,82 @@ func (repo *RemoteRepo) InstallerPath(component string, architecture string) str
|
||||
return fmt.Sprintf("%s/installer-%s/current/images/SHA256SUMS", component, architecture)
|
||||
}
|
||||
|
||||
// AppStreamPaths returns dep11 file paths from ReleaseFiles for a given component
|
||||
func (repo *RemoteRepo) AppStreamPaths(component string) []string {
|
||||
prefix := component + "/dep11/"
|
||||
var paths []string
|
||||
for path := range repo.ReleaseFiles {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
// DownloadAppStreamFiles downloads AppStream (DEP-11) metadata files and imports them into the pool
|
||||
func (repo *RemoteRepo) DownloadAppStreamFiles(progress aptly.Progress, d aptly.Downloader,
|
||||
packagePool aptly.PackagePool, checksumStorage aptly.ChecksumStorage, ignoreChecksums bool) error {
|
||||
|
||||
repo.AppStreamFiles = make(map[string]string)
|
||||
|
||||
for _, component := range repo.Components {
|
||||
paths := repo.AppStreamPaths(component)
|
||||
if len(paths) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, relativePath := range paths {
|
||||
info, ok := repo.ReleaseFiles[relativePath]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
url := repo.IndexesRootURL().ResolveReference(&url.URL{Path: relativePath}).String()
|
||||
|
||||
if progress != nil {
|
||||
progress.Printf("Downloading AppStream file %s...\n", relativePath)
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "aptly-appstream-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp dir for AppStream file %s: %s", relativePath, err)
|
||||
}
|
||||
|
||||
tempPath := path.Join(tempDir, path.Base(relativePath))
|
||||
|
||||
var expected *utils.ChecksumInfo
|
||||
if !ignoreChecksums {
|
||||
expected = &info
|
||||
}
|
||||
|
||||
err = d.DownloadWithChecksum(gocontext.TODO(), url, tempPath, expected, ignoreChecksums)
|
||||
if err != nil {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
// Skip files that are not found (some repos list dep11 files but don't serve them)
|
||||
if herr, ok := err.(*http.Error); ok && (herr.Code == 404 || herr.Code == 403) {
|
||||
if progress != nil {
|
||||
progress.ColoredPrintf("@y[!]@| @!skipping AppStream file %s: not found@|", relativePath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unable to download AppStream file %s: %s", relativePath, err)
|
||||
}
|
||||
|
||||
basename := path.Base(relativePath)
|
||||
poolPath, err := packagePool.Import(tempPath, basename, &info, true, checksumStorage)
|
||||
_ = os.RemoveAll(tempDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to import AppStream file %s: %s", relativePath, err)
|
||||
}
|
||||
|
||||
repo.AppStreamFiles[relativePath] = poolPath
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackageURL returns URL of package file relative to repository root
|
||||
// architecture
|
||||
func (repo *RemoteRepo) PackageURL(filename string) *url.URL {
|
||||
@@ -574,7 +661,7 @@ func (repo *RemoteRepo) DownloadPackageIndexes(progress aptly.Progress, d aptly.
|
||||
if progress != nil {
|
||||
progress.ColoredPrintf("@y[!]@| @!skipping package %s: duplicate in packages index@|", p)
|
||||
}
|
||||
} else if err != nil {
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -612,7 +699,19 @@ func (repo *RemoteRepo) ApplyFilter(dependencyOptions int, filterQuery PackageQu
|
||||
}
|
||||
|
||||
// BuildDownloadQueue builds queue, discards current PackageList
|
||||
func (repo *RemoteRepo) BuildDownloadQueue(packagePool aptly.PackagePool, packageCollection *PackageCollection, checksumStorage aptly.ChecksumStorage, skipExistingPackages bool) (queue []PackageDownloadTask, downloadSize int64, err error) {
|
||||
func (repo *RemoteRepo) BuildDownloadQueue(packagePool aptly.PackagePool, packageCollection *PackageCollection, checksumStorage aptly.ChecksumStorage, skipExistingPackages, latestOnly bool) (queue []PackageDownloadTask, downloadSize int64, err error) {
|
||||
if repo.packageList == nil {
|
||||
err = fmt.Errorf("package list is empty, please (re)download package indexes")
|
||||
return
|
||||
}
|
||||
|
||||
if latestOnly {
|
||||
repo.packageList, err = repo.packageList.FilterLatest()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
queue = make([]PackageDownloadTask, 0, repo.packageList.Len())
|
||||
seen := make(map[string]int, repo.packageList.Len())
|
||||
|
||||
|
||||
+164
-26
@@ -2,6 +2,7 @@ package deb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
@@ -90,8 +91,8 @@ type RemoteRepoSuite struct {
|
||||
var _ = Suite(&RemoteRepoSuite{})
|
||||
|
||||
func (s *RemoteRepoSuite) SetUpTest(c *C) {
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
s.flat, _ = NewRemoteRepo("exp42", "http://repos.express42.com/virool/precise/", "./", []string{}, []string{}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.flat, _ = NewRemoteRepo("exp42", "http://repos.express42.com/virool/precise/", "./", []string{}, []string{}, false, false, false, false)
|
||||
s.downloader = http.NewFakeDownloader().ExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/Release", exampleReleaseFile)
|
||||
s.progress = console.NewProgress(false)
|
||||
s.db, _ = goleveldb.NewOpenDB(c.MkDir())
|
||||
@@ -108,7 +109,7 @@ func (s *RemoteRepoSuite) TearDownTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestInvalidURL(c *C) {
|
||||
_, err := NewRemoteRepo("s", "http://lolo%2", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
_, err := NewRemoteRepo("s", "http://lolo%2", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, ErrorMatches, ".*(hexadecimal escape in host|percent-encoded characters in host|invalid URL escape).*")
|
||||
}
|
||||
|
||||
@@ -117,12 +118,15 @@ func (s *RemoteRepoSuite) TestFlatCreation(c *C) {
|
||||
c.Check(s.flat.Distribution, Equals, "./")
|
||||
c.Check(s.flat.Components, IsNil)
|
||||
|
||||
flat2, _ := NewRemoteRepo("flat2", "http://pkg.jenkins-ci.org/debian-stable", "binary/", []string{}, []string{}, false, false, false)
|
||||
flat2, _ := NewRemoteRepo("flat2", "http://pkg.jenkins-ci.org/debian-stable", "binary/", []string{}, []string{}, false, false, false, false)
|
||||
c.Check(flat2.IsFlat(), Equals, true)
|
||||
c.Check(flat2.Distribution, Equals, "./binary/")
|
||||
|
||||
_, err := NewRemoteRepo("fl", "http://some.repo/", "./", []string{"main"}, []string{}, false, false, false)
|
||||
_, err := NewRemoteRepo("fl", "http://some.repo/", "./", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Check(err, ErrorMatches, "components aren't supported for flat repos")
|
||||
|
||||
_, err = NewRemoteRepo("fl", "http://some.repo/", "./", []string{}, []string{}, false, false, false, true)
|
||||
c.Check(err, ErrorMatches, "AppStream \\(DEP-11\\) metadata isn't supported for flat repos")
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestString(c *C) {
|
||||
@@ -135,6 +139,43 @@ func (s *RemoteRepoSuite) TestString(c *C) {
|
||||
s.flat.DownloadSources = true
|
||||
c.Check(s.repo.String(), Equals, "[yandex]: http://mirror.yandex.ru/debian/ squeeze [src] [udeb] [installer]")
|
||||
c.Check(s.flat.String(), Equals, "[exp42]: http://repos.express42.com/virool/precise/ ./ [src]")
|
||||
|
||||
s.repo.DownloadAppStream = true
|
||||
c.Check(s.repo.String(), Equals, "[yandex]: http://mirror.yandex.ru/debian/ squeeze [src] [udeb] [installer] [appstream]")
|
||||
|
||||
// AppStream is not supported for flat repos, so no flat test here
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestAppStreamPaths(c *C) {
|
||||
s.repo.ReleaseFiles = nil
|
||||
c.Check(s.repo.AppStreamPaths("main"), DeepEquals, []string(nil))
|
||||
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/binary-amd64/Packages": {Size: 100},
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 200},
|
||||
"main/dep11/Components-i386.yml.gz": {Size: 300},
|
||||
"main/dep11/icons-48x48.tar.gz": {Size: 400},
|
||||
"contrib/dep11/Components-amd64.yml.gz": {Size: 500},
|
||||
"main/source/Sources": {Size: 600},
|
||||
}
|
||||
|
||||
paths := s.repo.AppStreamPaths("main")
|
||||
c.Check(paths, DeepEquals, []string{
|
||||
"main/dep11/Components-amd64.yml.gz",
|
||||
"main/dep11/Components-i386.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz",
|
||||
})
|
||||
|
||||
paths = s.repo.AppStreamPaths("contrib")
|
||||
c.Check(paths, DeepEquals, []string{
|
||||
"contrib/dep11/Components-amd64.yml.gz",
|
||||
})
|
||||
|
||||
paths = s.repo.AppStreamPaths("non-free")
|
||||
c.Check(paths, DeepEquals, []string(nil))
|
||||
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{}
|
||||
c.Check(s.repo.AppStreamPaths("main"), DeepEquals, []string(nil))
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestNumPackages(c *C) {
|
||||
@@ -236,13 +277,13 @@ func (s *RemoteRepoSuite) TestFetchNullVerifier2(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestFetchWrongArchitecture(c *C) {
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{"xyz"}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{"xyz"}, false, false, false, false)
|
||||
err := s.repo.Fetch(s.downloader, nil, true)
|
||||
c.Assert(err, ErrorMatches, "architecture xyz not available in repo.*")
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestFetchWrongComponent(c *C) {
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"xyz"}, []string{"i386"}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"xyz"}, []string{"i386"}, false, false, false, false)
|
||||
err := s.repo.Fetch(s.downloader, nil, true)
|
||||
c.Assert(err, ErrorMatches, "component xyz not available in repo.*")
|
||||
}
|
||||
@@ -281,7 +322,7 @@ func (s *RemoteRepoSuite) TestDownload(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -308,7 +349,7 @@ func (s *RemoteRepoSuite) TestDownload(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -329,7 +370,7 @@ func (s *RemoteRepoSuite) TestDownload(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -356,7 +397,7 @@ func (s *RemoteRepoSuite) TestDownloadWithInstaller(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3)+int64(len(exampleInstallerManifestFile)))
|
||||
c.Check(queue, HasLen, 2)
|
||||
@@ -382,6 +423,35 @@ func (s *RemoteRepoSuite) TestDownloadWithInstaller(c *C) {
|
||||
c.Check(pkg.Name, Equals, "installer")
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestBuildDownloadQueueLatestOnly(c *C) {
|
||||
s.repo.Architectures = []string{"i386"}
|
||||
|
||||
err := s.repo.Fetch(s.downloader, nil, true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/binary-i386/Packages.bz2", &http.Error{Code: 404})
|
||||
s.downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/binary-i386/Packages.gz", &http.Error{Code: 404})
|
||||
s.downloader.ExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/binary-i386/Packages", examplePackagesFile)
|
||||
|
||||
err = s.repo.DownloadPackageIndexes(s.progress, s.downloader, nil, s.collectionFactory, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
stanza := packageStanza.Copy()
|
||||
stanza["Package"] = "amanda-client"
|
||||
stanza["Version"] = "1:3.4.0-1"
|
||||
stanza["Filename"] = "pool/main/a/amanda/amanda-client_3.4.0-1_i386.deb"
|
||||
|
||||
newest := NewPackageFromControlFile(stanza)
|
||||
_ = s.repo.packageList.Add(newest)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, true)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(queue, HasLen, 1)
|
||||
c.Check(queue[0].File.DownloadURL(), Equals, "pool/main/a/amanda/amanda-client_3.4.0-1_i386.deb")
|
||||
c.Check(size, Equals, int64(187518))
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
s.repo.Architectures = []string{"i386"}
|
||||
s.repo.DownloadSources = true
|
||||
@@ -400,7 +470,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -444,7 +514,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -469,7 +539,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -493,7 +563,7 @@ func (s *RemoteRepoSuite) TestDownloadFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -521,7 +591,7 @@ func (s *RemoteRepoSuite) TestDownloadFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -543,7 +613,7 @@ func (s *RemoteRepoSuite) TestDownloadFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -574,7 +644,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -620,7 +690,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -646,7 +716,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -655,6 +725,74 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(s.flat.packageRefs, NotNil)
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestDownloadAppStreamFiles(c *C) {
|
||||
// No dep11 entries
|
||||
s.repo.Components = []string{"main"}
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/binary-amd64/Packages": {Size: 100},
|
||||
"main/source/Sources": {Size: 200},
|
||||
}
|
||||
|
||||
err := s.repo.DownloadAppStreamFiles(s.progress, s.downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 0)
|
||||
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 16},
|
||||
"main/dep11/icons-48x48.tar.gz": {Size: 16},
|
||||
"main/binary-amd64/Packages": {Size: 100},
|
||||
}
|
||||
|
||||
downloader := http.NewFakeDownloader()
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", "dep11-components")
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/icons-48x48.tar.gz", "dep11-icons-data")
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 2)
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Not(Equals), "")
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/icons-48x48.tar.gz"], Not(Equals), "")
|
||||
|
||||
// 404 skipped
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 16},
|
||||
"main/dep11/icons-48x48.tar.gz": {Size: 15},
|
||||
}
|
||||
|
||||
downloader = http.NewFakeDownloader()
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", "dep11-components")
|
||||
downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/icons-48x48.tar.gz", &http.Error{Code: 404, URL: "http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/icons-48x48.tar.gz"})
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 1)
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Not(Equals), "")
|
||||
|
||||
// Generic download error propagated
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 18},
|
||||
}
|
||||
|
||||
downloader = http.NewFakeDownloader()
|
||||
downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", fmt.Errorf("connection refused"))
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, ErrorMatches, "unable to download AppStream file.*connection refused")
|
||||
|
||||
// Bypass checksum validation
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 999, MD5: "bad", SHA256: "bad"},
|
||||
}
|
||||
|
||||
downloader = http.NewFakeDownloader()
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", "dep11-components")
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, true)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 1)
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Not(Equals), "")
|
||||
}
|
||||
|
||||
type RemoteRepoCollectionSuite struct {
|
||||
PackageListMixinSuite
|
||||
db database.Storage
|
||||
@@ -677,7 +815,7 @@ func (s *RemoteRepoCollectionSuite) TestAddByName(c *C) {
|
||||
_, err := s.collection.ByName("yandex")
|
||||
c.Assert(err, ErrorMatches, "*.not found")
|
||||
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(s.collection.Add(repo), IsNil)
|
||||
c.Assert(s.collection.Add(repo), ErrorMatches, ".*already exists")
|
||||
|
||||
@@ -695,7 +833,7 @@ func (s *RemoteRepoCollectionSuite) TestByUUID(c *C) {
|
||||
_, err := s.collection.ByUUID("some-uuid")
|
||||
c.Assert(err, ErrorMatches, "*.not found")
|
||||
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(s.collection.Add(repo), IsNil)
|
||||
|
||||
r, err := s.collection.ByUUID(repo.UUID)
|
||||
@@ -709,7 +847,7 @@ func (s *RemoteRepoCollectionSuite) TestByUUID(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(s.collection.Update(repo), IsNil)
|
||||
|
||||
collection := NewRemoteRepoCollection(s.db)
|
||||
@@ -730,7 +868,7 @@ func (s *RemoteRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
_ = s.collection.Add(repo)
|
||||
|
||||
count := 0
|
||||
@@ -752,10 +890,10 @@ func (s *RemoteRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoCollectionSuite) TestDrop(c *C) {
|
||||
repo1, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo1, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
_ = s.collection.Add(repo1)
|
||||
|
||||
repo2, _ := NewRemoteRepo("tyndex", "http://mirror.yandex.ru/debian/", "wheezy", []string{"main"}, []string{}, false, false, false)
|
||||
repo2, _ := NewRemoteRepo("tyndex", "http://mirror.yandex.ru/debian/", "wheezy", []string{"main"}, []string{}, false, false, false, false)
|
||||
_ = s.collection.Add(repo2)
|
||||
|
||||
r1, _ := s.collection.ByUUID(repo1.UUID)
|
||||
|
||||
+25
-13
@@ -40,6 +40,9 @@ type Snapshot struct {
|
||||
NotAutomatic string
|
||||
ButAutomaticUpgrades string
|
||||
|
||||
// AppStream files: relative path → pool path (pass-through from mirror)
|
||||
AppStreamFiles map[string]string `json:",omitempty"`
|
||||
|
||||
packageRefs *PackageRefList
|
||||
}
|
||||
|
||||
@@ -59,6 +62,7 @@ func NewSnapshotFromRepository(name string, repo *RemoteRepo) (*Snapshot, error)
|
||||
Origin: repo.Meta["Origin"],
|
||||
NotAutomatic: repo.Meta["NotAutomatic"],
|
||||
ButAutomaticUpgrades: repo.Meta["ButAutomaticUpgrades"],
|
||||
AppStreamFiles: repo.AppStreamFiles,
|
||||
packageRefs: repo.packageRefs,
|
||||
}, nil
|
||||
}
|
||||
@@ -94,14 +98,28 @@ func NewSnapshotFromRefList(name string, sources []*Snapshot, list *PackageRefLi
|
||||
sourceUUIDs[i] = sources[i].UUID
|
||||
}
|
||||
|
||||
// Merge AppStreamFiles from all source snapshots
|
||||
var mergedAppStream map[string]string
|
||||
for _, source := range sources {
|
||||
if len(source.AppStreamFiles) > 0 {
|
||||
if mergedAppStream == nil {
|
||||
mergedAppStream = make(map[string]string)
|
||||
}
|
||||
for k, v := range source.AppStreamFiles {
|
||||
mergedAppStream[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Snapshot{
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
SourceKind: "snapshot",
|
||||
SourceIDs: sourceUUIDs,
|
||||
Description: description,
|
||||
packageRefs: list,
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
SourceKind: "snapshot",
|
||||
SourceIDs: sourceUUIDs,
|
||||
Description: description,
|
||||
AppStreamFiles: mergedAppStream,
|
||||
packageRefs: list,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,12 +143,6 @@ func (s *Snapshot) Key() []byte {
|
||||
return []byte("S" + s.UUID)
|
||||
}
|
||||
|
||||
// ResourceKey is a unique identifier of the resource
|
||||
// this snapshot uses. Instead of uuid it uses name
|
||||
// which needs to be unique as well.
|
||||
func (s *Snapshot) ResourceKey() []byte {
|
||||
return []byte("S" + s.Name)
|
||||
}
|
||||
|
||||
// RefKey is a unique id for package reference list
|
||||
func (s *Snapshot) RefKey() []byte {
|
||||
|
||||
@@ -31,8 +31,7 @@ func BenchmarkSnapshotCollectionForEach(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
collection = NewSnapshotCollection(db)
|
||||
|
||||
|
||||
_ = collection.ForEach(func(s *Snapshot) error {
|
||||
_ = collection.ForEach(func(s *Snapshot) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+60
-4
@@ -19,7 +19,7 @@ var _ = Suite(&SnapshotSuite{})
|
||||
|
||||
func (s *SnapshotSuite) SetUpTest(c *C) {
|
||||
s.SetUpPackages()
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.repo.packageRefs = s.reflist
|
||||
}
|
||||
|
||||
@@ -101,6 +101,62 @@ func (s *SnapshotSuite) TestEncodeDecode(c *C) {
|
||||
c.Assert(snapshot2.packageRefs, IsNil)
|
||||
}
|
||||
|
||||
func (s *SnapshotSuite) TestSnapshotFromRepositoryAppStream(c *C) {
|
||||
s.repo.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "ab/cd/Components-amd64.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz": "ef/gh/icons-48x48.tar.gz",
|
||||
}
|
||||
snapshot, err := NewSnapshotFromRepository("snap-as", s.repo)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(snapshot.AppStreamFiles, DeepEquals, s.repo.AppStreamFiles)
|
||||
|
||||
s.repo.AppStreamFiles = nil
|
||||
snapshot2, err := NewSnapshotFromRepository("snap-no-as", s.repo)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(snapshot2.AppStreamFiles, IsNil)
|
||||
}
|
||||
|
||||
func (s *SnapshotSuite) TestSnapshotFromRefListAppStreamMerge(c *C) {
|
||||
snap1, _ := NewSnapshotFromRepository("snap1", s.repo)
|
||||
snap1.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "aa/bb/Components-amd64.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz": "cc/dd/icons-48x48.tar.gz",
|
||||
}
|
||||
|
||||
snap2, _ := NewSnapshotFromRepository("snap2", s.repo)
|
||||
snap2.AppStreamFiles = map[string]string{
|
||||
"contrib/dep11/Components-amd64.yml.gz": "ee/ff/Components-amd64.yml.gz",
|
||||
"main/dep11/Components-amd64.yml.gz": "xx/yy/Components-amd64.yml.gz",
|
||||
}
|
||||
|
||||
merged := NewSnapshotFromRefList("merged", []*Snapshot{snap1, snap2}, s.reflist, "Merged")
|
||||
|
||||
c.Check(len(merged.AppStreamFiles), Equals, 3)
|
||||
c.Check(merged.AppStreamFiles["main/dep11/icons-48x48.tar.gz"], Equals, "cc/dd/icons-48x48.tar.gz")
|
||||
c.Check(merged.AppStreamFiles["contrib/dep11/Components-amd64.yml.gz"], Equals, "ee/ff/Components-amd64.yml.gz")
|
||||
c.Check(merged.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Equals, "xx/yy/Components-amd64.yml.gz")
|
||||
|
||||
snap3, _ := NewSnapshotFromRepository("snap3", s.repo)
|
||||
snap3.AppStreamFiles = nil
|
||||
snap4, _ := NewSnapshotFromRepository("snap4", s.repo)
|
||||
snap4.AppStreamFiles = nil
|
||||
merged2 := NewSnapshotFromRefList("merged2", []*Snapshot{snap3, snap4}, s.reflist, "Merged2")
|
||||
c.Check(merged2.AppStreamFiles, IsNil)
|
||||
}
|
||||
|
||||
func (s *SnapshotSuite) TestEncodeDecodeAppStream(c *C) {
|
||||
snapshot, _ := NewSnapshotFromRepository("snap-as-enc", s.repo)
|
||||
snapshot.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "ab/cd/Components-amd64.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz": "ef/gh/icons-48x48.tar.gz",
|
||||
}
|
||||
|
||||
decoded := &Snapshot{}
|
||||
c.Assert(decoded.Decode(snapshot.Encode()), IsNil)
|
||||
c.Check(decoded.Name, Equals, snapshot.Name)
|
||||
c.Check(decoded.AppStreamFiles, DeepEquals, snapshot.AppStreamFiles)
|
||||
}
|
||||
|
||||
type SnapshotCollectionSuite struct {
|
||||
PackageListMixinSuite
|
||||
db database.Storage
|
||||
@@ -118,11 +174,11 @@ func (s *SnapshotCollectionSuite) SetUpTest(c *C) {
|
||||
s.collection = NewSnapshotCollection(s.db)
|
||||
s.SetUpPackages()
|
||||
|
||||
s.repo1, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
s.repo1, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.repo1.packageRefs = s.reflist
|
||||
s.snapshot1, _ = NewSnapshotFromRepository("snap1", s.repo1)
|
||||
|
||||
s.repo2, _ = NewRemoteRepo("android", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false)
|
||||
s.repo2, _ = NewRemoteRepo("android", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.repo2.packageRefs = s.reflist
|
||||
s.snapshot2, _ = NewSnapshotFromRepository("snap2", s.repo2)
|
||||
|
||||
@@ -223,7 +279,7 @@ func (s *SnapshotCollectionSuite) TestFindByRemoteRepoSource(c *C) {
|
||||
c.Check(s.collection.ByRemoteRepoSource(s.repo1), DeepEquals, []*Snapshot{s.snapshot1})
|
||||
c.Check(s.collection.ByRemoteRepoSource(s.repo2), DeepEquals, []*Snapshot{s.snapshot2})
|
||||
|
||||
repo3, _ := NewRemoteRepo("other", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false)
|
||||
repo3, _ := NewRemoteRepo("other", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false, false)
|
||||
|
||||
c.Check(s.collection.ByRemoteRepoSource(repo3), DeepEquals, []*Snapshot(nil))
|
||||
}
|
||||
|
||||
+7
-7
@@ -30,16 +30,16 @@ func CompareVersions(ver1, ver2 string) int {
|
||||
|
||||
// parseVersions breaks down full version to components (possibly empty)
|
||||
func parseVersion(ver string) (epoch, upstream, debian string) {
|
||||
i := strings.LastIndex(ver, "-")
|
||||
if i != -1 {
|
||||
debian, ver = ver[i+1:], ver[:i]
|
||||
}
|
||||
|
||||
i = strings.Index(ver, ":")
|
||||
i := strings.Index(ver, ":")
|
||||
if i != -1 {
|
||||
epoch, ver = ver[:i], ver[i+1:]
|
||||
}
|
||||
|
||||
i = strings.Index(ver, "-")
|
||||
if i != -1 {
|
||||
debian, ver = ver[i+1:], ver[:i]
|
||||
}
|
||||
|
||||
upstream = ver
|
||||
|
||||
return
|
||||
@@ -50,7 +50,7 @@ func compareLexicographic(s1, s2 string) int {
|
||||
i := 0
|
||||
l1, l2 := len(s1), len(s2)
|
||||
|
||||
for !(i == l1 && i == l2) { // break if s1 equal to s2
|
||||
for !(i == l1 && i == l2) { // break if s1 equal to s2
|
||||
|
||||
if i == l2 {
|
||||
// s1 is longer than s2
|
||||
|
||||
+3
-2
@@ -20,10 +20,10 @@ func (s *VersionSuite) TestParseVersion(c *C) {
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", "1"})
|
||||
|
||||
e, u, d = parseVersion("1.3-pre4-1")
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3-pre4", "1"})
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3", "pre4-1"})
|
||||
|
||||
e, u, d = parseVersion("4:1.3-pre4-1")
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3-pre4", "1"})
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3", "pre4-1"})
|
||||
}
|
||||
|
||||
func (s *VersionSuite) TestCompareLexicographic(c *C) {
|
||||
@@ -100,6 +100,7 @@ func (s *VersionSuite) TestCompareVersions(c *C) {
|
||||
c.Check(CompareVersions("1.0-133-avc", "1.0"), Equals, 1)
|
||||
|
||||
c.Check(CompareVersions("5.2.0.3", "5.2.0.283"), Equals, -1)
|
||||
c.Check(CompareVersions("4.3.5a", "4.3.5-rc3-1"), Equals, 1)
|
||||
}
|
||||
|
||||
func (s *VersionSuite) TestParseDependency(c *C) {
|
||||
|
||||
Vendored
+84
@@ -70,6 +70,9 @@ ppa_distributor_id: ubuntu
|
||||
# Codename for short PPA url expansion
|
||||
ppa_codename: ""
|
||||
|
||||
# PPA Base URL (default: launchpad)
|
||||
# # ppa_baseurl: http://ppa.launchpad.net
|
||||
|
||||
|
||||
# Aptly Server
|
||||
###############
|
||||
@@ -193,6 +196,35 @@ filesystem_publish_endpoints:
|
||||
#
|
||||
# `aptly publish snapshot wheezy-main s3:test:`
|
||||
#
|
||||
|
||||
# JFrog Artifactory Endpoint Support
|
||||
#
|
||||
# aptly can be configured to publish repositories directly to JFrog Artifactory. First,
|
||||
# publishing endpoints should be described in the aptly configuration file.
|
||||
#
|
||||
# The destination Artifactory repo should be of the "generic" type, not "debian".
|
||||
#
|
||||
# In order to publish to JFrog, specify endpoint as `jfrog:endpoint-name:` before
|
||||
# publishing prefix on the command line, e.g.:
|
||||
#
|
||||
# `aptly publish snapshot wheezy-main jfrog:test:`
|
||||
#
|
||||
jfrog_publish_endpoints:
|
||||
# # Endpoint Name
|
||||
# test:
|
||||
# # JFrog URL
|
||||
# url: "https://artifactory.example.com/artifactory/"
|
||||
# # Repository
|
||||
# repository: apt-local
|
||||
# # Jfrog credentials to authenticate to Artifactory. If not supplied, the
|
||||
# # environment variables `JFROG_USERNAME`, `JFROG_PASSWORD`, `JFROG_APIKEY`,
|
||||
# # and `JFROG_ACCESSTOKEN` can be used
|
||||
# # Authentication requires one of: user+pass, api key, or access token
|
||||
# username: admin
|
||||
# password: password
|
||||
# api_key: api_key
|
||||
# access_token: access_token
|
||||
|
||||
s3_publish_endpoints:
|
||||
# # Endpoint Name
|
||||
# test:
|
||||
@@ -253,6 +285,58 @@ s3_publish_endpoints:
|
||||
# # Enables detailed request/response dump for each S3 operation
|
||||
# debug: false
|
||||
|
||||
# GCS Endpoint Support
|
||||
#
|
||||
# aptly can be configured to publish repositories directly to Google Cloud
|
||||
# Storage. First, publishing endpoints should be described in the aptly
|
||||
# configuration file. Each endpoint has a name and associated settings.
|
||||
#
|
||||
# In order to publish to GCS, specify endpoint as `gcs:endpoint-name:` before
|
||||
# publishing prefix on the command line, e.g.:
|
||||
#
|
||||
# `aptly publish snapshot wheezy-main gcs:test:`
|
||||
#
|
||||
gcs_publish_endpoints:
|
||||
# # Endpoint Name
|
||||
# test:
|
||||
# # Bucket name
|
||||
# bucket: test-bucket
|
||||
# # Prefix (optional)
|
||||
# # publishing under specified prefix in the bucket, defaults to
|
||||
# # no prefix (bucket root)
|
||||
# prefix: ""
|
||||
# # Credentials File (optional)
|
||||
# # Path to a service account credentials JSON file
|
||||
# credentials_file: ""
|
||||
# # Service Account JSON (optional)
|
||||
# # Inline service account credentials JSON payload
|
||||
# service_account_json: ""
|
||||
# # Project (optional)
|
||||
# # Quota project used for GCS requests
|
||||
# project: ""
|
||||
# # Endpoint (optional)
|
||||
# # Override the GCS endpoint (e.g. for staging or a fake server);
|
||||
# # leave empty to use the default GCS endpoint
|
||||
# endpoint: ""
|
||||
# # Default ACLs (optional)
|
||||
# # assign ACL to published files:
|
||||
# # * private (default)
|
||||
# # * public-read (public repository)
|
||||
# # * none (don't set ACL)
|
||||
# acl: private
|
||||
# # Storage Class (optional)
|
||||
# # GCS storage class, e.g. `STANDARD`
|
||||
# storage_class: STANDARD
|
||||
# # Encryption Key (optional)
|
||||
# # Customer-supplied encryption key (32-byte AES-256 key)
|
||||
# encryption_key: ""
|
||||
# # Disable MultiDel (optional)
|
||||
# # Kept for parity with S3 settings; GCS deletes are one-by-one
|
||||
# disable_multidel: false
|
||||
# # Debug (optional)
|
||||
# # Enables detailed logs for each GCS operation
|
||||
# debug: false
|
||||
|
||||
# Swift Endpoint Support
|
||||
#
|
||||
# aptly can publish a repository directly to OpenStack Swift.
|
||||
|
||||
Vendored
+52
@@ -1,3 +1,55 @@
|
||||
aptly (1.6.3) stable; urgency=medium
|
||||
|
||||
* NEW FEATURES:
|
||||
* Google Cloud Storage (GCS) publish backend (https://github.com/aptly-dev/aptly/pull/1550)
|
||||
* dput-compatible file upload API (https://github.com/aptly-dev/aptly/pull/1436)
|
||||
* JFrog Artifactory publish backend (https://github.com/aptly-dev/aptly/pull/1553)
|
||||
* AppStream (DEP-11) mirror support (https://github.com/aptly-dev/aptly/pull/1543)
|
||||
* Multiple GPG keys support (https://github.com/aptly-dev/aptly/pull/1479)
|
||||
* GPG key list & delete API (https://github.com/aptly-dev/aptly/pull/1558)
|
||||
* Edit mirror API endpoint (https://github.com/aptly-dev/aptly/pull/1535)
|
||||
* NumPackages in list responses (https://github.com/aptly-dev/aptly/pull/1559)
|
||||
* Mirror latest packages (https://github.com/aptly-dev/aptly/pull/1513)
|
||||
* Reproducible builds / `SOURCE_DATE_EPOCH` support (https://github.com/aptly-dev/aptly/pull/1537), https://github.com/aptly-dev/aptly/pull/1542)
|
||||
* `Release` file `Version` field support (https://github.com/aptly-dev/aptly/pull/1533)
|
||||
* `InRelease` file `Signed-By` field support (https://github.com/aptly-dev/aptly/pull/1518), https://github.com/aptly-dev/aptly/pull/1519)
|
||||
* GCP / Google Artifact Registry authentication (https://github.com/aptly-dev/aptly/pull/1505)
|
||||
* Update publish label & origin (https://github.com/aptly-dev/aptly/pull/1484)
|
||||
* Ubuntu 26.04 / resolute builds (https://github.com/aptly-dev/aptly/pull/1571)
|
||||
* BUG FIXES:
|
||||
* Race condition & concurrency fixes for the REST API (https://github.com/aptly-dev/aptly/pull/1574)
|
||||
* Fix empty line in `Package-List` for source packages (https://github.com/aptly-dev/aptly/pull/1588)
|
||||
* Publish: check storage exists before publishing (https://github.com/aptly-dev/aptly/pull/1587)
|
||||
* S3 publish race condition (https://github.com/aptly-dev/aptly/pull/1594)
|
||||
* Repo edit name optionally (https://github.com/aptly-dev/aptly/pull/1593)
|
||||
* Fix crash in `aptly db recover` (https://github.com/aptly-dev/aptly/pull/1565)
|
||||
* Fix deadlocks in task list (https://github.com/aptly-dev/aptly/pull/1529)
|
||||
* Fix S3 re-upload issue (https://github.com/aptly-dev/aptly/pull/1480)
|
||||
* Fix `aptly repo edit` API (https://github.com/aptly-dev/aptly/pull/1493)
|
||||
* Fix out-of-disk-space error handling (https://github.com/aptly-dev/aptly/pull/1504)
|
||||
* Fix `aptly mirror update` removing unrelated params (https://github.com/aptly-dev/aptly/pull/1466)
|
||||
* Fix concurrent pool linking race condition (https://github.com/aptly-dev/aptly/pull/1481)
|
||||
* Fix `dpkg`-compliant version comparison (https://github.com/aptly-dev/aptly/pull/1509)
|
||||
* Fix Swagger property casing and spec errors (https://github.com/aptly-dev/aptly/pull/1510), https://github.com/aptly-dev/aptly/pull/1498)
|
||||
* Remove useless nil check (https://github.com/aptly-dev/aptly/pull/1482)
|
||||
* Format Go code with gofmt (https://github.com/aptly-dev/aptly/pull/1483)
|
||||
* DEPENDENCIES CHANGES:
|
||||
* Go toolchain → 1.25.0
|
||||
* `go.opentelemetry.io/otel` → v1.41.0 (https://github.com/aptly-dev/aptly/pull/1586)
|
||||
* `go.opentelemetry.io/otel/sdk` → v1.43.0 (https://github.com/aptly-dev/aptly/pull/1584)
|
||||
* `github.com/go-jose/go-jose/v4` → v4.1.4 (https://github.com/aptly-dev/aptly/pull/1585)
|
||||
* `github.com/go-git/go-git/v5` → v5.19.1 (https://github.com/aptly-dev/aptly/pull/1590)
|
||||
* `github.com/ulikunitz/xz` → v0.5.15 (fixes 32-bit build failures)
|
||||
* `golang.org/x/crypto` → v0.45.0 (https://github.com/aptly-dev/aptly/pull/1506)
|
||||
* `google.golang.org/grpc` → v1.79.3 (https://github.com/aptly-dev/aptly/pull/1546)
|
||||
* `github.com/aws/aws-sdk-go-v2/service/s3` → v1.97.3 (https://github.com/aptly-dev/aptly/pull/1554)
|
||||
* `github.com/cloudflare/circl` → v1.6.3 (https://github.com/aptly-dev/aptly/pull/1461), https://github.com/aptly-dev/aptly/pull/1541)
|
||||
* `requests` (Python, system tests) → 2.33.0 (https://github.com/aptly-dev/aptly/pull/1460), https://github.com/aptly-dev/aptly/pull/1547)
|
||||
* `github.com/ProtonMail/go-crypto` → v1.4.0
|
||||
* `golang.org/x/net` → v0.48.0
|
||||
|
||||
-- André Roth <neolynx@gmail.com> Wed, 24 Jun 2026 18:47:05 +0200
|
||||
|
||||
aptly (1.6.2) stable; urgency=medium
|
||||
|
||||
* doc: add swagger doc for /api/gpg/key (https://github.com/aptly-dev/aptly/pull/1456)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
|
||||
In order to add debian package files to a local repository, files are first uploaded to a temporary directory.
|
||||
Then the directory (or a specific file within) is added to a repository. After adding to a repositorty, the directory resp. files are removed bt default.
|
||||
Then the directory (or a specific file within) is added to a repository. After adding to a repository, the directory resp. files are removed bt default.
|
||||
|
||||
All uploaded files are stored under `<rootDir>/upload/<tempdir>` directory.
|
||||
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# GPG Keys Management
|
||||
|
||||
GPG keys are used by aptly to verify the authenticity of remote repository Release files when creating mirrors. This document describes the API endpoints for managing GPG keys in the aptly keyring.
|
||||
|
||||
## Overview
|
||||
|
||||
Aptly uses GNU Privacy Guard (GPG) to verify signed repository metadata. You must add the repository's GPG public key to aptly's keyring before creating mirrors that verify signatures.
|
||||
|
||||
Keys are stored in the aptly keyring (default: `trustedkeys.gpg`). You can have multiple keyrings and specify which one to use via the `Keyring` parameter.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### List GPG Keys
|
||||
|
||||
**GET /api/gpg/keys**
|
||||
|
||||
Lists all public GPG keys currently installed in the aptly keyring.
|
||||
|
||||
**Parameters:**
|
||||
- `keyring` (query, optional): Keyring file to list keys from. Default: `trustedkeys.gpg`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"Keys": [
|
||||
{
|
||||
"KeyID": "8B48AD6246925553",
|
||||
"Fingerprint": "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0",
|
||||
"Validity": "f",
|
||||
"UserIDs": ["John Doe <john@example.com>"],
|
||||
"CreatedAt": "1611864000"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- `200 OK`: Keys successfully retrieved
|
||||
- `400 Bad Request`: GPG execution failed or invalid parameters
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl http://localhost:8080/api/gpg/keys
|
||||
curl "http://localhost:8080/api/gpg/keys?keyring=custom.gpg"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Add GPG Key
|
||||
|
||||
**POST /api/gpg/key**
|
||||
|
||||
Adds a GPG public key to the aptly keyring. Keys can be added in two ways:
|
||||
1. Provide the ASCII-armored key directly
|
||||
2. Provide a key server and key ID(s) to download from
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
"GpgKeyArmor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "8B48AD6246925553"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `Keyring` (optional): Keyring file to add keys to. Default: `trustedkeys.gpg`
|
||||
- `GpgKeyArmor` (optional): ASCII-armored GPG public key
|
||||
- `Keyserver` (optional): Keyserver URL (e.g., `hkp://keyserver.ubuntu.com:80`)
|
||||
- `GpgKeyID` (optional): Space-separated key IDs to download from keyserver
|
||||
|
||||
**Status Codes:**
|
||||
- `200 OK`: Key successfully added
|
||||
- `400 Bad Request`: Invalid parameters or GPG execution failed
|
||||
|
||||
**Example - From ASCII Key:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"GpgKeyArmor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v2\n...\n-----END PGP PUBLIC KEY BLOCK-----"
|
||||
}'
|
||||
```
|
||||
|
||||
**Example - From Keyserver:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "8B48AD6246925553 A1B2C3D4E5F67890"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete GPG Key
|
||||
|
||||
**DELETE /api/gpg/key**
|
||||
|
||||
Removes a GPG key from the aptly keyring.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
"GpgKeyID": "8B48AD6246925553"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `Keyring` (optional): Keyring file to delete from. Default: `trustedkeys.gpg`
|
||||
- `GpgKeyID` (required): Key ID or fingerprint to delete
|
||||
|
||||
**Status Codes:**
|
||||
- `200 OK`: Key successfully deleted
|
||||
- `400 Bad Request`: Invalid parameters or GPG execution failed
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"GpgKeyID": "8B48AD6246925553"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Verify Downloaded Repository Metadata
|
||||
|
||||
Before creating a mirror from a signed repository, add the repository's GPG key:
|
||||
|
||||
```bash
|
||||
# Add the key from a keyserver
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "EB9B46B91F2D3B7E"
|
||||
}'
|
||||
|
||||
# Now create a mirror with signature verification
|
||||
# (signature verification configured in mirror settings)
|
||||
```
|
||||
|
||||
### 2. Manage Multiple Keyrings
|
||||
|
||||
Aptly supports using different keyrings for different purposes. For example, one for Debian repositories and another for custom internal repositories:
|
||||
|
||||
```bash
|
||||
# Add key to Debian keyring
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Keyring": "debian-keys.gpg",
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "EB9B46B91F2D3B7E"
|
||||
}'
|
||||
|
||||
# List keys in Debian keyring
|
||||
curl "http://localhost:8080/api/gpg/keys?keyring=debian-keys.gpg"
|
||||
```
|
||||
|
||||
### 3. Remove Compromised Keys
|
||||
|
||||
If a GPG key is compromised, remove it from the keyring immediately:
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"GpgKeyID": "COMPROMISED_KEY_ID"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Validity Values
|
||||
|
||||
Keys retrieved from `GET /api/gpg/keys` have a `Validity` field with the following possible values:
|
||||
|
||||
- `u` — Unknown validity
|
||||
- `f` — Full trust
|
||||
- `m` — Marginal trust
|
||||
- `n` — Never trust
|
||||
- `-` — Trust not set
|
||||
|
||||
The trust level is typically managed in your GPG configuration and does not affect aptly's ability to verify signatures.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"failed to list keys"**
|
||||
- Check that the keyring file exists and is readable
|
||||
- Verify GPG is installed and configured
|
||||
|
||||
**"unable to delete key: no public key"**
|
||||
- The key might not exist in the keyring
|
||||
- Verify the key ID is correct by listing keys first
|
||||
|
||||
**"invalid request body"**
|
||||
- Ensure the JSON is properly formatted
|
||||
- For POST requests, provide either `GpgKeyArmor` or (`Keyserver` + `GpgKeyID`)
|
||||
- For DELETE requests, `GpgKeyID` is required
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Search Package Collection
|
||||
<div>
|
||||
Perform operations on the whole collection of packages in apty database.
|
||||
Perform operations on the whole collection of packages in aptly database.
|
||||
</div>
|
||||
|
||||
|
||||
+22
-3
@@ -5,17 +5,36 @@ Publish snapshot or local repo as Debian repository to be used as APT source on
|
||||
|
||||
The published repository is signed with the user's GnuPG key.
|
||||
|
||||
Repositories can be published to local directories, Amazon S3 buckets, Azure or Swift Storage.
|
||||
Repositories can be published to local directories, Amazon S3 buckets, Azure, Swift, or JFrog Artifactory Storage.
|
||||
|
||||
#### GPG Keys
|
||||
|
||||
GPG key is required to sign any published repository. The key pari should be generated before publishing.
|
||||
|
||||
Publiс part of the key should be exported from your keyring using `gpg --export --armor` and imported on the system which uses a published repository.
|
||||
Public part of the key should be exported from your keyring using `gpg --export --armor` and imported on the system which uses a published repository.
|
||||
|
||||
* Multiple signing keys can be defined in aptly.conf using the gpgKeys array:
|
||||
```
|
||||
"gpgKeys": [
|
||||
"KEY_ID_x",
|
||||
"KEY_ID_y"
|
||||
]
|
||||
```
|
||||
|
||||
* It is also possible to pass multiple keys via the CLI using the repeatable `--gpg-key` flag:
|
||||
```
|
||||
aptly publish repo my-repo --gpg-key=KEY_ID_a --gpg-key=KEY_ID_b
|
||||
```
|
||||
* When using the REST API, the `gpgKey` parameter supports a comma-separated list of key IDs:
|
||||
```
|
||||
"gpgKey": "KEY_ID_a,KEY_ID_b"
|
||||
```
|
||||
* If `--gpg-key` is specified on the command line, or `gpgKey` is provided via the REST API, it takes precedence over any gpgKeys configuration in aptly.conf.
|
||||
* With multi-key support, aptly will sign all Release files (both clearsigned and detached signatures) with each provided key, ensuring a smooth key rotation process while maintaining compatibility for existing clients.
|
||||
|
||||
#### Parameters
|
||||
|
||||
Publish APIs use following convention to identify published repositories: `/api/publish/:prefix/:distribution`. `:distribution` is distribution name, while `:prefix` is `[<storage>:]<prefix>` (storage is optional, it defaults to empty string), if publishing prefix contains slashes `/`, they should be replaced with underscores (`_`) and underscores
|
||||
should be replaced with double underscore (`__`). To specify root `:prefix`, use `:.`, as `.` is ambigious in URLs.
|
||||
should be replaced with double underscore (`__`). To specify root `:prefix`, use `:.`, as `.` is ambiguous in URLs.
|
||||
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Manage Local Repositories
|
||||
<div>
|
||||
A local repository is a collection of versionned packages (usually custom packages created internally).
|
||||
A local repository is a collection of versioned packages (usually custom packages created internally).
|
||||
|
||||
Packages can be added, removed, moved or copied between repos.
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = source.Close()
|
||||
_ = source.Close()
|
||||
}()
|
||||
|
||||
sourceInfo, err := source.Stat()
|
||||
|
||||
+30
-8
@@ -15,6 +15,10 @@ import (
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// syncFile is a seam to allow tests to force fsync failures (e.g. ENOSPC).
|
||||
// In production it calls (*os.File).Sync().
|
||||
var syncFile = func(f *os.File) error { return f.Sync() }
|
||||
|
||||
// PublishedStorage abstract file system with public dirs (published repos)
|
||||
type PublishedStorage struct {
|
||||
rootPath string
|
||||
@@ -99,7 +103,17 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
}()
|
||||
|
||||
_, err = io.Copy(f, source)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync to ensure all data is written to disk and catch ENOSPC errors
|
||||
err = syncFile(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error syncing file %s: %s", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes single file under public path
|
||||
@@ -136,6 +150,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
|
||||
baseName := filepath.Base(fileName)
|
||||
poolPath := filepath.Join(storage.rootPath, publishedPrefix, publishedRelPath, filepath.Dir(fileName))
|
||||
destinationPath := filepath.Join(poolPath, baseName)
|
||||
|
||||
var localSourcePool aptly.LocalPackagePool
|
||||
if storage.linkMethod != LinkMethodCopy {
|
||||
@@ -154,7 +169,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
|
||||
var dstStat os.FileInfo
|
||||
|
||||
dstStat, err = os.Stat(filepath.Join(poolPath, baseName))
|
||||
dstStat, err = os.Stat(destinationPath)
|
||||
if err == nil {
|
||||
// already exists, check source file
|
||||
|
||||
@@ -173,7 +188,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
} else {
|
||||
// if source and destination have the same checksums, no need to copy
|
||||
var dstMD5 string
|
||||
dstMD5, err = utils.MD5ChecksumForFile(filepath.Join(poolPath, baseName))
|
||||
dstMD5, err = utils.MD5ChecksumForFile(destinationPath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -204,11 +219,11 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
|
||||
// source and destination have different inodes, if !forced, this is fatal error
|
||||
if !force {
|
||||
return fmt.Errorf("error linking file to %s: file already exists and is different", filepath.Join(poolPath, baseName))
|
||||
return fmt.Errorf("error linking file to %s: file already exists and is different", destinationPath)
|
||||
}
|
||||
|
||||
// forced, so remove destination
|
||||
err = os.Remove(filepath.Join(poolPath, baseName))
|
||||
err = os.Remove(destinationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -223,7 +238,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
}
|
||||
|
||||
var dst *os.File
|
||||
dst, err = os.Create(filepath.Join(poolPath, baseName))
|
||||
dst, err = os.Create(destinationPath)
|
||||
if err != nil {
|
||||
_ = r.Close()
|
||||
return err
|
||||
@@ -242,11 +257,18 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync to ensure all data is written to disk and catch ENOSPC errors
|
||||
err = syncFile(dst)
|
||||
if err != nil {
|
||||
_ = dst.Close()
|
||||
return fmt.Errorf("error syncing file %s: %s", destinationPath, err)
|
||||
}
|
||||
|
||||
err = dst.Close()
|
||||
} else if storage.linkMethod == LinkMethodSymLink {
|
||||
err = localSourcePool.Symlink(sourcePath, filepath.Join(poolPath, baseName))
|
||||
err = localSourcePool.Symlink(sourcePath, destinationPath)
|
||||
} else {
|
||||
err = localSourcePool.Link(sourcePath, filepath.Join(poolPath, baseName))
|
||||
err = localSourcePool.Link(sourcePath, destinationPath)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
@@ -11,6 +17,77 @@ import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type fakeProgress struct{ bytes.Buffer }
|
||||
|
||||
func (p *fakeProgress) Start() {}
|
||||
func (p *fakeProgress) Shutdown() {}
|
||||
func (p *fakeProgress) Flush() {}
|
||||
func (p *fakeProgress) InitBar(count int64, isBytes bool, barType aptly.BarType) {
|
||||
}
|
||||
func (p *fakeProgress) ShutdownBar() {}
|
||||
func (p *fakeProgress) AddBar(count int) {}
|
||||
func (p *fakeProgress) SetBar(count int) {}
|
||||
func (p *fakeProgress) Printf(msg string, a ...interface{}) {
|
||||
}
|
||||
func (p *fakeProgress) ColoredPrintf(msg string, a ...interface{}) {
|
||||
}
|
||||
func (p *fakeProgress) PrintfStdErr(msg string, a ...interface{}) {
|
||||
}
|
||||
|
||||
type fakeRSC struct {
|
||||
*bytes.Reader
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (r *fakeRSC) Close() error { return r.closeErr }
|
||||
|
||||
type fakePool struct {
|
||||
sizeErr error
|
||||
openFn func(string) (aptly.ReadSeekerCloser, error)
|
||||
}
|
||||
|
||||
type fakeLocalPool struct {
|
||||
fakePool
|
||||
statErr error
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Stat(path string) (os.FileInfo, error) { return nil, p.statErr }
|
||||
func (p *fakeLocalPool) GenerateTempPath(filename string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (p *fakeLocalPool) Link(path, dstPath string) error { return nil }
|
||||
func (p *fakeLocalPool) Symlink(path, dstPath string) error { return nil }
|
||||
func (p *fakeLocalPool) FullPath(path string) string { return path }
|
||||
|
||||
func (p *fakePool) Verify(poolPath, basename string, checksums *utils.ChecksumInfo, checksumStorage aptly.ChecksumStorage) (string, bool, error) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (p *fakePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, move bool, storage aptly.ChecksumStorage) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakePool) LegacyPath(filename string, checksums *utils.ChecksumInfo) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakePool) Size(path string) (int64, error) {
|
||||
if p.sizeErr != nil {
|
||||
return 0, p.sizeErr
|
||||
}
|
||||
return int64(len(path)), nil
|
||||
}
|
||||
|
||||
func (p *fakePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
if p.openFn != nil {
|
||||
return p.openFn(path)
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
func (p *fakePool) FilepathList(progress aptly.Progress) ([]string, error) { return nil, nil }
|
||||
func (p *fakePool) Remove(path string) (int64, error) { return 0, nil }
|
||||
|
||||
type PublishedStorageSuite struct {
|
||||
root string
|
||||
storage *PublishedStorage
|
||||
@@ -69,6 +146,14 @@ func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFileReturnsErrorIfSourceMissing(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("ppa/dists/squeeze/Release", filepath.Join(s.root, "no-such-file"))
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelist(c *C) {
|
||||
err := s.storage.MkDir("ppa/pool/main/a/ab/")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -134,6 +219,11 @@ func (s *PublishedStorageSuite) TestSymLink(c *C) {
|
||||
c.Assert(linkTarget, Equals, "ppa/dists/squeeze/Release")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestReadLinkReturnsErrorOnMissingPath(c *C) {
|
||||
_, err := s.storage.ReadLink("does/not/exist")
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestHardLink(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -163,6 +253,18 @@ func (s *PublishedStorageSuite) TestRemoveDirs(c *C) {
|
||||
c.Assert(os.IsNotExist(err), Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirsWithProgress(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("ppa/dists/squeeze/Release", "/dev/null")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
p := &fakeProgress{}
|
||||
err = s.storage.RemoveDirs("ppa/dists/", p)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemove(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -337,3 +439,277 @@ func (s *PublishedStorageSuite) TestRootRemove(c *C) {
|
||||
dirStorage := NewPublishedStorage(pwd, "", "")
|
||||
c.Assert(func() { _ = dirStorage.RemoveDirs("", nil) }, PanicMatches, "trying to remove the root directory")
|
||||
}
|
||||
|
||||
// DiskFullSuite uses a loopback mount; requires Linux + root.
|
||||
|
||||
type DiskFullSuite struct {
|
||||
root string
|
||||
}
|
||||
|
||||
var _ = Suite(&DiskFullSuite{})
|
||||
|
||||
func (s *DiskFullSuite) SetUpTest(c *C) {
|
||||
if runtime.GOOS != "linux" {
|
||||
c.Skip("disk full tests only run on Linux")
|
||||
}
|
||||
|
||||
s.root = c.MkDir()
|
||||
}
|
||||
|
||||
func (s *DiskFullSuite) TestPutFileOutOfSpace(c *C) {
|
||||
mountPoint := "/smallfs"
|
||||
if os.Geteuid() == 0 {
|
||||
mountPoint = filepath.Join(s.root, "smallfs")
|
||||
err := os.MkdirAll(mountPoint, 0777)
|
||||
c.Assert(err, IsNil)
|
||||
fsImage := filepath.Join(s.root, "small.img")
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+fsImage, "bs=1M", "count=1")
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
cmd = exec.Command("mkfs.ext4", "-F", fsImage)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
cmd = exec.Command("mount", "-o", "loop", fsImage, mountPoint)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
defer func() {
|
||||
_ = exec.Command("umount", mountPoint).Run()
|
||||
}()
|
||||
}
|
||||
|
||||
storage := NewPublishedStorage(mountPoint, "", "")
|
||||
largeFile := filepath.Join(s.root, "largefile")
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+largeFile, "bs=1M", "count=2")
|
||||
err := cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.PutFile("testfile", largeFile)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "no space left on device") ||
|
||||
strings.Contains(err.Error(), "sync"), Equals, true,
|
||||
Commentf("Expected disk full error, got: %v", err))
|
||||
}
|
||||
|
||||
func (s *DiskFullSuite) TestLinkFromPoolCopyOutOfSpace(c *C) {
|
||||
mountPoint := "/smallfs"
|
||||
if os.Geteuid() == 0 {
|
||||
mountPoint = filepath.Join(s.root, "smallfs")
|
||||
err := os.MkdirAll(mountPoint, 0777)
|
||||
c.Assert(err, IsNil)
|
||||
fsImage := filepath.Join(s.root, "small.img")
|
||||
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+fsImage, "bs=1M", "count=1")
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
cmd = exec.Command("mkfs.ext4", "-F", fsImage)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
cmd = exec.Command("mount", "-o", "loop", fsImage, mountPoint)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
defer func() {
|
||||
_ = exec.Command("umount", mountPoint).Run()
|
||||
}()
|
||||
}
|
||||
|
||||
storage := NewPublishedStorage(mountPoint, "copy", "")
|
||||
|
||||
poolPath := filepath.Join(s.root, "pool")
|
||||
pool := NewPackagePool(poolPath, false)
|
||||
cs := NewMockChecksumStorage()
|
||||
|
||||
largeFile := filepath.Join(s.root, "package.deb")
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+largeFile, "bs=1M", "count=2")
|
||||
err := cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
sourceChecksum, err := utils.ChecksumsForFile(largeFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
srcPoolPath, err := pool.Import(largeFile, "package.deb",
|
||||
&utils.ChecksumInfo{MD5: "d41d8cd98f00b204e9800998ecf8427e"}, false, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.LinkFromPool("", "pool/main/p/package", "package.deb",
|
||||
pool, srcPoolPath, sourceChecksum, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "no space left on device") ||
|
||||
strings.Contains(err.Error(), "sync"), Equals, true,
|
||||
Commentf("Expected disk full error, got: %v", err))
|
||||
}
|
||||
|
||||
type DiskFullNoRootSuite struct {
|
||||
root string
|
||||
}
|
||||
|
||||
var _ = Suite(&DiskFullNoRootSuite{})
|
||||
|
||||
func (s *DiskFullNoRootSuite) SetUpTest(c *C) {
|
||||
s.root = c.MkDir()
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestSyncIsCalled(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
sourceFile := filepath.Join(s.root, "source.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("test content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
err = storage.PutFile("dest.txt", sourceFile)
|
||||
c.Assert(err, IsNil)
|
||||
content, err := os.ReadFile(filepath.Join(s.root, "dest.txt"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(string(content), Equals, "test content")
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopySyncIsCalled(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
poolPath := filepath.Join(s.root, "pool")
|
||||
pool := NewPackagePool(poolPath, false)
|
||||
cs := NewMockChecksumStorage()
|
||||
|
||||
pkgFile := filepath.Join(s.root, "package.deb")
|
||||
err := os.WriteFile(pkgFile, []byte("package content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
sourceChecksum, err := utils.ChecksumsForFile(pkgFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
srcPoolPath, err := pool.Import(pkgFile, "package.deb",
|
||||
&utils.ChecksumInfo{MD5: "d41d8cd98f00b204e9800998ecf8427e"}, false, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.LinkFromPool("", "pool/main/p/package", "package.deb",
|
||||
pool, srcPoolPath, sourceChecksum, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
destPath := filepath.Join(s.root, "pool/main/p/package/package.deb")
|
||||
content, err := os.ReadFile(destPath)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(string(content), Equals, "package content")
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestPutFileSyncErrorIsReturned(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
|
||||
sourceFile := filepath.Join(s.root, "source-syncfail.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("test content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(_ *os.File) error { return syscall.ENOSPC }
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
err = storage.PutFile("dest-syncfail.txt", sourceFile)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "error syncing file"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopySyncErrorIsReturned(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
poolPath := filepath.Join(s.root, "pool")
|
||||
pool := NewPackagePool(poolPath, false)
|
||||
cs := NewMockChecksumStorage()
|
||||
|
||||
pkgFile := filepath.Join(s.root, "package-syncfail.deb")
|
||||
err := os.WriteFile(pkgFile, []byte("package content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
sourceChecksum, err := utils.ChecksumsForFile(pkgFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
srcPoolPath, err := pool.Import(pkgFile, "package-syncfail.deb",
|
||||
&utils.ChecksumInfo{MD5: "d41d8cd98f00b204e9800998ecf8427e"}, false, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(_ *os.File) error { return syscall.ENOSPC }
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
err = storage.LinkFromPool("", "pool/main/p/package", "package-syncfail.deb",
|
||||
pool, srcPoolPath, sourceChecksum, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "error syncing file"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestPutFileFailsIfDestinationDirMissing(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
|
||||
sourceFile := filepath.Join(s.root, "src.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("x"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.PutFile("missingdir/dest.txt", sourceFile)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolRejectsNonLocalPoolForHardlink(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
pool := &fakePool{}
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "cannot link"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyReturnsErrorIfOpenFails(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
pool := &fakePool{openFn: func(string) (aptly.ReadSeekerCloser, error) { return nil, io.ErrUnexpectedEOF }}
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyReturnsErrorIfReaderCloseFails(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
|
||||
pool := &fakePool{openFn: func(string) (aptly.ReadSeekerCloser, error) {
|
||||
return &fakeRSC{Reader: bytes.NewReader([]byte("data")), closeErr: io.ErrClosedPipe}, nil
|
||||
}}
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, Equals, io.ErrClosedPipe)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyReturnsErrorIfSizeFailsWhenDestExists(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "size")
|
||||
pool := &fakePool{sizeErr: io.ErrUnexpectedEOF, openFn: func(string) (aptly.ReadSeekerCloser, error) {
|
||||
return &fakeRSC{Reader: bytes.NewReader([]byte("data")), closeErr: nil}, nil
|
||||
}}
|
||||
|
||||
destDir := filepath.Join(s.root, "pool/main/p/pkg")
|
||||
c.Assert(os.MkdirAll(destDir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(destDir, "x.deb"), []byte("old"), 0644), IsNil)
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, Equals, io.ErrUnexpectedEOF)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyChecksumReturnsErrorIfDstMD5Fails(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
pool := &fakePool{openFn: func(string) (aptly.ReadSeekerCloser, error) {
|
||||
return &fakeRSC{Reader: bytes.NewReader([]byte("data")), closeErr: nil}, nil
|
||||
}}
|
||||
|
||||
// Make destinationPath a directory so MD5ChecksumForFile fails.
|
||||
destDir := filepath.Join(s.root, "pool/main/p/pkg")
|
||||
c.Assert(os.MkdirAll(destDir, 0777), IsNil)
|
||||
c.Assert(os.MkdirAll(filepath.Join(destDir, "x.deb"), 0777), IsNil)
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolHardlinkReturnsErrorIfStatFailsWhenDestExists(c *C) {
|
||||
storage := NewPublishedStorage(c.MkDir(), "hardlink", "")
|
||||
pool := &fakeLocalPool{statErr: errors.New("stat failed")}
|
||||
|
||||
destDir := filepath.Join(storage.rootPath, "pool", "main", "p", "pkg")
|
||||
c.Assert(os.MkdirAll(destDir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(destDir, "x.deb"), []byte("x"), 0644), IsNil)
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package gcs handles publishing to Google Cloud Storage.
|
||||
package gcs
|
||||
@@ -0,0 +1,12 @@
|
||||
package gcs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Launch gocheck tests.
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
package gcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/api/googleapi"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
// PublishedStorage abstracts published files hosted on GCS.
|
||||
type PublishedStorage struct {
|
||||
client *storage.Client
|
||||
bucket *storage.BucketHandle
|
||||
bucketName string
|
||||
prefix string
|
||||
acl string
|
||||
storageClass string
|
||||
encryptionKey string
|
||||
disableMultiDel bool
|
||||
debug bool
|
||||
pathCache map[string]string
|
||||
}
|
||||
|
||||
var (
|
||||
_ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
||||
)
|
||||
|
||||
// NewPublishedStorage creates a GCS-backed published storage.
|
||||
func NewPublishedStorage(bucket, prefix, credentialsFile, serviceAccountJSON,
|
||||
project, endpoint, defaultACL, storageClass, encryptionKey string,
|
||||
disableMultiDel, debug bool) (*PublishedStorage, error) {
|
||||
|
||||
ctx := context.TODO()
|
||||
opts := make([]option.ClientOption, 0, 4)
|
||||
|
||||
if endpoint != "" {
|
||||
opts = append(opts, option.WithEndpoint(endpoint), option.WithoutAuthentication())
|
||||
} else if credentialsFile != "" {
|
||||
opts = append(opts, option.WithAuthCredentialsFile(option.ServiceAccount, credentialsFile))
|
||||
} else if serviceAccountJSON != "" {
|
||||
opts = append(opts, option.WithAuthCredentialsJSON(option.ServiceAccount, []byte(serviceAccountJSON)))
|
||||
}
|
||||
|
||||
if project != "" {
|
||||
opts = append(opts, option.WithQuotaProject(project))
|
||||
}
|
||||
|
||||
// When pointing at a non-production endpoint (an explicit override or the
|
||||
// STORAGE_EMULATOR_HOST hook the GCS Go client honours natively), force
|
||||
// JSON reads. The default XML download API uses virtual-host-style URLs
|
||||
// (https://<bucket>.storage.googleapis.com/...) that emulators and most
|
||||
// dev/staging endpoints can't serve under a single listener; the JSON API
|
||||
// uses path-based URLs that work the same against real GCS or an emulator.
|
||||
if endpoint != "" || os.Getenv("STORAGE_EMULATOR_HOST") != "" {
|
||||
opts = append(opts, storage.WithJSONReads())
|
||||
}
|
||||
|
||||
client, err := storage.NewClient(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &PublishedStorage{
|
||||
client: client,
|
||||
bucket: client.Bucket(bucket),
|
||||
bucketName: bucket,
|
||||
prefix: prefix,
|
||||
acl: defaultACL,
|
||||
storageClass: storageClass,
|
||||
encryptionKey: encryptionKey,
|
||||
disableMultiDel: disableMultiDel,
|
||||
debug: debug,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) String() string {
|
||||
return fmt.Sprintf("GCS: %s/%s", g.bucketName, g.prefix)
|
||||
}
|
||||
|
||||
// MkDir creates directory recursively under public path.
|
||||
func (g *PublishedStorage) MkDir(_ string) error {
|
||||
// no-op for GCS
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) objectPath(path string) string {
|
||||
return filepath.Join(g.prefix, path)
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) objectHandle(path string) *storage.ObjectHandle {
|
||||
obj := g.bucket.Object(g.objectPath(path))
|
||||
if g.encryptionKey != "" {
|
||||
obj = obj.Key([]byte(g.encryptionKey))
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// PutFile puts file into published storage at specified path.
|
||||
func (g *PublishedStorage) PutFile(path string, sourceFilename string) error {
|
||||
source, err := os.Open(sourceFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: PutFile '%s'", path)
|
||||
}
|
||||
|
||||
err = g.putFile(path, source, "")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error uploading %s to %s", sourceFilename, g))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) applyACL(obj *storage.ObjectHandle) error {
|
||||
switch g.acl {
|
||||
case "", "none", "private":
|
||||
return nil
|
||||
case "public-read":
|
||||
return obj.ACL().Set(context.TODO(), storage.AllUsers, storage.RoleReader)
|
||||
default:
|
||||
return fmt.Errorf("unsupported GCS ACL value: %s", g.acl)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) putFile(path string, source io.Reader, sourceMD5 string) error {
|
||||
obj := g.objectHandle(path)
|
||||
writer := obj.NewWriter(context.TODO())
|
||||
|
||||
if g.storageClass != "" {
|
||||
writer.StorageClass = g.storageClass
|
||||
}
|
||||
if sourceMD5 != "" {
|
||||
writer.Metadata = map[string]string{"Md5": sourceMD5}
|
||||
}
|
||||
|
||||
if _, err := io.Copy(writer, source); err != nil {
|
||||
_ = writer.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.applyACL(obj)
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) getMD5(path string) (string, error) {
|
||||
attrs, err := g.objectHandle(path).Attrs(context.TODO())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if attrs.Metadata != nil {
|
||||
if md5, ok := attrs.Metadata["Md5"]; ok && md5 != "" {
|
||||
return strings.ToLower(md5), nil
|
||||
}
|
||||
}
|
||||
|
||||
return strings.ToLower(hex.EncodeToString(attrs.MD5)), nil
|
||||
}
|
||||
|
||||
// Remove removes single file under public path.
|
||||
func (g *PublishedStorage) Remove(path string) error {
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: Remove '%s'", path)
|
||||
}
|
||||
|
||||
err := g.objectHandle(path).Delete(context.TODO())
|
||||
if err != nil {
|
||||
if err == storage.ErrObjectNotExist {
|
||||
return nil
|
||||
}
|
||||
|
||||
var apiErr *googleapi.Error
|
||||
if errors.As(err, &apiErr) && apiErr.Code == 404 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, fmt.Sprintf("error deleting %s from %s", path, g))
|
||||
}
|
||||
|
||||
delete(g.pathCache, path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDirs removes directory structure under public path.
|
||||
func (g *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error {
|
||||
filelist, _, err := g.internalFilelist(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: RemoveDirs '%s'", path)
|
||||
}
|
||||
|
||||
for _, file := range filelist {
|
||||
objPath := filepath.Join(path, file)
|
||||
if err := g.Remove(objPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = g.disableMultiDel
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LinkFromPool links package file from pool to dist's pool location.
|
||||
func (g *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool aptly.PackagePool,
|
||||
sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
|
||||
publishedDirectory := filepath.Join(publishedPrefix, publishedRelPath)
|
||||
relPath := filepath.Join(publishedDirectory, fileName)
|
||||
poolPath := filepath.Join(g.prefix, relPath)
|
||||
|
||||
if g.pathCache == nil {
|
||||
paths, md5s, err := g.internalFilelist(filepath.Join(publishedPrefix, "pool"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error caching paths under prefix")
|
||||
}
|
||||
|
||||
g.pathCache = make(map[string]string, len(paths))
|
||||
for i := range paths {
|
||||
g.pathCache[filepath.Join(publishedPrefix, "pool", paths[i])] = md5s[i]
|
||||
}
|
||||
}
|
||||
|
||||
destinationMD5, exists := g.pathCache[relPath]
|
||||
sourceMD5 := strings.ToLower(sourceChecksums.MD5)
|
||||
|
||||
if exists {
|
||||
if sourceMD5 == "" {
|
||||
return fmt.Errorf("unable to compare object, MD5 checksum missing")
|
||||
}
|
||||
|
||||
if len(destinationMD5) != 32 {
|
||||
var err error
|
||||
destinationMD5, err = g.getMD5(relPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error verifying MD5 for %s: %s", g, poolPath))
|
||||
}
|
||||
g.pathCache[relPath] = destinationMD5
|
||||
}
|
||||
|
||||
if destinationMD5 == sourceMD5 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !force {
|
||||
return fmt.Errorf("error putting file to %s: file already exists and is different: %s", poolPath, g)
|
||||
}
|
||||
}
|
||||
|
||||
source, err := sourcePool.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: LinkFromPool '%s'", relPath)
|
||||
}
|
||||
|
||||
err = g.putFile(relPath, source, sourceMD5)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error uploading %s to %s: %s", sourcePath, g, poolPath))
|
||||
}
|
||||
|
||||
g.pathCache[relPath] = sourceMD5
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filelist returns list of files under prefix.
|
||||
func (g *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
paths, _, err := g.internalFilelist(prefix)
|
||||
return paths, err
|
||||
}
|
||||
|
||||
func (g *PublishedStorage) internalFilelist(prefix string) ([]string, []string, error) {
|
||||
paths := make([]string, 0, 1024)
|
||||
md5s := make([]string, 0, 1024)
|
||||
|
||||
fullPrefix := filepath.Join(g.prefix, prefix)
|
||||
if fullPrefix != "" {
|
||||
fullPrefix += "/"
|
||||
}
|
||||
|
||||
it := g.bucket.Objects(context.TODO(), &storage.Query{Prefix: fullPrefix})
|
||||
for {
|
||||
attrs, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessagef(err, "error listing under prefix %s in %s", fullPrefix, g)
|
||||
}
|
||||
|
||||
path := attrs.Name
|
||||
if fullPrefix != "" {
|
||||
path = strings.TrimPrefix(path, fullPrefix)
|
||||
}
|
||||
paths = append(paths, path)
|
||||
|
||||
if attrs.Metadata != nil {
|
||||
if md5, ok := attrs.Metadata["Md5"]; ok && md5 != "" {
|
||||
md5s = append(md5s, strings.ToLower(md5))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
md5s = append(md5s, strings.ToLower(hex.EncodeToString(attrs.MD5)))
|
||||
}
|
||||
|
||||
return paths, md5s, nil
|
||||
}
|
||||
|
||||
// RenameFile renames (moves) file.
|
||||
func (g *PublishedStorage) RenameFile(oldName, newName string) error {
|
||||
src := g.objectHandle(oldName)
|
||||
dst := g.objectHandle(newName)
|
||||
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: RenameFile %s -> %s", oldName, newName)
|
||||
}
|
||||
|
||||
_, err := dst.CopierFrom(src).Run(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", oldName, newName, g, err)
|
||||
}
|
||||
|
||||
err = g.applyACL(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.Remove(oldName)
|
||||
}
|
||||
|
||||
// SymLink creates a copy of src file and stores link information in metadata.
|
||||
func (g *PublishedStorage) SymLink(src string, dst string) error {
|
||||
source := g.objectHandle(src)
|
||||
dest := g.objectHandle(dst)
|
||||
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: SymLink %s -> %s", src, dst)
|
||||
}
|
||||
|
||||
_, err := dest.CopierFrom(source).Run(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error symlinking %s -> %s in %s: %s", src, dst, g, err)
|
||||
}
|
||||
|
||||
_, err = dest.Update(context.TODO(), storage.ObjectAttrsToUpdate{
|
||||
Metadata: map[string]string{"SymLink": src},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating symlink metadata %s -> %s in %s: %s", src, dst, g, err)
|
||||
}
|
||||
|
||||
return g.applyACL(dest)
|
||||
}
|
||||
|
||||
// HardLink uses symlink functionality as hard links do not exist on object stores.
|
||||
func (g *PublishedStorage) HardLink(src string, dst string) error {
|
||||
if g.debug {
|
||||
log.Debug().Msgf("GCS: HardLink %s -> %s", src, dst)
|
||||
}
|
||||
|
||||
return g.SymLink(src, dst)
|
||||
}
|
||||
|
||||
// FileExists returns true if path exists.
|
||||
func (g *PublishedStorage) FileExists(path string) (bool, error) {
|
||||
_, err := g.objectHandle(path).Attrs(context.TODO())
|
||||
if err != nil {
|
||||
if err == storage.ErrObjectNotExist {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var apiErr *googleapi.Error
|
||||
if errors.As(err, &apiErr) && apiErr.Code == 404 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ReadLink returns symbolic link target from metadata.
|
||||
func (g *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
attrs, err := g.objectHandle(path).Attrs(context.TODO())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return attrs.Metadata["SymLink"], nil
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
package gcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/fsouza/fake-gcs-server/fakestorage"
|
||||
. "gopkg.in/check.v1"
|
||||
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
)
|
||||
|
||||
type PublishedStorageSuite struct {
|
||||
srv *fakestorage.Server
|
||||
prevEmulatorHost string
|
||||
prevEmulatorHostSet bool
|
||||
storage, prefixedStorage *PublishedStorage
|
||||
noSuchBucketStorage *PublishedStorage
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishedStorageSuite{})
|
||||
|
||||
func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
var err error
|
||||
s.srv, err = fakestorage.NewServerWithOptions(fakestorage.Options{
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.srv, NotNil)
|
||||
|
||||
s.srv.CreateBucketWithOpts(fakestorage.CreateBucketOpts{Name: "test"})
|
||||
|
||||
// The cloud.google.com/go/storage client honors STORAGE_EMULATOR_HOST and
|
||||
// will route all requests (including media uploads) to the fake server.
|
||||
s.prevEmulatorHost, s.prevEmulatorHostSet = os.LookupEnv("STORAGE_EMULATOR_HOST")
|
||||
c.Assert(os.Setenv("STORAGE_EMULATOR_HOST", s.srv.URL()), IsNil)
|
||||
|
||||
s.storage, err = NewPublishedStorage("test", "", "", "", "", "", "", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
s.prefixedStorage, err = NewPublishedStorage("test", "lala", "", "", "", "", "", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
s.noSuchBucketStorage, err = NewPublishedStorage("no-bucket", "", "", "", "", "", "", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TearDownTest(c *C) {
|
||||
if s.prevEmulatorHostSet {
|
||||
_ = os.Setenv("STORAGE_EMULATOR_HOST", s.prevEmulatorHost)
|
||||
} else {
|
||||
_ = os.Unsetenv("STORAGE_EMULATOR_HOST")
|
||||
}
|
||||
s.srv.Stop()
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
r, err := s.storage.bucket.Object(path).NewReader(context.TODO())
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = r.Close() }()
|
||||
|
||||
body, err := io.ReadAll(r)
|
||||
c.Assert(err, IsNil)
|
||||
return body
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) AssertNoFile(c *C, path string) {
|
||||
_, err := s.storage.bucket.Object(path).Attrs(context.TODO())
|
||||
c.Assert(errors.Is(err, storage.ErrObjectNotExist), Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) PutFile(c *C, path string, data []byte) {
|
||||
w := s.storage.bucket.Object(path).NewWriter(context.TODO())
|
||||
_, err := w.Write(data)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(w.Close(), IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestString(c *C) {
|
||||
c.Check(s.storage.String(), Equals, "GCS: test/")
|
||||
c.Check(s.prefixedStorage.String(), Equals, "GCS: test/lala")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestMkDir(c *C) {
|
||||
c.Check(s.storage.MkDir("anything"), IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestApplyACLNoOpModes(c *C) {
|
||||
for _, acl := range []string{"", "none", "private"} {
|
||||
st := &PublishedStorage{acl: acl}
|
||||
c.Check(st.applyACL(nil), IsNil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestApplyACLUnsupported(c *C) {
|
||||
st := &PublishedStorage{acl: "bucket-owner-full-control"}
|
||||
err := st.applyACL(nil)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, ErrorMatches, "unsupported GCS ACL value: bucket-owner-full-control")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), []byte("welcome to gcs!"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("a/b.txt", filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "a/b.txt"), DeepEquals, []byte("welcome to gcs!"))
|
||||
|
||||
err = s.prefixedStorage.PutFile("a/b.txt", filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "lala/a/b.txt"), DeepEquals, []byte("welcome to gcs!"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFileMissingSource(c *C) {
|
||||
err := s.storage.PutFile("a/b.txt", filepath.Join(c.MkDir(), "does-not-exist"))
|
||||
c.Check(err, ErrorMatches, ".*no such file or directory.*")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelist(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a", "test/b", "lala/a", "lala/b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
sort.Strings(list)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a", "lala/b", "lala/c", "test/a", "test/b", "testa"})
|
||||
|
||||
list, err = s.storage.Filelist("test")
|
||||
c.Check(err, IsNil)
|
||||
sort.Strings(list)
|
||||
c.Check(list, DeepEquals, []string{"a", "b"})
|
||||
|
||||
list, err = s.storage.Filelist("test2")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
list, err = s.prefixedStorage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
sort.Strings(list)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemove(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.Remove("a/b")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b")
|
||||
|
||||
s.PutFile(c, "lala/xyz", []byte("test"))
|
||||
|
||||
err = s.prefixedStorage.Remove("xyz")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "lala/xyz")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveMissing(c *C) {
|
||||
c.Check(s.storage.Remove("does/not/exist"), IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirs(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a", "test/b", "lala/a", "lala/b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
err := s.storage.RemoveDirs("test", nil)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
sort.Strings(list)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a", "lala/b", "lala/c", "testa"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFile(c *C) {
|
||||
s.PutFile(c, "src", []byte("payload"))
|
||||
|
||||
err := s.storage.RenameFile("src", "dst")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "dst"), DeepEquals, []byte("payload"))
|
||||
s.AssertNoFile(c, "src")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestSymLink(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.SymLink("a/b", "a/b.link")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
link, err := s.storage.ReadLink("a/b.link")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(link, Equals, "a/b")
|
||||
|
||||
c.Check(s.GetFile(c, "a/b.link"), DeepEquals, []byte("test"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestHardLink(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.HardLink("a/b", "a/b.hard")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
link, err := s.storage.ReadLink("a/b.hard")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(link, Equals, "a/b")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFileExists(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
exists, err := s.storage.FileExists("a/b")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
|
||||
exists, err = s.storage.FileExists("a/b.invalid")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestObjectPath(c *C) {
|
||||
st := &PublishedStorage{prefix: "root"}
|
||||
c.Check(st.objectPath("dists/stable/Release"), Equals, filepath.Join("root", "dists/stable/Release"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
root := c.MkDir()
|
||||
pool := files.NewPackagePool(root, false)
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
c.Assert(os.WriteFile(tmpFile1, []byte("Contents"), 0644), IsNil)
|
||||
cksum1 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
c.Assert(os.WriteFile(tmpFile2, []byte("Spam"), 0644), IsNil)
|
||||
cksum2 := utils.ChecksumInfo{MD5: "e9dfd31cc505d51fc26975250750deab"}
|
||||
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "netboot/boot.img.gz")
|
||||
_ = os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
c.Assert(os.WriteFile(tmpFile3, []byte("Contents"), 0644), IsNil)
|
||||
cksum3 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
src1, err := pool.Import(tmpFile1, "mars-invaders_1.03.deb", &cksum1, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
src2, err := pool.Import(tmpFile2, "mars-invaders_1.03.deb", &cksum2, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
src3, err := pool.Import(tmpFile3, "netboot/boot.img.gz", &cksum3, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// first link from pool
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// duplicate link from pool (same MD5 → no-op)
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with conflict, no force
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src2, cksum2, false)
|
||||
c.Check(err, ErrorMatches, ".*file already exists and is different.*")
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with conflict and force
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src2, cksum2, true)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Spam"))
|
||||
|
||||
// for prefixed storage:
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// 2nd link from pool, providing wrong path for source file:
|
||||
// should hit the path cache and skip upload (which would otherwise fail).
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, "wrong-looks-like-pathcache-doesnt-work", cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "lala/pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with nested file name
|
||||
err = s.storage.LinkFromPool("", "dists/jessie/non-free/installer-i386/current/images", "netboot/boot.img.gz", pool, src3, cksum3, false)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.GetFile(c, "dists/jessie/non-free/installer-i386/current/images/netboot/boot.img.gz"), DeepEquals, []byte("Contents"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolMissingMD5(c *C) {
|
||||
publishedPrefix := "repo"
|
||||
publishedRelPath := "pool/main/a/aptly"
|
||||
fileName := "pkg.deb"
|
||||
relPath := filepath.Join(filepath.Join(publishedPrefix, publishedRelPath), fileName)
|
||||
|
||||
st := &PublishedStorage{pathCache: map[string]string{relPath: "0123456789abcdef0123456789abcdef"}}
|
||||
|
||||
err := st.LinkFromPool(publishedPrefix, publishedRelPath, fileName, nil, "", utils.ChecksumInfo{}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, ErrorMatches, "unable to compare object, MD5 checksum missing")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolDifferentMD5NoForce(c *C) {
|
||||
publishedPrefix := "repo"
|
||||
publishedRelPath := "pool/main/a/aptly"
|
||||
fileName := "pkg.deb"
|
||||
relPath := filepath.Join(filepath.Join(publishedPrefix, publishedRelPath), fileName)
|
||||
|
||||
st := &PublishedStorage{pathCache: map[string]string{relPath: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
|
||||
|
||||
err := st.LinkFromPool(publishedPrefix, publishedRelPath, fileName, nil, "", utils.ChecksumInfo{MD5: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, ErrorMatches, ".*file already exists and is different.*")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolSameMD5NoUpload(c *C) {
|
||||
publishedPrefix := "repo"
|
||||
publishedRelPath := "pool/main/a/aptly"
|
||||
fileName := "pkg.deb"
|
||||
relPath := filepath.Join(filepath.Join(publishedPrefix, publishedRelPath), fileName)
|
||||
|
||||
st := &PublishedStorage{pathCache: map[string]string{relPath: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
|
||||
|
||||
err := st.LinkFromPool(publishedPrefix, publishedRelPath, fileName, nil, "", utils.ChecksumInfo{MD5: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}, false)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// putWithMetadata uploads an object with arbitrary metadata directly via the
|
||||
// storage client, bypassing the production putFile path. Used to seed objects
|
||||
// that exercise getMD5 / metadata-handling branches.
|
||||
func (s *PublishedStorageSuite) putWithMetadata(c *C, path string, data []byte, metadata map[string]string) {
|
||||
w := s.storage.bucket.Object(path).NewWriter(context.TODO())
|
||||
w.Metadata = metadata
|
||||
_, err := w.Write(data)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(w.Close(), IsNil)
|
||||
}
|
||||
|
||||
// TestLinkFromPoolShortCachedMD5 exercises the LinkFromPool branch where the
|
||||
// path cache holds a non-32-char checksum (so getMD5 must be called to fetch
|
||||
// the real MD5 from object attrs), and along the way covers getMD5 plus the
|
||||
// Md5-metadata branch in internalFilelist.
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolShortCachedMD5(c *C) {
|
||||
root := c.MkDir()
|
||||
pool := files.NewPackagePool(root, false)
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFile := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
c.Assert(os.WriteFile(tmpFile, []byte("Contents"), 0644), IsNil)
|
||||
cksum := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
src, err := pool.Import(tmpFile, "mars-invaders_1.03.deb", &cksum, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Seed the bucket with a short Md5 metadata value so internalFilelist
|
||||
// returns it (covering the metadata branch) and LinkFromPool then has to
|
||||
// re-fetch via getMD5 because len != 32.
|
||||
relPath := filepath.Join("pool/main/m/mars-invaders", "mars-invaders_1.03.deb")
|
||||
s.putWithMetadata(c, relPath, []byte("Contents"), map[string]string{"Md5": "short"})
|
||||
|
||||
// force=true so the conflict (the seeded "short" md5 will never match
|
||||
// sourceMD5 once getMD5 normalises) resolves into a fresh upload rather
|
||||
// than an error — what matters here is that we traverse the getMD5 +
|
||||
// short-cache + internalFilelist md5-metadata branches.
|
||||
err = s.storage.LinkFromPool("", "pool/main/m/mars-invaders", "mars-invaders_1.03.deb", pool, src, cksum, true)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// TestLinkFromPoolMissingSource covers the source-pool open error path.
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolMissingSource(c *C) {
|
||||
pool := files.NewPackagePool(c.MkDir(), false)
|
||||
|
||||
err := s.storage.LinkFromPool("", "pool/x", "y.deb", pool, "non-existent-pool-key", utils.ChecksumInfo{MD5: "33333333333333333333333333333333"}, false)
|
||||
c.Check(err, ErrorMatches, ".*no such file or directory.*")
|
||||
}
|
||||
|
||||
// TestPutFilePublicReadACL covers the applyACL public-read branch and the
|
||||
// ACL().Set call against the fake server.
|
||||
func (s *PublishedStorageSuite) TestPutFilePublicReadACL(c *C) {
|
||||
st, err := NewPublishedStorage("test", "", "", "", "", "", "public-read", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
dir := c.MkDir()
|
||||
src := filepath.Join(dir, "f")
|
||||
c.Assert(os.WriteFile(src, []byte("hello"), 0644), IsNil)
|
||||
|
||||
c.Check(st.PutFile("a/b.txt", src), IsNil)
|
||||
c.Check(s.GetFile(c, "a/b.txt"), DeepEquals, []byte("hello"))
|
||||
}
|
||||
|
||||
// TestPutFileUnsupportedACL covers the default (error) branch of applyACL
|
||||
// when invoked from the production putFile flow.
|
||||
func (s *PublishedStorageSuite) TestPutFileUnsupportedACL(c *C) {
|
||||
st, err := NewPublishedStorage("test", "", "", "", "", "", "bucket-owner-full-control", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
dir := c.MkDir()
|
||||
src := filepath.Join(dir, "f")
|
||||
c.Assert(os.WriteFile(src, []byte("hello"), 0644), IsNil)
|
||||
|
||||
err = st.PutFile("a/b.txt", src)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, ErrorMatches, ".*unsupported GCS ACL value.*")
|
||||
}
|
||||
|
||||
// TestPutFileWithStorageClass covers the storageClass branch in putFile.
|
||||
func (s *PublishedStorageSuite) TestPutFileWithStorageClass(c *C) {
|
||||
st, err := NewPublishedStorage("test", "", "", "", "", "", "", "NEARLINE", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
dir := c.MkDir()
|
||||
src := filepath.Join(dir, "f")
|
||||
c.Assert(os.WriteFile(src, []byte("hi"), 0644), IsNil)
|
||||
|
||||
c.Check(st.PutFile("a/b.txt", src), IsNil)
|
||||
|
||||
attrs, err := s.storage.bucket.Object("a/b.txt").Attrs(context.TODO())
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(attrs.StorageClass, Equals, "NEARLINE")
|
||||
}
|
||||
|
||||
// TestRemoveDirsNoSuchBucket covers the internalFilelist error path inside
|
||||
// RemoveDirs (and the iterator error branch in internalFilelist itself).
|
||||
func (s *PublishedStorageSuite) TestRemoveDirsNoSuchBucket(c *C) {
|
||||
err := s.noSuchBucketStorage.RemoveDirs("a/b", nil)
|
||||
c.Check(err, ErrorMatches, ".*error listing under prefix.*")
|
||||
}
|
||||
|
||||
// TestFilelistNoSuchBucket also covers the iterator error path.
|
||||
func (s *PublishedStorageSuite) TestFilelistNoSuchBucket(c *C) {
|
||||
_, err := s.noSuchBucketStorage.Filelist("")
|
||||
c.Check(err, ErrorMatches, ".*error listing under prefix.*")
|
||||
}
|
||||
|
||||
// TestRemoveCacheEviction verifies that a successful Remove evicts the entry
|
||||
// from pathCache (covers the delete(g.pathCache, ...) line).
|
||||
func (s *PublishedStorageSuite) TestRemoveCacheEviction(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
s.storage.pathCache = map[string]string{"a/b": "deadbeefdeadbeefdeadbeefdeadbeef"}
|
||||
c.Check(s.storage.Remove("a/b"), IsNil)
|
||||
_, present := s.storage.pathCache["a/b"]
|
||||
c.Check(present, Equals, false)
|
||||
}
|
||||
|
||||
// TestDebugMode exercises the if-debug log branches across the main verbs.
|
||||
func (s *PublishedStorageSuite) TestDebugMode(c *C) {
|
||||
st, err := NewPublishedStorage("test", "", "", "", "", "", "", "", "", false, true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
dir := c.MkDir()
|
||||
src := filepath.Join(dir, "f")
|
||||
c.Assert(os.WriteFile(src, []byte("dbg"), 0644), IsNil)
|
||||
|
||||
c.Check(st.PutFile("d/a", src), IsNil)
|
||||
c.Check(st.RenameFile("d/a", "d/b"), IsNil)
|
||||
c.Check(st.SymLink("d/b", "d/b.link"), IsNil)
|
||||
c.Check(st.HardLink("d/b", "d/b.hard"), IsNil)
|
||||
c.Check(st.Remove("d/b"), IsNil)
|
||||
c.Check(st.RemoveDirs("d", nil), IsNil)
|
||||
|
||||
pool := files.NewPackagePool(c.MkDir(), false)
|
||||
cs := files.NewMockChecksumStorage()
|
||||
tmp := filepath.Join(c.MkDir(), "x.deb")
|
||||
c.Assert(os.WriteFile(tmp, []byte("Contents"), 0644), IsNil)
|
||||
cksum := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
srcKey, err := pool.Import(tmp, "x.deb", &cksum, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(st.LinkFromPool("", "pool/x", "x.deb", pool, srcKey, cksum, false), IsNil)
|
||||
}
|
||||
|
||||
// TestObjectHandleWithEncryptionKey covers the encryptionKey branch in
|
||||
// objectHandle. fsouza doesn't enforce CSEK headers but we just need to walk
|
||||
// the code path.
|
||||
func (s *PublishedStorageSuite) TestObjectHandleWithEncryptionKey(c *C) {
|
||||
st := &PublishedStorage{
|
||||
bucket: s.storage.bucket,
|
||||
bucketName: "test",
|
||||
encryptionKey: "0123456789abcdef0123456789abcdef",
|
||||
}
|
||||
c.Check(st.objectHandle("a/b"), NotNil)
|
||||
}
|
||||
|
||||
// TestReadLinkMissing covers the Attrs-error return in ReadLink.
|
||||
func (s *PublishedStorageSuite) TestReadLinkMissing(c *C) {
|
||||
_, err := s.storage.ReadLink("does/not/exist")
|
||||
c.Check(err, ErrorMatches, ".*object doesn't exist.*")
|
||||
}
|
||||
|
||||
// TestFileExistsNoSuchBucket exercises FileExists' wrapped-googleapi-404 path.
|
||||
func (s *PublishedStorageSuite) TestFileExistsNoSuchBucket(c *C) {
|
||||
exists, err := s.noSuchBucketStorage.FileExists("a/b")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
|
||||
// TestNewPublishedStorageWithEndpoint exercises the endpoint-injection branch
|
||||
// in NewPublishedStorage (the production knob, separate from the env-var path).
|
||||
func (s *PublishedStorageSuite) TestNewPublishedStorageWithEndpoint(c *C) {
|
||||
saved := os.Getenv("STORAGE_EMULATOR_HOST")
|
||||
c.Assert(os.Unsetenv("STORAGE_EMULATOR_HOST"), IsNil)
|
||||
defer func() { _ = os.Setenv("STORAGE_EMULATOR_HOST", saved) }()
|
||||
|
||||
st, err := NewPublishedStorage("test", "", "", "", "", s.srv.URL()+"/storage/v1/", "", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = st.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// TestNewPublishedStorageWithProject covers the project!="" → WithQuotaProject
|
||||
// branch. WithQuotaProject is incompatible with WithEndpoint, so this test
|
||||
// relies on the STORAGE_EMULATOR_HOST env var (still set from SetUpTest) for
|
||||
// fake-server routing.
|
||||
func (s *PublishedStorageSuite) TestNewPublishedStorageWithProject(c *C) {
|
||||
st, err := NewPublishedStorage("test", "", "", "", "fake-project", "", "", "", "", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = st.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/aptly-dev/aptly
|
||||
|
||||
go 1.24
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/AlekSi/pointer v1.1.0
|
||||
@@ -13,8 +13,8 @@ require (
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/jlaffaye/ftp v0.2.0 // indirect
|
||||
github.com/kjk/lzma v0.0.0-20120628231508-2a7c55cad4a2
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/klauspost/pgzip v1.2.5
|
||||
github.com/klauspost/compress v1.18.2
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
@@ -32,29 +32,44 @@ require (
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
|
||||
github.com/ugorji/go/codec v1.2.11
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/term v0.30.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.org/x/time v0.14.0
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.4.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect
|
||||
@@ -62,12 +77,26 @@ require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudflare/circl v1.4.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/forPelevin/gomoji v1.3.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
@@ -76,57 +105,102 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/renameio/v2 v2.0.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jfrog/archiver/v3 v3.6.1 // indirect
|
||||
github.com/jfrog/build-info-go v1.11.0 // indirect
|
||||
github.com/jfrog/gofrog v1.7.6 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/xattr v0.4.12 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.59.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.60.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5
|
||||
github.com/ProtonMail/go-crypto v1.4.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.67.1
|
||||
github.com/aws/smithy-go v1.22.1
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
|
||||
github.com/aws/smithy-go v1.24.2
|
||||
github.com/fsouza/fake-gcs-server v1.53.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jfrog/jfrog-client-go v1.55.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
go.etcd.io/etcd/client/v3 v3.5.15
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
google.golang.org/api v0.266.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/pubsub/v2 v2.4.0 h1:oMKNiBQpXImRWnHYla9uSU66ZzByZwBSCJOEs/pTKVg=
|
||||
cloud.google.com/go/pubsub/v2 v2.4.0/go.mod h1:2lS/XQKq5qtOMs6kHBK+WX1ytUC36kLl2ig3zqsGUx8=
|
||||
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
|
||||
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
|
||||
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
@@ -12,62 +39,84 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 h1:cf+OIKbkmMHBaC3u7
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1/go.mod h1:ap1dmS6vQKJxSMNiGJcq4QuUQkOynyD93gLw6MDF7ek=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo=
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg=
|
||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55 h1:jbGlDKdzAZ92NzK65hUP98ri0/r50vVVvmZsFP/nIqo=
|
||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/awalterschulze/gographviz v2.0.1+incompatible h1:XIECBRq9VPEQqkQL5pw2OtjCAdrtIgFKoJU8eT98AS8=
|
||||
github.com/awalterschulze/gographviz v2.0.1+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.67.1 h1:LXLnDfjT/P6SPIaCE86xCOjJROPn4FNB2EdN68vMK5c=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.67.1/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheggaaa/pb v1.0.25 h1:tFpebHTkI7QZx1q1rWGOKhbunhZ3fMaxTvHDWn1bH/4=
|
||||
@@ -78,23 +127,56 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/forPelevin/gomoji v1.3.0 h1:WPIOLWB1bvRYlKZnSSEevLt3IfKlLs+tK+YA9fFYlkE=
|
||||
github.com/forPelevin/gomoji v1.3.0/go.mod h1:mM6GtmCgpoQP2usDArc6GjbXrti5+FffolyQfGgPboQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fsouza/fake-gcs-server v1.53.1 h1:/gjEYut23/MMhe4daYJ5yIBGPUmLAYupgITuoWG3+jI=
|
||||
github.com/fsouza/fake-gcs-server v1.53.1/go.mod h1:kF+DadfinC7mlc1/2d/ZDHS9VyUk1hTcXJ6VwLSlzfM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
@@ -103,6 +185,25 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@@ -127,31 +228,61 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
||||
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -161,23 +292,39 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI=
|
||||
github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw=
|
||||
github.com/jfrog/build-info-go v1.11.0 h1:qEONCgaHKlW3e2y0zIwTZVbgS/ERZrPlBWEbOYJbaSU=
|
||||
github.com/jfrog/build-info-go v1.11.0/go.mod h1:szdz9+WzB7+7PGnILLUgyY+OF5qD5geBT7UGNIxibyw=
|
||||
github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s=
|
||||
github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4=
|
||||
github.com/jfrog/jfrog-client-go v1.55.0 h1:dZq7sLjUJMps8X1I5coVUChprtR7xklp7oSfmZnI48w=
|
||||
github.com/jfrog/jfrog-client-go v1.55.0/go.mod h1:/e2kaF1oZTmSRgMIk7wYna5xMtNY7Xk8ahpSNZQ2d3s=
|
||||
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
||||
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kjk/lzma v0.0.0-20120628231508-2a7c55cad4a2 h1:TVZQgMi+I83S3rCuE65HnmDO6+wFPRi3n2LOzr+tr68=
|
||||
github.com/kjk/lzma v0.0.0-20120628231508-2a7c55cad4a2/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -205,6 +352,14 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mkrautz/goar v0.0.0-20150919110319-282caa8bd9da h1:Iu5QFXIMK/YrHJ0NgUnK0rqYTTyb0ldt/rqNenAj39U=
|
||||
github.com/mkrautz/goar v0.0.0-20150919110319-282caa8bd9da/go.mod h1:NfnmoBY0gGkr3/NmI+DP/UXbZvOCurCUYAzOdYJjlOc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -219,6 +374,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/ncw/swift v1.0.53 h1:luHjjTNtekIEvHg5KdAFIBaH7bWfNkefwFnpDffSIks=
|
||||
github.com/ncw/swift v1.0.53/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
@@ -231,21 +388,34 @@ github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
|
||||
github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI=
|
||||
github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
|
||||
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
@@ -254,13 +424,20 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
|
||||
github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/smira/commander v0.0.0-20140515201010-f408b00e68d5 h1:jLFwP6SDEUHmb6QSu5n2FHseWzMio1ou1FV9p7W6p7I=
|
||||
github.com/smira/commander v0.0.0-20140515201010-f408b00e68d5/go.mod h1:XTQy55hw5s3pxmC42m7X0/b+9naXQ1rGN9Of6BGIZmU=
|
||||
github.com/smira/flag v0.0.0-20170926215700-695ea5e84e76 h1:OM075OkN4x9IB1mbzkzaKaJjFxx8Mfss8Z3E1LHwawQ=
|
||||
@@ -269,10 +446,14 @@ github.com/smira/go-ftp-protocol v0.0.0-20140829150050-066b75c2b70d h1:rvtR4+9N2
|
||||
github.com/smira/go-ftp-protocol v0.0.0-20140829150050-066b75c2b70d/go.mod h1:Jm7yHrROA5tC42gyJ5EwiR8EWp0PUy0qOc4sE7Y8Uzo=
|
||||
github.com/smira/go-xz v0.1.0 h1:1zVLT1sITUKcWNysfHMLZWJ2Yh7yJfhREsgmUdK4zb0=
|
||||
github.com/smira/go-xz v0.1.0/go.mod h1:OmdEWnIIkuLzRLHGF4YtjDzF9VFUevEcP6YxDPRqVrs=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -282,8 +463,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
@@ -292,27 +473,74 @@ github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
|
||||
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ=
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.einride.tech/aip v0.79.0 h1:19zdPlZzlUvxOA8syAFw4LkdJdXepzyTl6gt9XEeqdU=
|
||||
go.einride.tech/aip v0.79.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
@@ -320,121 +548,152 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
||||
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -443,6 +702,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -452,4 +713,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@ func NewDownloader(downLimit int64, maxTries int, progress aptly.Progress) aptly
|
||||
transport.DisableCompression = true
|
||||
initTransport(&transport)
|
||||
transport.RegisterProtocol("ftp", &protocol.FTPRoundTripper{})
|
||||
transport.RegisterProtocol("ar+https", NewGCPRoundTripper(&transport))
|
||||
|
||||
downloader := &downloaderImpl{
|
||||
progress: progress,
|
||||
@@ -240,7 +241,7 @@ func (downloader *downloaderImpl) download(req *http.Request, url, destination s
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !go1.7
|
||||
// +build !go1.7
|
||||
|
||||
package http
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
// gcpRoundTripper wraps http.RoundTripper to add Google Cloud authentication.
|
||||
// It delays GCP authentication initialization until the first actual request is made.
|
||||
// This avoids unnecessary credential loading when ar+https protocol is not actually used.
|
||||
//
|
||||
// It uses Application Default Credentials (ADC) which checks:
|
||||
// 1. GOOGLE_APPLICATION_CREDENTIALS environment variable
|
||||
// 2. gcloud auth application-default credentials
|
||||
// 3. GCE/GKE metadata server
|
||||
// See https://cloud.google.com/docs/authentication/application-default-credentials for usage details.
|
||||
type gcpRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
initOnce sync.Once
|
||||
tokenSrc oauth2.TokenSource
|
||||
initErr error
|
||||
}
|
||||
|
||||
func (t *gcpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Lazy initialization: only initialize GCP credentials on first request
|
||||
t.initOnce.Do(func() {
|
||||
creds, err := google.FindDefaultCredentials(context.Background(),
|
||||
"https://www.googleapis.com/auth/cloud-platform")
|
||||
if err != nil {
|
||||
t.initErr = fmt.Errorf("failed to find default credentials: %w", err)
|
||||
return
|
||||
}
|
||||
t.tokenSrc = creds.TokenSource
|
||||
})
|
||||
|
||||
reqCopy := req.Clone(req.Context())
|
||||
reqCopy.URL.Scheme = strings.TrimPrefix(reqCopy.URL.Scheme, "ar+")
|
||||
|
||||
// Fall back to base transport if GCP auth initialization failed
|
||||
if t.initErr != nil {
|
||||
return t.base.RoundTrip(reqCopy)
|
||||
}
|
||||
|
||||
token, err := t.tokenSrc.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OAuth2 token: %w", err)
|
||||
}
|
||||
token.SetAuthHeader(reqCopy)
|
||||
|
||||
return t.base.RoundTrip(reqCopy)
|
||||
}
|
||||
|
||||
// NewGCPRoundTripper creates a new RoundTripper that handles GCP authentication for ar+https protocol.
|
||||
func NewGCPRoundTripper(base http.RoundTripper) http.RoundTripper {
|
||||
return &gcpRoundTripper{
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TestGCPAuthTransport_RoundTrip(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
t.Error("Expected Authorization header, got none")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
transport := NewGCPRoundTripper(http.DefaultTransport)
|
||||
|
||||
if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" {
|
||||
t.Skip("Skipping test: GOOGLE_APPLICATION_CREDENTIALS not set")
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
resp, err := client.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCPAuthTransport_RoundTrip_with_dummy_tokenSource(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != "Bearer dummy-token" {
|
||||
t.Errorf("Expected Authorization header 'Bearer dummy-token', got '%s'", auth)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Use a dummy token source for testing
|
||||
transport := &gcpRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
tokenSrc: oauth2.StaticTokenSource(&oauth2.Token{
|
||||
AccessToken: "dummy-token",
|
||||
}),
|
||||
}
|
||||
transport.initOnce.Do(func() {}) // Mark as initialized for testing
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
resp, err := client.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer func(){ _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCPAuthTransport_RoundTrip_with_InvalidCredentials(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Create a temporary invalid credentials file
|
||||
tmpFile, err := os.CreateTemp("", "invalid_credentials.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %s", err)
|
||||
}
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
if _, err := tmpFile.WriteString(`{"invalid": "data"}`); err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %s", err)
|
||||
}
|
||||
_ = tmpFile.Close()
|
||||
|
||||
defaultEnv := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
_ = os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", tmpFile.Name())
|
||||
defer func() { _ = os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", defaultEnv) }()
|
||||
|
||||
transport := &gcpRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
resp, err := client.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %s", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("Expected status 403, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if transport.initErr == nil {
|
||||
t.Error("Expected init error due to invalid credentials, got none")
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -49,9 +49,9 @@ func (d *GrabDownloader) Download(ctx context.Context, url string, destination s
|
||||
|
||||
func (d *GrabDownloader) DownloadWithChecksum(ctx context.Context, url string, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool) error {
|
||||
maxTries := d.maxTries
|
||||
// FIXME: const delayMax = time.Duration(5 * time.Minute)
|
||||
// FIXME: const delayMax = time.Duration(5 * time.Minute)
|
||||
delay := time.Duration(1 * time.Second)
|
||||
// FIXME: const delayMultiplier = 2
|
||||
// FIXME: const delayMultiplier = 2
|
||||
err := fmt.Errorf("no tries available")
|
||||
for maxTries > 0 {
|
||||
err = d.download(ctx, url, destination, expected, ignoreMismatch)
|
||||
@@ -133,17 +133,17 @@ func (d *GrabDownloader) download(_ context.Context, url string, destination str
|
||||
|
||||
resp := d.client.Do(req)
|
||||
|
||||
<-resp.Done
|
||||
<-resp.Done
|
||||
// download is complete
|
||||
|
||||
// Loop:
|
||||
// for {
|
||||
// select {
|
||||
// case <-resp.Done:
|
||||
// // download is complete
|
||||
// break Loop
|
||||
// }
|
||||
// }
|
||||
// Loop:
|
||||
// for {
|
||||
// select {
|
||||
// case <-resp.Done:
|
||||
// // download is complete
|
||||
// break Loop
|
||||
// }
|
||||
// }
|
||||
err = resp.Err()
|
||||
if err != nil && err == grab.ErrBadChecksum && ignoreMismatch {
|
||||
fmt.Printf("Ignoring checksum mismatch for %s\n", url)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package jfrog handles publishing to JFrog Artifactory
|
||||
package jfrog
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package jfrog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
aptly_utils "github.com/aptly-dev/aptly/utils"
|
||||
"github.com/jfrog/jfrog-client-go/artifactory"
|
||||
"github.com/jfrog/jfrog-client-go/artifactory/auth"
|
||||
"github.com/jfrog/jfrog-client-go/artifactory/services"
|
||||
"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
|
||||
"github.com/jfrog/jfrog-client-go/config"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PublishedStorage represents published repository on JFrog Artifactory
|
||||
type PublishedStorage struct {
|
||||
manager artifactory.ArtifactoryServicesManager
|
||||
repository string
|
||||
prefix string
|
||||
plusWorkaround bool
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
||||
)
|
||||
|
||||
func createPublishedStorageConfig(url, user, password, apiKey, accessToken string) (config.Config, error) {
|
||||
artDetails := auth.NewArtifactoryDetails()
|
||||
artDetails.SetUrl(url)
|
||||
|
||||
if user == "" {
|
||||
user = os.Getenv("JFROG_USERNAME");
|
||||
}
|
||||
if password == "" {
|
||||
password = os.Getenv("JFROG_PASSWORD");
|
||||
}
|
||||
if apiKey == "" {
|
||||
apiKey = os.Getenv("JFROG_APIKEY");
|
||||
}
|
||||
if accessToken == "" {
|
||||
accessToken = os.Getenv("JFROG_ACCESSTOKEN");
|
||||
}
|
||||
|
||||
if user != "" && password != "" {
|
||||
artDetails.SetUser(user)
|
||||
artDetails.SetPassword(password)
|
||||
} else if apiKey != "" {
|
||||
artDetails.SetApiKey(apiKey)
|
||||
} else if accessToken != "" {
|
||||
artDetails.SetAccessToken(accessToken)
|
||||
}
|
||||
|
||||
return config.NewConfigBuilder().
|
||||
SetServiceDetails(artDetails).
|
||||
SetDryRun(false).
|
||||
Build()
|
||||
}
|
||||
|
||||
// NewPublishedStorageRaw creates jfrog PublishedStorage from raw connection specs
|
||||
func NewPublishedStorageRaw(
|
||||
repository, url, user, password, apiKey, accessToken, prefix string,
|
||||
plusWorkaround, debug bool,
|
||||
) (*PublishedStorage, error) {
|
||||
|
||||
serviceConfig, err := createPublishedStorageConfig(url, user, password, apiKey, accessToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error building jfrog client config")
|
||||
}
|
||||
|
||||
manager, err := artifactory.New(serviceConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating jfrog manager")
|
||||
}
|
||||
|
||||
return &PublishedStorage{
|
||||
manager: manager,
|
||||
repository: repository,
|
||||
prefix: prefix,
|
||||
plusWorkaround: plusWorkaround,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewPublishedStorage creates published storage from aptly configuration struct
|
||||
func NewPublishedStorage(
|
||||
account string, root aptly_utils.JFrogPublishRoot,
|
||||
) (*PublishedStorage, error) {
|
||||
return NewPublishedStorageRaw(
|
||||
root.Repository, root.URL, root.User, root.Password, root.APIKey, root.AccessToken,
|
||||
root.Prefix, root.PlusWorkaround, root.Debug)
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) String() string {
|
||||
return fmt.Sprintf("jfrog:%s:%s", storage.repository, storage.prefix)
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) MkDir(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) PutFile(path string, sourceFilename string) error {
|
||||
targetPath := filepath.Join(storage.repository, storage.prefix, path)
|
||||
if storage.plusWorkaround {
|
||||
targetPath = strings.Replace(targetPath, "+", "%2B", -1)
|
||||
}
|
||||
|
||||
params := services.NewUploadParams()
|
||||
params.Pattern = sourceFilename
|
||||
params.Target = targetPath
|
||||
params.Flat = true
|
||||
|
||||
_, _, err := storage.manager.UploadFiles(artifactory.UploadServiceOptions{}, params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) Remove(path string) error {
|
||||
targetPath := filepath.Join(storage.repository, storage.prefix, path)
|
||||
if storage.plusWorkaround {
|
||||
targetPath = strings.Replace(targetPath, "+", "%2B", -1)
|
||||
}
|
||||
|
||||
deleteParams := services.NewDeleteParams()
|
||||
deleteParams.SetPattern(targetPath)
|
||||
|
||||
res, err := storage.manager.GetPathsToDelete(deleteParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = res.Close() }()
|
||||
_, err = storage.manager.DeleteFiles(res)
|
||||
return err
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) RemoveDirs(path string, progress aptly.Progress) error {
|
||||
return storage.Remove(path)
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool aptly.PackagePool, sourcePath string, sourceMD5 aptly_utils.ChecksumInfo, force bool) error {
|
||||
sourceFilename := sourcePath
|
||||
cleanup := func() {}
|
||||
|
||||
if sourcePool != nil {
|
||||
if localPool, ok := sourcePool.(aptly.LocalPackagePool); ok {
|
||||
sourceFilename = localPool.FullPath(sourcePath)
|
||||
} else {
|
||||
src, err := sourcePool.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "aptly-jfrog-pool-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tmpFile, src); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
return err
|
||||
}
|
||||
|
||||
sourceFilename = tmpFile.Name()
|
||||
cleanup = func() {
|
||||
_ = os.Remove(sourceFilename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer cleanup()
|
||||
|
||||
return storage.PutFile(filepath.Join(publishedPrefix, publishedRelPath, fileName), sourceFilename)
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
searchPattern := filepath.Join(storage.repository, storage.prefix, prefix, "*")
|
||||
params := services.NewSearchParams()
|
||||
params.Pattern = searchPattern
|
||||
|
||||
reader, err := storage.manager.SearchFiles(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
var paths []string
|
||||
|
||||
for element := new(utils.ResultItem); reader.NextRecord(element) == nil; element = new(utils.ResultItem) {
|
||||
path := element.Path + "/" + element.Name
|
||||
relPath := strings.TrimPrefix(path, storage.repository+"/"+storage.prefix+"/")
|
||||
if storage.plusWorkaround {
|
||||
relPath = strings.Replace(relPath, "%2B", "+", -1)
|
||||
}
|
||||
paths = append(paths, relPath)
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) RenameFile(oldName, newName string) error {
|
||||
oldTarget := filepath.Join(storage.repository, storage.prefix, oldName)
|
||||
newTarget := filepath.Join(storage.repository, storage.prefix, newName)
|
||||
|
||||
if storage.plusWorkaround {
|
||||
oldTarget = strings.Replace(oldTarget, "+", "%2B", -1)
|
||||
newTarget = strings.Replace(newTarget, "+", "%2B", -1)
|
||||
}
|
||||
|
||||
params := services.NewMoveCopyParams()
|
||||
params.Pattern = oldTarget
|
||||
params.Target = newTarget
|
||||
params.Flat = true
|
||||
|
||||
_, _, err := storage.manager.Move(params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) SymLink(src string, dst string) error {
|
||||
oldTarget := filepath.Join(storage.repository, storage.prefix, src)
|
||||
newTarget := filepath.Join(storage.repository, storage.prefix, dst)
|
||||
|
||||
if storage.plusWorkaround {
|
||||
oldTarget = strings.Replace(oldTarget, "+", "%2B", -1)
|
||||
newTarget = strings.Replace(newTarget, "+", "%2B", -1)
|
||||
}
|
||||
|
||||
params := services.NewMoveCopyParams()
|
||||
params.Pattern = oldTarget
|
||||
params.Target = newTarget
|
||||
params.Flat = true
|
||||
|
||||
props := utils.NewProperties()
|
||||
props.AddProperty("SymLink", src)
|
||||
params.SetTargetProps(props)
|
||||
|
||||
_, _, err := storage.manager.Copy(params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) HardLink(src string, dst string) error {
|
||||
return storage.SymLink(src, dst)
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) FileExists(path string) (bool, error) {
|
||||
targetPath := filepath.Join(storage.repository, storage.prefix, path)
|
||||
if storage.plusWorkaround {
|
||||
targetPath = strings.Replace(targetPath, "+", "%2B", -1)
|
||||
}
|
||||
|
||||
params := services.NewSearchParams()
|
||||
params.Pattern = targetPath
|
||||
|
||||
reader, err := storage.manager.SearchFiles(params)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
length, err := reader.Length()
|
||||
isEmpty := length == 0
|
||||
return !isEmpty, err
|
||||
}
|
||||
|
||||
func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
targetPath := filepath.Join(storage.repository, storage.prefix, path)
|
||||
if storage.plusWorkaround {
|
||||
targetPath = strings.Replace(targetPath, "+", "%2B", -1)
|
||||
}
|
||||
|
||||
props, err := storage.manager.GetItemProps(targetPath)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
for k, v := range props.Properties {
|
||||
if k == "SymLink" && len(v) > 0 {
|
||||
return v[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
package jfrog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
aptly_utils "github.com/aptly-dev/aptly/utils"
|
||||
"github.com/jfrog/jfrog-client-go/artifactory"
|
||||
"github.com/jfrog/jfrog-client-go/artifactory/services"
|
||||
jfrogutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
|
||||
"github.com/jfrog/jfrog-client-go/utils/io/content"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Setenv("JFROG_USERNAME", "userfromenv")
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type fakeJFrogManager struct {
|
||||
artifactory.EmptyArtifactoryServicesManager
|
||||
|
||||
uploadParams []services.UploadParams
|
||||
uploadErr error
|
||||
|
||||
deleteParams []services.DeleteParams
|
||||
getPathsToDelete *content.ContentReader
|
||||
getPathsDeleteErr error
|
||||
deleteErr error
|
||||
deleteCalled bool
|
||||
|
||||
searchParams []services.SearchParams
|
||||
searchReader *content.ContentReader
|
||||
searchErr error
|
||||
|
||||
moveParams []services.MoveCopyParams
|
||||
moveErr error
|
||||
|
||||
copyParams []services.MoveCopyParams
|
||||
copyErr error
|
||||
|
||||
itemProps *jfrogutils.ItemProperties
|
||||
itemPropsErr error
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) UploadFiles(_ artifactory.UploadServiceOptions, params ...services.UploadParams) (int, int, error) {
|
||||
m.uploadParams = append(m.uploadParams, params...)
|
||||
return len(params), 0, m.uploadErr
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) GetPathsToDelete(params services.DeleteParams) (*content.ContentReader, error) {
|
||||
m.deleteParams = append(m.deleteParams, params)
|
||||
if m.getPathsDeleteErr != nil {
|
||||
return nil, m.getPathsDeleteErr
|
||||
}
|
||||
if m.getPathsToDelete != nil {
|
||||
return m.getPathsToDelete, nil
|
||||
}
|
||||
return content.NewEmptyContentReader("results"), nil
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) DeleteFiles(_ *content.ContentReader) (int, error) {
|
||||
m.deleteCalled = true
|
||||
return 1, m.deleteErr
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) SearchFiles(params services.SearchParams) (*content.ContentReader, error) {
|
||||
m.searchParams = append(m.searchParams, params)
|
||||
if m.searchErr != nil {
|
||||
return nil, m.searchErr
|
||||
}
|
||||
if m.searchReader != nil {
|
||||
return m.searchReader, nil
|
||||
}
|
||||
return content.NewEmptyContentReader("results"), nil
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) Move(params ...services.MoveCopyParams) (int, int, error) {
|
||||
m.moveParams = append(m.moveParams, params...)
|
||||
return len(params), 0, m.moveErr
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) Copy(params ...services.MoveCopyParams) (int, int, error) {
|
||||
m.copyParams = append(m.copyParams, params...)
|
||||
return len(params), 0, m.copyErr
|
||||
}
|
||||
|
||||
func (m *fakeJFrogManager) GetItemProps(_ string) (*jfrogutils.ItemProperties, error) {
|
||||
if m.itemPropsErr != nil {
|
||||
return nil, m.itemPropsErr
|
||||
}
|
||||
if m.itemProps != nil {
|
||||
return m.itemProps, nil
|
||||
}
|
||||
return &jfrogutils.ItemProperties{}, nil
|
||||
}
|
||||
|
||||
type resultFixture struct {
|
||||
Results []jfrogutils.ResultItem `json:"results"`
|
||||
}
|
||||
|
||||
type fakeLocalPool struct{}
|
||||
|
||||
func (p *fakeLocalPool) Verify(string, string, *aptly_utils.ChecksumInfo, aptly.ChecksumStorage) (string, bool, error) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Import(string, string, *aptly_utils.ChecksumInfo, bool, aptly.ChecksumStorage) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) LegacyPath(string, *aptly_utils.ChecksumInfo) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Size(string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Open(string) (aptly.ReadSeekerCloser, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) FilepathList(aptly.Progress) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Remove(string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Stat(string) (os.FileInfo, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) GenerateTempPath(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Link(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Symlink(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) FullPath(path string) string {
|
||||
return filepath.Join("/var/lib/aptly/pool", path)
|
||||
}
|
||||
|
||||
type fakeRemotePool struct {
|
||||
openPath string
|
||||
openErr error
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) Verify(string, string, *aptly_utils.ChecksumInfo, aptly.ChecksumStorage) (string, bool, error) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) Import(string, string, *aptly_utils.ChecksumInfo, bool, aptly.ChecksumStorage) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) LegacyPath(string, *aptly_utils.ChecksumInfo) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) Size(string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) Open(string) (aptly.ReadSeekerCloser, error) {
|
||||
if p.openErr != nil {
|
||||
return nil, p.openErr
|
||||
}
|
||||
return os.Open(p.openPath)
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) FilepathList(aptly.Progress) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *fakeRemotePool) Remove(string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func createReader(c *C, results []jfrogutils.ResultItem) *content.ContentReader {
|
||||
filePath := filepath.Join(c.MkDir(), "results.json")
|
||||
data, err := json.Marshal(resultFixture{Results: results})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(os.WriteFile(filePath, data, 0o644), IsNil)
|
||||
return content.NewContentReader(filePath, "results")
|
||||
}
|
||||
|
||||
type PublishedStorageSuite struct {
|
||||
manager *fakeJFrogManager
|
||||
storage *PublishedStorage
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishedStorageSuite{})
|
||||
|
||||
func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
s.manager = &fakeJFrogManager{}
|
||||
s.storage = &PublishedStorage{
|
||||
manager: s.manager,
|
||||
repository: "repo",
|
||||
prefix: "prefix",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestStringAndMkDir(c *C) {
|
||||
c.Assert(s.storage.String(), Equals, "jfrog:repo:prefix")
|
||||
c.Assert(s.storage.MkDir("anything"), IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
err := s.storage.PutFile("pool/main/a+b.deb", "/tmp/source.deb")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(s.manager.uploadParams), Equals, 1)
|
||||
c.Assert(s.manager.uploadParams[0].Pattern, Equals, "/tmp/source.deb")
|
||||
c.Assert(s.manager.uploadParams[0].Target, Equals, filepath.Join("repo", "prefix", "pool/main/a+b.deb"))
|
||||
c.Assert(s.manager.uploadParams[0].Flat, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFilePlusWorkaroundAndError(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.uploadErr = errors.New("upload failed")
|
||||
|
||||
err := s.storage.PutFile("pool/main/a+b.deb", "/tmp/source.deb")
|
||||
c.Assert(err, ErrorMatches, "upload failed")
|
||||
c.Assert(s.manager.uploadParams[0].Target, Equals, filepath.Join("repo", "prefix", "pool/main/a%2Bb.deb"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemove(c *C) {
|
||||
s.manager.getPathsToDelete = createReader(c, []jfrogutils.ResultItem{})
|
||||
|
||||
err := s.storage.Remove("dists/stable+main")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(s.manager.deleteParams), Equals, 1)
|
||||
c.Assert(s.manager.deleteParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "dists/stable+main"))
|
||||
c.Assert(s.manager.deleteCalled, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemovePlusWorkaround(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.getPathsToDelete = createReader(c, []jfrogutils.ResultItem{})
|
||||
|
||||
err := s.storage.Remove("pool/a+b.deb")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.manager.deleteParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "pool/a%2Bb.deb"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveErrors(c *C) {
|
||||
s.manager.getPathsDeleteErr = errors.New("search delete failed")
|
||||
err := s.storage.Remove("x")
|
||||
c.Assert(err, ErrorMatches, "search delete failed")
|
||||
|
||||
s.manager.getPathsDeleteErr = nil
|
||||
s.manager.getPathsToDelete = createReader(c, []jfrogutils.ResultItem{})
|
||||
s.manager.deleteErr = errors.New("delete failed")
|
||||
err = s.storage.Remove("x")
|
||||
c.Assert(err, ErrorMatches, "delete failed")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirsDelegatesToRemove(c *C) {
|
||||
s.manager.getPathsToDelete = createReader(c, []jfrogutils.ResultItem{})
|
||||
c.Assert(s.storage.RemoveDirs("x", nil), IsNil)
|
||||
c.Assert(len(s.manager.deleteParams), Equals, 1)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolDelegatesToPutFile(c *C) {
|
||||
err := s.storage.LinkFromPool("", "pool/main/p", "pkg.deb", nil, "/tmp/source.deb", aptly_utils.ChecksumInfo{}, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.manager.uploadParams[0].Target, Equals, filepath.Join("repo", "prefix", "pool/main/p", "pkg.deb"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolUsesLocalPoolFullPath(c *C) {
|
||||
pool := &fakeLocalPool{}
|
||||
poolPath := "e3/48/84d71bb98002bf0c775479aa31ee_accountsservice_0.6.55-0ubuntu11_amd64.deb"
|
||||
|
||||
err := s.storage.LinkFromPool("", "pool/main/p", "pkg.deb", pool, poolPath, aptly_utils.ChecksumInfo{}, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.manager.uploadParams[0].Pattern, Equals, filepath.Join("/var/lib/aptly/pool", poolPath))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPoolCopiesFromRemotePool(c *C) {
|
||||
tmpFile := filepath.Join(c.MkDir(), "source.deb")
|
||||
c.Assert(os.WriteFile(tmpFile, []byte("package-bytes"), 0o644), IsNil)
|
||||
|
||||
pool := &fakeRemotePool{openPath: tmpFile}
|
||||
err := s.storage.LinkFromPool("", "pool/main/p", "pkg.deb", pool, "hash/path/pkg.deb", aptly_utils.ChecksumInfo{}, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
uploadPath := s.manager.uploadParams[0].Pattern
|
||||
c.Assert(uploadPath, Not(Equals), "hash/path/pkg.deb")
|
||||
_, statErr := os.Stat(uploadPath)
|
||||
c.Assert(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelist(c *C) {
|
||||
s.manager.searchReader = createReader(c, []jfrogutils.ResultItem{
|
||||
{Path: "repo/prefix/pool/main/a", Name: "a.deb", Actual_Md5: "m1"},
|
||||
{Path: "repo/prefix/pool/main/b", Name: "b.deb", Actual_Md5: "m2"},
|
||||
})
|
||||
|
||||
list, err := s.storage.Filelist("pool/main")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(list, DeepEquals, []string{"pool/main/a/a.deb", "pool/main/b/b.deb"})
|
||||
c.Assert(s.manager.searchParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "pool/main", "*"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelistPlusWorkaroundAndSearchError(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.searchReader = createReader(c, []jfrogutils.ResultItem{
|
||||
{Path: "repo/prefix/pool/main", Name: "a%2Bb.deb", Actual_Md5: "m1"},
|
||||
})
|
||||
|
||||
list, err := s.storage.Filelist("pool/main")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(list, DeepEquals, []string{"pool/main/a+b.deb"})
|
||||
|
||||
s.manager.searchErr = errors.New("search failed")
|
||||
_, err = s.storage.Filelist("pool/main")
|
||||
c.Assert(err, ErrorMatches, "search failed")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFile(c *C) {
|
||||
err := s.storage.RenameFile("old+name", "new+name")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(s.manager.moveParams), Equals, 1)
|
||||
c.Assert(s.manager.moveParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "old+name"))
|
||||
c.Assert(s.manager.moveParams[0].Target, Equals, filepath.Join("repo", "prefix", "new+name"))
|
||||
c.Assert(s.manager.moveParams[0].Flat, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFilePlusWorkaroundAndError(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.moveErr = errors.New("move failed")
|
||||
err := s.storage.RenameFile("old+name", "new+name")
|
||||
c.Assert(err, ErrorMatches, "move failed")
|
||||
c.Assert(s.manager.moveParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "old%2Bname"))
|
||||
c.Assert(s.manager.moveParams[0].Target, Equals, filepath.Join("repo", "prefix", "new%2Bname"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestSymLinkAndHardLink(c *C) {
|
||||
err := s.storage.SymLink("src+name", "dst+name")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(s.manager.copyParams), Equals, 1)
|
||||
c.Assert(s.manager.copyParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "src+name"))
|
||||
c.Assert(s.manager.copyParams[0].Target, Equals, filepath.Join("repo", "prefix", "dst+name"))
|
||||
c.Assert(s.manager.copyParams[0].Flat, Equals, true)
|
||||
targetProps := s.manager.copyParams[0].TargetProps.ToMap()
|
||||
c.Assert(targetProps["SymLink"], DeepEquals, []string{"src+name"})
|
||||
|
||||
err = s.storage.HardLink("a", "b")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(s.manager.copyParams), Equals, 2)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestSymLinkPlusWorkaroundAndError(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.copyErr = errors.New("copy failed")
|
||||
|
||||
err := s.storage.SymLink("src+name", "dst+name")
|
||||
c.Assert(err, ErrorMatches, "copy failed")
|
||||
c.Assert(s.manager.copyParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "src%2Bname"))
|
||||
c.Assert(s.manager.copyParams[0].Target, Equals, filepath.Join("repo", "prefix", "dst%2Bname"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFileExists(c *C) {
|
||||
s.manager.searchReader = createReader(c, []jfrogutils.ResultItem{{Path: "repo/prefix/pool", Name: "x"}})
|
||||
ok, err := s.storage.FileExists("pool/x")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(s.manager.searchParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "pool/x"))
|
||||
|
||||
s.manager.searchReader = content.NewEmptyContentReader("results")
|
||||
ok, err = s.storage.FileExists("pool/y")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(ok, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFileExistsSearchErrorAndPlusWorkaround(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.searchErr = errors.New("search failed")
|
||||
ok, err := s.storage.FileExists("pool/a+b")
|
||||
c.Assert(ok, Equals, false)
|
||||
c.Assert(err, ErrorMatches, "search failed")
|
||||
c.Assert(s.manager.searchParams[0].Pattern, Equals, filepath.Join("repo", "prefix", "pool/a%2Bb"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestReadLink(c *C) {
|
||||
s.manager.itemProps = &jfrogutils.ItemProperties{
|
||||
Properties: map[string][]string{
|
||||
"SymLink": {"src/file"},
|
||||
},
|
||||
}
|
||||
|
||||
link, err := s.storage.ReadLink("path/to/link")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(link, Equals, "src/file")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestReadLinkNoPropertyAndErrors(c *C) {
|
||||
s.manager.itemProps = &jfrogutils.ItemProperties{Properties: map[string][]string{"Other": {"value"}}}
|
||||
link, err := s.storage.ReadLink("path/to/link")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(link, Equals, "")
|
||||
|
||||
s.manager.itemPropsErr = errors.New("props failed")
|
||||
link, err = s.storage.ReadLink("path/to/link")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(link, Equals, "")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestReadLinkPlusWorkaround(c *C) {
|
||||
s.storage.plusWorkaround = true
|
||||
s.manager.itemProps = &jfrogutils.ItemProperties{}
|
||||
_, _ = s.storage.ReadLink("a+b")
|
||||
// Ensure the method runs with plusWorkaround path conversion.
|
||||
c.Assert(s.manager.itemPropsErr, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestCreatePublishedStorageConfig(c *C) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
withUserPassword, err := createPublishedStorageConfig(server.URL, "user", "password", "", "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
withUserPasswordDetails := withUserPassword.GetServiceDetails()
|
||||
c.Assert(withUserPasswordDetails, NotNil)
|
||||
c.Assert(withUserPasswordDetails.GetUser(), Equals, "user")
|
||||
|
||||
withAPIKey, err := createPublishedStorageConfig(server.URL, "", "", "api-123", "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
withAPIKeyDetails := withAPIKey.GetServiceDetails()
|
||||
c.Assert(withAPIKeyDetails, NotNil)
|
||||
c.Assert(withAPIKeyDetails.GetApiKey(), Equals, "api-123")
|
||||
|
||||
withAccessToken, err := createPublishedStorageConfig(server.URL, "", "", "", "token")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
withAccessTokenDetails := withAccessToken.GetServiceDetails()
|
||||
c.Assert(withAccessTokenDetails, NotNil)
|
||||
c.Assert(withAccessTokenDetails.GetAccessToken(), Equals, "token")
|
||||
|
||||
withUserPasswordFromEnv, err := createPublishedStorageConfig(server.URL, "", "password", "", "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
withUserPasswordFromEnvDetails := withUserPasswordFromEnv.GetServiceDetails()
|
||||
c.Assert(withUserPasswordFromEnvDetails, NotNil)
|
||||
c.Assert(withUserPasswordFromEnvDetails.GetUser(), Equals, "userfromenv")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestNewPublishedStorageRaw(c *C) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
withUserPassword, err := NewPublishedStorageRaw("repo", server.URL, "user", "password", "", "", "prefix", true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(withUserPassword, NotNil)
|
||||
c.Assert(withUserPassword.String(), Equals, "jfrog:repo:prefix")
|
||||
|
||||
withAPIKey, err := NewPublishedStorageRaw("repo", server.URL, "", "", "api-key", "", "prefix", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(withAPIKey, NotNil)
|
||||
|
||||
withToken, err := NewPublishedStorageRaw("repo", server.URL, "", "", "", "token", "prefix", false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(withToken, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestNewPublishedStorageRawManagerError(c *C) {
|
||||
// An SSH URL causes artifactory.New() to fail (no SSH key configured),
|
||||
// exercising the error return on lines 59-61.
|
||||
_, err := NewPublishedStorageRaw("repo", "ssh://example.local/artifactory", "", "", "", "", "", false, false)
|
||||
c.Assert(err, ErrorMatches, "error creating jfrog manager: .*")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestNewPublishedStorage(c *C) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
storage, err := NewPublishedStorage("test", aptly_utils.JFrogPublishRoot{
|
||||
Repository: "repo",
|
||||
URL: server.URL,
|
||||
AccessToken: "token",
|
||||
Prefix: "pref",
|
||||
PlusWorkaround: true,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(storage, NotNil)
|
||||
c.Assert(storage.String(), Equals, "jfrog:repo:pref")
|
||||
}
|
||||
|
||||
var _ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
||||
+103
@@ -307,6 +307,66 @@ The legacy json configuration is still supported (and also supports comments):
|
||||
// }
|
||||
},
|
||||
|
||||
// GCS Endpoint Support
|
||||
//
|
||||
// aptly can be configured to publish repositories directly to Google Cloud
|
||||
// Storage\. First, publishing endpoints should be described in the aptly
|
||||
// configuration file\. Each endpoint has a name and associated settings\.
|
||||
//
|
||||
// In order to publish to GCS, specify endpoint as `gcs:endpoint\-name:` before
|
||||
// publishing prefix on the command line, e\.g\.:
|
||||
//
|
||||
// `aptly publish snapshot wheezy\-main gcs:test:`
|
||||
//
|
||||
"GcsPublishEndpoints": {
|
||||
// // Endpoint Name
|
||||
// "test": {
|
||||
|
||||
// // Bucket name
|
||||
// "bucket": "test\-bucket",
|
||||
|
||||
// // Prefix (optional)
|
||||
// // publishing under specified prefix in the bucket, defaults to
|
||||
// // no prefix (bucket root)
|
||||
// "prefix": "",
|
||||
|
||||
// // Credentials file (optional)
|
||||
// // Path to a service account credentials JSON file\. If omitted,
|
||||
// // Application Default Credentials are used\.
|
||||
// "credentialsFile": "",
|
||||
|
||||
// // Service Account JSON (optional)
|
||||
// // Inline service account credentials JSON content\.
|
||||
// "serviceAccountJSON": "",
|
||||
|
||||
// // Project (optional)
|
||||
// // Quota project to bill requests to\.
|
||||
// "project": "",
|
||||
|
||||
// // Default ACLs (optional)
|
||||
// // assign ACL to published files\. Useful values: `private` (default),
|
||||
// // `public\-read` (public repository), or `none` (don't set ACL)\.
|
||||
// "acl": "private",
|
||||
|
||||
// // Storage Class (optional)
|
||||
// // GCS storage class, e\.g\. `STANDARD`\.
|
||||
// "storageClass": "STANDARD",
|
||||
|
||||
// // Encryption Key (optional)
|
||||
// // Customer-supplied encryption key (32-byte AES-256 key) for object operations\.
|
||||
// "encryptionKey": "",
|
||||
|
||||
// // Disable MultiDel (optional)
|
||||
// // Kept for parity with S3 settings\.
|
||||
// // GCS deletes are currently performed one object at a time\.
|
||||
// "disableMultiDel": false,
|
||||
|
||||
// // Debug (optional)
|
||||
// // Enables detailed GCS operation logs
|
||||
// "debug": false
|
||||
// }
|
||||
},
|
||||
|
||||
// Swift Endpoint Support
|
||||
//
|
||||
// aptly could be configured to publish repository directly to OpenStack Swift\. First,
|
||||
@@ -615,6 +675,10 @@ gpg keyring to use when verifying Release file (could be specified multiple time
|
||||
max download tries till process fails with download error
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-appstream\fR
|
||||
download AppStream (DEP\-11) metadata
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-installer\fR
|
||||
download additional not packaged installer files
|
||||
.
|
||||
@@ -738,6 +802,10 @@ max download tries till process fails with download error
|
||||
\-\fBskip\-existing\-packages\fR
|
||||
do not check file existence for packages listed in the internal database of the mirror
|
||||
.
|
||||
.TP
|
||||
\-\fBlatest\fR
|
||||
download only latest version of each package (per architecture)
|
||||
.
|
||||
.SH "RENAMES MIRROR"
|
||||
\fBaptly\fR \fBmirror\fR \fBrename\fR \fIold\-name\fR \fInew\-name\fR
|
||||
.
|
||||
@@ -786,6 +854,10 @@ disable verification of Release file signatures
|
||||
gpg keyring to use when verifying Release file (could be specified multiple times)
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-appstream\fR
|
||||
download AppStream (DEP\-11) metadata
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-installer\fR
|
||||
download additional not packaged installer files
|
||||
.
|
||||
@@ -1561,6 +1633,14 @@ $ aptly publish repo testing
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBversion\fR
|
||||
version of the release
|
||||
.
|
||||
.TP
|
||||
\-\fBacquire\-by\-hash\fR
|
||||
provide index files by hash
|
||||
.
|
||||
@@ -1702,6 +1782,14 @@ $ aptly publish snapshot wheezy\-main
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBversion\fR
|
||||
version of the release
|
||||
.
|
||||
.TP
|
||||
\-\fBacquire\-by\-hash\fR
|
||||
provide index files by hash
|
||||
.
|
||||
@@ -2061,6 +2149,10 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBbatch\fR
|
||||
run GPG with detached tty
|
||||
.
|
||||
@@ -2167,6 +2259,14 @@ $ aptly publish update wheezy ppa
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBversion\fR
|
||||
version of the release
|
||||
.
|
||||
.TP
|
||||
\-\fBbatch\fR
|
||||
run GPG with detached tty
|
||||
.
|
||||
@@ -2452,6 +2552,9 @@ show yaml config
|
||||
.SH "ENVIRONMENT"
|
||||
If environment variable \fBHTTP_PROXY\fR is set \fBaptly\fR would use its value to proxy all HTTP requests\.
|
||||
.
|
||||
.P
|
||||
If environment variable \fBSOURCE_DATE_EPOCH\fR is set to a Unix timestamp, \fBaptly\fR would use that timestamp for the \fBDate\fR and \fBValid\-Until\fR fields in the \fBRelease\fR file when publishing\. This enables reproducible builds as specified by \fIhttps://reproducible\-builds\.org/specs/source\-date\-epoch/\fR\.
|
||||
.
|
||||
.SH "RETURN VALUES"
|
||||
\fBaptly\fR exists with:
|
||||
.
|
||||
|
||||
@@ -296,6 +296,89 @@ The legacy json configuration is still supported (and also supports comments):
|
||||
// }
|
||||
},
|
||||
|
||||
// GCS Endpoint Support
|
||||
//
|
||||
// aptly can be configured to publish repositories directly to Google Cloud
|
||||
// Storage. First, publishing endpoints should be described in the aptly
|
||||
// configuration file. Each endpoint has a name and associated settings.
|
||||
//
|
||||
// In order to publish to GCS, specify endpoint as `gcs:endpoint-name:` before
|
||||
// publishing prefix on the command line, e.g.:
|
||||
//
|
||||
// `aptly publish snapshot wheezy-main gcs:test:`
|
||||
//
|
||||
"GcsPublishEndpoints": {
|
||||
// // Endpoint Name
|
||||
// "test": {
|
||||
|
||||
// // Bucket name
|
||||
// "bucket": "test-bucket",
|
||||
|
||||
// // Prefix (optional)
|
||||
// // publishing under specified prefix in the bucket, defaults to
|
||||
// // no prefix (bucket root)
|
||||
// "prefix": "",
|
||||
|
||||
// // Credentials file (optional)
|
||||
// // Path to a service account credentials JSON file. If omitted,
|
||||
// // Application Default Credentials are used.
|
||||
// "credentialsFile": "",
|
||||
|
||||
// // Service Account JSON (optional)
|
||||
// // Inline service account credentials JSON content.
|
||||
// "serviceAccountJSON": "",
|
||||
|
||||
// // Project (optional)
|
||||
// // Quota project to bill requests to.
|
||||
// "project": "",
|
||||
|
||||
// // Default ACLs (optional)
|
||||
// // assign ACL to published files. Useful values: `private` (default),
|
||||
// // `public-read` (public repository), or `none` (don't set ACL).
|
||||
// "acl": "private",
|
||||
|
||||
// // Storage Class (optional)
|
||||
// // GCS storage class, e.g. `STANDARD`.
|
||||
// "storageClass": "STANDARD",
|
||||
|
||||
// // Encryption Key (optional)
|
||||
// // Customer-supplied encryption key (32-byte AES-256 key) for object operations.
|
||||
// "encryptionKey": "",
|
||||
|
||||
// // Disable MultiDel (optional)
|
||||
// // Kept for parity with S3 settings.
|
||||
// // GCS deletes are currently performed one object at a time.
|
||||
// "disableMultiDel": false,
|
||||
|
||||
// // Debug (optional)
|
||||
// // Enables detailed GCS operation logs
|
||||
// "debug": false
|
||||
// }
|
||||
},
|
||||
|
||||
// JFrog Artifactory Endpoint Support
|
||||
// aptly could be configured to publish repository directly to JFrog Artifactory. First,
|
||||
// endpoints should be described in aptly.conf:
|
||||
//
|
||||
// The destination Artifactory repo should be of the "generic" type, not "debian".
|
||||
// Authentication requires one of: user+pass, api key, or access token
|
||||
//
|
||||
// In order to publish to JFrog, specify endpoint as `jfrog:endpoint-name:` before
|
||||
// publishing prefix on the command line, e.g.:
|
||||
//
|
||||
// `aptly publish snapshot wheezy-main jfrog:test:`
|
||||
//
|
||||
"JFrogPublishEndpoints": {
|
||||
"test": {
|
||||
"url": "https://artifactory.example.com/artifactory/",
|
||||
"repository": "apt-local",
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
"api_key": "api_key",
|
||||
"access_token": "access_token"
|
||||
}
|
||||
}
|
||||
|
||||
// Swift Endpoint Support
|
||||
//
|
||||
// aptly could be configured to publish repository directly to OpenStack Swift. First,
|
||||
@@ -533,6 +616,11 @@ For example, default aptly display format could be presented with the following
|
||||
If environment variable `HTTP_PROXY` is set `aptly` would use its value
|
||||
to proxy all HTTP requests.
|
||||
|
||||
If environment variable `SOURCE_DATE_EPOCH` is set to a Unix timestamp,
|
||||
`aptly` would use that timestamp for the `Date` and `Valid-Until` fields
|
||||
in the `Release` file when publishing. This enables reproducible builds
|
||||
as specified by https://reproducible-builds.org/specs/source-date-epoch/.
|
||||
|
||||
## RETURN VALUES
|
||||
|
||||
`aptly` exists with:
|
||||
|
||||
+18
-4
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ var (
|
||||
type GpgSigner struct {
|
||||
gpg string
|
||||
version GPGVersion
|
||||
keyRef string
|
||||
keyRefs []string
|
||||
keyring, secretKeyring string
|
||||
passphrase, passphraseFile string
|
||||
batch bool
|
||||
@@ -35,7 +36,14 @@ func (g *GpgSigner) SetBatch(batch bool) {
|
||||
|
||||
// SetKey sets key ID to use when signing files
|
||||
func (g *GpgSigner) SetKey(keyRef string) {
|
||||
g.keyRef = keyRef
|
||||
keyRef = strings.TrimSpace(keyRef)
|
||||
if keyRef != "" {
|
||||
if g.keyRefs == nil {
|
||||
g.keyRefs = []string{keyRef}
|
||||
} else {
|
||||
g.keyRefs = append(g.keyRefs, keyRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetKeyRing allows to set custom keyring and secretkeyring
|
||||
@@ -57,8 +65,8 @@ func (g *GpgSigner) gpgArgs() []string {
|
||||
args = append(args, "--secret-keyring", g.secretKeyring)
|
||||
}
|
||||
|
||||
if g.keyRef != "" {
|
||||
args = append(args, "-u", g.keyRef)
|
||||
for _, k := range g.keyRefs {
|
||||
args = append(args, "-u", k)
|
||||
}
|
||||
|
||||
if g.passphrase != "" || g.passphraseFile != "" {
|
||||
@@ -82,6 +90,12 @@ func (g *GpgSigner) gpgArgs() []string {
|
||||
}
|
||||
}
|
||||
|
||||
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
||||
if _, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
||||
args = append(args, "--faked-system-time", epoch)
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -74,6 +75,14 @@ func (g *GoSigner) Init() error {
|
||||
},
|
||||
}
|
||||
|
||||
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
||||
if sec, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
||||
t := time.Unix(sec, 0).UTC()
|
||||
g.signerConfig.Time = func() time.Time { return t }
|
||||
g.signerConfig.NonDeterministicSignaturesViaNotation = packet.BoolPointer(false)
|
||||
}
|
||||
}
|
||||
|
||||
if g.passphraseFile != "" {
|
||||
passF, err := os.Open(g.passphraseFile)
|
||||
if err != nil {
|
||||
|
||||
+31
-8
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
@@ -51,6 +52,7 @@ type PublishedStorage struct {
|
||||
plusWorkaround bool
|
||||
disableMultiDel bool
|
||||
pathCache map[string]string
|
||||
pathCacheMutex sync.RWMutex
|
||||
|
||||
// True if the bucket encrypts objects by default.
|
||||
encryptByDefault bool
|
||||
@@ -251,7 +253,9 @@ func (storage *PublishedStorage) Remove(path string) error {
|
||||
_ = storage.Remove(strings.Replace(path, "+", " ", -1))
|
||||
}
|
||||
|
||||
storage.pathCacheMutex.Lock()
|
||||
delete(storage.pathCache, path)
|
||||
storage.pathCacheMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -280,7 +284,9 @@ func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting path %s from %s: %s", filelist[i], storage, err)
|
||||
}
|
||||
storage.pathCacheMutex.Lock()
|
||||
delete(storage.pathCache, filepath.Join(path, filelist[i]))
|
||||
storage.pathCacheMutex.Unlock()
|
||||
}
|
||||
} else {
|
||||
numParts := (len(filelist) + page - 1) / page
|
||||
@@ -313,9 +319,11 @@ func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting multiple paths from %s: %s", storage, err)
|
||||
}
|
||||
storage.pathCacheMutex.Lock()
|
||||
for i := range part {
|
||||
delete(storage.pathCache, filepath.Join(path, part[i]))
|
||||
}
|
||||
storage.pathCacheMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,20 +345,31 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
relPath := filepath.Join(publishedDirectory, fileName)
|
||||
poolPath := filepath.Join(storage.prefix, relPath)
|
||||
|
||||
if storage.pathCache == nil {
|
||||
paths, md5s, err := storage.internalFilelist(filepath.Join(publishedPrefix, "pool"), true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error caching paths under prefix")
|
||||
}
|
||||
storage.pathCacheMutex.RLock()
|
||||
cacheNil := storage.pathCache == nil
|
||||
storage.pathCacheMutex.RUnlock()
|
||||
|
||||
storage.pathCache = make(map[string]string, len(paths))
|
||||
if cacheNil {
|
||||
storage.pathCacheMutex.Lock()
|
||||
if storage.pathCache == nil {
|
||||
paths, md5s, err := storage.internalFilelist(filepath.Join(publishedPrefix, "pool"), true)
|
||||
if err != nil {
|
||||
storage.pathCacheMutex.Unlock()
|
||||
return errors.Wrap(err, "error caching paths under prefix")
|
||||
}
|
||||
|
||||
for i := range paths {
|
||||
storage.pathCache[filepath.Join("pool", paths[i])] = md5s[i]
|
||||
storage.pathCache = make(map[string]string, len(paths))
|
||||
|
||||
for i := range paths {
|
||||
storage.pathCache[filepath.Join(publishedPrefix, "pool", paths[i])] = md5s[i]
|
||||
}
|
||||
}
|
||||
storage.pathCacheMutex.Unlock()
|
||||
}
|
||||
|
||||
storage.pathCacheMutex.RLock()
|
||||
destinationMD5, exists := storage.pathCache[relPath]
|
||||
storage.pathCacheMutex.RUnlock()
|
||||
sourceMD5 := sourceChecksums.MD5
|
||||
|
||||
if exists {
|
||||
@@ -367,7 +386,9 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
err = errors.Wrap(err, fmt.Sprintf("error verifying MD5 for %s: %s", storage, poolPath))
|
||||
return err
|
||||
}
|
||||
storage.pathCacheMutex.Lock()
|
||||
storage.pathCache[relPath] = destinationMD5
|
||||
storage.pathCacheMutex.Unlock()
|
||||
}
|
||||
|
||||
if destinationMD5 == sourceMD5 {
|
||||
@@ -388,7 +409,9 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
log.Debug().Msgf("S3: LinkFromPool '%s'", relPath)
|
||||
err = storage.putFile(relPath, source, sourceMD5)
|
||||
if err == nil {
|
||||
storage.pathCacheMutex.Lock()
|
||||
storage.pathCache[relPath] = sourceMD5
|
||||
storage.pathCacheMutex.Unlock()
|
||||
} else {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s: %s", sourcePath, storage, poolPath))
|
||||
}
|
||||
|
||||
+7
-6
@@ -112,9 +112,11 @@ func NewServer(config *Config) (*Server, error) {
|
||||
buckets: make(map[string]*bucket),
|
||||
config: config,
|
||||
}
|
||||
go func() { _ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
srv.serveHTTP(w, req)
|
||||
})) }()
|
||||
go func() {
|
||||
_ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
srv.serveHTTP(w, req)
|
||||
}))
|
||||
}()
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
@@ -527,14 +529,13 @@ func (bucketResource) post(a *action) interface{} {
|
||||
// and dashes (-). You can use uppercase letters for buckets only in the
|
||||
// US Standard region.
|
||||
//
|
||||
// Must start with a number or letter
|
||||
// # Must start with a number or letter
|
||||
//
|
||||
// Must be between 3 and 255 characters long
|
||||
// # Must be between 3 and 255 characters long
|
||||
//
|
||||
// There's one extra rule (Must not be formatted as an IP address (e.g., 192.168.5.4)
|
||||
// but the real S3 server does not seem to check that rule, so we will not
|
||||
// check it either.
|
||||
//
|
||||
func validBucketName(name string) bool {
|
||||
if len(name) < 3 || len(name) > 255 {
|
||||
return false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user