Merge branch 'to-cherry'
This commit is contained in:
commit
99b7f35d8e
12 changed files with 70 additions and 21 deletions
5
.env.example
Normal file
5
.env.example
Normal 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
6
.gitignore
vendored
|
|
@ -14,3 +14,9 @@ mutagen.yml.lock
|
||||||
notes.md
|
notes.md
|
||||||
smb-password.txt
|
smb-password.txt
|
||||||
cache.db
|
cache.db
|
||||||
|
|
||||||
|
data/
|
||||||
|
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
|
||||||
37
README.md
37
README.md
|
|
@ -76,13 +76,13 @@ Zerobyte can be customized using environment variables. Below are the available
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
| :-------------------- | :------------------------------------------------------------------------------------------------------------ | :--------- |
|
| :-------------------- | :----------------------------------------------------------------------------------------------------------------- | :--------- |
|
||||||
| `PORT` | The port the web interface and API will listen on. | `4096` |
|
| `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` |
|
| `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` |
|
| `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` |
|
||||||
| `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` |
|
| `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` |
|
| `TZ` | Timezone for the container (e.g., `Europe/Paris`). **Crucial for accurate backup scheduling.** | `UTC` |
|
||||||
|
|
||||||
### Secret References
|
### Secret References
|
||||||
|
|
||||||
|
|
@ -281,3 +281,28 @@ For a complete list of third-party software licenses and attributions, please re
|
||||||
## Contributing
|
## 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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,7 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
|
className={cn("flex items-center gap-2 w-full pr-2 text-sm py-1.5 text-left", className)}
|
||||||
style={{ paddingLeft }}
|
style={{ paddingLeft }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function GridBackground({ children, className, containerClassName }: Grid
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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-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)]",
|
"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)]",
|
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { FolderOpen } from "lucide-react";
|
||||||
import { FileTree } from "~/client/components/file-tree";
|
import { FileTree } from "~/client/components/file-tree";
|
||||||
import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
|
import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
|
||||||
import { useFileBrowser } from "../hooks/use-file-browser";
|
import { useFileBrowser } from "../hooks/use-file-browser";
|
||||||
|
import { parseError } from "../lib/errors";
|
||||||
|
|
||||||
type VolumeFileBrowserProps = {
|
type VolumeFileBrowserProps = {
|
||||||
volumeName: string;
|
volumeName: string;
|
||||||
|
|
@ -66,7 +67,7 @@ export const VolumeFileBrowser = ({
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full min-h-50">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import { X } from "lucide-react";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
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 { 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,13 +22,11 @@ import {
|
||||||
} from "~/client/components/ui/form";
|
} from "~/client/components/ui/form";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
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 { Textarea } from "~/client/components/ui/textarea";
|
||||||
import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
|
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 type { BackupSchedule, Volume } from "~/client/lib/types";
|
||||||
import { deepClean } from "~/utils/object";
|
import { deepClean } from "~/utils/object";
|
||||||
|
import { cronToFormValues } from "../lib/cron-utils";
|
||||||
|
|
||||||
const internalFormSchema = type({
|
const internalFormSchema = type({
|
||||||
name: "1 <= string <= 128",
|
name: "1 <= string <= 128",
|
||||||
|
|
@ -377,7 +377,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
withCheckboxes={true}
|
withCheckboxes={true}
|
||||||
foldersOnly={false}
|
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 && (
|
{selectedPaths.size > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
|
|
@ -641,8 +641,8 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="xl:sticky xl:top-6 xl:self-start">
|
||||||
<Card className="h-full">
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Schedule summary</CardTitle>
|
<CardTitle>Schedule summary</CardTitle>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
export type SystemCapabilities = {
|
export type SystemCapabilities = {
|
||||||
|
|
@ -55,6 +57,11 @@ async function detectRclone(): Promise<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectSysAdmin(): Promise<boolean> {
|
async function detectSysAdmin(): Promise<boolean> {
|
||||||
|
if (process.platform !== "linux") {
|
||||||
|
logger.warn("sysAdmin capability: disabled. Non-Linux platform detected");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const procStatus = await fs.readFile("/proc/self/status", "utf-8");
|
const procStatus = await fs.readFile("/proc/self/status", "utf-8");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
export const OPERATION_TIMEOUT = 5000;
|
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 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];
|
export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE];
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const runDbMigrations = () => {
|
||||||
} else if (config.__prod__) {
|
} else if (config.__prod__) {
|
||||||
migrationsFolder = path.join("/app", "assets", "migrations");
|
migrationsFolder = path.join("/app", "assets", "migrations");
|
||||||
} else {
|
} else {
|
||||||
migrationsFolder = path.join("/app", "app", "drizzle");
|
migrationsFolder = path.join(process.cwd(), "app", "drizzle");
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate(db, { migrationsFolder });
|
migrate(db, { migrationsFolder });
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import path from "node:path";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { throttle } from "es-toolkit";
|
import { throttle } from "es-toolkit";
|
||||||
import { type } from "arktype";
|
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 { config as appConfig } from "../core/config";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { cryptoUtils } from "./crypto";
|
import { cryptoUtils } from "./crypto";
|
||||||
|
|
@ -107,7 +107,7 @@ export const buildRepoUrl = (config: RepositoryConfig): string => {
|
||||||
|
|
||||||
export const buildEnv = async (config: RepositoryConfig) => {
|
export const buildEnv = async (config: RepositoryConfig) => {
|
||||||
const env: Record<string, string> = {
|
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",
|
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "oxlint --type-aware",
|
"lint": "oxlint --type-aware",
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"dev": "bunx --bun vite",
|
"dev": "NODE_ENV=development bunx --bun vite",
|
||||||
"start": "bun ./dist/server/index.js",
|
"start": "bun ./dist/server/index.js",
|
||||||
"cli:dev": "bun run app/server/cli/main.ts",
|
"cli:dev": "bun run app/server/cli/main.ts",
|
||||||
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue