diff --git a/backend/driveclone b/backend/driveclone index 102b0a9..99ce547 100755 Binary files a/backend/driveclone and b/backend/driveclone differ diff --git a/backend/main.go b/backend/main.go index e426590..27006b3 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1145,6 +1145,7 @@ func (s *Server) createTarLz4Temp(uid int64, rel, base string) (string, string, if _, err := exec.LookPath("lz4"); err != nil { return "", "", "", fmt.Errorf("lz4 format requires 'lz4' binary on server; choose zip or tar.gz in settings") } + cleanupStaleTempArchives(1 * time.Hour) tarPath, err := s.createTarTemp(uid, rel, base) if err != nil { return "", "", "", err @@ -1157,6 +1158,7 @@ func (s *Server) createTarLz4Temp(uid int64, rel, base string) (string, string, } outPath := outTmp.Name() outTmp.Close() + _ = os.Remove(outPath) cmd := exec.Command("lz4", "-z", "-q", tarPath, outPath) if out, err := cmd.CombinedOutput(); err != nil { @@ -1536,6 +1538,7 @@ func createTarLz4FromLocalDir(root, baseName string) (string, string, string, er if _, err := exec.LookPath("lz4"); err != nil { return "", "", "", fmt.Errorf("lz4 format requires 'lz4' binary on server; choose zip or tar.gz in settings") } + cleanupStaleTempArchives(1 * time.Hour) tarPath, err := createTarFromLocalDir(root) if err != nil { return "", "", "", err @@ -1548,6 +1551,7 @@ func createTarLz4FromLocalDir(root, baseName string) (string, string, string, er } outPath := outTmp.Name() outTmp.Close() + _ = os.Remove(outPath) cmd := exec.Command("lz4", "-z", "-q", tarPath, outPath) if out, err := cmd.CombinedOutput(); err != nil { os.Remove(outPath) @@ -1556,6 +1560,35 @@ func createTarLz4FromLocalDir(root, baseName string) (string, string, string, er return outPath, baseName + ".tar.lz4", "application/x-lz4", nil } +func cleanupStaleTempArchives(maxIdle time.Duration) { + tmpDir := os.TempDir() + entries, err := os.ReadDir(tmpDir) + if err != nil { + return + } + + cutoff := time.Now().Add(-maxIdle) + for _, entry := range entries { + name := entry.Name() + if !strings.HasPrefix(name, "filez-") && !strings.HasPrefix(name, "filez-batch-") { + continue + } + if !(strings.HasSuffix(name, ".tar.lz4") || strings.HasSuffix(name, ".tar") || strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".zip") || strings.HasSuffix(name, ".rar")) { + continue + } + + info, err := entry.Info() + if err != nil || info.IsDir() { + continue + } + if info.ModTime().After(cutoff) { + continue + } + + _ = os.Remove(filepath.Join(tmpDir, name)) + } +} + func createRarFromLocalDir(root, baseName string) (string, string, string, error) { if _, err := exec.LookPath("rar"); err != nil { return "", "", "", fmt.Errorf("rar format requires 'rar' binary on server; choose zip or tar.gz in settings") diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index afe6f96..f626914 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -50,6 +50,7 @@ type ProtocolProfile = { } type ProtocolInfo = { ftp?: ProtocolProfile; ftps?: ProtocolProfile } type MarkdownRendererProps = { children?: string } +type CachedUIPrefs = { username: string; theme: Theme; colorMode: ColorMode; archiveFormat: ArchiveFormat } const AdminPanel = lazy(() => import('./lazy/AdminPanel')) const TransferSection = lazy(() => import('./lazy/TransferSection')) @@ -64,6 +65,7 @@ function resolveModuleValue(mod: unknown): T | null { const themeOptions: Theme[] = ['dracula', 'nord', 'monokai', 'solarized', 'github'] const modeOptions: ColorMode[] = ['auto', 'light', 'dark'] +const archiveOptions: ArchiveFormat[] = ['zip', 'rar', 'tar.gz', 'lz4'] const shareTTLPresets: Array<{ value: string; label: string }> = [ { value: '60', label: '1h' }, { value: '120', label: '2h' }, @@ -92,6 +94,7 @@ const dict: Record> = { signIn: 'Sign in', signInGoogle: 'Continue with Google', signInAdmin: 'Sign in as admin', + loading: 'Loading', username: 'Username', password: 'Password', logout: 'Logout', @@ -192,6 +195,7 @@ const dict: Record> = { signIn: 'Anmelden', signInGoogle: 'Mit Google fortfahren', signInAdmin: 'Als Admin anmelden', + loading: 'Laden', username: 'Benutzername', password: 'Passwort', logout: 'Abmelden', @@ -292,6 +296,7 @@ const dict: Record> = { signIn: 'Войти', signInGoogle: 'Продолжить через Google', signInAdmin: 'Войти как админ', + loading: 'Загрузка', username: 'Логин', password: 'Пароль', logout: 'Выйти', @@ -396,6 +401,54 @@ function detectMode(): Exclude { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } +const uiPrefsCookieName = 'filez_ui_prefs' + +function cookieValue(name: string): string { + const chunks = document.cookie ? document.cookie.split('; ') : [] + for (const chunk of chunks) { + const idx = chunk.indexOf('=') + if (idx <= 0) continue + if (chunk.slice(0, idx) === name) return chunk.slice(idx + 1) + } + return '' +} + +function asTheme(v: string): Theme { + return themeOptions.includes(v as Theme) ? (v as Theme) : 'dracula' +} + +function asColorMode(v: string): ColorMode { + return modeOptions.includes(v as ColorMode) ? (v as ColorMode) : 'auto' +} + +function asArchiveFormat(v: string): ArchiveFormat { + return archiveOptions.includes(v as ArchiveFormat) ? (v as ArchiveFormat) : 'zip' +} + +function readCachedUIPrefs(): CachedUIPrefs | null { + const raw = cookieValue(uiPrefsCookieName) + if (!raw) return null + try { + const parsed = JSON.parse(decodeURIComponent(raw)) as Partial + const username = String(parsed.username ?? '').trim().toLowerCase() + if (!username) return null + return { + username, + theme: asTheme(String(parsed.theme ?? '')), + colorMode: asColorMode(String(parsed.colorMode ?? '')), + archiveFormat: asArchiveFormat(String(parsed.archiveFormat ?? '')), + } + } catch { + return null + } +} + +function writeCachedUIPrefs(prefs: CachedUIPrefs) { + const maxAge = 60 * 60 * 24 * 30 + const payload = encodeURIComponent(JSON.stringify(prefs)) + document.cookie = `${uiPrefsCookieName}=${payload}; Max-Age=${maxAge}; Path=/; SameSite=Lax` +} + const imageExt = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg']) const codeExt = new Set(['ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'c', 'cpp', 'css', 'html']) const docExt = new Set(['txt', 'md', 'pdf', 'doc', 'docx', 'odt', 'rtf']) @@ -497,8 +550,10 @@ export default function App() { const [route, setRoute] = useState(routeFromPath(window.location.pathname)) const [lang, setLang] = useState(detectLang()) const [err, setErr] = useState('') + const [bootstrapping, setBootstrapping] = useState(true) const [user, setUser] = useState(null) + const [cachedUIPrefs, setCachedUIPrefs] = useState(() => readCachedUIPrefs()) const [admin, setAdmin] = useState(null) const [files, setFiles] = useState([]) const [path, setPath] = useState('/') @@ -551,8 +606,11 @@ export default function App() { const driveHistoryRef = useRef(['/']) const driveHistoryIndexRef = useRef(0) - const effectiveTheme = useMemo(() => user?.theme ?? 'dracula', [user?.theme]) - const effectiveMode = useMemo(() => (user?.colorMode && user.colorMode !== 'auto' ? user.colorMode : detectMode()), [user?.colorMode]) + const effectiveTheme = useMemo(() => user?.theme ?? cachedUIPrefs?.theme ?? 'dracula', [user?.theme, cachedUIPrefs?.theme]) + const effectiveMode = useMemo(() => { + const mode = user?.colorMode ?? cachedUIPrefs?.colorMode ?? 'auto' + return mode !== 'auto' ? mode : detectMode() + }, [user?.colorMode, cachedUIPrefs?.colorMode]) const t = (k: string) => dict[lang][k] ?? k useEffect(() => { @@ -729,9 +787,18 @@ export default function App() { }, []) const bootstrap = useCallback(async () => { + setBootstrapping(true) try { const me = await api('/api/auth/me') setUser(me) + const nextPrefs: CachedUIPrefs = { + username: me.username.toLowerCase(), + theme: me.theme, + colorMode: me.colorMode, + archiveFormat: me.archiveFormat, + } + setCachedUIPrefs(nextPrefs) + writeCachedUIPrefs(nextPrefs) const protocols = await api('/api/user/protocols') setProtocolInfo(protocols) const initial = routeFromPath(window.location.pathname) === 'drive' ? drivePathFromLocation() : '/' @@ -748,6 +815,8 @@ export default function App() { await loadUsers() } catch { setAdmin(null) + } finally { + setBootstrapping(false) } }, [loadFiles, loadUsers]) @@ -764,6 +833,14 @@ export default function App() { try { const me = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }) setUser(me) + const nextPrefs: CachedUIPrefs = { + username: me.username.toLowerCase(), + theme: me.theme, + colorMode: me.colorMode, + archiveFormat: me.archiveFormat, + } + setCachedUIPrefs(nextPrefs) + writeCachedUIPrefs(nextPrefs) const protocols = await api('/api/user/protocols') setProtocolInfo(protocols) setUsername('') @@ -860,7 +937,16 @@ export default function App() { async function savePref(theme: Theme, colorMode: ColorMode, archiveFormat: ArchiveFormat) { if (!user) return await api('/api/user/preferences', { method: 'POST', body: JSON.stringify({ theme, colorMode, archiveFormat }) }) - setUser({ ...user, theme, colorMode, archiveFormat }) + const nextUser = { ...user, theme, colorMode, archiveFormat } + setUser(nextUser) + const nextPrefs: CachedUIPrefs = { + username: nextUser.username.toLowerCase(), + theme: nextUser.theme, + colorMode: nextUser.colorMode, + archiveFormat: nextUser.archiveFormat, + } + setCachedUIPrefs(nextPrefs) + writeCachedUIPrefs(nextPrefs) } async function createUser(e: FormEvent) { @@ -1090,19 +1176,28 @@ export default function App() { }) } - return ( -
-
-
+ const authLoadingCard = ( + + +