fix(downloader): report aria2 per-peer progress; perf(ci): scope server COPY

Two follow-ups:

- aria2 peers now report progress (0..1) derived from the peer's piece bitfield
  (seeder => 1.0), matching qBittorrent. Previously the Peers list showed '—'
  for aria2 because the field was never populated.
- The server builder copied the whole repo via 'COPY . .', so a cmd/-only
  (downloader) change busted the layer and forced a full vite/tsup rebuild. Copy
  only the build inputs + final-stage sources (src, server, shared, public,
  index.html, vite.config.ts, tsconfig.json, migrations, scripts) so
  downloader-only pushes reuse the cached server image. Verified the builder
  stage still builds and produces dist/dist-server/migrations/entrypoint.
This commit is contained in:
saltbo
2026-06-19 16:19:50 -04:00
parent 348976205b
commit 5eac720dd4
3 changed files with 55 additions and 1 deletions
+10 -1
View File
@@ -11,7 +11,16 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
corepack enable \
&& pnpm install --frozen-lockfile
COPY . .
# Copy only what the JS build (vite + tsup) and the final server image consume,
# so a cmd/-only change (the Go downloader) doesn't bust this layer and force a
# full server rebuild. Keep in sync with the build inputs + the final-stage COPYs.
COPY index.html vite.config.ts tsconfig.json ./
COPY src ./src
COPY server ./server
COPY shared ./shared
COPY public ./public
COPY migrations ./migrations
COPY scripts ./scripts
# .git is excluded from the build context, so git describe cannot run here.
# The release workflow passes the tag via APP_VERSION and the commit SHA via
# APP_COMMIT; resolveAppVersion/resolveAppCommit read them.
+30
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"math/bits"
"net/http"
"net/url"
"os"
@@ -1026,6 +1027,34 @@ func aria2Leechers(peers []arigo.Peer) *int64 {
return &count
}
// aria2PeerProgress derives a peer's 0..1 completion from its piece bitfield
// (aria2 has no direct percentage like qBittorrent). A seeder has every piece.
// Spare bits past the last piece are zero, so dividing by the bitfield's bit
// length only differs from the true piece count by sub-1% — fine for a bar.
func aria2PeerProgress(peer arigo.Peer) *float64 {
if peer.Seeder {
full := 1.0
return &full
}
if peer.BitField == "" {
return nil
}
set := 0
for _, digit := range peer.BitField {
nibble, err := strconv.ParseUint(string(digit), 16, 8)
if err != nil {
return nil
}
set += bits.OnesCount8(uint8(nibble))
}
total := len(peer.BitField) * 4
if total == 0 {
return nil
}
progress := float64(set) / float64(total)
return &progress
}
func aria2Peers(peers []arigo.Peer, geoIP PeerGeoIPResolver) []client.DownloadTaskPeer {
out := make([]client.DownloadTaskPeer, 0, min(len(peers), 20))
for _, peer := range peers {
@@ -1036,6 +1065,7 @@ func aria2Peers(peers []arigo.Peer, geoIP PeerGeoIPResolver) []client.DownloadTa
up := int64(peer.UploadSpeed)
item := client.DownloadTaskPeer{
Address: fmt.Sprintf("%s:%d", peer.IP, peer.Port),
Progress: aria2PeerProgress(peer),
DownloadBps: &down,
UploadBps: &up,
}
+15
View File
@@ -201,6 +201,21 @@ func TestAria2SeedTimeMinutes(t *testing.T) {
}
}
func TestAria2PeerProgress(t *testing.T) {
if p := aria2PeerProgress(arigo.Peer{Seeder: true}); p == nil || *p != 1.0 {
t.Fatalf("expected seeder progress 1.0, got %v", p)
}
if p := aria2PeerProgress(arigo.Peer{BitField: "ff"}); p == nil || *p != 1.0 {
t.Fatalf("expected full bitfield 1.0, got %v", p)
}
if p := aria2PeerProgress(arigo.Peer{BitField: "f0"}); p == nil || *p != 0.5 {
t.Fatalf("expected half bitfield 0.5, got %v", p)
}
if p := aria2PeerProgress(arigo.Peer{BitField: ""}); p != nil {
t.Fatalf("expected nil progress for empty bitfield, got %v", p)
}
}
func TestQBittorrentStartArgsWritesManagedListenPort(t *testing.T) {
stateDir := t.TempDir()
downloadDir := t.TempDir()