feat(api): make device rate limits, OPDS page size, and conversion cache configurable

Move three more hardcoded values behind the settings registry. All
apply immediately on the next request (no restart needed).

device_auth.go:
- DeviceAuthMiddleware reads per-route device rate limits (sync /
  progress / metadata per minute) from the registry on each
  authenticated request via a rateLimitConfig() helper, falling back to
  the Default* constants when no registry is wired.
- The X-RateLimit-Limit response header previously hardcoded "60" for
  every request type; it now reflects the actual configured limit for
  the request type via rateLimitForRequestType().

opds.go:
- Default (50) and maximum (200) OPDS page sizes come from the
  registry's OpdsDefaultPageSize()/OpdsMaxPageSize() instead of inline
  literals, so catalog pagination can be tuned without a redeploy.

conversion_service.go:
- The 24h kepub cache lifetime is read from the registry via a
  cacheTTL() helper (was a bare 24 * time.Hour literal in the
  constructor). The field default is retained for tests that construct
  the service directly.
- conversion_service_test.go updated to assert both the field default
  and the cacheTTL() accessor return 24h.
This commit is contained in:
2026-08-10 08:01:28 -04:00
parent 457a38306d
commit d12911d3c8
4 changed files with 88 additions and 12 deletions
+40 -7
View File
@@ -25,6 +25,7 @@ type DeviceContext struct {
type DeviceAuthMiddleware struct {
db *database.Queries
rateLimiter *DeviceRateLimiter
settings *database.SettingsRegistry
}
func NewDeviceAuthMiddleware(db *database.Queries) *DeviceAuthMiddleware {
@@ -34,6 +35,41 @@ func NewDeviceAuthMiddleware(db *database.Queries) *DeviceAuthMiddleware {
}
}
// SetSettings wires the tunable settings registry so device rate limits are
// read live on each authenticated request.
func (m *DeviceAuthMiddleware) SetSettings(s *database.SettingsRegistry) { m.settings = s }
// rateLimitConfig returns the active device rate limits from the registry, or
// the historical defaults when no registry is wired.
func (m *DeviceAuthMiddleware) rateLimitConfig() DeviceRateLimitConfig {
if m.settings != nil {
dl := m.settings.DeviceRateLimits()
return DeviceRateLimitConfig{
SyncRequestsPerMinute: dl.Sync,
ProgressUpdatesPerMinute: dl.Progress,
MetadataRequestsPerMinute: dl.Metadata,
}
}
return DeviceRateLimitConfig{
SyncRequestsPerMinute: DefaultSyncRequestsPerMinute,
ProgressUpdatesPerMinute: DefaultProgressUpdatesPerMinute,
MetadataRequestsPerMinute: DefaultMetadataRequestsPerMinute,
}
}
// rateLimitForRequestType returns the configured per-minute limit for a given
// request type, for use in X-RateLimit-* headers.
func (m *DeviceAuthMiddleware) rateLimitForRequestType(requestType string, config DeviceRateLimitConfig) int {
switch requestType {
case "progress":
return config.ProgressUpdatesPerMinute
case "metadata":
return config.MetadataRequestsPerMinute
default: // "sync" and any unknown type
return config.SyncRequestsPerMinute
}
}
func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
var device database.Devices
@@ -115,15 +151,12 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF
deviceUUID := uuid.UUID(device.ID.Bytes)
deviceID := deviceUUID.String()
config := DeviceRateLimitConfig{
SyncRequestsPerMinute: 60,
ProgressUpdatesPerMinute: 120,
MetadataRequestsPerMinute: 30,
}
config := m.rateLimitConfig()
limitForType := m.rateLimitForRequestType(requestType, config)
if !m.rateLimiter.CheckRateLimit(deviceID, requestType, config) {
remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config)
c.Response().Header().Set("X-RateLimit-Limit", "60")
c.Response().Header().Set("X-RateLimit-Limit", strconv.Itoa(limitForType))
c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
c.Response().Header().Set("X-RateLimit-Reset", "60")
return c.JSON(http.StatusTooManyRequests, map[string]string{
@@ -134,7 +167,7 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF
}
remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config)
c.Response().Header().Set("X-RateLimit-Limit", "60")
c.Response().Header().Set("X-RateLimit-Limit", strconv.Itoa(limitForType))
c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
ctx := DeviceContext{