Merge branch 'to-cherry'

This commit is contained in:
Nicolas Meienberger 2026-01-09 21:25:52 +01:00
commit 99b7f35d8e
12 changed files with 70 additions and 21 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
DATABASE_URL=./data/zerobyte.db
RESTIC_PASS_FILE=./data/restic.pass
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes

6
.gitignore vendored
View file

@ -14,3 +14,9 @@ mutagen.yml.lock
notes.md
smb-password.txt
cache.db
data/
.env*
!.env.example
!.env.test

View file

@ -76,13 +76,13 @@ Zerobyte can be customized using environment variables. Below are the available
### Environment Variables
| Variable | Description | Default |
| :-------------------- | :------------------------------------------------------------------------------------------------------------ | :--------- |
| `PORT` | The port the web interface and API will listen on. | `4096` |
| Variable | Description | Default |
| :-------------------- | :----------------------------------------------------------------------------------------------------------------- | :--------- |
| `PORT` | The port the web interface and API will listen on. | `4096` |
| `RESTIC_HOSTNAME` | The hostname used by Restic when creating snapshots. Automatically detected if a custom hostname is set in Docker. | `zerobyte` |
| `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` |
| `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` |
| `TZ` | Timezone for the container (e.g., `Europe/Paris`). **Crucial for accurate backup scheduling.** | `UTC` |
| `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` |
| `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` |
| `TZ` | Timezone for the container (e.g., `Europe/Paris`). **Crucial for accurate backup scheduling.** | `UTC` |
### Secret References
@ -281,3 +281,28 @@ For a complete list of third-party software licenses and attributions, please re
## Contributing
Contributions by anyone are welcome! If you find a bug or have a feature request, please open an issue on GitHub. If you want to contribute code, feel free to fork the repository and submit a pull request. We require that all contributors sign a Contributor License Agreement (CLA) before we can accept your contributions. This is to protect both you and the project. Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for more details.
## Development (no Docker)
You can run Zerobyte locally during development without Docker:
```bash
bun install
bun run dev
```
For local development, create a `.env.local` file at the repo root and override the Docker paths:
```bash
# Example
DATABASE_URL=./data/zerobyte.db
RESTIC_PASS_FILE=./data/restic.pass
RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes
```
Notes:
- Remote mount backends (NFS/SMB/WebDAV/SFTP) rely on Linux mount tooling and `CAP_SYS_ADMIN`; on macOS they are expected to be unavailable.
- To actually run backups/repository checks, install `restic` on your machine (e.g. via Homebrew). If `restic` is not installed, the app still starts but backup operations will fail with a clear error.

View file

@ -501,6 +501,7 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
return (
<button
type="button"
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
style={{ paddingLeft }}
onClick={onClick}

View file

@ -11,7 +11,7 @@ export function GridBackground({ children, className, containerClassName }: Grid
return (
<div
className={cn(
"relative min-h-full w-full overflow-x-hidden",
"relative min-h-full w-full",
"bg-size-[20px_20px] sm:bg-size-[40px_40px]",
"bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",

View file

@ -3,6 +3,7 @@ import { FolderOpen } from "lucide-react";
import { FileTree } from "~/client/components/file-tree";
import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
import { useFileBrowser } from "../hooks/use-file-browser";
import { parseError } from "../lib/errors";
type VolumeFileBrowserProps = {
volumeName: string;
@ -66,7 +67,7 @@ export const VolumeFileBrowser = ({
if (error) {
return (
<div className="flex items-center justify-center h-full min-h-50">
<p className="text-destructive">Failed to load files: {(error as Error).message}</p>
<p className="text-destructive">Failed to load files: {parseError(error)?.message}</p>
</div>
);
}

View file

@ -6,7 +6,9 @@ import { X } from "lucide-react";
import { useCallback, useState } from "react";
import { useForm } from "react-hook-form";
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { CronInput } from "~/client/components/cron-input";
import { RepositoryIcon } from "~/client/components/repository-icon";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Checkbox } from "~/client/components/ui/checkbox";
import {
@ -20,13 +22,11 @@ import {
} from "~/client/components/ui/form";
import { Input } from "~/client/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { Button } from "~/client/components/ui/button";
import { Textarea } from "~/client/components/ui/textarea";
import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
import { CronInput } from "~/client/components/cron-input";
import { cronToFormValues } from "../lib/cron-utils";
import type { BackupSchedule, Volume } from "~/client/lib/types";
import { deepClean } from "~/utils/object";
import { cronToFormValues } from "../lib/cron-utils";
const internalFormSchema = type({
name: "1 <= string <= 128",
@ -377,7 +377,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
onSelectionChange={handleSelectionChange}
withCheckboxes={true}
foldersOnly={false}
className="flex-1 border rounded-md bg-card p-2 min-h-75 max-h-100 overflow-auto"
className="relative border rounded-md bg-card p-2 h-100 overflow-y-auto"
/>
{selectedPaths.size > 0 && (
<div className="mt-4">
@ -641,8 +641,8 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
</CardContent>
</Card>
</div>
<div className="h-full">
<Card className="h-full">
<div className="xl:sticky xl:top-6 xl:self-start">
<Card>
<CardHeader className="flex flex-row items-center justify-between gap-4">
<div>
<CardTitle>Schedule summary</CardTitle>

View file

@ -1,4 +1,6 @@
import * as fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { logger } from "../utils/logger";
export type SystemCapabilities = {
@ -55,6 +57,11 @@ async function detectRclone(): Promise<boolean> {
}
async function detectSysAdmin(): Promise<boolean> {
if (process.platform !== "linux") {
logger.warn("sysAdmin capability: disabled. Non-Linux platform detected");
return false;
}
try {
const procStatus = await fs.readFile("/proc/self/status", "utf-8");

View file

@ -1,7 +1,11 @@
export const OPERATION_TIMEOUT = 5000;
export const VOLUME_MOUNT_BASE = "/var/lib/zerobyte/volumes";
export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories";
export const VOLUME_MOUNT_BASE = process.env.ZEROBYTE_VOLUMES_DIR || "/var/lib/zerobyte/volumes";
export const REPOSITORY_BASE = process.env.ZEROBYTE_REPOSITORIES_DIR || "/var/lib/zerobyte/repositories";
export const RESTIC_CACHE_DIR = process.env.RESTIC_CACHE_DIR || "/var/lib/zerobyte/restic/cache";
export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/ironmount.db";
export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass";
export const RESTIC_PASS_FILE = process.env.RESTIC_PASS_FILE || "/var/lib/zerobyte/data/restic.pass";
export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE];

View file

@ -55,7 +55,7 @@ export const runDbMigrations = () => {
} else if (config.__prod__) {
migrationsFolder = path.join("/app", "assets", "migrations");
} else {
migrationsFolder = path.join("/app", "app", "drizzle");
migrationsFolder = path.join(process.cwd(), "app", "drizzle");
}
migrate(db, { migrationsFolder });

View file

@ -4,7 +4,7 @@ import path from "node:path";
import os from "node:os";
import { throttle } from "es-toolkit";
import { type } from "arktype";
import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES } from "../core/constants";
import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES, RESTIC_CACHE_DIR } from "../core/constants";
import { config as appConfig } from "../core/config";
import { logger } from "./logger";
import { cryptoUtils } from "./crypto";
@ -107,7 +107,7 @@ export const buildRepoUrl = (config: RepositoryConfig): string => {
export const buildEnv = async (config: RepositoryConfig) => {
const env: Record<string, string> = {
RESTIC_CACHE_DIR: "/var/lib/zerobyte/restic/cache",
RESTIC_CACHE_DIR,
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
};

View file

@ -5,7 +5,7 @@
"scripts": {
"lint": "oxlint --type-aware",
"build": "react-router build",
"dev": "bunx --bun vite",
"dev": "NODE_ENV=development bunx --bun vite",
"start": "bun ./dist/server/index.js",
"cli:dev": "bun run app/server/cli/main.ts",
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",