Skip to content

Commit e946e01

Browse files
committed
Improve storage controls and API compatibility
1 parent 6538464 commit e946e01

33 files changed

Lines changed: 2669 additions & 133 deletions

docs/arr-setup.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,26 @@ Same steps as Sonarr, but use category `movies-radarr` (or your preferred catego
5858
| Method | Path | Purpose |
5959
|--------|------|---------|
6060
| POST | `/api/v2/auth/login` | Cookie-based login |
61+
| POST | `/api/v2/auth/logout` | Clear session cookie |
6162
| GET | `/api/v2/app/version` | Client compatibility check |
6263
| GET | `/api/v2/app/webapiVersion` | API version (`2.11.0`) |
64+
| GET | `/api/v2/app/buildInfo` | Build information stub |
65+
| GET | `/api/v2/app/preferences` | qBittorrent preferences stub |
66+
| GET | `/api/v2/app/defaultSavePath` | Default save path (`/downloads`) |
67+
| GET | `/api/v2/transfer/info` | Global transfer state |
6368
| POST | `/api/v2/torrents/add` | Add magnet (`urls` + optional `category`) |
6469
| GET | `/api/v2/torrents/info` | List torrents |
70+
| GET | `/api/v2/torrents/properties` | Torrent properties by hash |
71+
| GET | `/api/v2/torrents/files` | Torrent file list by hash |
72+
| GET | `/api/v2/torrents/categories` | Category list |
73+
| POST | `/api/v2/torrents/createCategory` | Create category stub |
74+
| POST | `/api/v2/torrents/setCategory` | Assign category by hash |
6575
| POST | `/api/v2/torrents/delete` | Remove by info hash |
76+
| GET/POST | Additional qBit management probes | Safe stubs for tags, limits, pause/resume, recheck/reannounce |
6677
| GET | `/api/v2/sync/maindata` | Polling sync (full state stub) |
6778

79+
`GET /api/v2` returns a small compatibility descriptor for browser/manual checks. qBittorrent-compatible clients should still use the grouped endpoints above.
80+
6881
## curl sanity check
6982

7083
```bash

internal/api/admin/handlers.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ func (h *Handler) stats(w http.ResponseWriter, r *http.Request) {
104104
resp := map[string]any{
105105
"diskUsed": s.DiskUsed,
106106
"diskQuota": s.DiskQuota,
107+
"s3Used": s.S3Used,
108+
"s3Quota": s.S3Quota,
109+
"s3QuotaGb": h.effectiveS3QuotaGB(),
110+
"s3ObjectCount": s.S3ObjectCount,
111+
"s3Enabled": s.S3Enabled,
107112
"torrentCount": s.TorrentCount,
108113
"activeCount": s.ActiveCount,
109114
"downloadSpeed": s.DownloadSpeed,
@@ -301,14 +306,18 @@ func (h *Handler) maintenanceCleanup(w http.ResponseWriter, r *http.Request) {
301306
return
302307
}
303308
h.LogActivity(r.Context(), ActionMaintenance, map[string]any{
304-
"ageRemoved": result.AgeRemoved,
305-
"quotaRemoved": result.QuotaRemoved,
309+
"ageRemoved": result.AgeRemoved,
310+
"quotaRemoved": result.QuotaRemoved,
311+
"s3QuotaRemoved": result.S3QuotaRemoved,
306312
})
307313
writeJSON(w, http.StatusOK, map[string]any{
308-
"ageRemoved": result.AgeRemoved,
309-
"quotaRemoved": result.QuotaRemoved,
310-
"diskUsed": result.DiskUsed,
311-
"diskQuota": result.DiskQuota,
314+
"ageRemoved": result.AgeRemoved,
315+
"quotaRemoved": result.QuotaRemoved,
316+
"s3QuotaRemoved": result.S3QuotaRemoved,
317+
"diskUsed": result.DiskUsed,
318+
"diskQuota": result.DiskQuota,
319+
"s3Used": result.S3Used,
320+
"s3Quota": result.S3Quota,
312321
})
313322
}
314323

@@ -367,6 +376,8 @@ func (h *Handler) publicConfig(w http.ResponseWriter, r *http.Request) {
367376
"publicUrl": h.cfg.PublicURL,
368377
"retentionDays": h.effectiveRetentionDays(),
369378
"diskQuotaGb": h.effectiveDiskQuotaGB(),
379+
"s3QuotaGb": h.effectiveS3QuotaGB(),
380+
"s3Enabled": h.effectiveS3Enabled(),
370381
"rateLimitMbps": h.effectiveRateLimitMbps(),
371382
"webdavEnabled": h.cfg.WebDAVEnabled,
372383
"metricsEnabled": h.cfg.MetricsEnabled,

internal/api/admin/handlers_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,63 @@ func TestGetSettingsRedaction(t *testing.T) {
584584
_ = db
585585
}
586586

587+
func TestS3QuotaSettingsConfigAndStats(t *testing.T) {
588+
h, _, db := newTestHandler(t)
589+
ctx := context.Background()
590+
591+
patchRec := serve(t, h, http.MethodPatch, "/settings", []byte(`{"s3Enabled":true,"s3QuotaGb":9}`))
592+
if patchRec.Code != http.StatusOK {
593+
t.Fatalf("patch status = %d, body = %s", patchRec.Code, patchRec.Body.String())
594+
}
595+
var settingsResp map[string]any
596+
if err := json.Unmarshal(patchRec.Body.Bytes(), &settingsResp); err != nil {
597+
t.Fatalf("decode settings: %v", err)
598+
}
599+
if settingsResp["s3QuotaGb"] != float64(9) {
600+
t.Fatalf("settings s3QuotaGb = %v", settingsResp["s3QuotaGb"])
601+
}
602+
603+
rec := storage.TorrentRecord{
604+
ID: "S3API",
605+
InfoHash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
606+
Status: "downloaded",
607+
AddedAt: time.Now().UTC(),
608+
Files: []storage.TorrentFileRecord{
609+
{ID: 1, TorrentID: "S3API", Path: "/remote.mkv", Bytes: 2048, Selected: true, ObjectKey: "remote/movie.mkv", RemoteStored: true},
610+
},
611+
}
612+
if err := db.CreateTorrent(ctx, rec); err != nil {
613+
t.Fatalf("create torrent: %v", err)
614+
}
615+
616+
configRec := serve(t, h, http.MethodGet, "/config", nil)
617+
if configRec.Code != http.StatusOK {
618+
t.Fatalf("config status = %d", configRec.Code)
619+
}
620+
var configResp map[string]any
621+
if err := json.Unmarshal(configRec.Body.Bytes(), &configResp); err != nil {
622+
t.Fatalf("decode config: %v", err)
623+
}
624+
if configResp["s3QuotaGb"] != float64(9) || configResp["s3Enabled"] != true {
625+
t.Fatalf("config S3 fields = %+v", configResp)
626+
}
627+
628+
statsRec := serve(t, h, http.MethodGet, "/stats", nil)
629+
if statsRec.Code != http.StatusOK {
630+
t.Fatalf("stats status = %d", statsRec.Code)
631+
}
632+
var statsResp map[string]any
633+
if err := json.Unmarshal(statsRec.Body.Bytes(), &statsResp); err != nil {
634+
t.Fatalf("decode stats: %v", err)
635+
}
636+
if statsResp["s3Used"] != float64(2048) || statsResp["s3ObjectCount"] != float64(1) {
637+
t.Fatalf("stats S3 usage = %+v", statsResp)
638+
}
639+
if statsResp["s3QuotaGb"] != float64(9) || statsResp["s3Quota"] != float64(9*1024*1024*1024) {
640+
t.Fatalf("stats S3 quota = %+v", statsResp)
641+
}
642+
}
643+
587644
func TestDeleteSelfRejected(t *testing.T) {
588645
cfg := testConfig()
589646
cfg.MultiUserEnabled = true

internal/api/admin/settings.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ func (h *Handler) effectiveDiskQuotaGB() int64 {
5656
return h.cfg.DiskQuotaGB
5757
}
5858

59+
func (h *Handler) effectiveS3QuotaGB() int64 {
60+
if h.settings != nil {
61+
return h.settings.GetS3QuotaGB()
62+
}
63+
return objectstore.LoadFromEnv().QuotaGB
64+
}
65+
66+
func (h *Handler) effectiveS3Enabled() bool {
67+
if h.settings != nil {
68+
return h.settings.GetS3Enabled()
69+
}
70+
return objectstore.LoadFromEnv().Enabled
71+
}
72+
5973
func (h *Handler) effectiveRateLimitMbps() float64 {
6074
if h.settings != nil {
6175
return h.settings.GetDownloadRateLimitMbps()

internal/api/qbit/auth.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,33 @@ func (s *sessionStore) valid(sid string) bool {
5050
return true
5151
}
5252

53+
func (s *sessionStore) delete(sid string) {
54+
if sid == "" {
55+
return
56+
}
57+
s.mu.Lock()
58+
delete(s.sessions, sid)
59+
s.mu.Unlock()
60+
}
61+
5362
func (h *Handler) authMiddleware(next http.Handler) http.Handler {
5463
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5564
cookie, err := r.Cookie(sessionCookie)
56-
if err != nil || !h.sessions.valid(cookie.Value) {
57-
w.WriteHeader(http.StatusForbidden)
65+
if err == nil && h.sessions.valid(cookie.Value) {
66+
next.ServeHTTP(w, r)
67+
return
68+
}
69+
if user, password, ok := r.BasicAuth(); ok && h.authenticateLogin(r.Context(), user, password) {
70+
next.ServeHTTP(w, r)
5871
return
5972
}
60-
next.ServeHTTP(w, r)
73+
if h.auth != nil {
74+
if _, ok := h.auth.ValidateToken(r.Context(), r.Header.Get("Authorization")); ok {
75+
next.ServeHTTP(w, r)
76+
return
77+
}
78+
}
79+
w.WriteHeader(http.StatusForbidden)
6180
})
6281
}
6382

internal/api/qbit/convert.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,55 @@ func toQBitTorrent(rec *storage.TorrentRecord, category string) map[string]any {
133133
}
134134
}
135135

136+
func torrentProperties(rec *storage.TorrentRecord) map[string]any {
137+
size := torrentSize(rec)
138+
completed := downloadedBytes(rec)
139+
piecesHave := 0
140+
if torrentmgr.IsCompletedStatus(rec.Status) || completed >= size && size > 0 {
141+
piecesHave = 1
142+
}
143+
return map[string]any{
144+
"save_path": defaultSave,
145+
"creation_date": rec.AddedAt.Unix(),
146+
"piece_size": size,
147+
"comment": "",
148+
"total_wasted": int64(0),
149+
"total_uploaded": int64(0),
150+
"total_uploaded_session": int64(0),
151+
"total_downloaded": completed,
152+
"total_downloaded_session": completed,
153+
"up_limit": int64(0),
154+
"dl_limit": int64(0),
155+
"time_elapsed": timeActive(rec),
156+
"seeding_time": int64(0),
157+
"nb_connections": 0,
158+
"nb_connections_limit": -1,
159+
"share_ratio": float64(0),
160+
"addition_date": rec.AddedAt.Unix(),
161+
"completion_date": completionOn(rec),
162+
"created_by": "DebridNest",
163+
"dl_speed": rec.Speed,
164+
"dl_speed_avg": int64(0),
165+
"eta": etaSeconds(rec),
166+
"last_seen": int64(-1),
167+
"peers": 0,
168+
"peers_total": 0,
169+
"pieces_have": piecesHave,
170+
"pieces_num": 1,
171+
"reannounce": int64(-1),
172+
"seeds": rec.Seeders,
173+
"seeds_total": rec.Seeders,
174+
"total_size": size,
175+
}
176+
}
177+
178+
func qbitFilePriority(f storage.TorrentFileRecord) int {
179+
if !f.Selected {
180+
return 0
181+
}
182+
return 1
183+
}
184+
136185
func completionOn(rec *storage.TorrentRecord) int64 {
137186
if rec.EndedAt != nil {
138187
return rec.EndedAt.Unix()

0 commit comments

Comments
 (0)