mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-31 04:30:44 +00:00
Compare commits
195 Commits
v1.6.0
..
89177bbd77
| Author | SHA1 | Date | |
|---|---|---|---|
| 89177bbd77 | |||
| c0a0a44066 | |||
| b1dfa154c3 | |||
| d834c7210d | |||
| 8f6d9e5d7c | |||
| 337a95f8a4 | |||
| fe8e99115f | |||
| c7d89a7910 | |||
| 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 | |||
| d1bfd29dfd | |||
| 27ec594606 | |||
| f652a522fd | |||
| a794e87490 | |||
| 5b04d4fbe1 | |||
| 1566e193f6 | |||
| 601c8e9d52 | |||
| 8e5707dbcc | |||
| ad4d0c7b96 | |||
| a11e004943 | |||
| f605d86a4e | |||
| f8bde63081 | |||
| 887ce71005 | |||
| 87233ceafe | |||
| 27c15680e8 | |||
| cb72e2d70f | |||
| 2cafbc8484 | |||
| 6dbb28b2b8 | |||
| d8a4a28259 | |||
| 9a217171c8 | |||
| c67cafcf94 | |||
| f7057a9517 | |||
| ae5379d84a | |||
| c05068c2e8 | |||
| 22bc2f9d0f | |||
| c07bf2b108 | |||
| e447fc0f1e | |||
| e062df68c5 | |||
| 664a5cd675 | |||
| 9ef217b351 | |||
| 67bd15487d | |||
| ab18da351d | |||
| 1abb735bfa | |||
| 9397d8ab36 | |||
| 82300d6944 | |||
| cf3841e35c | |||
| 1a0bffdc51 | |||
| 666b5c9700 | |||
| 2eabc6045f | |||
| cc32e79f2a | |||
| 7074fc8856 | |||
| a7d85e5905 | |||
| cad4233d0d | |||
| 9b9894c07d | |||
| 8546cf31ce | |||
| aa0830ff0c | |||
| 4076941bd7 | |||
| 4170c9e995 | |||
| a862192bc4 | |||
| 5a18428666 | |||
| 0b5a627c84 |
@@ -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)
|
||||
|
||||
+129
-36
@@ -17,8 +17,32 @@ 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-tests
|
||||
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
|
||||
@@ -30,12 +54,12 @@ jobs:
|
||||
GOPROXY: "https://proxy.golang.org"
|
||||
|
||||
steps:
|
||||
- name: "Install packages"
|
||||
- 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
|
||||
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
|
||||
|
||||
- name: "Checkout repository"
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
@@ -45,7 +69,7 @@ jobs:
|
||||
run: |
|
||||
make flake8
|
||||
|
||||
- name: "Read go version from go.mod"
|
||||
- name: "Read Go Version"
|
||||
run: |
|
||||
gover=$(sed -n 's/^go \(.*\)/\1/p' go.mod)
|
||||
echo "Go Version: $gover"
|
||||
@@ -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:
|
||||
@@ -89,30 +102,63 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
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
|
||||
|
||||
- name: "Merge code coverage"
|
||||
- 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@latest
|
||||
~/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
|
||||
- name: "Upload Code Coverage"
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
uses: codecov/codecov-action@v5
|
||||
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 +167,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,11 +181,12 @@ jobs:
|
||||
image: ubuntu:20.04
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
env:
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
steps:
|
||||
- name: "Install packages"
|
||||
- name: "Install Build Packages"
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends make ca-certificates git curl build-essential devscripts dh-golang jq bash-completion lintian \
|
||||
@@ -148,13 +195,13 @@ jobs:
|
||||
gcc-i686-linux-gnu gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: "Checkout repository"
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Read go version from go.mod"
|
||||
- name: "Read Go Version"
|
||||
run: |
|
||||
gover=$(sed -n 's/^go \(.*\)/\1/p' go.mod)
|
||||
echo "Go Version: $gover"
|
||||
@@ -207,9 +254,27 @@ jobs:
|
||||
run: |
|
||||
.github/workflows/scripts/upload-artifacts.sh release ${{ matrix.suite }}
|
||||
|
||||
- name: "Get aptly version"
|
||||
env:
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
aptlyver=$(make -s version)
|
||||
echo "Aptly Version: $aptlyver"
|
||||
echo "VERSION=$aptlyver" >> $GITHUB_OUTPUT
|
||||
id: releaseversion
|
||||
|
||||
- name: "Upload CI Artifacts"
|
||||
if: github.ref != 'refs/heads/master' && !startsWith(github.event.ref, 'refs/tags')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.suite }}_${{ matrix.arch }}
|
||||
path: build/
|
||||
retention-days: 7
|
||||
|
||||
ci-binary-build:
|
||||
name: "Build"
|
||||
needs: test
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -221,13 +286,13 @@ jobs:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
steps:
|
||||
- name: "Checkout repository"
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Read go version from go.mod"
|
||||
- name: "Read Go Version"
|
||||
run: |
|
||||
echo "GOVER=$(sed -n 's/^go \(.*\)/\1/p' go.mod)" >> $GITHUB_OUTPUT
|
||||
id: goversion
|
||||
@@ -267,6 +332,15 @@ jobs:
|
||||
path: build/aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip
|
||||
compression-level: 0 # no compression
|
||||
|
||||
- name: "Upload CI Artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: "!startsWith(github.event.ref, 'refs/tags')"
|
||||
with:
|
||||
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
path: build/aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip
|
||||
compression-level: 0 # no compression
|
||||
retention-days: 7
|
||||
|
||||
gh-release:
|
||||
name: "Github Release"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -274,12 +348,31 @@ jobs:
|
||||
needs: ci-binary-build
|
||||
if: startsWith(github.event.ref, 'refs/tags')
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Get aptly version"
|
||||
env:
|
||||
FORCE_CI: ${{ steps.force_ci.outputs.FORCE_CI }}
|
||||
run: |
|
||||
aptlyver=$(make -s version)
|
||||
echo "Aptly Version: $aptlyver"
|
||||
echo "VERSION=$aptlyver" >> $GITHUB_OUTPUT
|
||||
id: releaseversion
|
||||
|
||||
- name: "Download Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: out/
|
||||
|
||||
- name: "Create Release Notes"
|
||||
run: |
|
||||
echo -e "## Changes\n\n" > out/release-notes.md
|
||||
dpkg-parsechangelog -S Changes | tail -n +4 >> out/release-notes.md
|
||||
|
||||
- name: "Release"
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "Aptly Release ${{ steps.releaseversion.outputs.VERSION }}"
|
||||
files: "out/**/aptly_*.zip"
|
||||
body_path: "out/release-notes.md"
|
||||
|
||||
@@ -35,7 +35,7 @@ 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
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
# 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.54.1
|
||||
version: v1.64.5
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
@@ -67,11 +67,14 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sleeptime=5
|
||||
retries=60
|
||||
wait_task()
|
||||
{
|
||||
_id=$1
|
||||
_success=0
|
||||
for t in `seq 180`
|
||||
sleep $sleeptime
|
||||
for t in `seq $retries`
|
||||
do
|
||||
jsonret=`curl -fsS -u $aptly_user:$aptly_password ${aptly_api}/api/tasks/$_id`
|
||||
_state=`echo $jsonret | jq .State`
|
||||
@@ -84,7 +87,7 @@ wait_task()
|
||||
echo Error: task failed
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
sleep $sleeptime
|
||||
done
|
||||
if [ "$_success" -ne 1 ]; then
|
||||
echo Error: task timeout
|
||||
|
||||
@@ -26,6 +26,8 @@ _testmain.go
|
||||
*.test
|
||||
|
||||
coverage.txt
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
*.pyc
|
||||
|
||||
|
||||
+10
-15
@@ -1,16 +1,11 @@
|
||||
run:
|
||||
tests: false
|
||||
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- goconst
|
||||
- gofmt
|
||||
- goimports
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- vetshadow
|
||||
settings:
|
||||
staticcheck:
|
||||
checks:
|
||||
- "all"
|
||||
- "-QF1004" # could use strings.ReplaceAll instead
|
||||
- "-QF1012" # Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...))
|
||||
- "-QF1003" # could use tagged switch
|
||||
- "-ST1000" # at least one file in a package should have a package comment
|
||||
- "-QF1001" # could apply De Morgan's law
|
||||
|
||||
@@ -68,3 +68,19 @@ List of contributors, in chronological order:
|
||||
* Blake Kostner (https://github.com/btkostner)
|
||||
* 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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -2,13 +2,32 @@ GOPATH=$(shell go env GOPATH)
|
||||
VERSION=$(shell make -s version)
|
||||
PYTHON?=python3
|
||||
BINPATH?=$(GOPATH)/bin
|
||||
GOLANGCI_LINT_VERSION=v1.54.1 # version supporting go 1.19
|
||||
GOLANGCI_LINT_VERSION=v2.0.2 # version supporting go 1.24
|
||||
COVERAGE_DIR?=$(shell mktemp -d)
|
||||
GOOS=$(shell go env GOHOSTOS)
|
||||
GOARCH=$(shell go env GOHOSTARCH)
|
||||
|
||||
# Uncomment to update system test gold files
|
||||
# CAPTURE := "--capture"
|
||||
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
|
||||
endif
|
||||
|
||||
help: ## Print this help
|
||||
@grep -E '^[a-zA-Z][a-zA-Z0-9_-]*:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@@ -50,7 +69,7 @@ swagger-install:
|
||||
echo "// @version $(VERSION)" >> docs/swagger.conf
|
||||
|
||||
azurite-start:
|
||||
azurite -l /tmp/aptly-azurite & \
|
||||
azurite -l /tmp/aptly-azurite > ~/.azurite.log 2>&1 & \
|
||||
echo $$! > ~/.azurite.pid
|
||||
|
||||
azurite-stop:
|
||||
@@ -58,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
|
||||
@@ -69,9 +88,9 @@ flake8: ## run flake8 on system test python files
|
||||
|
||||
lint: prepare
|
||||
# Install golangci-lint
|
||||
@test -f $(BINPATH)/golangci-lint || go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
@test -f $(BINPATH)/golangci-lint || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
# Running lint
|
||||
@PATH=$(BINPATH)/:$(PATH) golangci-lint run
|
||||
@NO_COLOR=true PATH=$(BINPATH)/:$(PATH) golangci-lint run --max-issues-per-linter=0 --max-same-issues=0
|
||||
|
||||
|
||||
build: prepare swagger ## Build aptly
|
||||
@@ -84,11 +103,11 @@ install:
|
||||
# go install -v
|
||||
@out=`mktemp`; if ! go install -v > $$out 2>&1; then cat $$out; rm -f $$out; echo "\nBuild failed\n"; exit 1; else rm -f $$out; fi
|
||||
|
||||
test: prepare swagger etcd-install ## Run unit tests
|
||||
test: prepare swagger etcd-install ## Run unit tests (add TEST=regex to specify which tests to run)
|
||||
@echo "\e[33m\e[1mStarting etcd ...\e[0m"
|
||||
@mkdir -p /tmp/aptly-etcd-data; system/t13_etcd/start-etcd.sh > /tmp/aptly-etcd-data/etcd.log 2>&1 &
|
||||
@echo "\e[33m\e[1mRunning go test ...\e[0m"
|
||||
go test -v ./... -gocheck.v=true -coverprofile=unit.out; echo $$? > .unit-test.ret
|
||||
faketime "$(TEST_FAKETIME)" go test -v ./... -gocheck.v=true -check.f "$(TEST)" -coverprofile=unit.out; echo $$? > .unit-test.ret
|
||||
@echo "\e[33m\e[1mStopping etcd ...\e[0m"
|
||||
@pid=`cat /tmp/etcd.pid`; kill $$pid
|
||||
@rm -f /tmp/aptly-etcd-data/etcd.log
|
||||
@@ -96,13 +115,13 @@ test: prepare swagger etcd-install ## Run unit tests
|
||||
|
||||
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) $(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"
|
||||
@@ -112,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)
|
||||
@@ -140,7 +160,7 @@ dpkg: prepare swagger ## Build debian packages
|
||||
mkdir -p build && mv ../*.deb build/ ; \
|
||||
cd build && ls -l *.deb
|
||||
|
||||
binaries: prepare swagger ## Build binary releases (FreeBSD, MacOS, Linux tar)
|
||||
binaries: prepare swagger ## Build binary releases (FreeBSD, macOS, Linux generic)
|
||||
# build aptly
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o build/tmp/aptly -ldflags='-extldflags=-static'
|
||||
# install
|
||||
@@ -162,44 +182,49 @@ binaries: prepare swagger ## Build binary releases (FreeBSD, MacOS, Linux tar)
|
||||
docker-image: ## Build aptly-dev docker image
|
||||
@docker build -f system/Dockerfile . -t aptly-dev
|
||||
|
||||
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
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
docker-unit-tests: ## Run unit tests in docker container (add TEST=regex to specify which tests to run)
|
||||
$(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 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
test \
|
||||
test TEST=$(TEST) \
|
||||
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==" \
|
||||
system-test TEST=$(TEST) \
|
||||
AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) \
|
||||
AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \
|
||||
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 tar) in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper binaries
|
||||
docker-binaries: ## Build binary releases (FreeBSD, macOS, Linux generic) in docker container
|
||||
@$(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
|
||||
@@ -216,4 +241,4 @@ clean: ## remove local build and module cache
|
||||
rm -f unit.out aptly.test VERSION docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
|
||||
find system/ -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true
|
||||
|
||||
.PHONY: help man prepare swagger version binaries build docker-release docker-system-test docker-unit-test docker-lint docker-build docker-image docker-man docker-shell docker-serve clean releasetype dpkg serve flake8
|
||||
.PHONY: help man prepare swagger version binaries build docker-release docker-system-tests docker-unit-test docker-lint docker-build docker-image docker-man docker-shell docker-serve clean releasetype dpkg serve flake8
|
||||
|
||||
+49
-40
@@ -1,7 +1,3 @@
|
||||
=====
|
||||
aptly
|
||||
=====
|
||||
|
||||
.. image:: https://github.com/aptly-dev/aptly/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/aptly-dev/aptly/actions
|
||||
|
||||
@@ -14,15 +10,18 @@ aptly
|
||||
.. image:: https://goreportcard.com/badge/github.com/aptly-dev/aptly
|
||||
:target: https://goreportcard.com/report/aptly-dev/aptly
|
||||
|
||||
aptly
|
||||
=====
|
||||
|
||||
Aptly is a swiss army knife for Debian repository management.
|
||||
|
||||
.. image:: http://www.aptly.info/img/aptly_logo.png
|
||||
:target: http://www.aptly.info/
|
||||
|
||||
Documentation is available at `http://www.aptly.info/ <http://www.aptly.info/>`_. For support please use
|
||||
mailing list `aptly-discuss <https://groups.google.com/forum/#!forum/aptly-discuss>`_.
|
||||
open `issues <https://github.com/aptly-dev/aptly/issues>`_ or `discussions <https://github.com/aptly-dev/aptly/discussions>`_.
|
||||
|
||||
Aptly features: ("+" means planned features)
|
||||
Aptly features:
|
||||
|
||||
* make mirrors of remote Debian/Ubuntu repositories, limiting by components/architectures
|
||||
* take snapshots of mirrors at any point in time, fixing state of repository at some moment of time
|
||||
@@ -32,62 +31,70 @@ Aptly features: ("+" means planned features)
|
||||
* filter repository by search query, pulling dependencies when required
|
||||
* publish self-made packages as Debian repositories
|
||||
* REST API for remote access
|
||||
* mirror repositories "as-is" (without resigning with user's key) (+)
|
||||
* support for yum repositories (+)
|
||||
|
||||
Current limitations:
|
||||
Any contributions are welcome! Please see `CONTRIBUTING.md <CONTRIBUTING.md>`_.
|
||||
|
||||
* translations are not supported yet
|
||||
Installation
|
||||
=============
|
||||
|
||||
Install Stable Version
|
||||
-----------------------
|
||||
Aptly can be installed on several operating systems.
|
||||
|
||||
To install aptly on Debian/Ubuntu, add new repository to ``/etc/apt/sources.list``::
|
||||
Debian / Ubuntu
|
||||
----------------
|
||||
|
||||
deb http://repo.aptly.info/ squeeze main
|
||||
Aptly is provided in the following debian packages:
|
||||
|
||||
And import key that is used to sign the release::
|
||||
* **aptly**: Includes the main Aptly binary, man pages, and shell completions
|
||||
* **aptly-api**: A systemd service for the REST API, using the global /etc/aptly.conf
|
||||
* **aptly-dbg**: Debug symbols for troubleshooting
|
||||
|
||||
$ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys EE727D4449467F0E
|
||||
The packages can be installed on official `Debian <https://packages.debian.org/search?keywords=aptly>`_ and `Ubuntu <https://packages.ubuntu.com/search?keywords=aptly>`_ distributions.
|
||||
|
||||
After that you can install aptly as any other software package::
|
||||
Upstream Debian Packages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
$ apt-get update
|
||||
$ apt-get install aptly
|
||||
If a newer version (not available in Debian/Ubuntu) of aptly is required, upstream debian packages (built from git tags) can be installed as follows:
|
||||
|
||||
Don't worry about squeeze part in repo name: aptly package should work on Debian squeeze+,
|
||||
Ubuntu 10.0+. Package contains aptly binary, man page and bash completion.
|
||||
Install the following APT key (as root)::
|
||||
|
||||
Other Binaries
|
||||
~~~~~~~~~~~~~~~~~
|
||||
wget -O /etc/apt/keyrings/aptly.asc https://www.aptly.info/pubkey.txt
|
||||
|
||||
Binary executables (depends almost only on libc) are available for download from `GitHub Releases <https://github.com/aptly-dev/aptly/releases>`_.
|
||||
Define Release APT sources in ``/etc/apt/sources.list.d/aptly.list``::
|
||||
|
||||
Install CI Version
|
||||
--------------------
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/release DIST main
|
||||
|
||||
More recent versions are available as CI builds (development, might be unstable).
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Debian GNU/Linux
|
||||
~~~~~~~~~~~~~~~~~
|
||||
Install aptly packages::
|
||||
|
||||
Install the following APT key::
|
||||
apt-get update
|
||||
apt-get install aptly
|
||||
apt-get install aptly-api # REST API systemd service
|
||||
|
||||
sudo wget -O /etc/apt/keyrings/aptly.asc https://www.aptly.info/pubkey.txt
|
||||
CI Builds
|
||||
~~~~~~~~~~
|
||||
|
||||
For testing new features or bugfixes, recent builds are available as CI builds (built from master, may be unstable!) and can be installed as follows:
|
||||
|
||||
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``
|
||||
|
||||
Contributing
|
||||
------------
|
||||
Note: same gpg key is used as for the Upstream Debian Packages.
|
||||
|
||||
Please follow detailed documentation in `CONTRIBUTING.md <CONTRIBUTING.md>`_.
|
||||
Other Operating Systems
|
||||
------------------------
|
||||
|
||||
Binary executables (depends almost only on libc) are available on `GitHub Releases <https://github.com/aptly-dev/aptly/releases>`_ for:
|
||||
|
||||
- macOS / darwin (amd64, arm64)
|
||||
- FreeBSD (amd64, arm64, 386, arm)
|
||||
- Generic Linux (amd64, arm64, 386, arm)
|
||||
|
||||
Integrations
|
||||
------------
|
||||
=============
|
||||
|
||||
Vagrant:
|
||||
|
||||
@@ -104,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
|
||||
@@ -124,3 +129,7 @@ GUI for aptly API:
|
||||
Scala sbt:
|
||||
|
||||
- `sbt aptly plugin <https://github.com/amalakar/sbt-aptly>`_ by Arup Malakar
|
||||
|
||||
Molior:
|
||||
|
||||
- `Molior Debian Build System <https://github.com/molior-dbs/molior>`_ by André Roth
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Creating a Release
|
||||
|
||||
- create branch release/1.x.y
|
||||
- update debian/changelog
|
||||
- create PR, merge when approved
|
||||
- on updated master, create release:
|
||||
```
|
||||
version=$(dpkg-parsechangelog -S Version)
|
||||
echo Releasing prod version $version
|
||||
git tag -a v$version -m 'aptly: release $version'
|
||||
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
|
||||
+21
-17
@@ -41,7 +41,10 @@ type aptlyVersion struct {
|
||||
// @Success 200 {object} aptlyVersion
|
||||
// @Router /api/version [get]
|
||||
func apiVersion(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"Version": aptly.Version})
|
||||
version := aptlyVersion{
|
||||
Version: aptly.Version,
|
||||
}
|
||||
c.JSON(200, version)
|
||||
}
|
||||
|
||||
type aptlyStatus struct {
|
||||
@@ -67,7 +70,8 @@ func apiReady(isReady *atomic.Value) func(*gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"Status": "Aptly is ready"})
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
c.JSON(200, status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +169,7 @@ func runTaskInBackground(name string, resources []string, proc task.Process) (ta
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer releaseDatabaseConnection()
|
||||
defer func() { _ = releaseDatabaseConnection() }()
|
||||
return proc(out, detail)
|
||||
})
|
||||
}
|
||||
@@ -174,18 +178,18 @@ func truthy(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch value.(type) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(value.(string)) {
|
||||
switch strings.ToLower(v) {
|
||||
case "n", "no", "f", "false", "0", "off":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case int:
|
||||
return !(value.(int) == 0)
|
||||
return v != 0
|
||||
case bool:
|
||||
return value.(bool)
|
||||
return v
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -210,11 +214,11 @@ func maybeRunTaskInBackground(c *gin.Context, name string, resources []string, p
|
||||
}
|
||||
|
||||
// wait for task to finish
|
||||
context.TaskList().WaitForTaskByID(task.ID)
|
||||
_, _ = context.TaskList().WaitForTaskByID(task.ID)
|
||||
|
||||
retValue, _ := context.TaskList().GetTaskReturnValueByID(task.ID)
|
||||
err, _ := context.TaskList().GetTaskErrorByID(task.ID)
|
||||
context.TaskList().DeleteTaskByID(task.ID)
|
||||
_, _ = context.TaskList().DeleteTaskByID(task.ID)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, retValue.Code, err)
|
||||
return
|
||||
@@ -282,11 +286,11 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
// filter packages by version
|
||||
if c.Request.URL.Query().Get("maximumVersion") == "1" {
|
||||
list.PrepareIndex()
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
versionQ, err := query.Parse(fmt.Sprintf("Name (%s), $Version (<= %s)", p.Name, p.Version))
|
||||
if err != nil {
|
||||
fmt.Println("filter packages by version, query string parse err: ", err)
|
||||
c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
|
||||
} else {
|
||||
tmpList, err := list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{versionQ},
|
||||
@@ -294,15 +298,15 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
|
||||
if err == nil {
|
||||
if tmpList.Len() > 0 {
|
||||
tmpList.ForEach(func(tp *deb.Package) error {
|
||||
_ = tmpList.ForEach(func(tp *deb.Package) error {
|
||||
list.Remove(tp)
|
||||
return nil
|
||||
})
|
||||
list.Add(p)
|
||||
_ = list.Add(p)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("filter packages by version, filter err: ", err)
|
||||
c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +315,7 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
@@ -322,7 +326,7 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
|
||||
}
|
||||
}
|
||||
|
||||
func AbortWithJSONError(c *gin.Context, code int, err error) *gin.Error {
|
||||
func AbortWithJSONError(c *gin.Context, code int, err error) {
|
||||
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
return c.AbortWithError(code, err)
|
||||
_ = c.AbortWithError(code, err)
|
||||
}
|
||||
|
||||
+17
-17
@@ -24,14 +24,14 @@ func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type ApiSuite struct {
|
||||
type APISuite struct {
|
||||
context *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&ApiSuite{})
|
||||
var _ = Suite(&APISuite{})
|
||||
|
||||
func createTestConfig() *os.File {
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
@@ -45,11 +45,11 @@ func createTestConfig() *os.File {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
file.Write(jsonString)
|
||||
_, _ = file.Write(jsonString)
|
||||
return file
|
||||
}
|
||||
|
||||
func (s *ApiSuite) setupContext() error {
|
||||
func (s *APISuite) setupContext() error {
|
||||
aptly.Version = "testVersion"
|
||||
file := createTestConfig()
|
||||
if nil == file {
|
||||
@@ -75,23 +75,23 @@ func (s *ApiSuite) setupContext() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ApiSuite) SetUpSuite(c *C) {
|
||||
func (s *APISuite) SetUpSuite(c *C) {
|
||||
err := s.setupContext()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TearDownSuite(c *C) {
|
||||
os.Remove(s.configFile.Name())
|
||||
func (s *APISuite) TearDownSuite(c *C) {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
s.context.Shutdown()
|
||||
}
|
||||
|
||||
func (s *ApiSuite) SetUpTest(c *C) {
|
||||
func (s *APISuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TearDownTest(c *C) {
|
||||
func (s *APISuite) TearDownTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *ApiSuite) HTTPRequest(method string, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
func (s *APISuite) HTTPRequest(method string, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
@@ -102,32 +102,32 @@ func (s *ApiSuite) HTTPRequest(method string, url string, body io.Reader) (*http
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestGinRunsInReleaseMode(c *C) {
|
||||
func (s *APISuite) TestGinRunsInReleaseMode(c *C) {
|
||||
c.Check(gin.Mode(), Equals, gin.ReleaseMode)
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestGetVersion(c *C) {
|
||||
func (s *APISuite) TestGetVersion(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/version", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Version\":\""+aptly.Version+"\"}")
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestGetReadiness(c *C) {
|
||||
func (s *APISuite) TestGetReadiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/ready", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is ready\"}")
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestGetHealthiness(c *C) {
|
||||
func (s *APISuite) TestGetHealthiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/healthy", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is healthy\"}")
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestGetMetrics(c *C) {
|
||||
func (s *APISuite) TestGetMetrics(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/metrics", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
@@ -141,7 +141,7 @@ func (s *ApiSuite) TestGetMetrics(c *C) {
|
||||
c.Check(b, Matches, ".*aptly_build_info.*version=\"testVersion\".*")
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestRepoCreate(c *C) {
|
||||
func (s *APISuite) TestRepoCreate(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "dummy",
|
||||
})
|
||||
@@ -150,7 +150,7 @@ func (s *ApiSuite) TestRepoCreate(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ApiSuite) TestTruthy(c *C) {
|
||||
func (s *APISuite) TestTruthy(c *C) {
|
||||
c.Check(truthy("no"), Equals, false)
|
||||
c.Check(truthy("n"), Equals, false)
|
||||
c.Check(truthy("off"), Equals, false)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// @Success 200 {object} string "Output"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/db/cleanup [post]
|
||||
func apiDbCleanup(c *gin.Context) {
|
||||
func apiDBCleanup(c *gin.Context) {
|
||||
resources := []string{string(task.AllResourcesKey)}
|
||||
maybeRunTaskInBackground(c, "Clean up db", resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
var err error
|
||||
@@ -109,8 +109,8 @@ func apiDbCleanup(c *gin.Context) {
|
||||
|
||||
if toDelete.Len() > 0 {
|
||||
batch := db.CreateBatch()
|
||||
toDelete.ForEach(func(ref []byte) error {
|
||||
collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
_ = toDelete.ForEach(func(ref []byte) error {
|
||||
_ = collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
+41
-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,69 @@ 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 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 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+219
-10
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -12,27 +13,60 @@ 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 {
|
||||
// Keyserver, when downloading GpgKeyIDs
|
||||
Keyserver string `json:"Keyserver" example:"hkp://keyserver.ubuntu.com:80"`
|
||||
// GpgKeyIDs to download from Keyserver, comma separated list
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500,8B48AD6246925553"`
|
||||
// Armored gpg public ket, instead of downloading from keyserver
|
||||
GpgKeyArmor string `json:"GpgKeyArmor" example:""`
|
||||
// Keyring for adding the keys (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Add ASCII armored gpg public key, do not download from keyserver
|
||||
GpgKeyArmor string `json:"GpgKeyArmor" example:""`
|
||||
|
||||
// Keyserver to download keys provided in `GpgKeyID`
|
||||
Keyserver string `json:"Keyserver" example:"hkp://keyserver.ubuntu.com:80"`
|
||||
// Keys do download from `Keyserver`, separated by space
|
||||
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)
|
||||
// @Description * By providing a `Keyserver` and one or more key IDs in `GpgKeyID`, separated by space (leave GpgKeyArmor empty)
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgAddKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/gpg [post]
|
||||
// @Router /api/gpg/key [post]
|
||||
func apiGPGAddKey(c *gin.Context) {
|
||||
b := gpgAddKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
@@ -60,7 +94,7 @@ func apiGPGAddKey(c *gin.Context) {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
defer func() { _ = os.RemoveAll(tempdir) }()
|
||||
|
||||
keypath := filepath.Join(tempdir, "key")
|
||||
keyfile, e := os.Create(keypath)
|
||||
@@ -100,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.*`)
|
||||
})
|
||||
}
|
||||
+12
-12
@@ -67,17 +67,17 @@ func (s *MiddlewareSuite) TestJSONMiddleware4xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, s.logReader)
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/", nil)
|
||||
s.logWriter.Close()
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "warn")
|
||||
@@ -130,17 +130,17 @@ func (s *MiddlewareSuite) TestJSONMiddleware2xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, s.logReader)
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy", nil)
|
||||
s.logWriter.Close()
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "info")
|
||||
@@ -153,17 +153,17 @@ func (s *MiddlewareSuite) TestJSONMiddleware5xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, s.logReader)
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/ready", nil)
|
||||
s.logWriter.Close()
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "error")
|
||||
@@ -176,17 +176,17 @@ func (s *MiddlewareSuite) TestJSONMiddlewareRaw(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, s.logReader)
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy?test=raw", nil)
|
||||
s.logWriter.Close()
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
fmt.Println(capturedOutput)
|
||||
|
||||
|
||||
+186
-59
@@ -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))
|
||||
@@ -232,6 +273,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)
|
||||
@@ -319,7 +361,7 @@ func apiMirrorsPackages(c *gin.Context) {
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
list.ForEach(func(p *deb.Package) error {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
@@ -330,29 +372,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 +507,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
|
||||
@@ -394,14 +542,6 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -418,27 +558,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))
|
||||
@@ -461,11 +580,19 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, b.IgnoreSignatures, b.SkipComponentCheck)
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, 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(), collectionFactory.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
|
||||
|
||||
@@ -481,7 +608,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), b.SkipExistingPackages)
|
||||
collectionFactory.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,7 +618,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
e := context.ReOpenDatabase()
|
||||
if e == nil {
|
||||
remote.MarkAsIdle()
|
||||
collection.Update(remote)
|
||||
_ = collection.Update(remote)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -579,7 +706,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
file.Close()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
@@ -653,7 +780,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Finalizing download...", b.Name)
|
||||
remote.FinalizeDownload(collectionFactory, out)
|
||||
_ = remote.FinalizeDownload(collectionFactory, out)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
|
||||
+67
-2
@@ -4,12 +4,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MirrorSuite struct {
|
||||
ApiSuite
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&MirrorSuite{})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type PackagesSuite struct {
|
||||
ApiSuite
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&PackagesSuite{})
|
||||
|
||||
+87
-14
@@ -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
|
||||
@@ -341,9 +359,17 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
|
||||
if b.Version != "" {
|
||||
published.Version = b.Version
|
||||
}
|
||||
|
||||
duplicate := collection.CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
@@ -376,8 +402,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 +427,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"
|
||||
@@ -462,16 +495,32 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("Unable to update: %s", err)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
@@ -487,12 +536,12 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
|
||||
result, err := published.Update(collectionFactory, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("Unable to update: %s", err)
|
||||
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())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("Unable to update: %s", err)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = collection.Update(published)
|
||||
@@ -625,7 +674,7 @@ func apiPublishAddSource(c *gin.Context) {
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -739,7 +788,7 @@ func apiPublishSetSources(c *gin.Context) {
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -789,7 +838,7 @@ func apiPublishDropChanges(c *gin.Context) {
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -869,7 +918,7 @@ func apiPublishUpdateSource(c *gin.Context) {
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -932,7 +981,7 @@ func apiPublishRemoveSource(c *gin.Context) {
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.Update(published)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save to DB: %s", err)
|
||||
@@ -955,8 +1004,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
|
||||
@@ -1021,10 +1078,26 @@ func apiPublishUpdate(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
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(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
|
||||
+61
-44
@@ -24,19 +24,19 @@ 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")
|
||||
c.Writer.Flush()
|
||||
c.Writer.WriteString("<pre>\n")
|
||||
_, _ = c.Writer.WriteString("<pre>\n")
|
||||
if len(localRepos) == 0 {
|
||||
c.Writer.WriteString("<a href=\"-/\">default</a>\n")
|
||||
_, _ = c.Writer.WriteString("<a href=\"-/\">default</a>\n")
|
||||
}
|
||||
for publishPrefix := range localRepos {
|
||||
c.Writer.WriteString(fmt.Sprintf("<a href=\"%[1]s/\">%[1]s</a>\n", publishPrefix))
|
||||
_, _ = c.Writer.WriteString(fmt.Sprintf("<a href=\"%[1]s/\">%[1]s</a>\n", publishPrefix))
|
||||
}
|
||||
c.Writer.WriteString("</pre>")
|
||||
_, _ = c.Writer.WriteString("</pre>")
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -69,17 +69,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 +102,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:""`
|
||||
}
|
||||
|
||||
@@ -107,9 +116,9 @@ type repoCreateParams struct {
|
||||
// @Description {"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}
|
||||
// @Description ```
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Consume json
|
||||
// @Param request body repoCreateParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.LocalRepo
|
||||
// @Failure 404 {object} Error "Source snapshot not found"
|
||||
// @Failure 409 {object} Error "Local repo already exists"
|
||||
@@ -171,15 +180,17 @@ type reposEditParams struct {
|
||||
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
|
||||
// @Produce json
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body reposEditParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.LocalRepo "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
@@ -193,17 +204,18 @@ func apiReposEdit(c *gin.Context) {
|
||||
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 {
|
||||
if b.Name != nil && *b.Name != name {
|
||||
_, err := collection.ByName(*b.Name)
|
||||
if err == nil {
|
||||
// already exists
|
||||
AbortWithJSONError(c, 404, err)
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("local repo with name %q already exists", *b.Name))
|
||||
return
|
||||
}
|
||||
repo.Name = *b.Name
|
||||
@@ -231,8 +243,8 @@ func apiReposEdit(c *gin.Context) {
|
||||
// @Summary Get Repository Info
|
||||
// @Description Returns basic information about local repository.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Repository name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.LocalRepo
|
||||
// @Failure 404 {object} Error "Repository not found"
|
||||
// @Router /api/repos/{name} [get]
|
||||
@@ -254,9 +266,10 @@ func apiReposShow(c *gin.Context) {
|
||||
// @Description Cannot drop repos that are published.
|
||||
// @Description Needs force=1 to drop repos used as source by other repos.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Param force query int false "force: 1 to enable"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "Repo object"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 404 {object} Error "Repo Conflict"
|
||||
@@ -306,12 +319,12 @@ func apiReposDrop(c *gin.Context) {
|
||||
// @Description ["Pi386 aptly 0.8 966561016b44ed80"]
|
||||
// @Description ```
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Snapshot to search"
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param q query string true "Package query (e.g Name%20(~%20matlab))"
|
||||
// @Param withDeps query string true "Set to 1 to include dependencies when evaluating package query"
|
||||
// @Param format query string true "Set to 'details' to return extra info about each package"
|
||||
// @Param maximumVersion query string true "Set to 1 to only return the highest version for each package name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 404 {object} Error "Internal Server Error"
|
||||
@@ -406,9 +419,11 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
// @Description
|
||||
// @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
|
||||
// @Produce json
|
||||
// @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
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
@@ -426,9 +441,11 @@ func apiReposPackagesAdd(c *gin.Context) {
|
||||
// @Description
|
||||
// @Description Any package(s) can be removed from a local repository. Package references from a local repository can be retrieved with GET /api/repos/:name/packages.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Consume json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
@@ -449,7 +466,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"
|
||||
@@ -570,7 +587,7 @@ func apiReposPackageFromDir(c *gin.Context) {
|
||||
}
|
||||
|
||||
// atempt to remove dir, if it fails, that's fine: probably it's not empty
|
||||
os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
_ = os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
}
|
||||
|
||||
if failedFiles == nil {
|
||||
@@ -607,11 +624,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 "Source repo"
|
||||
// @Param src path string true "Destination repo"
|
||||
// @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"
|
||||
@@ -762,12 +779,15 @@ func apiReposCopyPackage(c *gin.Context) {
|
||||
// @Summary Include File from Directory
|
||||
// @Description Allows automatic processing of .changes file controlling package upload (uploaded using File Upload API) to the local repository. i.e. Exposes repo include command in api.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param file path string true "File/packages to include"
|
||||
// @Param forceReplace query int false "when value is set to 1, when adding package that conflicts with existing package, remove existing package"
|
||||
// @Param noRemoveFiles query int false "when value is set to 1, don’t remove files that have been imported successfully into repository"
|
||||
// @Param acceptUnsigned query int false "when value is set to 1, accept unsigned .changes files"
|
||||
// @Param ignoreSignature query int false "when value is set to 1 disable verification of .changes file signature"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/repos/{name}/include/{dir}/{file} [post]
|
||||
@@ -776,26 +796,22 @@ func apiReposIncludePackageFromFile(c *gin.Context) {
|
||||
apiReposIncludePackageFromDir(c)
|
||||
}
|
||||
|
||||
type reposIncludePackageFromDirReport struct {
|
||||
Warnings []string
|
||||
Added []string
|
||||
Deleted []string
|
||||
}
|
||||
|
||||
type reposIncludePackageFromDirResponse struct {
|
||||
Report reposIncludePackageFromDirReport
|
||||
Report *aptly.RecordingResultReporter
|
||||
FailedFiles []string
|
||||
}
|
||||
|
||||
// @Summary Include Directory
|
||||
// @Description Allows automatic processing of .changes file controlling package upload (uploaded using File Upload API) to the local repository. i.e. Exposes repo include command in api.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param forceReplace query int false "when value is set to 1, when adding package that conflicts with existing package, remove existing package"
|
||||
// @Param noRemoveFiles query int false "when value is set to 1, don’t remove files that have been imported successfully into repository"
|
||||
// @Param acceptUnsigned query int false "when value is set to 1, accept unsigned .changes files"
|
||||
// @Param ignoreSignature query int false "when value is set to 1 disable verification of .changes file signature"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} reposIncludePackageFromDirResponse "Response"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/repos/{name}/include/{dir} [post]
|
||||
@@ -836,7 +852,7 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
}
|
||||
|
||||
var resources []string
|
||||
if len(repoTemplate.Tree.Root.Nodes) > 1 {
|
||||
if len(repoTemplate.Root.Nodes) > 1 {
|
||||
resources = append(resources, task.AllLocalReposResourcesKey)
|
||||
} else {
|
||||
// repo template string is simple text so only use resource key of specific repository
|
||||
@@ -876,7 +892,7 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
|
||||
if !noRemoveFiles {
|
||||
// atempt to remove dir, if it fails, that's fine: probably it's not empty
|
||||
os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
_ = os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
}
|
||||
|
||||
if failedFiles == nil {
|
||||
@@ -896,9 +912,10 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
out.Printf("Failed files: %s\n", strings.Join(failedFiles, ", "))
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{
|
||||
"Report": reporter,
|
||||
"FailedFiles": failedFiles,
|
||||
}}, nil
|
||||
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.*")
|
||||
}
|
||||
+21
-17
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
@@ -12,13 +11,19 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/aptly-dev/aptly/docs"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
// _ "github.com/aptly-dev/aptly/docs" // import docs
|
||||
// swaggerFiles "github.com/swaggo/files"
|
||||
// ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -56,26 +61,22 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
router.UseRawPath = true
|
||||
|
||||
if c.Config().LogFormat == "json" {
|
||||
c.StructuredLogging(true)
|
||||
utils.SetupJSONLogger(c.Config().LogLevel, os.Stdout)
|
||||
gin.DefaultWriter = utils.LogWriter{Logger: log.Logger}
|
||||
router.Use(JSONLogger())
|
||||
} else {
|
||||
c.StructuredLogging(false)
|
||||
utils.SetupDefaultLogger(c.Config().LogLevel)
|
||||
router.Use(gin.Logger())
|
||||
}
|
||||
|
||||
router.Use(gin.Recovery(), gin.ErrorLogger())
|
||||
|
||||
if c.Config().EnableSwaggerEndpoint {
|
||||
router.GET("docs.html", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
|
||||
})
|
||||
router.Use(redirectSwagger)
|
||||
url := ginSwagger.URL("/docs/doc.json")
|
||||
router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
}
|
||||
// if c.Config().EnableSwaggerEndpoint {
|
||||
// router.GET("docs.html", func(c *gin.Context) {
|
||||
// c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
|
||||
// })
|
||||
// router.Use(redirectSwagger)
|
||||
// url := ginSwagger.URL("/docs/doc.json")
|
||||
// router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
// }
|
||||
|
||||
if c.Config().EnableMetricsEndpoint {
|
||||
MetricsCollectorRegistrar.Register(router)
|
||||
@@ -163,12 +164,15 @@ 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)
|
||||
}
|
||||
|
||||
{
|
||||
@@ -220,7 +224,7 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
api.GET("/graph.:ext", apiGraph)
|
||||
}
|
||||
{
|
||||
api.POST("/db/cleanup", apiDbCleanup)
|
||||
api.POST("/db/cleanup", apiDBCleanup)
|
||||
}
|
||||
{
|
||||
api.GET("/tasks", apiTasksList)
|
||||
|
||||
+17
-9
@@ -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)
|
||||
}
|
||||
@@ -217,10 +226,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"
|
||||
@@ -555,7 +563,7 @@ func apiSnapshotsMerge(c *gin.Context) {
|
||||
}
|
||||
|
||||
if len(body.Sources) < 1 {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("At least one source snapshot is required"))
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("minimum one source snapshot is required"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -765,7 +773,7 @@ func apiSnapshotsPull(c *gin.Context) {
|
||||
addedPackages := []string{}
|
||||
alreadySeen := map[string]bool{}
|
||||
|
||||
destinationPackageList.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
_ = destinationPackageList.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
key := pkg.Architecture + "_" + pkg.Name
|
||||
_, seen := alreadySeen[key]
|
||||
|
||||
@@ -781,7 +789,7 @@ func apiSnapshotsPull(c *gin.Context) {
|
||||
|
||||
// If !allMatches, add only first matching name-arch package
|
||||
if !seen || allMatches {
|
||||
toPackageList.Add(pkg)
|
||||
_ = toPackageList.Add(pkg)
|
||||
addedPackages = append(addedPackages, pkg.String())
|
||||
}
|
||||
|
||||
|
||||
@@ -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.*")
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package aptly
|
||||
|
||||
// Default aptly.conf (filled in at link time)
|
||||
// AptlyConf holds the default aptly.conf (filled in at link time)
|
||||
var AptlyConf []byte
|
||||
|
||||
+41
-48
@@ -5,35 +5,28 @@ package azure
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
)
|
||||
|
||||
func isBlobNotFound(err error) bool {
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
return respErr.StatusCode == 404 // BlobNotFound
|
||||
}
|
||||
return false
|
||||
storageError, ok := err.(azblob.StorageError)
|
||||
return ok && storageError.ServiceCode() == azblob.ServiceCodeBlobNotFound
|
||||
}
|
||||
|
||||
type azContext struct {
|
||||
client *azblob.Client
|
||||
container string
|
||||
container azblob.ContainerURL
|
||||
prefix string
|
||||
}
|
||||
|
||||
func newAzContext(accountName, accountKey, container, prefix, endpoint string) (*azContext, error) {
|
||||
cred, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -42,14 +35,15 @@ func newAzContext(accountName, accountKey, container, prefix, endpoint string) (
|
||||
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
|
||||
}
|
||||
|
||||
serviceClient, err := azblob.NewClientWithSharedKeyCredential(endpoint, cred, nil)
|
||||
url, err := url.Parse(fmt.Sprintf("%s/%s", endpoint, container))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containerURL := azblob.NewContainerURL(*url, azblob.NewPipeline(credential, azblob.PipelineOptions{}))
|
||||
|
||||
result := &azContext{
|
||||
client: serviceClient,
|
||||
container: container,
|
||||
container: containerURL,
|
||||
prefix: prefix,
|
||||
}
|
||||
|
||||
@@ -60,6 +54,10 @@ func (az *azContext) blobPath(path string) string {
|
||||
return filepath.Join(az.prefix, path)
|
||||
}
|
||||
|
||||
func (az *azContext) blobURL(path string) azblob.BlobURL {
|
||||
return az.container.NewBlobURL(az.blobPath(path))
|
||||
}
|
||||
|
||||
func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (paths []string, md5s []string, err error) {
|
||||
const delimiter = "/"
|
||||
paths = make([]string, 0, 1024)
|
||||
@@ -69,33 +67,27 @@ func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (p
|
||||
prefix += delimiter
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
maxResults := int32(1)
|
||||
pager := az.client.NewListBlobsFlatPager(az.container, &azblob.ListBlobsFlatOptions{
|
||||
Prefix: &prefix,
|
||||
MaxResults: &maxResults,
|
||||
Include: azblob.ListBlobsInclude{Metadata: true},
|
||||
})
|
||||
|
||||
// Iterate over each page
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
for marker := (azblob.Marker{}); marker.NotDone(); {
|
||||
listBlob, err := az.container.ListBlobsFlatSegment(
|
||||
context.Background(), marker, azblob.ListBlobsSegmentOptions{
|
||||
Prefix: prefix,
|
||||
MaxResults: 1,
|
||||
Details: azblob.BlobListingDetails{Metadata: true}})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error listing under prefix %s in %s: %s", prefix, az, err)
|
||||
}
|
||||
|
||||
for _, blob := range page.Segment.BlobItems {
|
||||
if prefix == "" {
|
||||
paths = append(paths, *blob.Name)
|
||||
} else {
|
||||
name := *blob.Name
|
||||
paths = append(paths, name[len(prefix):])
|
||||
}
|
||||
b := *blob
|
||||
md5 := b.Properties.ContentMD5
|
||||
md5s = append(md5s, fmt.Sprintf("%x", md5))
|
||||
marker = listBlob.NextMarker
|
||||
|
||||
for _, blob := range listBlob.Segment.BlobItems {
|
||||
if prefix == "" {
|
||||
paths = append(paths, blob.Name)
|
||||
} else {
|
||||
paths = append(paths, blob.Name[len(prefix):])
|
||||
}
|
||||
md5s = append(md5s, fmt.Sprintf("%x", blob.Properties.ContentMD5))
|
||||
}
|
||||
|
||||
if progress != nil {
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
progress.AddBar(1)
|
||||
@@ -105,27 +97,28 @@ func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (p
|
||||
return paths, md5s, nil
|
||||
}
|
||||
|
||||
func (az *azContext) putFile(blobName string, source io.Reader, sourceMD5 string) error {
|
||||
uploadOptions := &azblob.UploadFileOptions{
|
||||
BlockSize: 4 * 1024 * 1024,
|
||||
Concurrency: 8,
|
||||
func (az *azContext) putFile(blob azblob.BlobURL, source io.Reader, sourceMD5 string) error {
|
||||
uploadOptions := azblob.UploadStreamToBlockBlobOptions{
|
||||
BufferSize: 4 * 1024 * 1024,
|
||||
MaxBuffers: 8,
|
||||
}
|
||||
|
||||
path := az.blobPath(blobName)
|
||||
if len(sourceMD5) > 0 {
|
||||
decodedMD5, err := hex.DecodeString(sourceMD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadOptions.HTTPHeaders = &blob.HTTPHeaders{
|
||||
BlobContentMD5: decodedMD5,
|
||||
uploadOptions.BlobHTTPHeaders = azblob.BlobHTTPHeaders{
|
||||
ContentMD5: decodedMD5,
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if file, ok := source.(*os.File); ok {
|
||||
_, err = az.client.UploadFile(context.TODO(), az.container, path, file, uploadOptions)
|
||||
}
|
||||
_, err := azblob.UploadStreamToBlockBlob(
|
||||
context.Background(),
|
||||
source,
|
||||
blob.ToBlockBlobURL(),
|
||||
uploadOptions,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+24
-21
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pkg/errors"
|
||||
@@ -40,7 +41,10 @@ func (pool *PackagePool) buildPoolPath(filename string, checksums *utils.Checksu
|
||||
return filepath.Join(hash[0:2], hash[2:4], hash[4:32]+"_"+filename)
|
||||
}
|
||||
|
||||
func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.ChecksumStorage) (*utils.ChecksumInfo, error) {
|
||||
func (pool *PackagePool) ensureChecksums(
|
||||
poolPath string,
|
||||
checksumStorage aptly.ChecksumStorage,
|
||||
) (*utils.ChecksumInfo, error) {
|
||||
targetChecksums, err := checksumStorage.Get(poolPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -48,7 +52,8 @@ func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.
|
||||
|
||||
if targetChecksums == nil {
|
||||
// we don't have checksums stored yet for this file
|
||||
download, err := pool.az.client.DownloadStream(context.Background(), pool.az.container, poolPath, nil)
|
||||
blob := pool.az.blobURL(poolPath)
|
||||
download, err := blob.Download(context.Background(), 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return nil, nil
|
||||
@@ -58,7 +63,7 @@ func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.
|
||||
}
|
||||
|
||||
targetChecksums = &utils.ChecksumInfo{}
|
||||
*targetChecksums, err = utils.ChecksumsForReader(download.Body)
|
||||
*targetChecksums, err = utils.ChecksumsForReader(download.Body(azblob.RetryReaderOptions{}))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error checksumming blob at %s", poolPath)
|
||||
}
|
||||
@@ -87,49 +92,46 @@ func (pool *PackagePool) LegacyPath(_ string, _ *utils.ChecksumInfo) (string, er
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Size(path string) (int64, error) {
|
||||
serviceClient := pool.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(pool.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
|
||||
props, err := blobClient.GetProperties(context.TODO(), nil)
|
||||
blob := pool.az.blobURL(path)
|
||||
props, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return *props.ContentLength, nil
|
||||
return props.ContentLength(), nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
blob := pool.az.blobURL(path)
|
||||
|
||||
temp, err := os.CreateTemp("", "blob-download")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating tempfile for %s", path)
|
||||
return nil, errors.Wrap(err, "error creating temporary file for blob download")
|
||||
}
|
||||
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
_, err = pool.az.client.DownloadFile(context.TODO(), pool.az.container, path, temp, nil)
|
||||
err = azblob.DownloadBlobToFile(context.Background(), blob, 0, 0, temp, azblob.DownloadFromBlobOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error downloading blob %s", path)
|
||||
return nil, errors.Wrapf(err, "error downloading blob at %s", path)
|
||||
}
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Remove(path string) (int64, error) {
|
||||
serviceClient := pool.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(pool.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
|
||||
props, err := blobClient.GetProperties(context.TODO(), nil)
|
||||
blob := pool.az.blobURL(path)
|
||||
props, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
return 0, errors.Wrapf(err, "error getting props of %s from %s", path, pool)
|
||||
}
|
||||
|
||||
_, err = pool.az.client.DeleteBlob(context.Background(), pool.az.container, path, nil)
|
||||
_, err = blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error deleting %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return *props.ContentLength, nil
|
||||
return props.ContentLength(), nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, _ bool, checksumStorage aptly.ChecksumStorage) (string, error) {
|
||||
@@ -143,6 +145,7 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
}
|
||||
|
||||
path := pool.buildPoolPath(basename, checksums)
|
||||
blob := pool.az.blobURL(path)
|
||||
targetChecksums, err := pool.ensureChecksums(path, checksumStorage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -158,7 +161,7 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
err = pool.az.putFile(path, source, checksums.MD5)
|
||||
err = pool.az.putFile(blob, source, checksums.MD5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
@@ -50,10 +50,8 @@ 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,
|
||||
})
|
||||
cnt := s.pool.az.container
|
||||
_, err = cnt.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessContainer)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedPool, err = NewPackagePool(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
|
||||
+58
-67
@@ -3,21 +3,20 @@ package azure
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/lease"
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PublishedStorage abstract file system with published files (actually hosted on Azure)
|
||||
type PublishedStorage struct {
|
||||
container azblob.ContainerURL
|
||||
prefix string
|
||||
az *azContext
|
||||
pathCache map[string]map[string]string
|
||||
@@ -67,7 +66,7 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(path, source, sourceMD5)
|
||||
err = storage.az.putFile(storage.az.blobURL(path), source, sourceMD5)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s", sourceFilename, storage))
|
||||
}
|
||||
@@ -77,15 +76,14 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
|
||||
// RemoveDirs removes directory structure under public path
|
||||
func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error {
|
||||
path = storage.az.blobPath(path)
|
||||
filelist, err := storage.Filelist(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, filename := range filelist {
|
||||
blob := filepath.Join(path, filename)
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, blob, nil)
|
||||
blob := storage.az.blobURL(filepath.Join(path, filename))
|
||||
_, err := blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting path %s from %s: %s", filename, storage, err)
|
||||
}
|
||||
@@ -96,8 +94,8 @@ func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error
|
||||
|
||||
// Remove removes single file under public path
|
||||
func (storage *PublishedStorage) Remove(path string) error {
|
||||
path = storage.az.blobPath(path)
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, path, nil)
|
||||
blob := storage.az.blobURL(path)
|
||||
_, err := blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error deleting %s from %s: %s", path, storage, err))
|
||||
}
|
||||
@@ -116,8 +114,9 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
|
||||
relFilePath := filepath.Join(publishedRelPath, fileName)
|
||||
prefixRelFilePath := filepath.Join(publishedPrefix, relFilePath)
|
||||
poolPath := storage.az.blobPath(prefixRelFilePath)
|
||||
// prefixRelFilePath := filepath.Join(publishedPrefix, relFilePath)
|
||||
// FIXME: check how to integrate publishedPrefix:
|
||||
poolPath := storage.az.blobPath(fileName)
|
||||
|
||||
if storage.pathCache == nil {
|
||||
storage.pathCache = make(map[string]map[string]string)
|
||||
@@ -160,7 +159,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(relFilePath, source, sourceMD5)
|
||||
err = storage.az.putFile(storage.az.blobURL(relFilePath), source, sourceMD5)
|
||||
if err == nil {
|
||||
pathCache[relFilePath] = sourceMD5
|
||||
} else {
|
||||
@@ -177,58 +176,57 @@ func (storage *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
}
|
||||
|
||||
// Internal copy or move implementation
|
||||
func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadata map[string]*string, move bool) error {
|
||||
func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadata azblob.Metadata, move bool) error {
|
||||
const leaseDuration = 30
|
||||
leaseID := uuid.NewRandom().String()
|
||||
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
srcBlobClient := containerClient.NewBlobClient(src)
|
||||
blobLeaseClient, err := lease.NewBlobClient(srcBlobClient, &lease.BlobClientOptions{LeaseID: to.Ptr(leaseID)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
dstBlobURL := storage.az.blobURL(dst)
|
||||
srcBlobURL := storage.az.blobURL(src)
|
||||
leaseResp, err := srcBlobURL.AcquireLease(context.Background(), "", leaseDuration, azblob.ModifiedAccessConditions{})
|
||||
if err != nil || leaseResp.StatusCode() != http.StatusCreated {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", srcBlobURL)
|
||||
}
|
||||
defer srcBlobURL.BreakLease(context.Background(), azblob.LeaseBreakNaturally, azblob.ModifiedAccessConditions{})
|
||||
srcBlobLeaseID := leaseResp.LeaseID()
|
||||
|
||||
_, err = blobLeaseClient.AcquireLease(context.Background(), leaseDuration, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
}
|
||||
defer blobLeaseClient.BreakLease(context.Background(), &lease.BlobBreakOptions{BreakPeriod: to.Ptr(int32(60))})
|
||||
|
||||
dstBlobClient := containerClient.NewBlobClient(dst)
|
||||
copyResp, err := dstBlobClient.StartCopyFromURL(context.Background(), srcBlobClient.URL(), &blob.StartCopyFromURLOptions{
|
||||
Metadata: metadata,
|
||||
})
|
||||
|
||||
copyResp, err := dstBlobURL.StartCopyFromURL(
|
||||
context.Background(),
|
||||
srcBlobURL.URL(),
|
||||
metadata,
|
||||
azblob.ModifiedAccessConditions{},
|
||||
azblob.BlobAccessConditions{},
|
||||
azblob.DefaultAccessTier,
|
||||
nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", src, dst, storage, err)
|
||||
}
|
||||
|
||||
copyStatus := *copyResp.CopyStatus
|
||||
copyStatus := copyResp.CopyStatus()
|
||||
for {
|
||||
if copyStatus == blob.CopyStatusTypeSuccess {
|
||||
if copyStatus == azblob.CopyStatusSuccess {
|
||||
if move {
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, src, &blob.DeleteOptions{
|
||||
AccessConditions: &blob.AccessConditions{
|
||||
LeaseAccessConditions: &blob.LeaseAccessConditions{
|
||||
LeaseID: &leaseID,
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err = srcBlobURL.Delete(
|
||||
context.Background(),
|
||||
azblob.DeleteSnapshotsOptionNone,
|
||||
azblob.BlobAccessConditions{
|
||||
LeaseAccessConditions: azblob.LeaseAccessConditions{LeaseID: srcBlobLeaseID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else if copyStatus == blob.CopyStatusTypePending {
|
||||
} else if copyStatus == azblob.CopyStatusPending {
|
||||
time.Sleep(1 * time.Second)
|
||||
getMetadata, err := dstBlobClient.GetProperties(context.TODO(), nil)
|
||||
blobPropsResp, err := dstBlobURL.GetProperties(
|
||||
context.Background(),
|
||||
azblob.BlobAccessConditions{LeaseAccessConditions: azblob.LeaseAccessConditions{LeaseID: srcBlobLeaseID}},
|
||||
azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting copy progress %s", dst)
|
||||
return fmt.Errorf("error getting destination blob properties %s", dstBlobURL)
|
||||
}
|
||||
copyStatus = *getMetadata.CopyStatus
|
||||
copyStatus = blobPropsResp.CopyStatus()
|
||||
|
||||
_, err = blobLeaseClient.RenewLease(context.Background(), nil)
|
||||
_, err = srcBlobURL.RenewLease(context.Background(), srcBlobLeaseID, azblob.ModifiedAccessConditions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error renewing source blob lease %s", src)
|
||||
return fmt.Errorf("error renewing source blob lease %s", srcBlobURL)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", dst, src, storage, copyStatus)
|
||||
@@ -243,9 +241,7 @@ func (storage *PublishedStorage) RenameFile(oldName, newName string) error {
|
||||
|
||||
// SymLink creates a copy of src file and adds link information as meta data
|
||||
func (storage *PublishedStorage) SymLink(src string, dst string) error {
|
||||
metadata := make(map[string]*string)
|
||||
metadata["SymLink"] = &src
|
||||
return storage.internalCopyOrMoveBlob(src, dst, metadata, false /* do not remove src */)
|
||||
return storage.internalCopyOrMoveBlob(src, dst, azblob.Metadata{"SymLink": src}, false /* move */)
|
||||
}
|
||||
|
||||
// HardLink using symlink functionality as hard links do not exist
|
||||
@@ -255,33 +251,28 @@ func (storage *PublishedStorage) HardLink(src string, dst string) error {
|
||||
|
||||
// FileExists returns true if path exists
|
||||
func (storage *PublishedStorage) FileExists(path string) (bool, error) {
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
blob := storage.az.blobURL(path)
|
||||
resp, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("error checking if blob %s exists: %v", path, err)
|
||||
return false, err
|
||||
} else if resp.StatusCode() == http.StatusOK {
|
||||
return true, nil
|
||||
}
|
||||
return true, nil
|
||||
return false, fmt.Errorf("error checking if blob %s exists %d", blob, resp.StatusCode())
|
||||
}
|
||||
|
||||
// ReadLink returns the symbolic link pointed to by path.
|
||||
// This simply reads text file created with SymLink
|
||||
func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
props, err := blobClient.GetProperties(context.Background(), nil)
|
||||
blob := storage.az.blobURL(path)
|
||||
resp, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get blob properties: %v", err)
|
||||
return "", err
|
||||
} else if resp.StatusCode() != http.StatusOK {
|
||||
return "", fmt.Errorf("error checking if blob %s exists %d", blob, resp.StatusCode())
|
||||
}
|
||||
|
||||
metadata := props.Metadata
|
||||
if originalBlob, exists := metadata["original_blob"]; exists {
|
||||
return *originalBlob, nil
|
||||
}
|
||||
return "", fmt.Errorf("error reading link %s: %v", path, err)
|
||||
return resp.NewMetadata()["SymLink"], nil
|
||||
}
|
||||
|
||||
+23
-26
@@ -7,11 +7,8 @@ import (
|
||||
"io/ioutil"
|
||||
"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-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -69,10 +66,8 @@ 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,
|
||||
})
|
||||
cnt := s.storage.az.container
|
||||
_, err = cnt.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessContainer)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedStorage, err = NewPublishedStorage(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
@@ -80,39 +75,41 @@ 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)
|
||||
cnt := s.storage.az.container
|
||||
_, err := cnt.Delete(context.Background(), azblob.ContainerAccessConditions{})
|
||||
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)
|
||||
blob := s.storage.az.container.NewBlobURL(path)
|
||||
resp, err := blob.Download(context.Background(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
c.Assert(err, IsNil)
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
body := resp.Body(azblob.RetryReaderOptions{MaxRetryRequests: 3})
|
||||
data, err := ioutil.ReadAll(body)
|
||||
c.Assert(err, IsNil)
|
||||
return data
|
||||
}
|
||||
|
||||
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)
|
||||
_, err := s.storage.az.container.NewBlobURL(path).GetProperties(
|
||||
context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
c.Assert(err, NotNil)
|
||||
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
storageError, ok := err.(azblob.StorageError)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(storageError.StatusCode, Equals, 404)
|
||||
c.Assert(string(storageError.ServiceCode()), Equals, string(string(azblob.StorageErrorCodeBlobNotFound)))
|
||||
}
|
||||
|
||||
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)
|
||||
_, err := azblob.UploadBufferToBlockBlob(
|
||||
context.Background(),
|
||||
data,
|
||||
s.storage.az.container.NewBlockBlobURL(path),
|
||||
azblob.UploadToBlockBlobOptions{
|
||||
BlobHTTPHeaders: azblob.BlobHTTPHeaders{
|
||||
ContentMD5: hash[:],
|
||||
},
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
@@ -333,7 +330,7 @@ func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
|
||||
// 2nd link from pool, providing wrong path for source file
|
||||
//
|
||||
// this test should check that file already exists in Azure and skip upload (which would fail if not skipped)
|
||||
// this test should check that file already exists in S3 and skip upload (which would fail if not skipped)
|
||||
s.prefixedStorage.pathCache = nil
|
||||
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)
|
||||
|
||||
+4
-4
@@ -46,7 +46,7 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
if err == nil && len(listeners) == 1 {
|
||||
listener := listeners[0]
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String())
|
||||
err = http.Serve(listener, api.Router(context))
|
||||
if err != nil {
|
||||
@@ -67,7 +67,7 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
if _, ok := <-sigchan; ok {
|
||||
fmt.Printf("\nShutdown signal received, waiting for background tasks...\n")
|
||||
context.TaskList().Wait()
|
||||
server.Shutdown(stdcontext.Background())
|
||||
_ = server.Shutdown(stdcontext.Background())
|
||||
}
|
||||
})()
|
||||
defer close(sigchan)
|
||||
@@ -75,14 +75,14 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
listenURL, err := url.Parse(listen)
|
||||
if err == nil && listenURL.Scheme == "unix" {
|
||||
file := listenURL.Path
|
||||
os.Remove(file)
|
||||
_ = os.Remove(file)
|
||||
|
||||
var listener net.Listener
|
||||
listener, err = net.Listen("unix", file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on: %s\n%s", file, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
err = server.Serve(listener)
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ package environment to new version.`,
|
||||
Flag: *flag.NewFlagSet("aptly", flag.ExitOnError),
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdConfig(),
|
||||
makeCmdDb(),
|
||||
makeCmdDB(),
|
||||
makeCmdGraph(),
|
||||
makeCmdMirror(),
|
||||
makeCmdRepo(),
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/smira/commander"
|
||||
"gopkg.in/yaml.v3"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func aptlyConfigShow(_ *commander.Command, _ []string) error {
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func makeCmdDb() *commander.Command {
|
||||
func makeCmdDB() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "db",
|
||||
Short: "manage aptly's internal database and package pool",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdDbCleanup(),
|
||||
makeCmdDbRecover(),
|
||||
makeCmdDBCleanup(),
|
||||
makeCmdDBRecover(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+18
-7
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// aptly db cleanup
|
||||
func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -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{}
|
||||
@@ -48,13 +49,17 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("mirror %s", repo.Name)
|
||||
repo.RefList().ForEach(func(key []byte) error {
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, poolPath := range repo.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -81,7 +86,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("local repo %s", repo.Name)
|
||||
repo.RefList().ForEach(func(key []byte) error {
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
@@ -113,11 +118,16 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("snapshot %s", snapshot.Name)
|
||||
snapshot.RefList().ForEach(func(key []byte) error {
|
||||
_ = snapshot.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for _, poolPath := range snapshot.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -146,7 +156,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
if verbose {
|
||||
description := fmt.Sprintf("published repository %s:%s/%s component %s",
|
||||
published.Storage, published.Prefix, published.Distribution, component)
|
||||
published.RefList(component).ForEach(func(key []byte) error {
|
||||
_ = published.RefList(component).ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return 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()
|
||||
|
||||
@@ -291,9 +302,9 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDbCleanup() *commander.Command {
|
||||
func makeCmdDBCleanup() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDbCleanup,
|
||||
Run: aptlyDBCleanup,
|
||||
UsageLine: "cleanup",
|
||||
Short: "cleanup DB and package pool",
|
||||
Long: `
|
||||
|
||||
+45
-4
@@ -1,13 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
)
|
||||
|
||||
// aptly db recover
|
||||
func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
func aptlyDBRecover(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -16,14 +19,19 @@ func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
context.Progress().Printf("Recovering database...\n")
|
||||
err = goleveldb.RecoverDB(context.DBPath())
|
||||
if err = goleveldb.RecoverDB(context.DBPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.Progress().Printf("Checking database integrity...\n")
|
||||
err = checkIntegrity()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDbRecover() *commander.Command {
|
||||
func makeCmdDBRecover() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDbRecover,
|
||||
Run: aptlyDBRecover,
|
||||
UsageLine: "recover",
|
||||
Short: "recover DB after crash",
|
||||
Long: `
|
||||
@@ -38,3 +46,36 @@ Example:
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func checkIntegrity() error {
|
||||
return context.NewCollectionFactory().LocalRepoCollection().ForEach(checkRepo)
|
||||
}
|
||||
|
||||
func checkRepo(repo *deb.LocalRepo) error {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := collectionFactory.LocalRepoCollection()
|
||||
|
||||
err := repos.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load complete repo %q: %s", repo.Name, err)
|
||||
}
|
||||
|
||||
dangling, err := deb.FindDanglingReferences(repo.RefList(), collectionFactory.PackageCollection())
|
||||
if err != nil {
|
||||
return fmt.Errorf("find dangling references: %w", err)
|
||||
}
|
||||
|
||||
if len(dangling.Refs) > 0 {
|
||||
for _, ref := range dangling.Refs {
|
||||
context.Progress().Printf("Removing dangling database reference %q\n", ref)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(repo.RefList().Subtract(dangling))
|
||||
|
||||
if err = repos.Update(repo); err != nil {
|
||||
return fmt.Errorf("update repo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -38,8 +38,8 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempfile.Close()
|
||||
os.Remove(tempfile.Name())
|
||||
_ = tempfile.Close()
|
||||
_ = os.Remove(tempfile.Name())
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
output := context.Flags().Lookup("output").Value.String()
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ func getVerifier(flags *flag.FlagSet) (pgp.Verifier, error) {
|
||||
verifier.AddKeyring(keyRing)
|
||||
}
|
||||
|
||||
err := verifier.InitKeyring(ignoreSignatures == false) // be verbose only if verifying signatures is requested
|
||||
err := verifier.InitKeyring(!ignoreSignatures) // be verbose only if verifying signatures is requested
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ func aptlyMirrorListTxt(cmd *commander.Command, _ []string) error {
|
||||
|
||||
repos := make([]string, collectionFactory.RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
_ = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
@@ -42,7 +42,7 @@ func aptlyMirrorListTxt(cmd *commander.Command, _ []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
@@ -70,13 +70,13 @@ func aptlyMirrorListJSON(_ *commander.Command, _ []string) error {
|
||||
|
||||
repos := make([]*deb.RemoteRepo, context.NewCollectionFactory().RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
context.NewCollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
_ = context.NewCollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
repos[i] = repo
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
|
||||
+7
-2
@@ -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
|
||||
@@ -86,7 +91,7 @@ func aptlyMirrorShowTxt(_ *commander.Command, args []string) error {
|
||||
if repo.LastDownloadDate.IsZero() {
|
||||
fmt.Printf("Unable to show package list, mirror hasn't been downloaded yet.\n")
|
||||
} else {
|
||||
ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +124,7 @@ func aptlyMirrorShowJSON(_ *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
list.ForEachIndexed(func(p *deb.Package) error {
|
||||
_ = list.ForEachIndexed(func(p *deb.Package) error {
|
||||
repo.Packages = append(repo.Packages, p.GetFullName())
|
||||
return nil
|
||||
})
|
||||
|
||||
+15
-4
@@ -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)
|
||||
@@ -101,7 +111,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
err = context.ReOpenDatabase()
|
||||
if err == nil {
|
||||
repo.MarkAsIdle()
|
||||
collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
_ = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -173,7 +183,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
file.Close()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
@@ -261,7 +271,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: download errors:\n %s", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
repo.FinalizeDownload(collectionFactory, context.Progress())
|
||||
_ = repo.FinalizeDownload(collectionFactory, context.Progress())
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
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")
|
||||
|
||||
@@ -40,7 +40,7 @@ func aptlyPackageSearch(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
PrintPackageList(result, format, "")
|
||||
_ = PrintPackageList(result, format, "")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+3
-3
@@ -84,8 +84,8 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
result := q.Query(collectionFactory.PackageCollection())
|
||||
|
||||
err = result.ForEach(func(p *deb.Package) error {
|
||||
p.Stanza().WriteTo(w, p.IsSource, false, false)
|
||||
w.Flush()
|
||||
_ = p.Stanza().WriteTo(w, p.IsSource, false, false)
|
||||
_ = w.Flush()
|
||||
fmt.Printf("\n")
|
||||
|
||||
if withFiles {
|
||||
@@ -109,7 +109,7 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
if withReferences {
|
||||
fmt.Printf("References to package:\n")
|
||||
printReferencesTo(p, collectionFactory)
|
||||
_ = printReferencesTo(p, collectionFactory)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
|
||||
+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",
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ func aptlyPublishListTxt(cmd *commander.Command, _ []string) error {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(published)
|
||||
|
||||
@@ -99,7 +99,7 @@ func aptlyPublishListJSON(_ *commander.Command, _ []string) error {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].GetPath() < repos[j].GetPath()
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+12
-2
@@ -150,13 +150,21 @@ 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)
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
return fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
@@ -230,7 +238,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 +255,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
|
||||
}
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ func aptlyRepoListTxt(cmd *commander.Command, _ []string) error {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := make([]string, collectionFactory.LocalRepoCollection().Len())
|
||||
i := 0
|
||||
collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
_ = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
@@ -47,7 +47,7 @@ func aptlyRepoListTxt(cmd *commander.Command, _ []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
@@ -76,7 +76,7 @@ func aptlyRepoListJSON(_ *commander.Command, _ []string) error {
|
||||
|
||||
repos := make([]*deb.LocalRepo, context.NewCollectionFactory().LocalRepoCollection().Len())
|
||||
i := 0
|
||||
context.NewCollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
_ = context.NewCollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := context.NewCollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
@@ -87,7 +87,7 @@ func aptlyRepoListJSON(_ *commander.Command, _ []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
context.CloseDatabase()
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ func aptlyRepoRemove(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
|
||||
toRemove.ForEach(func(p *deb.Package) error {
|
||||
_ = toRemove.ForEach(func(p *deb.Package) error {
|
||||
list.Remove(p)
|
||||
context.Progress().ColoredPrintf("@r[-]@| %s removed", p)
|
||||
return nil
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func aptlyRepoShowTxt(_ *commander.Command, args []string) error {
|
||||
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -33,7 +33,7 @@ func aptlySnapshotListTxt(cmd *commander.Command, _ []string) error {
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
|
||||
if raw {
|
||||
collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
_ = collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
fmt.Printf("%s\n", snapshot.Name)
|
||||
return nil
|
||||
})
|
||||
@@ -68,7 +68,7 @@ func aptlySnapshotListJSON(cmd *commander.Command, _ []string) error {
|
||||
|
||||
jsonSnapshots := make([]*deb.Snapshot, collection.Len())
|
||||
i := 0
|
||||
collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
_ = collection.ForEachSorted(sortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
jsonSnapshots[i] = snapshot
|
||||
i++
|
||||
return nil
|
||||
|
||||
@@ -116,7 +116,7 @@ func aptlySnapshotPull(cmd *commander.Command, args []string) error {
|
||||
|
||||
alreadySeen := map[string]bool{}
|
||||
|
||||
result.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
_ = result.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
key := pkg.Architecture + "_" + pkg.Name
|
||||
_, seen := alreadySeen[key]
|
||||
|
||||
@@ -132,7 +132,7 @@ func aptlySnapshotPull(cmd *commander.Command, args []string) error {
|
||||
|
||||
// If !allMatches, add only first matching name-arch package
|
||||
if !seen || allMatches {
|
||||
packageList.Add(pkg)
|
||||
_ = packageList.Add(pkg)
|
||||
context.Progress().ColoredPrintf("@g[+]@| %s added", pkg)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ func aptlySnapshotMirrorRepoSearch(cmd *commander.Command, args []string) error
|
||||
}
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
PrintPackageList(result, format, "")
|
||||
_ = PrintPackageList(result, format, "")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func aptlySnapshotShowTxt(_ *commander.Command, args []string) error {
|
||||
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
ListPackagesRefList(snapshot.RefList(), collectionFactory)
|
||||
_ = ListPackagesRefList(snapshot.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -139,7 +139,7 @@ func aptlySnapshotShowJSON(_ *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
list.ForEachIndexed(func(p *deb.Package) error {
|
||||
_ = list.ForEachIndexed(func(p *deb.Package) error {
|
||||
snapshot.Packages = append(snapshot.Packages, p.GetFullName())
|
||||
return nil
|
||||
})
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-shellwords"
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ func aptlyTaskRun(cmd *commander.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+48
-12
@@ -22,34 +22,36 @@
|
||||
|
||||
__aptly_mirror_list()
|
||||
{
|
||||
aptly mirror list -raw
|
||||
aptly ${aptly_global_opts[@]} mirror list -raw
|
||||
}
|
||||
|
||||
__aptly_repo_list()
|
||||
{
|
||||
aptly repo list -raw
|
||||
aptly ${aptly_global_opts[@]} repo list -raw
|
||||
}
|
||||
|
||||
__aptly_snapshot_list()
|
||||
{
|
||||
aptly snapshot list -raw
|
||||
aptly ${aptly_global_opts[@]} snapshot list -raw
|
||||
}
|
||||
|
||||
__aptly_published_distributions()
|
||||
{
|
||||
aptly publish list -raw | cut -d ' ' -f 2 | sort | uniq
|
||||
aptly ${aptly_global_opts[@]} publish list -raw | cut -d ' ' -f 2 | sort | uniq
|
||||
}
|
||||
|
||||
__aptly_published_prefixes()
|
||||
{
|
||||
aptly publish list -raw | cut -d ' ' -f 1 | sort | uniq
|
||||
aptly ${aptly_global_opts[@]} publish list -raw | cut -d ' ' -f 1 | sort | uniq
|
||||
}
|
||||
|
||||
__aptly_prefixes_for_distribution()
|
||||
{
|
||||
aptly publish list -raw | awk -v dist="$1" '{ if (dist == $2) print $1 }' | sort | uniq
|
||||
aptly ${aptly_global_opts[@]} publish list -raw | awk -v dist="$1" '{ if (dist == $2) print $1 }' | sort | uniq
|
||||
}
|
||||
|
||||
|
||||
|
||||
_aptly()
|
||||
{
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
@@ -57,7 +59,12 @@ _aptly()
|
||||
prevprev="${COMP_WORDS[COMP_CWORD-2]}"
|
||||
|
||||
commands="api config db graph mirror package publish repo serve snapshot task version"
|
||||
options="-architectures= -config= -db-open-attempts= -dep-follow-all-variants -dep-follow-recommends -dep-follow-source -dep-follow-suggests -dep-verbose-resolve -gpg-provider="
|
||||
|
||||
options="-architectures -config -db-open-attempts -dep-follow-all-variants -dep-follow-recommends -dep-follow-source -dep-follow-suggests -dep-verbose-resolve -gpg-provider"
|
||||
options_without_arg="-dep-follow-all-variants -dep-follow-recommends -dep-follow-source -dep-follow-suggests -dep-verbose-resolve"
|
||||
options_with_arg="-architectures -db-open-attempts -gpg-provider"
|
||||
options_with_path_arg="-config"
|
||||
|
||||
db_subcommands="cleanup recover"
|
||||
mirror_subcommands="create drop edit show list rename search update"
|
||||
publish_subcommands="drop list repo snapshot switch update source"
|
||||
@@ -69,12 +76,41 @@ _aptly()
|
||||
config_subcommands="show"
|
||||
api_subcommands="serve"
|
||||
|
||||
local cmd subcmd numargs numoptions i
|
||||
local cmd subcmd numargs numoptions i aptly_global_opts
|
||||
|
||||
numargs=0
|
||||
numoptions=0
|
||||
|
||||
for opt in "${options_with_path_arg[@]}"; do
|
||||
[[ "$prev" == "$opt" ]] || continue
|
||||
compopt -o filenames 2>/dev/null
|
||||
_filedir
|
||||
return 0
|
||||
done
|
||||
|
||||
for (( i=1; i < $COMP_CWORD; i++ )); do
|
||||
word=${COMP_WORDS[i]}
|
||||
if [[ "$word" == -*=* ]]; then
|
||||
for o in "${options[@]}"; do
|
||||
[[ ${word%%=*} == "$o" ]] && aptly_global_opts+=("$word")
|
||||
done
|
||||
else
|
||||
for o in "${options_with_arg[@]}" ""${options_with_path_arg[@]}"" ; do
|
||||
if [[ "$word" == "$o" ]]; then
|
||||
if (( i + 1 < COMP_CWORD )); then
|
||||
aptly_global_opts+=("$word" "${COMP_WORDS[i+1]}")
|
||||
else
|
||||
aptly_global_opts+=("$word")
|
||||
fi
|
||||
(( i++ ))
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
for o in ${options_without_arg[@]}; do
|
||||
[[ "$word" == "$o" ]] && aptly_global_opts+=("$word")
|
||||
done
|
||||
|
||||
if [[ -n "$cmd" ]]; then
|
||||
if [[ ! -n "$subcmd" ]]; then
|
||||
subcmd=${COMP_WORDS[i]}
|
||||
@@ -167,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
|
||||
@@ -175,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
|
||||
@@ -227,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
|
||||
@@ -339,7 +375,7 @@ _aptly()
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-accept-unsigned -force-replace -ignore-signatures -keyring= -no-remove-files -repo= -uploaders-file=" -- ${cur}))
|
||||
else
|
||||
comptopt -o filenames 2>/dev/null
|
||||
compopt -o filenames 2>/dev/null
|
||||
COMPREPLY=($(compgen -f -- ${cur}))
|
||||
return 0
|
||||
fi
|
||||
|
||||
+3
-2
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wsxiaoys/terminal/color"
|
||||
@@ -78,7 +79,7 @@ func (p *Progress) InitBar(count int64, isBytes bool, _ aptly.BarType) {
|
||||
if p.bar != nil {
|
||||
panic("bar already initialized")
|
||||
}
|
||||
if RunningOnTerminal() {
|
||||
if utils.RunningOnTerminal() {
|
||||
p.bar = pb.New(0)
|
||||
p.bar.Total = count
|
||||
p.bar.NotPrint = true
|
||||
@@ -141,7 +142,7 @@ func (p *Progress) PrintfStdErr(msg string, a ...interface{}) {
|
||||
|
||||
// ColoredPrintf does printf in colored way + newline
|
||||
func (p *Progress) ColoredPrintf(msg string, a ...interface{}) {
|
||||
if RunningOnTerminal() {
|
||||
if utils.RunningOnTerminal() {
|
||||
p.queue <- printTask{code: codePrint, message: color.Sprintf(msg, a...) + "\n"}
|
||||
} else {
|
||||
// stip color marks
|
||||
|
||||
@@ -11,7 +11,7 @@ func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type ProgressSuite struct {}
|
||||
type ProgressSuite struct{}
|
||||
|
||||
var _ = Suite(&ProgressSuite{})
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// RunningOnTerminal checks whether stdout is terminal
|
||||
func RunningOnTerminal() bool {
|
||||
return term.IsTerminal(syscall.Stdout)
|
||||
}
|
||||
+22
-13
@@ -100,6 +100,7 @@ func (context *AptlyContext) config() *utils.ConfigStructure {
|
||||
configLocations := []string{homeLocation, "/usr/local/etc/aptly.conf", "/etc/aptly.conf"}
|
||||
|
||||
for _, configLocation := range configLocations {
|
||||
// FIXME: check if exists, check if readable
|
||||
err = utils.LoadConfig(configLocation, &utils.Config)
|
||||
if os.IsPermission(err) || os.IsNotExist(err) {
|
||||
continue
|
||||
@@ -115,7 +116,7 @@ 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)
|
||||
_ = utils.SaveConfigRaw(homeLocation, aptly.AptlyConf)
|
||||
err = utils.LoadConfig(homeLocation, &utils.Config)
|
||||
if err != nil {
|
||||
Fatal(fmt.Errorf("error loading config file %s: %s", homeLocation, err))
|
||||
@@ -123,6 +124,14 @@ func (context *AptlyContext) config() *utils.ConfigStructure {
|
||||
}
|
||||
}
|
||||
|
||||
if utils.Config.LogFormat == "json" {
|
||||
context.StructuredLogging(true)
|
||||
utils.SetupJSONLogger(utils.Config.LogLevel, os.Stdout)
|
||||
} else {
|
||||
context.StructuredLogging(false)
|
||||
utils.SetupDefaultLogger(utils.Config.LogLevel)
|
||||
}
|
||||
|
||||
context.configLoaded = true
|
||||
|
||||
}
|
||||
@@ -233,7 +242,7 @@ func (context *AptlyContext) newDownloader(progress aptly.Progress) aptly.Downlo
|
||||
// If flag is defined prefer it to global setting
|
||||
maxTries = maxTriesFlag.Value.Get().(int)
|
||||
}
|
||||
var downloader string = context.config().Downloader
|
||||
var downloader = context.config().Downloader
|
||||
downloaderFlag := context.flags.Lookup("downloader")
|
||||
if downloaderFlag != nil {
|
||||
downloader = downloaderFlag.Value.String()
|
||||
@@ -295,8 +304,8 @@ func (context *AptlyContext) _database() (database.Storage, error) {
|
||||
switch context.config().DatabaseBackend.Type {
|
||||
case "leveldb":
|
||||
dbPath := filepath.Join(context.config().GetRootDir(), "db")
|
||||
if len(context.config().DatabaseBackend.DbPath) != 0 {
|
||||
dbPath = context.config().DatabaseBackend.DbPath
|
||||
if len(context.config().DatabaseBackend.DBPath) != 0 {
|
||||
dbPath = context.config().DatabaseBackend.DBPath
|
||||
}
|
||||
context.database, err = goleveldb.NewDB(dbPath)
|
||||
case "etcd":
|
||||
@@ -444,7 +453,7 @@ func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedSto
|
||||
} 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:]))
|
||||
Fatal(fmt.Errorf("published Azure storage %v not configured", name[6:]))
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -589,17 +598,17 @@ func (context *AptlyContext) Shutdown() {
|
||||
|
||||
if aptly.EnableDebug {
|
||||
if context.fileMemProfile != nil {
|
||||
pprof.WriteHeapProfile(context.fileMemProfile)
|
||||
context.fileMemProfile.Close()
|
||||
_ = pprof.WriteHeapProfile(context.fileMemProfile)
|
||||
_ = context.fileMemProfile.Close()
|
||||
context.fileMemProfile = nil
|
||||
}
|
||||
if context.fileCPUProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
context.fileCPUProfile.Close()
|
||||
_ = context.fileCPUProfile.Close()
|
||||
context.fileCPUProfile = nil
|
||||
}
|
||||
if context.fileMemProfile != nil {
|
||||
context.fileMemProfile.Close()
|
||||
_ = context.fileMemProfile.Close()
|
||||
context.fileMemProfile = nil
|
||||
}
|
||||
}
|
||||
@@ -607,7 +616,7 @@ func (context *AptlyContext) Shutdown() {
|
||||
context.taskList.Stop()
|
||||
}
|
||||
if context.database != nil {
|
||||
context.database.Close()
|
||||
_ = context.database.Close()
|
||||
context.database = nil
|
||||
}
|
||||
if context.downloader != nil {
|
||||
@@ -652,7 +661,7 @@ func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pprof.StartCPUProfile(context.fileCPUProfile)
|
||||
_ = pprof.StartCPUProfile(context.fileCPUProfile)
|
||||
}
|
||||
|
||||
memprofile := flags.Lookup("memprofile").Value.String()
|
||||
@@ -672,7 +681,7 @@ func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
context.fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
|
||||
_, _ = context.fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
|
||||
|
||||
go func() {
|
||||
var stats runtime.MemStats
|
||||
@@ -682,7 +691,7 @@ func NewContext(flags *flag.FlagSet) (*AptlyContext, error) {
|
||||
for {
|
||||
runtime.ReadMemStats(&stats)
|
||||
if context.fileMemStats != nil {
|
||||
context.fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
||||
_, _ = context.fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
||||
(time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
|
||||
time.Sleep(interval)
|
||||
} else {
|
||||
|
||||
@@ -14,8 +14,7 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
type EtcDDBSuite struct {
|
||||
url string
|
||||
db database.Storage
|
||||
db database.Storage
|
||||
}
|
||||
|
||||
var _ = Suite(&EtcDDBSuite{})
|
||||
@@ -67,17 +66,17 @@ func (s *EtcDDBSuite) TestDelete(c *C) {
|
||||
func (s *EtcDDBSuite) TestByPrefix(c *C) {
|
||||
//c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{})
|
||||
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
_ = s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
_ = s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
_ = s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
@@ -109,7 +108,7 @@ func (s *EtcDDBSuite) TestHasPrefix(c *C) {
|
||||
//c.Check(s.db.HasPrefix([]byte(nil)), Equals, false)
|
||||
//c.Check(s.db.HasPrefix([]byte{0x80}), Equals, false)
|
||||
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
|
||||
c.Check(s.db.HasPrefix([]byte(nil)), Equals, true)
|
||||
c.Check(s.db.HasPrefix([]byte{0x80}), Equals, true)
|
||||
@@ -124,15 +123,17 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
value2 = []byte("value2")
|
||||
)
|
||||
transaction, err := s.db.OpenTransaction()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.db.Put(key, value)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
transaction.Put(key2, value2)
|
||||
_ = transaction.Put(key2, value2)
|
||||
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)
|
||||
@@ -155,4 +156,3 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
_, err = transaction.Get(key)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package etcddb
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/pborman/uuid"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/google/uuid"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
type EtcDStorage struct {
|
||||
@@ -16,7 +16,7 @@ type EtcDStorage struct {
|
||||
|
||||
// CreateTemporary creates new DB of the same type in temp dir
|
||||
func (s *EtcDStorage) CreateTemporary() (database.Storage, error) {
|
||||
tmp := uuid.NewRandom().String()
|
||||
tmp := uuid.NewString()
|
||||
return &EtcDStorage{
|
||||
url: s.url,
|
||||
db: s.db,
|
||||
@@ -145,7 +145,7 @@ func (s *EtcDStorage) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reopen tries to open (re-open) the database
|
||||
// Open returns the database
|
||||
func (s *EtcDStorage) Open() error {
|
||||
if s.db != nil {
|
||||
return nil
|
||||
|
||||
@@ -67,8 +67,7 @@ func (t *transaction) Commit() (err error) {
|
||||
// Discard is safe to call after Commit(), it would be no-op
|
||||
func (t *transaction) Discard() {
|
||||
t.ops = []clientv3.Op{}
|
||||
t.tmpdb.Drop()
|
||||
return
|
||||
_ = t.tmpdb.Drop()
|
||||
}
|
||||
|
||||
// transaction should implement database.Transaction
|
||||
|
||||
@@ -51,8 +51,8 @@ func RecoverDB(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
db.Close()
|
||||
stor.Close()
|
||||
_ = db.Close()
|
||||
_ = stor.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -119,17 +119,17 @@ func (s *LevelDBSuite) TestDelete(c *C) {
|
||||
func (s *LevelDBSuite) TestByPrefix(c *C) {
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{})
|
||||
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
_ = s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
_ = s.db.Put([]byte{0x90, 0x01}, []byte{0x04})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
_ = s.db.Put([]byte{0x00, 0x01}, []byte{0x05})
|
||||
c.Check(s.db.FetchByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x01}, {0x02}, {0x03}})
|
||||
c.Check(s.db.KeysByPrefix([]byte{0x80}), DeepEquals, [][]byte{{0x80, 0x01}, {0x80, 0x02}, {0x80, 0x03}})
|
||||
|
||||
@@ -161,7 +161,7 @@ func (s *LevelDBSuite) TestHasPrefix(c *C) {
|
||||
c.Check(s.db.HasPrefix([]byte(nil)), Equals, false)
|
||||
c.Check(s.db.HasPrefix([]byte{0x80}), Equals, false)
|
||||
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
|
||||
c.Check(s.db.HasPrefix([]byte(nil)), Equals, true)
|
||||
c.Check(s.db.HasPrefix([]byte{0x80}), Equals, true)
|
||||
@@ -180,8 +180,8 @@ func (s *LevelDBSuite) TestBatch(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
batch := s.db.CreateBatch()
|
||||
batch.Put(key2, value2)
|
||||
batch.Delete(key)
|
||||
_ = batch.Put(key2, value2)
|
||||
_ = batch.Delete(key)
|
||||
|
||||
v, err := s.db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
@@ -202,9 +202,9 @@ func (s *LevelDBSuite) TestBatch(c *C) {
|
||||
}
|
||||
|
||||
func (s *LevelDBSuite) TestCompactDB(c *C) {
|
||||
s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
_ = s.db.Put([]byte{0x80, 0x01}, []byte{0x01})
|
||||
_ = s.db.Put([]byte{0x80, 0x03}, []byte{0x03})
|
||||
_ = s.db.Put([]byte{0x80, 0x02}, []byte{0x02})
|
||||
|
||||
c.Check(s.db.CompactDB(), IsNil)
|
||||
}
|
||||
|
||||
+8
-8
@@ -60,14 +60,14 @@ func (c *Changes) VerifyAndParse(acceptUnsigned, ignoreSignature bool, verifier
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer input.Close()
|
||||
defer func() { _ = input.Close() }()
|
||||
|
||||
isClearSigned, err := verifier.IsClearSigned(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
input.Seek(0, 0)
|
||||
_, _ = input.Seek(0, 0)
|
||||
|
||||
if !isClearSigned && !acceptUnsigned {
|
||||
return fmt.Errorf(".changes file is not signed and unsigned processing hasn't been enabled")
|
||||
@@ -79,7 +79,7 @@ func (c *Changes) VerifyAndParse(acceptUnsigned, ignoreSignature bool, verifier
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
input.Seek(0, 0)
|
||||
_, _ = input.Seek(0, 0)
|
||||
|
||||
c.SignatureKeys = keyInfo.GoodKeys
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (c *Changes) VerifyAndParse(acceptUnsigned, ignoreSignature bool, verifier
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer text.Close()
|
||||
defer func() { _ = text.Close() }()
|
||||
} else {
|
||||
text = input
|
||||
}
|
||||
@@ -307,7 +307,7 @@ func ImportChangesFiles(changesFiles []string, reporter aptly.ResultReporter, ac
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
_ = changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ func ImportChangesFiles(changesFiles []string, reporter aptly.ResultReporter, ac
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
_ = changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ func ImportChangesFiles(changesFiles []string, reporter aptly.ResultReporter, ac
|
||||
if err != nil {
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("unable to process file %s: %s", changes.ChangesName, err)
|
||||
changes.Cleanup()
|
||||
_ = changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ func ImportChangesFiles(changesFiles []string, reporter aptly.ResultReporter, ac
|
||||
failedFiles = append(failedFiles, path)
|
||||
reporter.Warning("changes file skipped due to uploaders config: %s, keys %#v: %s",
|
||||
changes.ChangesName, changes.SignatureKeys, err)
|
||||
changes.Cleanup()
|
||||
_ = changes.Cleanup()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -51,7 +51,7 @@ func (s *ChangesSuite) SetUpTest(c *C) {
|
||||
|
||||
func (s *ChangesSuite) TearDownTest(c *C) {
|
||||
s.progress.Shutdown()
|
||||
s.db.Close()
|
||||
_ = s.db.Close()
|
||||
}
|
||||
|
||||
func (s *ChangesSuite) TestParseAndVerify(c *C) {
|
||||
@@ -108,13 +108,13 @@ func (s *ChangesSuite) TestImportChangesFiles(c *C) {
|
||||
|
||||
for _, path := range origFailedFiles {
|
||||
filename := filepath.Join(s.Dir, filepath.Base(path))
|
||||
utils.CopyFile(path, filename)
|
||||
_ = utils.CopyFile(path, filename)
|
||||
expectedFailedFiles = append(expectedFailedFiles, filename)
|
||||
}
|
||||
|
||||
for _, path := range origProcessedFiles {
|
||||
filename := filepath.Join(s.Dir, filepath.Base(path))
|
||||
utils.CopyFile(path, filename)
|
||||
_ = utils.CopyFile(path, filename)
|
||||
expectedProcessedFiles = append(expectedProcessedFiles, filename)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func (s *ChecksumCollectionSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *ChecksumCollectionSuite) TearDownTest(c *C) {
|
||||
s.db.Close()
|
||||
_ = s.db.Close()
|
||||
}
|
||||
|
||||
func (s *ChecksumCollectionSuite) TestFlow(c *C) {
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ContentsIndex calculates mapping from files to packages, with sorting and aggregation
|
||||
@@ -20,7 +20,7 @@ type ContentsIndex struct {
|
||||
func NewContentsIndex(db database.Storage) *ContentsIndex {
|
||||
return &ContentsIndex{
|
||||
db: db,
|
||||
prefix: []byte(uuid.New()),
|
||||
prefix: []byte(uuid.NewString()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/kjk/lzma"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/smira/go-xz"
|
||||
xz "github.com/smira/go-xz"
|
||||
)
|
||||
|
||||
// Source kinds
|
||||
@@ -35,7 +35,7 @@ func GetControlFileFromDeb(packageFile string) (Stanza, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
library := ar.NewReader(file)
|
||||
for {
|
||||
@@ -66,14 +66,14 @@ func GetControlFileFromDeb(packageFile string) (Stanza, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to ungzip %s from %s", header.Name, packageFile)
|
||||
}
|
||||
defer ungzip.Close()
|
||||
defer func() { _ = ungzip.Close() }()
|
||||
tarInput = ungzip
|
||||
case "control.tar.xz":
|
||||
unxz, err := xz.NewReader(bufReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to unxz %s from %s", header.Name, packageFile)
|
||||
}
|
||||
defer unxz.Close()
|
||||
defer func() { _ = unxz.Close() }()
|
||||
tarInput = unxz
|
||||
case "control.tar.zst":
|
||||
unzstd, err := zstd.NewReader(bufReader)
|
||||
@@ -116,10 +116,10 @@ func GetControlFileFromDsc(dscFile string, verifier pgp.Verifier) (Stanza, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
isClearSigned, err := verifier.IsClearSigned(file)
|
||||
file.Seek(0, 0)
|
||||
_, _ = file.Seek(0, 0)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -132,7 +132,7 @@ func GetControlFileFromDsc(dscFile string, verifier pgp.Verifier) (Stanza, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer text.Close()
|
||||
defer func() { _ = text.Close() }()
|
||||
} else {
|
||||
text = file
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func GetContentsFromDeb(file io.Reader, packageFile string) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to ungzip data.tar.gz from %s", packageFile)
|
||||
}
|
||||
defer ungzip.Close()
|
||||
defer func() { _ = ungzip.Close() }()
|
||||
tarInput = ungzip
|
||||
}
|
||||
case "data.tar.bz2":
|
||||
@@ -191,11 +191,11 @@ func GetContentsFromDeb(file io.Reader, packageFile string) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to unxz data.tar.xz from %s", packageFile)
|
||||
}
|
||||
defer unxz.Close()
|
||||
defer func() { _ = unxz.Close() }()
|
||||
tarInput = unxz
|
||||
case "data.tar.lzma":
|
||||
unlzma := lzma.NewReader(bufReader)
|
||||
defer unlzma.Close()
|
||||
defer func() { _ = unlzma.Close() }()
|
||||
tarInput = unlzma
|
||||
case "data.tar.zst":
|
||||
unzstd, err := zstd.NewReader(bufReader)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
)
|
||||
|
||||
// FindDanglingReferences finds references that exist in the given PackageRefList, but not in the given PackageCollection.
|
||||
// It returns all such references, so they can be removed from the database.
|
||||
func FindDanglingReferences(reflist *PackageRefList, packages *PackageCollection) (dangling *PackageRefList, err error) {
|
||||
dangling = &PackageRefList{}
|
||||
|
||||
err = reflist.ForEach(func(key []byte) error {
|
||||
ok, err := isDangling(packages, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
dangling.Refs = append(dangling.Refs, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dangling, nil
|
||||
}
|
||||
|
||||
func isDangling(packages *PackageCollection, key []byte) (bool, error) {
|
||||
_, err := packages.ByKey(key)
|
||||
if errors.Is(err, database.ErrNotFound) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("get reference %q: %w", key, err)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package deb_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
)
|
||||
|
||||
func TestFindDanglingReferences(t *testing.T) {
|
||||
reflist := deb.NewPackageRefList()
|
||||
reflist.Refs = [][]byte{[]byte("P existing 1.2.3"), []byte("P dangling 1.2.3")}
|
||||
|
||||
db, _ := goleveldb.NewOpenDB(t.TempDir())
|
||||
packages := deb.NewPackageCollection(db)
|
||||
|
||||
if err := packages.Update(&deb.Package{Name: "existing", Version: "1.2.3"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dangling, err := deb.FindDanglingReferences(reflist, packages)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exp := &deb.PackageRefList{
|
||||
Refs: [][]byte{[]byte("P dangling 1.2.3")},
|
||||
}
|
||||
|
||||
compareRefs(t, exp, dangling)
|
||||
}
|
||||
|
||||
func compareRefs(t *testing.T, exp, got *deb.PackageRefList) {
|
||||
t.Helper()
|
||||
|
||||
if len(exp.Refs) != len(got.Refs) {
|
||||
t.Fatalf("refs length mismatch: exp %d, got %d", len(exp.Refs), len(got.Refs))
|
||||
}
|
||||
|
||||
for i := range exp.Refs {
|
||||
if !bytes.Equal(exp.Refs[i], got.Refs[i]) {
|
||||
t.Fatalf("refs do not match: exp %q, got %q", exp.Refs[i], got.Refs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,11 @@ var (
|
||||
"Version",
|
||||
"Codename",
|
||||
"Date",
|
||||
"Valid-Until",
|
||||
"NotAutomatic",
|
||||
"ButAutomaticUpgrades",
|
||||
"Acquire-By-Hash",
|
||||
"Signed-By",
|
||||
"Architectures",
|
||||
"Architecture",
|
||||
"Components",
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ func (s *ControlFileSuite) TestCanonicalCase(c *C) {
|
||||
func (s *ControlFileSuite) TestLongFields(c *C) {
|
||||
f, err := os.Open("long.stanza")
|
||||
c.Assert(err, IsNil)
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
r := NewControlFileReader(f, false, false)
|
||||
stanza, e := r.ReadStanza()
|
||||
|
||||
+11
-11
@@ -12,15 +12,15 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
var err error
|
||||
|
||||
graph := gographviz.NewEscape()
|
||||
graph.SetDir(true)
|
||||
graph.SetName("aptly")
|
||||
_ = graph.SetDir(true)
|
||||
_ = graph.SetName("aptly")
|
||||
|
||||
var labelStart string
|
||||
var labelEnd string
|
||||
|
||||
switch layout {
|
||||
case "vertical":
|
||||
graph.AddAttr("aptly", "rankdir", "LR")
|
||||
_ = graph.AddAttr("aptly", "rankdir", "LR")
|
||||
labelStart = ""
|
||||
labelEnd = ""
|
||||
case "horizontal":
|
||||
@@ -38,7 +38,7 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
return e
|
||||
}
|
||||
|
||||
graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
_ = graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "darkgoldenrod1",
|
||||
@@ -60,7 +60,7 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
return e
|
||||
}
|
||||
|
||||
graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
_ = graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "mediumseagreen",
|
||||
@@ -75,7 +75,7 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectionFactory.SnapshotCollection().ForEach(func(snapshot *Snapshot) error {
|
||||
_ = collectionFactory.SnapshotCollection().ForEach(func(snapshot *Snapshot) error {
|
||||
existingNodes[snapshot.UUID] = true
|
||||
return nil
|
||||
})
|
||||
@@ -91,7 +91,7 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
description = "Snapshot from repo"
|
||||
}
|
||||
|
||||
graph.AddNode("aptly", snapshot.UUID, map[string]string{
|
||||
_ = graph.AddNode("aptly", snapshot.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "cadetblue1",
|
||||
@@ -103,7 +103,7 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
for _, uuid := range snapshot.SourceIDs {
|
||||
_, exists := existingNodes[uuid]
|
||||
if exists {
|
||||
graph.AddEdge(uuid, snapshot.UUID, true, nil)
|
||||
_ = graph.AddEdge(uuid, snapshot.UUID, true, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,8 +114,8 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectionFactory.PublishedRepoCollection().ForEach(func(repo *PublishedRepo) error {
|
||||
graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
_ = collectionFactory.PublishedRepoCollection().ForEach(func(repo *PublishedRepo) error {
|
||||
_ = graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "darkolivegreen1",
|
||||
@@ -127,7 +127,7 @@ func BuildGraph(collectionFactory *CollectionFactory, layout string) (gographviz
|
||||
for _, uuid := range repo.Sources {
|
||||
_, exists := existingNodes[uuid]
|
||||
if exists {
|
||||
graph.AddEdge(uuid, repo.UUID, true, nil)
|
||||
_ = graph.AddEdge(uuid, repo.UUID, true, nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -59,24 +59,24 @@ func (file *indexFile) Finalize(signer pgp.Signer) error {
|
||||
if file.discardable {
|
||||
return nil
|
||||
}
|
||||
file.BufWriter()
|
||||
_, _ = file.BufWriter()
|
||||
}
|
||||
|
||||
err := file.w.Flush()
|
||||
if err != nil {
|
||||
file.tempFile.Close()
|
||||
_ = file.tempFile.Close()
|
||||
return fmt.Errorf("unable to write to index file: %s", err)
|
||||
}
|
||||
|
||||
if file.compressable {
|
||||
err = utils.CompressFile(file.tempFile, file.onlyGzip || file.parent.skipBz2)
|
||||
if err != nil {
|
||||
file.tempFile.Close()
|
||||
_ = file.tempFile.Close()
|
||||
return fmt.Errorf("unable to compress index file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
file.tempFile.Close()
|
||||
_ = file.tempFile.Close()
|
||||
|
||||
exts := []string{""}
|
||||
cksumExts := exts
|
||||
@@ -220,11 +220,11 @@ func packageIndexByHash(file *indexFile, ext string, hash string, sum string) er
|
||||
// If we managed to resolve the link target: delete it. This is the
|
||||
// oldest physical index file we no longer need. Once we drop our
|
||||
// old symlink we'll essentially forget about it existing at all.
|
||||
file.parent.publishedStorage.Remove(linkTarget)
|
||||
_ = file.parent.publishedStorage.Remove(linkTarget)
|
||||
}
|
||||
file.parent.publishedStorage.Remove(oldIndexPath)
|
||||
_ = file.parent.publishedStorage.Remove(oldIndexPath)
|
||||
}
|
||||
file.parent.publishedStorage.RenameFile(indexPath, oldIndexPath)
|
||||
_ = file.parent.publishedStorage.RenameFile(indexPath, oldIndexPath)
|
||||
}
|
||||
|
||||
// create symlink
|
||||
|
||||
+36
-2
@@ -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 {
|
||||
@@ -438,7 +471,7 @@ func (l *PackageList) Scan(q PackageQuery) (result *PackageList) {
|
||||
result = NewPackageListWithDuplicates(l.duplicatesAllowed, 0)
|
||||
for _, pkg := range l.packages {
|
||||
if q.Matches(pkg) {
|
||||
result.Add(pkg)
|
||||
_ = result.Add(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +489,7 @@ func (l *PackageList) SearchByKey(arch, name, version string) (result *PackageLi
|
||||
|
||||
pkg := l.packages["P"+arch+" "+name+" "+version]
|
||||
if pkg != nil {
|
||||
result.Add(pkg)
|
||||
_ = result.Add(pkg)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -598,6 +631,7 @@ func (l *PackageList) Filter(options FilterOptions) (*PackageList, error) {
|
||||
//
|
||||
// when follow-all-variants is enabled, we need to try to expand anyway,
|
||||
// as even if dependency is satisfied now, there might be other ways to satisfy dependency
|
||||
// FIXME: do not search twice
|
||||
if result.Search(dep, false, true) != nil {
|
||||
if options.DependencyOptions&DepVerboseResolve == DepVerboseResolve && options.Progress != nil {
|
||||
options.Progress.ColoredPrintf("@{y}Already satisfied dependency@|: %s with %s", &dep, result.Search(dep, true, true))
|
||||
|
||||
+59
-10
@@ -96,7 +96,7 @@ func (s *PackageListSuite) SetUpTest(c *C) {
|
||||
{Name: "dpkg", Version: "1.7", Architecture: "source", SourceArchitecture: "any", IsSource: true, deps: &PackageDependencies{}},
|
||||
}
|
||||
for _, p := range s.packages {
|
||||
s.il.Add(p)
|
||||
_ = s.il.Add(p)
|
||||
}
|
||||
s.il.PrepareIndex()
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *PackageListSuite) SetUpTest(c *C) {
|
||||
{Name: "app", Version: "3.0", Architecture: "amd64", deps: &PackageDependencies{PreDepends: []string{"dpkg >= 1.6)"}, Depends: []string{"lib (>> 0.9)", "data (>= 1.0)"}}},
|
||||
}
|
||||
for _, p := range s.packages2 {
|
||||
s.il2.Add(p)
|
||||
_ = s.il2.Add(p)
|
||||
}
|
||||
s.il2.PrepareIndex()
|
||||
|
||||
@@ -202,8 +202,8 @@ func (s *PackageListSuite) TestRemoveWhenIndexed(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestForeach(c *C) {
|
||||
s.list.Add(s.p1)
|
||||
s.list.Add(s.p3)
|
||||
_ = s.list.Add(s.p1)
|
||||
_ = s.list.Add(s.p3)
|
||||
|
||||
Len := 0
|
||||
err := s.list.ForEach(func(*Package) error {
|
||||
@@ -232,21 +232,21 @@ func (s *PackageListSuite) TestIndex(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestAppend(c *C) {
|
||||
s.list.Add(s.p1)
|
||||
s.list.Add(s.p3)
|
||||
_ = s.list.Add(s.p1)
|
||||
_ = s.list.Add(s.p3)
|
||||
|
||||
err := s.list.Append(s.il)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(s.list.Len(), Equals, 16)
|
||||
|
||||
list := NewPackageList()
|
||||
list.Add(s.p4)
|
||||
_ = list.Add(s.p4)
|
||||
|
||||
err = s.list.Append(list)
|
||||
c.Check(err, ErrorMatches, "package already exists and is different: .*")
|
||||
|
||||
s.list.PrepareIndex()
|
||||
c.Check(func() { s.list.Append(s.il) }, Panics, "Append not supported when indexed")
|
||||
c.Check(func() { _ = s.list.Append(s.il) }, Panics, "Append not supported when indexed")
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestSearch(c *C) {
|
||||
@@ -312,7 +312,7 @@ func (s *PackageListSuite) TestSearch(c *C) {
|
||||
|
||||
func (s *PackageListSuite) TestFilter(c *C) {
|
||||
c.Check(func() {
|
||||
s.list.Filter(FilterOptions{
|
||||
_, _ = s.list.Filter(FilterOptions{
|
||||
Queries: []PackageQuery{&PkgQuery{"abcd", "0.3", "i386"}},
|
||||
})
|
||||
}, Panics, "list not indexed, can't filter")
|
||||
@@ -479,7 +479,7 @@ func (s *PackageListSuite) TestVerifyDependencies(c *C) {
|
||||
{Pkg: "mail-agent", Relation: VersionDontCare, Version: "", Architecture: "arm"}})
|
||||
|
||||
for _, p := range s.sourcePackages {
|
||||
s.il.Add(p)
|
||||
_ = s.il.Add(p)
|
||||
}
|
||||
|
||||
missing, err = s.il.VerifyDependencies(DepFollowSource, []string{"i386", "amd64"}, s.il, nil)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+10
-9
@@ -7,7 +7,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ type LocalRepo struct {
|
||||
// NewLocalRepo creates new instance of Debian local repository
|
||||
func NewLocalRepo(name string, comment string) *LocalRepo {
|
||||
return &LocalRepo{
|
||||
UUID: uuid.New(),
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
Comment: comment,
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (repo *LocalRepo) Encode() []byte {
|
||||
var buf bytes.Buffer
|
||||
|
||||
encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
|
||||
encoder.Encode(repo)
|
||||
_ = encoder.Encode(repo)
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func (collection *LocalRepoCollection) search(filter func(*LocalRepo) bool, uniq
|
||||
return result
|
||||
}
|
||||
|
||||
collection.db.ProcessByPrefix([]byte("L"), func(_, blob []byte) error {
|
||||
_ = collection.db.ProcessByPrefix([]byte("L"), func(_, blob []byte) error {
|
||||
r := &LocalRepo{}
|
||||
if err := r.Decode(blob); err != nil {
|
||||
log.Printf("Error decoding local repo: %s\n", err)
|
||||
@@ -159,15 +159,17 @@ func (collection *LocalRepoCollection) Add(repo *LocalRepo) error {
|
||||
// Update stores updated information about repo in DB
|
||||
func (collection *LocalRepoCollection) Update(repo *LocalRepo) error {
|
||||
batch := collection.db.CreateBatch()
|
||||
batch.Put(repo.Key(), repo.Encode())
|
||||
_ = batch.Put(repo.Key(), repo.Encode())
|
||||
if repo.packageRefs != nil {
|
||||
batch.Put(repo.RefKey(), repo.packageRefs.Encode())
|
||||
_ = batch.Put(repo.RefKey(), repo.packageRefs.Encode())
|
||||
}
|
||||
return batch.Write()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -247,7 +248,7 @@ func (collection *LocalRepoCollection) Drop(repo *LocalRepo) error {
|
||||
delete(collection.cache, repo.UUID)
|
||||
|
||||
batch := collection.db.CreateBatch()
|
||||
batch.Delete(repo.Key())
|
||||
batch.Delete(repo.RefKey())
|
||||
_ = batch.Delete(repo.Key())
|
||||
_ = batch.Delete(repo.RefKey())
|
||||
return batch.Write()
|
||||
}
|
||||
|
||||
+22
-10
@@ -21,8 +21,8 @@ var _ = Suite(&LocalRepoSuite{})
|
||||
func (s *LocalRepoSuite) SetUpTest(c *C) {
|
||||
s.db, _ = goleveldb.NewOpenDB(c.MkDir())
|
||||
s.list = NewPackageList()
|
||||
s.list.Add(&Package{Name: "lib", Version: "1.7", Architecture: "i386"})
|
||||
s.list.Add(&Package{Name: "app", Version: "1.9", Architecture: "amd64"})
|
||||
_ = s.list.Add(&Package{Name: "lib", Version: "1.7", Architecture: "i386"})
|
||||
_ = s.list.Add(&Package{Name: "app", Version: "1.9", Architecture: "amd64"})
|
||||
|
||||
s.reflist = NewPackageRefListFromPackageList(s.list)
|
||||
|
||||
@@ -31,7 +31,7 @@ func (s *LocalRepoSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *LocalRepoSuite) TearDownTest(c *C) {
|
||||
s.db.Close()
|
||||
_ = s.db.Close()
|
||||
}
|
||||
|
||||
func (s *LocalRepoSuite) TestString(c *C) {
|
||||
@@ -88,14 +88,14 @@ func (s *LocalRepoCollectionSuite) SetUpTest(c *C) {
|
||||
s.collection = NewLocalRepoCollection(s.db)
|
||||
|
||||
s.list = NewPackageList()
|
||||
s.list.Add(&Package{Name: "lib", Version: "1.7", Architecture: "i386"})
|
||||
s.list.Add(&Package{Name: "app", Version: "1.9", Architecture: "amd64"})
|
||||
_ = s.list.Add(&Package{Name: "lib", Version: "1.7", Architecture: "i386"})
|
||||
_ = s.list.Add(&Package{Name: "app", Version: "1.9", Architecture: "amd64"})
|
||||
|
||||
s.reflist = NewPackageRefListFromPackageList(s.list)
|
||||
}
|
||||
|
||||
func (s *LocalRepoCollectionSuite) TearDownTest(c *C) {
|
||||
s.db.Close()
|
||||
_ = s.db.Close()
|
||||
}
|
||||
|
||||
func (s *LocalRepoCollectionSuite) TestAddByName(c *C) {
|
||||
@@ -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)
|
||||
@@ -156,7 +168,7 @@ func (s *LocalRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||
|
||||
func (s *LocalRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||
repo := NewLocalRepo("local1", "Comment 1")
|
||||
s.collection.Add(repo)
|
||||
_ = s.collection.Add(repo)
|
||||
|
||||
count := 0
|
||||
err := s.collection.ForEach(func(*LocalRepo) error {
|
||||
@@ -178,10 +190,10 @@ func (s *LocalRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||
|
||||
func (s *LocalRepoCollectionSuite) TestDrop(c *C) {
|
||||
repo1 := NewLocalRepo("local1", "Comment 1")
|
||||
s.collection.Add(repo1)
|
||||
_ = s.collection.Add(repo1)
|
||||
|
||||
repo2 := NewLocalRepo("local2", "Comment 2")
|
||||
s.collection.Add(repo2)
|
||||
_ = s.collection.Add(repo2)
|
||||
|
||||
r1, _ := s.collection.ByUUID(repo1.UUID)
|
||||
c.Check(r1, Equals, repo1)
|
||||
@@ -208,6 +220,6 @@ func (s *LocalRepoCollectionSuite) TestDropNonExisting(c *C) {
|
||||
_, err := s.collection.ByUUID(repo.UUID)
|
||||
c.Check(err, ErrorMatches, "local repo .* not found")
|
||||
|
||||
err = s.collection.Drop(repo)
|
||||
_ = s.collection.Drop(repo)
|
||||
c.Check(s.collection.Drop(repo), ErrorMatches, "local repo not found")
|
||||
}
|
||||
|
||||
+1
-1
@@ -565,7 +565,7 @@ func (p *Package) CalculateContents(packagePool aptly.PackagePool, progress aptl
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
contents, err := GetContentsFromDeb(reader, file.Filename)
|
||||
if err != nil {
|
||||
|
||||
@@ -309,7 +309,7 @@ func (collection *PackageCollection) Scan(q PackageQuery) (result *PackageList)
|
||||
}
|
||||
|
||||
if q.Matches(pkg) {
|
||||
result.Add(pkg)
|
||||
_ = result.Add(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ func (collection *PackageCollection) SearchByKey(arch, name, version string) (re
|
||||
}
|
||||
|
||||
if pkg.Architecture == arch && pkg.Name == name && pkg.Version == version {
|
||||
result.Add(pkg)
|
||||
_ = result.Add(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (s *PackageCollectionSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackageCollectionSuite) TearDownTest(c *C) {
|
||||
s.db.Close()
|
||||
_ = s.db.Close()
|
||||
}
|
||||
|
||||
func (s *PackageCollectionSuite) TestUpdate(c *C) {
|
||||
@@ -67,7 +67,7 @@ func (s *PackageCollectionSuite) TestByKey(c *C) {
|
||||
|
||||
func (s *PackageCollectionSuite) TestByKeyOld0_3(c *C) {
|
||||
key := []byte("Pi386 vmware-view-open-client 4.5.0-297975+dfsg-4+b1")
|
||||
s.db.Put(key, old0_3Package)
|
||||
_ = s.db.Put(key, old0_3Package)
|
||||
|
||||
p, err := s.collection.ByKey(key)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
@@ -64,7 +64,7 @@ func (files PackageFiles) Hash() uint64 {
|
||||
|
||||
for _, f := range files {
|
||||
h.Write([]byte(f.Filename))
|
||||
binary.Write(h, binary.BigEndian, f.Checksums.Size)
|
||||
_ = binary.Write(h, binary.BigEndian, f.Checksums.Size)
|
||||
h.Write([]byte(f.Checksums.MD5))
|
||||
h.Write([]byte(f.Checksums.SHA1))
|
||||
h.Write([]byte(f.Checksums.SHA256))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package deb
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
@@ -39,7 +39,7 @@ func (s *PackageFilesSuite) TestVerify(c *C) {
|
||||
c.Check(result, Equals, false)
|
||||
|
||||
tmpFilepath := filepath.Join(c.MkDir(), "file")
|
||||
c.Assert(ioutil.WriteFile(tmpFilepath, []byte("abcde"), 0777), IsNil)
|
||||
c.Assert(os.WriteFile(tmpFilepath, []byte("abcde"), 0777), IsNil)
|
||||
|
||||
s.files[0].PoolPath, _ = packagePool.Import(tmpFilepath, s.files[0].Filename, &s.files[0].Checksums, false, s.cs)
|
||||
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@ package deb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
@@ -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")
|
||||
@@ -395,7 +395,7 @@ func (s *PackageSuite) TestLinkFromPool(c *C) {
|
||||
p := NewPackageFromControlFile(s.stanza)
|
||||
|
||||
tmpFilepath := filepath.Join(c.MkDir(), "file")
|
||||
c.Assert(ioutil.WriteFile(tmpFilepath, nil, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(tmpFilepath, nil, 0777), IsNil)
|
||||
|
||||
p.Files()[0].PoolPath, _ = packagePool.Import(tmpFilepath, p.Files()[0].Filename, &p.Files()[0].Checksums, false, cs)
|
||||
|
||||
@@ -434,7 +434,7 @@ func (s *PackageSuite) TestDownloadList(c *C) {
|
||||
})
|
||||
|
||||
tmpFilepath := filepath.Join(c.MkDir(), "file")
|
||||
c.Assert(ioutil.WriteFile(tmpFilepath, []byte("abcde"), 0777), IsNil)
|
||||
c.Assert(os.WriteFile(tmpFilepath, []byte("abcde"), 0777), IsNil)
|
||||
p.Files()[0].PoolPath, _ = packagePool.Import(tmpFilepath, p.Files()[0].Filename, &p.Files()[0].Checksums, false, cs)
|
||||
|
||||
list, err = p.DownloadList(packagePool, cs)
|
||||
@@ -449,7 +449,7 @@ func (s *PackageSuite) TestVerifyFiles(c *C) {
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFilepath := filepath.Join(c.MkDir(), "file")
|
||||
c.Assert(ioutil.WriteFile(tmpFilepath, []byte("abcde"), 0777), IsNil)
|
||||
c.Assert(os.WriteFile(tmpFilepath, []byte("abcde"), 0777), IsNil)
|
||||
|
||||
p.Files()[0].PoolPath, _ = packagePool.Import(tmpFilepath, p.Files()[0].Filename, &p.Files()[0].Checksums, false, cs)
|
||||
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+81
-14
@@ -9,10 +9,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ugorji/go/codec"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
@@ -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
|
||||
|
||||
@@ -354,7 +361,7 @@ func walkUpTree(source interface{}, collectionFactory *CollectionFactory) (rootD
|
||||
func NewPublishedRepo(storage, prefix, distribution string, architectures []string,
|
||||
components []string, sources []interface{}, collectionFactory *CollectionFactory, multiDist bool) (*PublishedRepo, error) {
|
||||
result := &PublishedRepo{
|
||||
UUID: uuid.New(),
|
||||
UUID: uuid.NewString(),
|
||||
Storage: storage,
|
||||
Architectures: architectures,
|
||||
Sources: make(map[string]string),
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -631,7 +640,7 @@ func (p *PublishedRepo) Components() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// Components returns sorted list of published repo source names
|
||||
// SourceNames returns sorted list of published repo source names
|
||||
func (p *PublishedRepo) SourceNames() []string {
|
||||
var sources = []string{}
|
||||
|
||||
@@ -702,7 +711,7 @@ func (p *PublishedRepo) Encode() []byte {
|
||||
var buf bytes.Buffer
|
||||
|
||||
encoder := codec.NewEncoder(&buf, &codec.MsgpackHandle{})
|
||||
encoder.Encode(p)
|
||||
_ = encoder.Encode(p)
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -884,7 +893,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
indexes := newIndexFiles(publishedStorage, basePath, tempDir, suffix, p.AcquireByHash, p.SkipBz2)
|
||||
|
||||
@@ -970,7 +979,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
contentIndexesMap[key] = contentIndex
|
||||
}
|
||||
|
||||
contentIndex.Push(qualifiedName, contents, batch)
|
||||
_ = contentIndex.Push(qualifiedName, contents, batch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,6 +1055,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 +1111,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 +1173,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"] = ""
|
||||
@@ -1275,11 +1342,11 @@ func (collection *PublishedRepoCollection) CheckDuplicate(repo *PublishedRepo) *
|
||||
// Update stores updated information about repo in DB
|
||||
func (collection *PublishedRepoCollection) Update(repo *PublishedRepo) error {
|
||||
batch := collection.db.CreateBatch()
|
||||
batch.Put(repo.Key(), repo.Encode())
|
||||
_ = batch.Put(repo.Key(), repo.Encode())
|
||||
|
||||
if repo.SourceKind == SourceLocalRepo {
|
||||
for component, item := range repo.sourceItems {
|
||||
batch.Put(repo.RefKey(component), item.packageRefs.Encode())
|
||||
_ = batch.Put(repo.RefKey(component), item.packageRefs.Encode())
|
||||
}
|
||||
}
|
||||
return batch.Write()
|
||||
@@ -1324,7 +1391,7 @@ func (collection *PublishedRepoCollection) LoadShallow(repo *PublishedRepo, coll
|
||||
|
||||
// LoadComplete loads complete information on the sources of the repo *and* their packages
|
||||
func (collection *PublishedRepoCollection) LoadComplete(repo *PublishedRepo, collectionFactory *CollectionFactory) (err error) {
|
||||
collection.LoadShallow(repo, collectionFactory)
|
||||
_ = collection.LoadShallow(repo, collectionFactory)
|
||||
|
||||
if repo.SourceKind == SourceSnapshot {
|
||||
for _, item := range repo.sourceItems {
|
||||
@@ -1502,7 +1569,7 @@ func (collection *PublishedRepoCollection) listReferencedFilesByComponent(prefix
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packageList.ForEach(func(p *Package) error {
|
||||
_ = packageList.ForEach(func(p *Package) error {
|
||||
poolDir, err := p.PoolDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1575,7 +1642,7 @@ func (collection *PublishedRepoCollection) CleanupPrefixComponentFiles(published
|
||||
return err
|
||||
}
|
||||
|
||||
packageList.ForEach(func(p *Package) error {
|
||||
_ = packageList.ForEach(func(p *Package) error {
|
||||
poolDir, err := p.PoolDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1709,10 +1776,10 @@ func (collection *PublishedRepoCollection) Remove(publishedStorageProvider aptly
|
||||
}
|
||||
|
||||
batch := collection.db.CreateBatch()
|
||||
batch.Delete(repo.Key())
|
||||
_ = batch.Delete(repo.Key())
|
||||
|
||||
for _, component := range repo.Components() {
|
||||
batch.Delete(repo.RefKey(component))
|
||||
_ = batch.Delete(repo.RefKey(component))
|
||||
}
|
||||
|
||||
return batch.Write()
|
||||
|
||||
@@ -19,13 +19,13 @@ func BenchmarkListReferencedFiles(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
db, err := goleveldb.NewOpenDB(tmpDir)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
factory := NewCollectionFactory(db)
|
||||
packageCollection := factory.PackageCollection()
|
||||
@@ -49,7 +49,7 @@ func BenchmarkListReferencedFiles(b *testing.B) {
|
||||
Filename: fmt.Sprintf("pkg-shared_%d.deb", pkgIndex),
|
||||
}})
|
||||
|
||||
packageCollection.UpdateInTransaction(p, transaction)
|
||||
_ = packageCollection.UpdateInTransaction(p, transaction)
|
||||
sharedRefs.Refs = append(sharedRefs.Refs, p.Key(""))
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func BenchmarkListReferencedFiles(b *testing.B) {
|
||||
Filename: fmt.Sprintf("pkg%d_%d.deb", repoIndex, pkgIndex),
|
||||
}})
|
||||
|
||||
packageCollection.UpdateInTransaction(p, transaction)
|
||||
_ = packageCollection.UpdateInTransaction(p, transaction)
|
||||
refs.Refs = append(refs.Refs, p.Key(""))
|
||||
}
|
||||
|
||||
@@ -92,16 +92,16 @@ func BenchmarkListReferencedFiles(b *testing.B) {
|
||||
repo.DefaultDistribution = fmt.Sprintf("dist%d", repoIndex)
|
||||
repo.DefaultComponent = defaultComponent
|
||||
repo.UpdateRefList(refs.Merge(sharedRefs, false, true))
|
||||
repoCollection.Add(repo)
|
||||
_ = repoCollection.Add(repo)
|
||||
|
||||
publish, err := NewPublishedRepo("", "test", "", nil, []string{defaultComponent}, []interface{}{repo}, factory, false)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
publishCollection.Add(publish)
|
||||
_ = publishCollection.Add(publish)
|
||||
}
|
||||
|
||||
db.CompactDB()
|
||||
_ = db.CompactDB()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user