feat(files): MAX_VERSIONS_KEPT statt zweier Konstanten
Die Zahl stand doppelt im Quelltext: als const im Backend und noch einmal im Verlaufs-Dialog, verbunden nur durch den Kommentar "muss zu maxVersionsKept im Backend passen". Ein Satz ist keine Zusicherung - beim ersten Aendern haette der Dialog eine andere Zahl genannt als die, die der Server anwendet. Jetzt eine .env-Variable (Vorgabe 5), die der Server ueber /api/v1/public/config mitliefert. Werte unter 1 werden verworfen: Die Zahl geht als OFFSET in die Aufraeumabfrage, und OFFSET 0 loeschte jede Fassung - aus "keine Grenze" wuerde "kein Verlauf". Gemessen: MAX_VERSIONS_KEPT=3, Datei siebenmal ueberschrieben, uebrig bleiben v6, v5, v4 bei aktueller v7. Dazu ein README-Abschnitt "Versionen" - Standard aus, warum das so ist, Kontingent, und dass jedes Strg+S in Office eine Fassung anlegt. Bisher stand zu alldem nichts im README. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fc8bb51d5
commit
ebff565367
10 files changed
+109
-4
No files matched your search
@@ -33,6 +33,10 @@ APP_PORT=8080
|
|||||||
# davor gehört der Proxy). 0.0.0.0 = das ganze Netz – dann unverschlüsselt.
|
# davor gehört der Proxy). 0.0.0.0 = das ganze Netz – dann unverschlüsselt.
|
||||||
APP_BIND=127.0.0.1
|
APP_BIND=127.0.0.1
|
||||||
|
|
||||||
|
# Wie viele frühere Fassungen einer Datei aufgehoben werden. Gilt nur für
|
||||||
|
# Dateien, an denen „Keep versions" eingeschaltet ist – Standard ist aus.
|
||||||
|
MAX_VERSIONS_KEPT=5
|
||||||
|
|
||||||
# ── Erster Start ──────────────────────────────────────────────────────────────
|
# ── Erster Start ──────────────────────────────────────────────────────────────
|
||||||
# Legt den Admin an. Danach ADMIN_PASSWORD leeren.
|
# Legt den Admin an. Danach ADMIN_PASSWORD leeren.
|
||||||
# Auch hier: ein $ muss $$ heißen (siehe DB_PASSWORD).
|
# Auch hier: ein $ muss $$ heißen (siehe DB_PASSWORD).
|
||||||
|
|||||||
@@ -206,6 +206,26 @@ Datenbank nicht kennt; die Cloud sieht sie nicht, und der GC räumt sie
|
|||||||
irgendwann weg. Zum Hineinlegen ist WebDAV der Weg. Dazu `APP_UID`/`APP_GID`
|
irgendwann weg. Zum Hineinlegen ist WebDAV der Weg. Dazu `APP_UID`/`APP_GID`
|
||||||
aus `id -u` und `id -g`, sonst gehören die Dateien einem Container-Nutzer.
|
aus `id -u` und `id -g`, sonst gehören die Dateien einem Container-Nutzer.
|
||||||
|
|
||||||
|
### Versionen
|
||||||
|
|
||||||
|
Eine Datei kann frühere Fassungen aufheben. **Standard ist aus**, eingeschaltet
|
||||||
|
wird es je Datei über Rechtsklick → „Keep versions".
|
||||||
|
|
||||||
|
Warum nicht für alle: Eine 50-MB-Datei, die täglich per WebDAV synchronisiert
|
||||||
|
wird, läge sonst nach einer Woche sechsmal auf der Platte. Der Schalter macht
|
||||||
|
sichtbar, wo das gewollt ist.
|
||||||
|
|
||||||
|
`MAX_VERSIONS_KEPT=5` bestimmt, wie viele Vorgänger bleiben – zusammen mit der
|
||||||
|
aktuellen Fassung also sechs Stände. Was darüber hinausgeht, wird beim
|
||||||
|
Speichern samt Blob gelöscht. Die Fassungen zählen auf dein Kontingent.
|
||||||
|
|
||||||
|
Kleiner als 1 wird verworfen: Die Zahl geht als `OFFSET` in die Aufräumabfrage,
|
||||||
|
und `OFFSET 0` löschte jede Fassung – aus „keine Grenze" würde „kein Verlauf".
|
||||||
|
Wer keinen Verlauf will, lässt den Schalter an der Datei aus.
|
||||||
|
|
||||||
|
Zu beachten: **Jedes Strg+S in Office legt eine Fassung an.** Beim Testen
|
||||||
|
sprang eine Datei dadurch von v1 auf v3, nicht auf v2.
|
||||||
|
|
||||||
### Volltextsuche (OCR)
|
### Volltextsuche (OCR)
|
||||||
|
|
||||||
Standardmäßig an. Fehlen `ocrmypdf`/`tesseract` im Abbild, schaltet sich der
|
Standardmäßig an. Fehlen `ocrmypdf`/`tesseract` im Abbild, schaltet sich der
|
||||||
|
|||||||
+19
@@ -706,6 +706,25 @@ ist die falsche – die Schnipsel haben deshalb **kein** Profil mehr.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Anzahl aufgehobener Fassungen (24.08.2026)
|
||||||
|
|
||||||
|
`MAX_VERSIONS_KEPT`, Vorgabe 5. Vorher eine Konstante – und zwar **zwei**:
|
||||||
|
eine im Backend, eine im Verlaufs-Dialog, verbunden nur durch den Kommentar
|
||||||
|
„muss zusammenpassen". Ein Satz ist keine Zusicherung. Der Dialog holt die
|
||||||
|
Zahl jetzt aus `/api/v1/public/config`.
|
||||||
|
|
||||||
|
- [ ] `MAX_VERSIONS_KEPT=3` → `/public/config` meldet 3 🔎
|
||||||
|
- [ ] Datei mit „Keep versions" siebenmal überschreiben → genau 3 Fassungen
|
||||||
|
im Verlauf, und zwar die neuesten 🔎 (gemessen: v6, v5, v4 bei
|
||||||
|
aktueller v7)
|
||||||
|
- [ ] Der Dialog nennt dieselbe Zahl wie der Server
|
||||||
|
- [ ] `MAX_VERSIONS_KEPT=0` wird verworfen, die Vorgabe greift – `OFFSET 0`
|
||||||
|
löschte sonst **jede** Fassung
|
||||||
|
- [ ] Unbrauchbarer Wert (`abc`) → Vorgabe, kein Startabbruch
|
||||||
|
- [ ] Ohne „Keep versions" an der Datei entsteht weiterhin kein Verlauf
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Erreichbarkeit von außen (24.08.2026)
|
## Erreichbarkeit von außen (24.08.2026)
|
||||||
|
|
||||||
`ERR_CONNECTION_REFUSED` beim Zugriff über die IP ist meist kein Fehler,
|
`ERR_CONNECTION_REFUSED` beim Zugriff über die IP ist meist kein Fehler,
|
||||||
|
|||||||
@@ -180,6 +180,10 @@ func main() {
|
|||||||
|
|
||||||
// fileHandler bekommt docStore mit: Der PDF-Betrachter holt sich darueber
|
// fileHandler bekommt docStore mit: Der PDF-Betrachter holt sich darueber
|
||||||
// den erkannten Text eines Scans, in Seiten zerlegt.
|
// den erkannten Text eines Scans, in Seiten zerlegt.
|
||||||
|
// Wie viele frühere Fassungen aufgehoben werden. Einmal hier gesetzt,
|
||||||
|
// danach nur gelesen – siehe files.SetMaxVersionsKept.
|
||||||
|
files.SetMaxVersionsKept(cfg.MaxVersionsKept)
|
||||||
|
|
||||||
fileHandler := files.NewHandler(fileStore, blobStore, auditor, aclStore, tagStore, docStore)
|
fileHandler := files.NewHandler(fileStore, blobStore, auditor, aclStore, tagStore, docStore)
|
||||||
|
|
||||||
shareStore := sharing.NewStore(pool)
|
shareStore := sharing.NewStore(pool)
|
||||||
@@ -373,6 +377,10 @@ func main() {
|
|||||||
// Damit die Oberflaeche den Menuepunkt „Edit in Office" gar nicht
|
// Damit die Oberflaeche den Menuepunkt „Edit in Office" gar nicht
|
||||||
// erst anbietet, wenn es keinen Office-Server gibt.
|
// erst anbietet, wenn es keinen Office-Server gibt.
|
||||||
"office_enabled": officeHandler.Enabled(),
|
"office_enabled": officeHandler.Enabled(),
|
||||||
|
// Damit der Verlaufs-Dialog dieselbe Zahl nennt, die der Server
|
||||||
|
// anwendet. Vorher stand sie zweimal im Quelltext, mit einem
|
||||||
|
// Kommentar „muss zusammenpassen" – das ist keine Zusicherung.
|
||||||
|
"max_versions_kept": cfg.MaxVersionsKept,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,26 @@ const maxUploadSize = 5 << 30 // 5 GB
|
|||||||
|
|
||||||
// maxVersionsKept begrenzt den Verlauf einer Datei mit aktivierter
|
// maxVersionsKept begrenzt den Verlauf einer Datei mit aktivierter
|
||||||
// Versionierung. Ältere Fassungen werden samt Blob entfernt.
|
// Versionierung. Ältere Fassungen werden samt Blob entfernt.
|
||||||
const maxVersionsKept = 5
|
//
|
||||||
|
// Eine Paketvariable, kein Feld an Handler: Der Wert wird beim Start einmal
|
||||||
|
// aus MAX_VERSIONS_KEPT gesetzt und danach nur gelesen. Ihn stattdessen
|
||||||
|
// durchzureichen hieße, vier Signaturen zu ändern – auch die von
|
||||||
|
// ReplaceContent, die der Office-Weg benutzt –, für eine Zahl, die sich zur
|
||||||
|
// Laufzeit nie ändert.
|
||||||
|
var maxVersionsKept = 5
|
||||||
|
|
||||||
|
// SetMaxVersionsKept wird einmal beim Start aufgerufen, bevor der erste
|
||||||
|
// Request läuft.
|
||||||
|
//
|
||||||
|
// Kleiner als 1 wird verworfen: Die Zahl geht als OFFSET in das DELETE der
|
||||||
|
// Aufräumabfrage, und `OFFSET 0` löschte **jede** Fassung – aus „keine Grenze"
|
||||||
|
// würde „kein Verlauf". Wer keinen Verlauf will, lässt die Versionierung an
|
||||||
|
// der Datei aus; dafür gibt es den Schalter schon.
|
||||||
|
func SetMaxVersionsKept(n int) {
|
||||||
|
if n >= 1 {
|
||||||
|
maxVersionsKept = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
store *Store
|
store *Store
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ type Config struct {
|
|||||||
TrustedProxies []*net.IPNet
|
TrustedProxies []*net.IPNet
|
||||||
|
|
||||||
DBHost string
|
DBHost string
|
||||||
|
// MaxVersionsKept: Wie viele frühere Fassungen einer Datei aufgehoben
|
||||||
|
// werden, wenn die Versionierung für sie eingeschaltet ist.
|
||||||
|
MaxVersionsKept int
|
||||||
DBPort string
|
DBPort string
|
||||||
DBName string
|
DBName string
|
||||||
DBUser string
|
DBUser string
|
||||||
@@ -152,6 +155,7 @@ func Load() (*Config, error) {
|
|||||||
PublicURL: getEnv("PUBLIC_URL", "http://localhost:8080"),
|
PublicURL: getEnv("PUBLIC_URL", "http://localhost:8080"),
|
||||||
|
|
||||||
DBHost: getEnv("DB_HOST", "localhost"),
|
DBHost: getEnv("DB_HOST", "localhost"),
|
||||||
|
MaxVersionsKept: getEnvInt("MAX_VERSIONS_KEPT", 5),
|
||||||
DBPort: getEnv("DB_PORT", "5432"),
|
DBPort: getEnv("DB_PORT", "5432"),
|
||||||
DBName: getEnv("DB_NAME", "aether"),
|
DBName: getEnv("DB_NAME", "aether"),
|
||||||
DBUser: getEnv("DB_USER", "aether"),
|
DBUser: getEnv("DB_USER", "aether"),
|
||||||
@@ -290,6 +294,21 @@ func parseCIDRList(raw string) ([]*net.IPNet, error) {
|
|||||||
return networks, nil
|
return networks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEnvInt liest eine Zahl. Ein unbrauchbarer Wert nimmt die Vorgabe, statt
|
||||||
|
// den Start zu verhindern: Eine Tippfehler in einer Nebensache soll nicht
|
||||||
|
// dazu führen, dass die Cloud gar nicht erst hochkommt.
|
||||||
|
func getEnvInt(key string, fallback int) int {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
if v == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(strings.TrimSpace(v))
|
||||||
|
if err != nil || n < 0 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
func getEnv(key, fallback string) string {
|
func getEnv(key, fallback string) string {
|
||||||
if v := os.Getenv(key); v != "" {
|
if v := os.Getenv(key); v != "" {
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ DATA_PATH=/pfad/zu/aether-data
|
|||||||
# `id -u` / `id -g` – sonst gehören die Dateien einem Container-Nutzer.
|
# `id -u` / `id -g` – sonst gehören die Dateien einem Container-Nutzer.
|
||||||
APP_UID=1000
|
APP_UID=1000
|
||||||
APP_GID=1000
|
APP_GID=1000
|
||||||
|
# Frühere Fassungen je Datei. Greift nur bei eingeschaltetem „Keep versions".
|
||||||
|
MAX_VERSIONS_KEPT=5
|
||||||
# Port auf dem Wirt. Im Container bleibt es 8080.
|
# Port auf dem Wirt. Im Container bleibt es 8080.
|
||||||
APP_PORT=8080
|
APP_PORT=8080
|
||||||
# Wer den Port erreichen darf. 127.0.0.1 = nur dieser Rechner (Vorgabe).
|
# Wer den Port erreichen darf. 127.0.0.1 = nur dieser Rechner (Vorgabe).
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ export interface PublicConfig {
|
|||||||
/** Gibt es einen Office-Server? Sonst bietet das Menue das Bearbeiten gar
|
/** Gibt es einen Office-Server? Sonst bietet das Menue das Bearbeiten gar
|
||||||
* nicht erst an – ein Punkt, der immer scheitert, ist schlimmer als keiner. */
|
* nicht erst an – ein Punkt, der immer scheitert, ist schlimmer als keiner. */
|
||||||
office_enabled?: boolean
|
office_enabled?: boolean
|
||||||
|
/** Wie viele frühere Fassungen der Server aufhebt (MAX_VERSIONS_KEPT). */
|
||||||
|
max_versions_kept?: number
|
||||||
primary_color?: string
|
primary_color?: string
|
||||||
accent_color?: string
|
accent_color?: string
|
||||||
external_apps: ExternalApp[]
|
external_apps: ExternalApp[]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { versionsApi, type FileItem, type FileVersion } from '../api/client'
|
import { versionsApi, type FileItem, type FileVersion } from '../api/client'
|
||||||
import { formatBytes } from '../pages/FileBrowser'
|
import { formatBytes } from '../pages/FileBrowser'
|
||||||
import { useOverlayDismiss } from '../hooks/useOverlayDismiss'
|
import { useOverlayDismiss } from '../hooks/useOverlayDismiss'
|
||||||
|
import { useAppSettings } from '../hooks/useAppSettings'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
file: FileItem
|
file: FileItem
|
||||||
@@ -9,8 +10,10 @@ interface Props {
|
|||||||
onRestored: (updated: FileItem) => void
|
onRestored: (updated: FileItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// Muss zu maxVersionsKept im Backend passen (internal/files/handler.go).
|
// Die Zahl kommt vom Server (MAX_VERSIONS_KEPT). Vorher stand sie hier fest
|
||||||
const MAX_VERSIONS_KEPT = 5
|
// mit dem Kommentar „muss zu maxVersionsKept im Backend passen" – ein Satz
|
||||||
|
// ist keine Zusicherung, und beim ersten Ändern nennt der Dialog sonst eine
|
||||||
|
// andere Zahl, als der Server anwendet.
|
||||||
|
|
||||||
function formatDate(iso: string): string {
|
function formatDate(iso: string): string {
|
||||||
return new Date(iso).toLocaleString(undefined, {
|
return new Date(iso).toLocaleString(undefined, {
|
||||||
@@ -20,6 +23,7 @@ function formatDate(iso: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function VersionsDialog({ file, onClose, onRestored }: Props) {
|
export default function VersionsDialog({ file, onClose, onRestored }: Props) {
|
||||||
|
const { maxVersionsKept: MAX_VERSIONS_KEPT } = useAppSettings()
|
||||||
const [versions, setVersions] = useState<FileVersion[]>([])
|
const [versions, setVersions] = useState<FileVersion[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [restoring, setRestoring] = useState<string | null>(null)
|
const [restoring, setRestoring] = useState<string | null>(null)
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ interface AppSettingsValue {
|
|||||||
globalColors: { primary?: string; accent?: string }
|
globalColors: { primary?: string; accent?: string }
|
||||||
/** Gibt es einen Office-Server? Steuert nur, ob der Menuepunkt erscheint. */
|
/** Gibt es einen Office-Server? Steuert nur, ob der Menuepunkt erscheint. */
|
||||||
officeEnabled: boolean
|
officeEnabled: boolean
|
||||||
|
/** Wie viele frühere Fassungen der Server aufhebt. Kommt vom Server, damit
|
||||||
|
* der Dialog nicht eine andere Zahl nennt als die, die angewendet wird. */
|
||||||
|
maxVersionsKept: number
|
||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Ctx = createContext<AppSettingsValue>({
|
const Ctx = createContext<AppSettingsValue>({
|
||||||
cloudName: 'Aether',
|
cloudName: 'Aether',
|
||||||
officeEnabled: false,
|
officeEnabled: false,
|
||||||
|
maxVersionsKept: 5,
|
||||||
logoUrl: '',
|
logoUrl: '',
|
||||||
faviconUrl: '',
|
faviconUrl: '',
|
||||||
appIconUrl: '',
|
appIconUrl: '',
|
||||||
@@ -84,6 +88,7 @@ export function AppSettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
const [appIconUrl, setAppIconUrl] = useState('')
|
const [appIconUrl, setAppIconUrl] = useState('')
|
||||||
const [globalColors, setGlobalColors] = useState<{ primary?: string; accent?: string }>({})
|
const [globalColors, setGlobalColors] = useState<{ primary?: string; accent?: string }>({})
|
||||||
const [officeEnabled, setOfficeEnabled] = useState(false)
|
const [officeEnabled, setOfficeEnabled] = useState(false)
|
||||||
|
const [maxVersionsKept, setMaxVersionsKept] = useState(5)
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -95,6 +100,9 @@ export function AppSettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
applyFavicon(cfg.favicon_url ?? '', cfg.apple_touch_icon_url ?? '')
|
applyFavicon(cfg.favicon_url ?? '', cfg.apple_touch_icon_url ?? '')
|
||||||
setGlobalColors({ primary: cfg.primary_color, accent: cfg.accent_color })
|
setGlobalColors({ primary: cfg.primary_color, accent: cfg.accent_color })
|
||||||
setOfficeEnabled(!!cfg.office_enabled)
|
setOfficeEnabled(!!cfg.office_enabled)
|
||||||
|
if (typeof cfg.max_versions_kept === 'number' && cfg.max_versions_kept > 0) {
|
||||||
|
setMaxVersionsKept(cfg.max_versions_kept)
|
||||||
|
}
|
||||||
applyTheme(cfg.primary_color, cfg.accent_color)
|
applyTheme(cfg.primary_color, cfg.accent_color)
|
||||||
applyThemeColor(cfg.primary_color)
|
applyThemeColor(cfg.primary_color)
|
||||||
document.title = cfg.cloud_name || 'Aether'
|
document.title = cfg.cloud_name || 'Aether'
|
||||||
@@ -106,7 +114,7 @@ export function AppSettingsProvider({ children }: { children: ReactNode }) {
|
|||||||
useEffect(() => { void refresh() }, [refresh])
|
useEffect(() => { void refresh() }, [refresh])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Ctx.Provider value={{ cloudName, logoUrl, faviconUrl, appIconUrl, globalColors, officeEnabled, refresh }}>{children}</Ctx.Provider>
|
<Ctx.Provider value={{ cloudName, logoUrl, faviconUrl, appIconUrl, globalColors, officeEnabled, maxVersionsKept, refresh }}>{children}</Ctx.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user