Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cmd/altmount/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func runServe(cmd *cobra.Command, args []string) error {
configManager.OnConfigChange(func(oldConfig, newConfig *config.Config) {
structuralChange := oldConfig.SegmentCache.CachePath != newConfig.SegmentCache.CachePath ||
oldConfig.SegmentCache.MaxSizeGB != newConfig.SegmentCache.MaxSizeGB ||
oldConfig.SegmentCache.ExpiryHours != newConfig.SegmentCache.ExpiryHours
intPtrValue(oldConfig.SegmentCache.ExpiryHours) != intPtrValue(newConfig.SegmentCache.ExpiryHours)

if !structuralChange {
return
Expand Down Expand Up @@ -439,3 +439,13 @@ func waitForHTTPServer(ctx context.Context, port int) error {
}
}


// intPtrValue returns the value pointed to by p, or 0 when p is nil. It is used
// to compare optional integer config fields (e.g. segment cache ExpiryHours) by
// value rather than by pointer identity.
func intPtrValue(p *int) int {
if p == nil {
return 0
}
return *p
}
10 changes: 9 additions & 1 deletion cmd/altmount/cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,18 @@ func initializeSegmentCache(ctx context.Context, cfg *config.Config, source *seg
return nil
}

// ExpiryHours is normalized in config.Validate (nil -> 24h); guard against
// nil defensively in case the cache is initialized outside the load path. A
// zero value is preserved and means "cache forever".
expiryHours := 24
if cfg.SegmentCache.ExpiryHours != nil {
expiryHours = *cfg.SegmentCache.ExpiryHours
}

mgrCfg := segcache.ManagerConfig{
CachePath: cfg.SegmentCache.CachePath,
MaxSizeBytes: int64(cfg.SegmentCache.MaxSizeGB) * 1024 * 1024 * 1024,
ExpiryDuration: time.Duration(cfg.SegmentCache.ExpiryHours) * time.Hour,
ExpiryDuration: time.Duration(expiryHours) * time.Hour,
}.WithDefaults()

mgr, err := segcache.NewManager(mgrCfg, slog.Default().With("component", "segcache"))
Expand Down
37 changes: 25 additions & 12 deletions frontend/src/components/config/StreamingConfigSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ export function StreamingConfigSection({
<div className="min-w-0">
<h4 className="font-bold text-base-content text-sm">Playback Failure Masking</h4>
<p className="mt-1 break-words text-[11px] text-base-content/50 leading-relaxed">
Debounce transient streaming errors (like propagation delays) by requiring multiple consecutive playback failures before triggering a redownload.
Debounce transient streaming errors (like propagation delays) by requiring multiple
consecutive playback failures before triggering a redownload.
</p>
</div>
<input
Expand Down Expand Up @@ -159,14 +160,17 @@ export function StreamingConfigSection({
<div className="min-w-0">
<h5 className="font-bold text-base-content text-xs">Failure Threshold</h5>
<p className="mt-1 break-words text-[10px] text-base-content/50 leading-relaxed">
Number of consecutive failures required before declaring a file corrupted and requesting repair.
Number of consecutive failures required before declaring a file corrupted and
requesting repair.
</p>
</div>
<div className="flex shrink-0 items-center gap-3">
<span className="font-black font-mono text-primary text-lg">
<span className="font-black font-mono text-lg text-primary">
{streamingData.failure_masking?.threshold ?? 3}
</span>
<span className="font-bold text-base-content/60 text-[10px] uppercase">failures</span>
<span className="font-bold text-[10px] text-base-content/60 uppercase">
failures
</span>
</div>
</div>
<input
Expand All @@ -189,7 +193,7 @@ export function StreamingConfigSection({
checkChanges(newData, cacheData);
}}
/>
<div className="flex justify-between px-1 font-black text-base-content/50 text-[10px]">
<div className="flex justify-between px-1 font-black text-[10px] text-base-content/50">
<span>1</span>
<span>3</span>
<span>5</span>
Expand Down Expand Up @@ -300,21 +304,30 @@ export function StreamingConfigSection({
Cache Expiry
</h4>
<p className="mt-1 break-words text-[11px] text-base-content/50 leading-relaxed">
How long cached segments are kept before automatic eviction.
How long cached segments are kept before automatic eviction. Set to 0 to keep
them forever (bounded only by the maximum cache size).
</p>
</div>
<div className="mt-1 flex shrink-0 items-center justify-start gap-3 sm:mt-0 sm:justify-end">
<span className="font-black font-mono text-primary text-xl">
{cacheData.expiry_hours}
</span>
<span className="font-bold text-base-content/60 text-xs uppercase">hours</span>
{cacheData.expiry_hours === 0 ? (
<span className="font-black font-mono text-primary text-xl">Forever</span>
) : (
<>
<span className="font-black font-mono text-primary text-xl">
{cacheData.expiry_hours}
</span>
<span className="font-bold text-base-content/60 text-xs uppercase">
hours
</span>
</>
)}
</div>
</div>

<div className="space-y-4">
<input
type="range"
min="1"
min="0"
max="168"
value={cacheData.expiry_hours}
step="1"
Expand All @@ -325,7 +338,7 @@ export function StreamingConfigSection({
}
/>
<div className="flex justify-between px-2 font-black text-base-content/50 text-xs">
<span>1h</span>
<span>Forever</span>
<span>42h</span>
<span>84h</span>
<span>126h</span>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/system/ProviderStatusTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,9 @@ export function ProviderStatusTable({
)}
{hasQuota && (
<div className="mt-1.5 border-base-200/60 border-t pt-1 text-[10px]">
<div className="font-semibold font-mono text-warning">
Quota: {formatBytes(provider.quota_used || 0, 2, false, true)} / {formatBytes(provider.quota_bytes || 0, 2, false, true)}
<div className="font-mono font-semibold text-warning">
Quota: {formatBytes(provider.quota_used || 0, 2, false, true)} /{" "}
{formatBytes(provider.quota_bytes || 0, 2, false, true)}
</div>
{provider.quota_reset_at && (
<div className="mt-0.5 font-mono text-base-content/40">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/HealthPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiClient } from "../api/client";
import { useQueryClient } from "@tanstack/react-query";
import { FileCheck, RefreshCw, RotateCcw, Settings, ShieldCheck, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { apiClient } from "../api/client";
import { ErrorAlert } from "../components/ui/ErrorAlert";
import { Pagination } from "../components/ui/Pagination";
import { useConfirm } from "../contexts/ModalContext";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ export function HealthTable({
onSelectAll={onSelectAll}
onSelectAllPages={onSelectAllPages}
onSort={onSort}
allowSelectAllPages={["pending", "checking", "corrupted", "repair_triggered"].includes(statusFilter)}
allowSelectAllPages={[
"pending",
"checking",
"corrupted",
"repair_triggered",
].includes(statusFilter)}
/>
<tbody>
{data.map((item: FileHealth) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function HealthTableHeader({
<tr>
<th className="w-16">
<div className="dropdown">
<label tabIndex={0} className="cursor-pointer flex items-center gap-1">
<label className="flex cursor-pointer items-center gap-1">
<input
type="checkbox"
className="checkbox checkbox-sm"
Expand All @@ -39,10 +39,7 @@ export function HealthTableHeader({
/>
<ChevronDown className="h-3 w-3" />
</label>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<ul className="dropdown-content menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
<li>
<button type="button" onClick={() => onSelectAll(true)}>
Select all on page
Expand Down
12 changes: 3 additions & 9 deletions frontend/src/pages/QueuePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { apiClient } from "../api/client";
import { useQueryClient } from "@tanstack/react-query";
import {
Activity,
Expand Down Expand Up @@ -28,6 +27,7 @@ import {
XOctagon,
} from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { apiClient } from "../api/client";
import { ImportMethods } from "../components/queue/ImportMethods";
import { QueueItemCard } from "../components/queue/QueueItemCard";
import { ErrorAlert } from "../components/ui/ErrorAlert";
Expand Down Expand Up @@ -778,10 +778,7 @@ export function QueuePage() {
<tr>
<th className="w-16">
<div className="dropdown">
<label
tabIndex={0}
className="cursor-pointer flex items-center gap-1"
>
<label className="flex cursor-pointer items-center gap-1">
<input
type="checkbox"
className="checkbox checkbox-sm"
Expand All @@ -793,10 +790,7 @@ export function QueuePage() {
/>
<ChevronDown className="h-3 w-3" />
</label>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<ul className="dropdown-content menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
<li>
<button type="button" onClick={() => handleSelectAll(true)}>
Select all on page
Expand Down
19 changes: 15 additions & 4 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,13 @@ func (n NetworkConfig) GetNoProxy() string { return n.NoProxy }
// When enabled, this cache replaces the FUSE VFS disk cache and additionally benefits WebDAV.
// Cache key: Usenet message ID. Cache unit: ~750KB decoded segment (matches one NNTP article).
type SegmentCacheConfig struct {
Enabled *bool `yaml:"enabled" mapstructure:"enabled" json:"enabled"`
CachePath string `yaml:"cache_path" mapstructure:"cache_path" json:"cache_path"`
MaxSizeGB int `yaml:"max_size_gb" mapstructure:"max_size_gb" json:"max_size_gb"`
ExpiryHours int `yaml:"expiry_hours" mapstructure:"expiry_hours" json:"expiry_hours"`
Enabled *bool `yaml:"enabled" mapstructure:"enabled" json:"enabled"`
CachePath string `yaml:"cache_path" mapstructure:"cache_path" json:"cache_path"`
MaxSizeGB int `yaml:"max_size_gb" mapstructure:"max_size_gb" json:"max_size_gb"`
// ExpiryHours controls how long cached segments are kept before automatic
// eviction. Set to 0 to disable expiry (cache forever, bounded only by
// MaxSizeGB via LRU eviction). Left unset (nil) it defaults to 24 hours.
ExpiryHours *int `yaml:"expiry_hours" mapstructure:"expiry_hours" json:"expiry_hours"`
}

// WebDAVConfig represents WebDAV server configuration
Expand Down Expand Up @@ -662,6 +665,14 @@ func (c *Config) Validate() error {
c.Streaming.MaxPrefetch = 60 // Default to 60 segments prefetched ahead if not set
}

// Segment cache expiry: nil (unset) defaults to 24 hours; an explicit 0 is
// preserved and means "cache forever" (bounded only by the size cap). A
// pointer is used so unset and explicit-0 can be distinguished.
if c.SegmentCache.ExpiryHours == nil {
defaultExpiryHours := 24
c.SegmentCache.ExpiryHours = &defaultExpiryHours
}

if c.Import.MaxProcessorWorkers <= 0 {
return fmt.Errorf("import max_processor_workers must be greater than 0")
}
Expand Down
8 changes: 5 additions & 3 deletions internal/nzbfilesystem/segcache/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ func DefaultManagerConfig() ManagerConfig {
}

// WithDefaults returns a copy with zero values replaced by defaults.
//
// ExpiryDuration is intentionally NOT defaulted here: a zero (or negative)
// duration means "cache forever" (see SegmentCache.Cleanup), and the expiry
// default is applied upstream in config.Validate. Overriding it here would make
// an explicit "forever" setting impossible.
func (cfg ManagerConfig) WithDefaults() ManagerConfig {
defaults := DefaultManagerConfig()
if cfg.CachePath == "" {
Expand All @@ -35,9 +40,6 @@ func (cfg ManagerConfig) WithDefaults() ManagerConfig {
if cfg.MaxSizeBytes <= 0 {
cfg.MaxSizeBytes = defaults.MaxSizeBytes
}
if cfg.ExpiryDuration <= 0 {
cfg.ExpiryDuration = defaults.ExpiryDuration
}
return cfg
}

Expand Down
Loading