From c726c6fc72ed40994c80dcfe58f6eefca635d680 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 17 Nov 2025 18:17:51 +0100 Subject: [PATCH 1/8] feat: custom local repository path --- app/client/api-client/types.gen.ts | 64 ++++++++++++- .../components/create-repository-form.tsx | 96 ++++++++++++++++++- app/schemas/restic.ts | 1 + app/server/utils/restic.ts | 2 +- 4 files changed, 156 insertions(+), 7 deletions(-) diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 5a3de70b..a36e74b6 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -741,12 +741,21 @@ export type ListRepositoriesResponses = { name: string; customPassword?: string; isExistingRepository?: boolean; + path?: string; } | { backend: 'rclone'; path: string; remote: string; customPassword?: string; isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; }; createdAt: number; id: string; @@ -754,7 +763,7 @@ export type ListRepositoriesResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; updatedAt: number; }>; }; @@ -799,12 +808,21 @@ export type CreateRepositoryData = { name: string; customPassword?: string; isExistingRepository?: boolean; + path?: string; } | { backend: 'rclone'; path: string; remote: string; customPassword?: string; isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; }; name: string; compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off'; @@ -919,12 +937,21 @@ export type GetRepositoryResponses = { name: string; customPassword?: string; isExistingRepository?: boolean; + path?: string; } | { backend: 'rclone'; path: string; remote: string; customPassword?: string; isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; }; createdAt: number; id: string; @@ -932,7 +959,7 @@ export type GetRepositoryResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; updatedAt: number; }; }; @@ -1166,12 +1193,21 @@ export type ListBackupSchedulesResponses = { name: string; customPassword?: string; isExistingRepository?: boolean; + path?: string; } | { backend: 'rclone'; path: string; remote: string; customPassword?: string; isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; }; createdAt: number; id: string; @@ -1179,7 +1215,7 @@ export type ListBackupSchedulesResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; updatedAt: number; }; repositoryId: string; @@ -1379,12 +1415,21 @@ export type GetBackupScheduleResponses = { name: string; customPassword?: string; isExistingRepository?: boolean; + path?: string; } | { backend: 'rclone'; path: string; remote: string; customPassword?: string; isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; }; createdAt: number; id: string; @@ -1392,7 +1437,7 @@ export type GetBackupScheduleResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; updatedAt: number; }; repositoryId: string; @@ -1573,12 +1618,21 @@ export type GetBackupScheduleForVolumeResponses = { name: string; customPassword?: string; isExistingRepository?: boolean; + path?: string; } | { backend: 'rclone'; path: string; remote: string; customPassword?: string; isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; }; createdAt: number; id: string; @@ -1586,7 +1640,7 @@ export type GetBackupScheduleForVolumeResponses = { lastError: string | null; name: string; status: 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3'; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3'; updatedAt: number; }; repositoryId: string; diff --git a/app/client/components/create-repository-form.tsx b/app/client/components/create-repository-form.tsx index 724eda8f..51ba64ab 100644 --- a/app/client/components/create-repository-form.tsx +++ b/app/client/components/create-repository-form.tsx @@ -10,12 +10,23 @@ import { Input } from "./ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { useQuery } from "@tanstack/react-query"; import { Alert, AlertDescription } from "./ui/alert"; -import { ExternalLink } from "lucide-react"; +import { ExternalLink, AlertTriangle } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { useSystemInfo } from "~/client/hooks/use-system-info"; import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic"; import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen"; import { Checkbox } from "./ui/checkbox"; +import { DirectoryBrowser } from "./directory-browser"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "./ui/alert-dialog"; export const formSchema = type({ name: "2<=string<=32", @@ -67,6 +78,8 @@ export const CreateRepositoryForm = ({ const watchedIsExistingRepository = watch("isExistingRepository"); const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default"); + const [showPathBrowser, setShowPathBrowser] = useState(false); + const [showPathWarning, setShowPathWarning] = useState(false); const { capabilities } = useSystemInfo(); @@ -247,6 +260,87 @@ export const CreateRepositoryForm = ({ )} + {watchedBackend === "local" && ( + <> + + Repository Directory +
+
+ {form.watch("path") || "/var/lib/ironmount/repositories"} +
+ +
+ + The directory where the repository will be stored. + +
+ + + + + + + Important: Host Mount Required + + +

+ When selecting a custom path, ensure it is mounted from the host machine into the + container. +

+

+ If the path is not a host mount, you will lose your repository data when the container + restarts. +

+

+ The default path /var/lib/ironmount/repositories is + already mounted from the host and is safe to use. +

+
+
+ + Cancel + { + setShowPathBrowser(true); + setShowPathWarning(false); + }} + > + I Understand, Continue + + +
+
+ + + + + Select Repository Directory + + Choose a directory from the filesystem to store the repository. + + +
+ form.setValue("path", path)} + selectedPath={form.watch("path") || "/var/lib/ironmount/repositories"} + /> +
+ + Cancel + setShowPathBrowser(false)}>Done + +
+
+ + )} + {watchedBackend === "s3" && ( <> { const buildRepoUrl = (config: RepositoryConfig): string => { switch (config.backend) { case "local": - return `${REPOSITORY_BASE}/${config.name}`; + return `${config.path}/${config.name}` || `${REPOSITORY_BASE}/${config.name}`; case "s3": return `s3:${config.endpoint}/${config.bucket}`; case "r2": { From 70e4c782ff50463d0595eac3072cbbfa5db51156 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 17 Nov 2025 21:09:46 +0100 Subject: [PATCH 2/8] fix: undefined path in local repo --- app/server/utils/restic.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 33b63e1d..d7236bfb 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -71,7 +71,7 @@ const ensurePassfile = async () => { const buildRepoUrl = (config: RepositoryConfig): string => { switch (config.backend) { case "local": - return `${config.path}/${config.name}` || `${REPOSITORY_BASE}/${config.name}`; + return config.path ? `${config.path}/${config.name}` : `${REPOSITORY_BASE}/${config.name}`; case "s3": return `s3:${config.endpoint}/${config.bucket}`; case "r2": { @@ -155,6 +155,9 @@ const init = async (config: RepositoryConfig) => { await ensurePassfile(); const repoUrl = buildRepoUrl(config); + + logger.info(`Initializing restic repository at ${repoUrl}...`); + const env = await buildEnv(config); const res = await $`restic init --repo ${repoUrl} --json`.env(env).nothrow(); From ef87ca816d1d1d260822a895d4d66dcb85052be6 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:43:51 +0100 Subject: [PATCH 3/8] docs: add contributing.md and clarify CLA requirements (#41) --- CONTRIBUTING.md | 167 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++ 2 files changed, 171 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ba4091fd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,167 @@ +# Contributing to Ironmount + +Thank you for your interest in contributing to Ironmount! We welcome contributions from the community and are grateful for your support in making this project better. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Contributor License Agreement (CLA)](#contributor-license-agreement-cla) +- [How to Contribute](#how-to-contribute) +- [Development Setup](#development-setup) +- [Submission Guidelines](#submission-guidelines) +- [Code Standards](#code-standards) +- [Community Guidelines](#community-guidelines) + +## Getting Started + +Before you begin: + +1. Check the [issues](https://github.com/nicotsx/ironmount/issues) to see if someone is already working on what you have in mind +2. For major changes, please open an issue first to discuss what you would like to change +3. Make sure you have read and agreed to our Contributor License Agreement (CLA) + +## Contributor License Agreement (CLA) + +### What is a CLA? + +A Contributor License Agreement (CLA) is a legal document in which you state you are entitled to contribute the code/documentation/translation to the project you're contributing to and are willing to have it used in distributions and derivative works. This means you grant us permission to use your contributions under our project's license terms. + +### Why do we need a CLA? + +We require a CLA for several important reasons: + +1. **License Flexibility**: It allows the project to evolve its licensing model if needed in the future without requiring re-approval from all past contributors, ensuring Ironmount can continue to operate and adapt to changing needs of the community. + +2. **Patent Protection**: The CLA includes a patent license grant, which protects the project and its users from potential patent claims related to your contributions. + +3. **Protecting Your Rights**: While you grant us rights to use your contributions, you retain ownership of your work and can use it for any other purpose. + +### How to Sign the CLA + +When you submit your first pull request, our CLA Assistant will automatically prompt you to sign the agreement via GitHub. The process is simple: + +1. Create your pull request +2. The CLA Assistant bot will comment on your PR +3. Follow the link provided to review and sign the CLA electronically +4. Once signed, the bot will update your PR status + +You only need to sign the CLA once, and it will cover all your future contributions to Ironmount. + +### Key Points of Our CLA + +- You grant us a non-exclusive, royalty-free license to use your contributions +- You retain ownership and all rights to your contributions +- You confirm that you have the right to make the contribution (it's your original work or you have permission) +- You're not required to provide support for your contributions +- The CLA does not guarantee that your contribution will be accepted or kept into the project + +For the complete CLA text, please see the [CLA document](https://cla-assistant.io/nicotsx/ironmount). + +## How to Contribute + +There are many ways to contribute to Ironmount: + +### Reporting Bugs + +If you find a bug, please open an issue with: +- A clear, descriptive title +- Steps to reproduce the issue +- Expected vs. actual behavior +- Your environment (OS, Docker version, Ironmount version) +- Any relevant logs or screenshots + +### Suggesting Features + +When suggesting a feature: +- Check if it's already been suggested +- Clearly describe the feature and its use case +- Explain why it would be valuable to other users +- Consider the scope and complexity + +### Contributing Code + +1. **Fork the repository** and create your branch from `main` +2. **Make your changes** following our code standards +3. **Test your changes** thoroughly +4. **Update documentation** if needed +5. **Commit your changes** with clear, descriptive commit messages +6. **Push to your fork** and submit a pull request + +### Improving Documentation + +Documentation improvements are always welcome! This includes: +- Fixing typos or clarifying existing docs +- Adding examples or use cases +- Writing guides or tutorials +- Improving README or other documentation files + +### Translations + +We welcome translations to make Ironmount accessible to more users worldwide. Please open an issue to discuss translation efforts before starting. + +## Development Setup + +1. **Clone your fork**: + ```bash + git clone https://github.com/your-username/ironmount.git + cd ironmount + ``` + +2. **Set up your development environment**: + ```bash + bun run start:dev + ``` + +3. **Create a feature branch**: + ```bash + git checkout -b feature/your-feature-name + ``` + +4. **Make your changes and test them** + +5. **Commit your changes**: + ```bash + git add . + git commit -m "Add your descriptive commit message" + ``` + +## Submission Guidelines + +### Pull Request Process + +1. **Update your branch** with the latest changes from main before submitting +2. **Ensure all tests pass** and your code builds successfully +3. **Write a clear PR description** that explains: + - What changes you made + - Why you made them + - Any breaking changes or migration notes + - Link to related issues + +4. **Be responsive** to feedback and review comments +5. **Keep PRs focused** - one feature or fix per PR when possible + +## Code Standards + +- Follow the existing code style and conventions +- Write clear, self-documenting code. No unless comments are necessary +- Ensure your code is properly formatted +- Keep security in mind - never commit sensitive data like passwords or API keys + +## Community Guidelines + +- Be respectful and constructive in all interactions +- Welcome newcomers and help them get started +- Assume good intentions +- Focus on what is best for the community and the project +- Show empathy towards other community members + +## Questions? + +If you have questions about contributing, feel free to: +- Open an issue with your question +- Check existing issues and discussions +- Reach out to the maintainers + +--- + +Thank you for contributing to Ironmount! diff --git a/README.md b/README.md index 2ea629ac..50afdf22 100644 --- a/README.md +++ b/README.md @@ -287,3 +287,7 @@ Ironmount includes [Restic](https://github.com/restic/restic) for backup functio - **License Text**: See [LICENSES/BSD-2-Clause-Restic.txt](LICENSES/BSD-2-Clause-Restic.txt) For a complete list of third-party software licenses and attributions, please refer to the [NOTICES.md](NOTICES.md) file. + +## 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. From 0e4c30262004ea90ddd48851ffb0774becc4eda3 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 20 Nov 2025 18:50:40 +0100 Subject: [PATCH 4/8] refactor: make healthchecks less expensive --- .../modules/backends/directory/directory-backend.ts | 6 ------ app/server/modules/backends/nfs/nfs-backend.ts | 12 ++++-------- app/server/modules/backends/smb/smb-backend.ts | 12 ++++-------- app/server/modules/backends/webdav/webdav-backend.ts | 12 ++++-------- app/server/modules/lifecycle/startup.ts | 4 ++-- 5 files changed, 14 insertions(+), 32 deletions(-) diff --git a/app/server/modules/backends/directory/directory-backend.ts b/app/server/modules/backends/directory/directory-backend.ts index ce3eae87..67d69cc9 100644 --- a/app/server/modules/backends/directory/directory-backend.ts +++ b/app/server/modules/backends/directory/directory-backend.ts @@ -1,5 +1,4 @@ import * as fs from "node:fs/promises"; -import * as npath from "node:path"; import { toMessage } from "../../../utils/errors"; import { logger } from "../../../utils/logger"; import type { VolumeBackend } from "../backend"; @@ -40,11 +39,6 @@ const checkHealth = async (config: BackendConfig) => { try { await fs.access(config.path); - // Try to create a temporary file to ensure write access - const tempFilePath = npath.join(config.path, `.healthcheck-${Date.now()}`); - await fs.writeFile(tempFilePath, "healthcheck"); - await fs.unlink(tempFilePath); - return { status: BACKEND_STATUS.mounted }; } catch (error) { logger.error("Directory health check failed:", error); diff --git a/app/server/modules/backends/nfs/nfs-backend.ts b/app/server/modules/backends/nfs/nfs-backend.ts index 248c6628..ec1af0fb 100644 --- a/app/server/modules/backends/nfs/nfs-backend.ts +++ b/app/server/modules/backends/nfs/nfs-backend.ts @@ -6,7 +6,7 @@ import { logger } from "../../../utils/logger"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils"; +import { executeMount, executeUnmount } from "../utils/backend-utils"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; const mount = async (config: BackendConfig, path: string) => { @@ -22,7 +22,7 @@ const mount = async (config: BackendConfig, path: string) => { return { status: BACKEND_STATUS.error, error: "NFS mounting is only supported on Linux hosts." }; } - const { status } = await checkHealth(path, config.readOnly ?? false); + const { status } = await checkHealth(path); if (status === "mounted") { return { status: BACKEND_STATUS.mounted }; } @@ -87,7 +87,7 @@ const unmount = async (path: string) => { } }; -const checkHealth = async (path: string, readOnly: boolean) => { +const checkHealth = async (path: string) => { const run = async () => { logger.debug(`Checking health of NFS volume at ${path}...`); await fs.access(path); @@ -98,10 +98,6 @@ const checkHealth = async (path: string, readOnly: boolean) => { throw new Error(`Path ${path} is not mounted as NFS.`); } - if (!readOnly) { - await createTestFile(path); - } - logger.debug(`NFS volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted }; }; @@ -117,5 +113,5 @@ const checkHealth = async (path: string, readOnly: boolean) => { export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({ mount: () => mount(config, path), unmount: () => unmount(path), - checkHealth: () => checkHealth(path, config.readOnly ?? false), + checkHealth: () => checkHealth(path), }); diff --git a/app/server/modules/backends/smb/smb-backend.ts b/app/server/modules/backends/smb/smb-backend.ts index 6da1b95b..cdc112ab 100644 --- a/app/server/modules/backends/smb/smb-backend.ts +++ b/app/server/modules/backends/smb/smb-backend.ts @@ -6,7 +6,7 @@ import { logger } from "../../../utils/logger"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils"; +import { executeMount, executeUnmount } from "../utils/backend-utils"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; const mount = async (config: BackendConfig, path: string) => { @@ -22,7 +22,7 @@ const mount = async (config: BackendConfig, path: string) => { return { status: BACKEND_STATUS.error, error: "SMB mounting is only supported on Linux hosts." }; } - const { status } = await checkHealth(path, config.readOnly ?? false); + const { status } = await checkHealth(path); if (status === "mounted") { return { status: BACKEND_STATUS.mounted }; } @@ -100,7 +100,7 @@ const unmount = async (path: string) => { } }; -const checkHealth = async (path: string, readOnly: boolean) => { +const checkHealth = async (path: string) => { const run = async () => { logger.debug(`Checking health of SMB volume at ${path}...`); await fs.access(path); @@ -111,10 +111,6 @@ const checkHealth = async (path: string, readOnly: boolean) => { throw new Error(`Path ${path} is not mounted as CIFS/SMB.`); } - if (!readOnly) { - await createTestFile(path); - } - logger.debug(`SMB volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted }; }; @@ -130,5 +126,5 @@ const checkHealth = async (path: string, readOnly: boolean) => { export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({ mount: () => mount(config, path), unmount: () => unmount(path), - checkHealth: () => checkHealth(path, config.readOnly ?? false), + checkHealth: () => checkHealth(path), }); diff --git a/app/server/modules/backends/webdav/webdav-backend.ts b/app/server/modules/backends/webdav/webdav-backend.ts index 03d34ae1..1e9b72f6 100644 --- a/app/server/modules/backends/webdav/webdav-backend.ts +++ b/app/server/modules/backends/webdav/webdav-backend.ts @@ -8,7 +8,7 @@ import { logger } from "../../../utils/logger"; import { getMountForPath } from "../../../utils/mountinfo"; import { withTimeout } from "../../../utils/timeout"; import type { VolumeBackend } from "../backend"; -import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils"; +import { executeMount, executeUnmount } from "../utils/backend-utils"; import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes"; const execFile = promisify(execFileCb); @@ -26,7 +26,7 @@ const mount = async (config: BackendConfig, path: string) => { return { status: BACKEND_STATUS.error, error: "WebDAV mounting is only supported on Linux hosts." }; } - const { status } = await checkHealth(path, config.readOnly ?? false); + const { status } = await checkHealth(path); if (status === "mounted") { return { status: BACKEND_STATUS.mounted }; } @@ -134,7 +134,7 @@ const unmount = async (path: string) => { } }; -const checkHealth = async (path: string, readOnly: boolean) => { +const checkHealth = async (path: string) => { const run = async () => { logger.debug(`Checking health of WebDAV volume at ${path}...`); await fs.access(path); @@ -145,10 +145,6 @@ const checkHealth = async (path: string, readOnly: boolean) => { throw new Error(`Path ${path} is not mounted as WebDAV.`); } - if (!readOnly) { - await createTestFile(path); - } - logger.debug(`WebDAV volume at ${path} is healthy and mounted.`); return { status: BACKEND_STATUS.mounted }; }; @@ -164,5 +160,5 @@ const checkHealth = async (path: string, readOnly: boolean) => { export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({ mount: () => mount(config, path), unmount: () => unmount(path), - checkHealth: () => checkHealth(path, config.readOnly ?? false), + checkHealth: () => checkHealth(path), }); diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts index 02b64723..4d314bff 100644 --- a/app/server/modules/lifecycle/startup.ts +++ b/app/server/modules/lifecycle/startup.ts @@ -33,8 +33,8 @@ export const startup = async () => { } Scheduler.build(CleanupDanglingMountsJob).schedule("0 * * * *"); - Scheduler.build(VolumeHealthCheckJob).schedule("*/5 * * * *"); - Scheduler.build(RepositoryHealthCheckJob).schedule("*/10 * * * *"); + Scheduler.build(VolumeHealthCheckJob).schedule("*/30 * * * *"); + Scheduler.build(RepositoryHealthCheckJob).schedule("0 * * * *"); Scheduler.build(BackupExecutionJob).schedule("* * * * *"); Scheduler.build(CleanupSessionsJob).schedule("0 0 * * *"); }; From cb0d23fd52a2f75080e0cabee1b49b9f587f7699 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:59:57 +0100 Subject: [PATCH 5/8] refactor: rebrand to zerobyte (#45) --- .github/workflows/release.yml | 6 +- CONTRIBUTING.md | 24 ++-- README.md | 112 +++++++++--------- app/client/components/app-sidebar.tsx | 12 +- app/client/components/auth-layout.tsx | 5 +- .../components/create-repository-form.tsx | 12 +- app/client/components/layout.tsx | 2 +- .../auth/routes/download-recovery-key.tsx | 2 +- app/client/modules/auth/routes/login.tsx | 4 +- app/client/modules/auth/routes/onboarding.tsx | 6 +- .../modules/backups/routes/backup-details.tsx | 2 +- app/client/modules/backups/routes/backups.tsx | 2 +- .../modules/backups/routes/create-backup.tsx | 2 +- .../repositories/routes/create-repository.tsx | 2 +- .../repositories/routes/repositories.tsx | 2 +- .../routes/repository-details.tsx | 2 +- .../repositories/routes/snapshot-details.tsx | 2 +- .../modules/settings/routes/settings.tsx | 2 +- .../modules/volumes/routes/create-volume.tsx | 2 +- .../modules/volumes/routes/volume-details.tsx | 2 +- app/client/modules/volumes/routes/volumes.tsx | 2 +- app/root.tsx | 2 +- app/server/core/constants.ts | 10 +- app/server/index.ts | 6 +- app/server/jobs/cleanup-dangling.ts | 6 +- .../modules/driver/driver.controller.ts | 10 +- app/server/modules/volumes/volume.service.ts | 2 +- app/server/utils/restic.ts | 6 +- docker-compose.yml | 14 +-- drizzle.config.ts | 2 +- mutagen.yml | 4 +- package.json | 6 +- public/images/favicon/site.webmanifest | 4 +- public/images/zerobyte.png | Bin 0 -> 8273 bytes 34 files changed, 141 insertions(+), 138 deletions(-) create mode 100644 public/images/zerobyte.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de29e554..16e8d9a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/${{ github.repository_owner }}/ironmount + images: ghcr.io/${{ github.repository_owner }}/zerobyte tags: | type=semver,pattern={{version}},prefix=v type=semver,pattern={{major}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }} @@ -62,8 +62,8 @@ jobs: type=semver,pattern={{major}}.{{minor}}.{{patch}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }} flavor: | latest=${{ needs.determine-release-type.outputs.release_type == 'release' }} - cache-from: type=registry,ref=ghcr.io/nicotsx/ironmount:buildcache - cache-to: type=registry,ref=ghcr.io/nicotsx/ironmount:buildcache,mode=max + cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache + cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max - name: Build and push images uses: docker/build-push-action@v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba4091fd..29b746aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to Ironmount +# Contributing to Zerobyte -Thank you for your interest in contributing to Ironmount! We welcome contributions from the community and are grateful for your support in making this project better. +Thank you for your interest in contributing to Zerobyte! We welcome contributions from the community and are grateful for your support in making this project better. ## Table of Contents @@ -16,7 +16,7 @@ Thank you for your interest in contributing to Ironmount! We welcome contributio Before you begin: -1. Check the [issues](https://github.com/nicotsx/ironmount/issues) to see if someone is already working on what you have in mind +1. Check the [issues](https://github.com/nicotsx/zerobyte/issues) to see if someone is already working on what you have in mind 2. For major changes, please open an issue first to discuss what you would like to change 3. Make sure you have read and agreed to our Contributor License Agreement (CLA) @@ -30,7 +30,7 @@ A Contributor License Agreement (CLA) is a legal document in which you state you We require a CLA for several important reasons: -1. **License Flexibility**: It allows the project to evolve its licensing model if needed in the future without requiring re-approval from all past contributors, ensuring Ironmount can continue to operate and adapt to changing needs of the community. +1. **License Flexibility**: It allows the project to evolve its licensing model if needed in the future without requiring re-approval from all past contributors, ensuring Zerobyte can continue to operate and adapt to changing needs of the community. 2. **Patent Protection**: The CLA includes a patent license grant, which protects the project and its users from potential patent claims related to your contributions. @@ -45,7 +45,7 @@ When you submit your first pull request, our CLA Assistant will automatically pr 3. Follow the link provided to review and sign the CLA electronically 4. Once signed, the bot will update your PR status -You only need to sign the CLA once, and it will cover all your future contributions to Ironmount. +You only need to sign the CLA once, and it will cover all your future contributions to Zerobyte. ### Key Points of Our CLA @@ -55,11 +55,11 @@ You only need to sign the CLA once, and it will cover all your future contributi - You're not required to provide support for your contributions - The CLA does not guarantee that your contribution will be accepted or kept into the project -For the complete CLA text, please see the [CLA document](https://cla-assistant.io/nicotsx/ironmount). +For the complete CLA text, please see the [CLA document](https://cla-assistant.io/nicotsx/zerobyte). ## How to Contribute -There are many ways to contribute to Ironmount: +There are many ways to contribute to Zerobyte: ### Reporting Bugs @@ -67,7 +67,7 @@ If you find a bug, please open an issue with: - A clear, descriptive title - Steps to reproduce the issue - Expected vs. actual behavior -- Your environment (OS, Docker version, Ironmount version) +- Your environment (OS, Docker version, Zerobyte version) - Any relevant logs or screenshots ### Suggesting Features @@ -97,14 +97,14 @@ Documentation improvements are always welcome! This includes: ### Translations -We welcome translations to make Ironmount accessible to more users worldwide. Please open an issue to discuss translation efforts before starting. +We welcome translations to make Zerobyte accessible to more users worldwide. Please open an issue to discuss translation efforts before starting. ## Development Setup 1. **Clone your fork**: ```bash - git clone https://github.com/your-username/ironmount.git - cd ironmount + git clone https://github.com/your-username/zerobyte.git + cd zerobyte ``` 2. **Set up your development environment**: @@ -164,4 +164,4 @@ If you have questions about contributing, feel free to: --- -Thank you for contributing to Ironmount! +Thank you for contributing to Zerobyte! diff --git a/README.md b/README.md index 50afdf22..fdf7faa0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
-

Ironmount

+

Zerobyte

Powerful backup automation for your remote storage
Encrypt, compress, and protect your data with ease

- - + +
- Demo + Demo

Backup management with scheduling and monitoring @@ -16,11 +16,11 @@

> [!WARNING] -> Ironmount is still in version 0.x.x and is subject to major changes from version to version. I am developing the core features and collecting feedbacks. Expect bugs! Please open issues or feature requests +> Zerobyte is still in version 0.x.x and is subject to major changes from version to version. I am developing the core features and collecting feedbacks. Expect bugs! Please open issues or feature requests ## Intro -Ironmount is a backup automation tool that helps you save your data across multiple storage backends. Built on top of Restic, it provides an modern web interface to schedule, manage, and monitor encrypted backups of your remote storage. +Zerobyte is a backup automation tool that helps you save your data across multiple storage backends. Built on top of Restic, it provides an modern web interface to schedule, manage, and monitor encrypted backups of your remote storage. ### Features @@ -31,13 +31,13 @@ Ironmount is a backup automation tool that helps you save your data across multi ## Installation -In order to run Ironmount, you need to have Docker and Docker Compose installed on your server. Then, you can use the provided `docker-compose.yml` file to start the application. +In order to run Zerobyte, you need to have Docker and Docker Compose installed on your server. Then, you can use the provided `docker-compose.yml` file to start the application. ```yaml services: - ironmount: - image: ghcr.io/nicotsx/ironmount:v0.10 - container_name: ironmount + zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.10 + container_name: zerobyte restart: unless-stopped cap_add: - SYS_ADMIN @@ -47,13 +47,13 @@ services: - /dev/fuse:/dev/fuse volumes: - /etc/localtime:/etc/localtime:ro - - /var/lib/ironmount:/var/lib/ironmount + - /var/lib/zerobyte:/var/lib/zerobyte ``` > [!WARNING] -> Do not try to change the location of the bind mount `/var/lib/ironmount` on your host or store it on a network share. You will likely face permission issues and strong performance degradation. +> Do not try to change the location of the bind mount `/var/lib/zerobyte` on your host or store it on a network share. You will likely face permission issues and strong performance degradation. -Then, run the following command to start Ironmount: +Then, run the following command to start Zerobyte: ```bash docker compose up -d @@ -63,17 +63,17 @@ Once the container is running, you can access the web interface at `http://` +- **Local directories** - Store backups on local disk at `/var/lib/zerobyte/repositories/` - **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc. - **Google Cloud Storage** - Google's cloud storage service - **Azure Blob Storage** - Microsoft Azure storage @@ -114,7 +114,7 @@ To create a repository, navigate to the "Repositories" section in the web interf ### Using rclone for cloud storage -Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service. +Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service. **Setup instructions:** @@ -134,12 +134,12 @@ Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage pro rclone listremotes ``` -4. **Mount the rclone config into the Ironmount container** by updating your `docker-compose.yml`: +4. **Mount the rclone config into the Zerobyte container** by updating your `docker-compose.yml`: ```diff services: - ironmount: - image: ghcr.io/nicotsx/ironmount:v0.10 - container_name: ironmount + zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.10 + container_name: zerobyte restart: unless-stopped cap_add: - SYS_ADMIN @@ -149,20 +149,20 @@ Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage pro - /dev/fuse:/dev/fuse volumes: - /etc/localtime:/etc/localtime:ro - - /var/lib/ironmount:/var/lib/ironmount + - /var/lib/zerobyte:/var/lib/zerobyte + - ~/.config/rclone:/root/.config/rclone ``` -5. **Restart the Ironmount container**: +5. **Restart the Zerobyte container**: ```bash docker compose down docker compose up -d ``` -6. **Create a repository** in Ironmount: +6. **Create a repository** in Zerobyte: - Select "rclone" as the repository type - Choose your configured remote from the dropdown - - Specify the path within your remote (e.g., `backups/ironmount`) + - Specify the path within your remote (e.g., `backups/zerobyte`) For a complete list of supported providers, see the [rclone documentation](https://rclone.org/). @@ -175,28 +175,28 @@ When creating a backup job, you can specify the following settings: - **Retention Policy**: Set rules for how long backups should be retained (e.g., keep daily backups for 7 days, weekly backups for 4 weeks) - **Paths**: Specify which files or directories to include in the backup -After configuring the backup job, save it and Ironmount will automatically execute the backup according to the defined schedule. +After configuring the backup job, save it and Zerobyte will automatically execute the backup according to the defined schedule. You can monitor the progress and status of your backup jobs in the "Backups" section of the web interface. -![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/backups-list.png?raw=true) +![Preview](https://github.com/nicotsx/zerobyte/blob/main/screenshots/backups-list.png?raw=true) ## Restoring data -Ironmount allows you to easily restore your data from backups. To restore data, navigate to the "Backups" section and select the backup job from which you want to restore data. You can then choose a specific backup snapshot and select the files or directories you wish to restore. The data you select will be restored to their original location. +Zerobyte allows you to easily restore your data from backups. To restore data, navigate to the "Backups" section and select the backup job from which you want to restore data. You can then choose a specific backup snapshot and select the files or directories you wish to restore. The data you select will be restored to their original location. -![Preview](https://github.com/nicotsx/ironmount/blob/main/screenshots/restoring.png?raw=true) +![Preview](https://github.com/nicotsx/zerobyte/blob/main/screenshots/restoring.png?raw=true) ## Propagating mounts to host -Ironmount is capable of propagating mounted volumes from within the container to the host system. This is particularly useful when you want to access the mounted data directly from the host to use it with other applications or services. +Zerobyte is capable of propagating mounted volumes from within the container to the host system. This is particularly useful when you want to access the mounted data directly from the host to use it with other applications or services. -In order to enable this feature, you need to change your bind mount `/var/lib/ironmount` to use the `:rshared` flag. Here is an example of how to set this up in your `docker-compose.yml` file: +In order to enable this feature, you need to change your bind mount `/var/lib/zerobyte` to use the `:rshared` flag. Here is an example of how to set this up in your `docker-compose.yml` file: ```diff services: - ironmount: - image: ghcr.io/nicotsx/ironmount:v0.10 - container_name: ironmount + zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.10 + container_name: zerobyte restart: unless-stopped ports: - "4096:4096" @@ -204,11 +204,11 @@ services: - /dev/fuse:/dev/fuse volumes: - /etc/localtime:/etc/localtime:ro -- - /var/lib/ironmount:/var/lib/ironmount -+ - /var/lib/ironmount:/var/lib/ironmount:rshared +- - /var/lib/zerobyte:/var/lib/zerobyte ++ - /var/lib/zerobyte:/var/lib/zerobyte:rshared ``` -Restart the Ironmount container to apply the changes: +Restart the Zerobyte container to apply the changes: ```bash docker compose down @@ -217,15 +217,15 @@ docker compose up -d ## Docker plugin -Ironmount can also be used as a Docker volume plugin, allowing you to mount your volumes directly into other Docker containers. This enables seamless integration with your containerized applications. +Zerobyte can also be used as a Docker volume plugin, allowing you to mount your volumes directly into other Docker containers. This enables seamless integration with your containerized applications. -In order to enable this feature, you need to run Ironmount with several items shared from the host. Here is an example of how to set this up in your `docker-compose.yml` file: +In order to enable this feature, you need to run Zerobyte with several items shared from the host. Here is an example of how to set this up in your `docker-compose.yml` file: ```diff services: - ironmount: - image: ghcr.io/nicotsx/ironmount:v0.10 - container_name: ironmount + zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.10 + container_name: zerobyte restart: unless-stopped cap_add: - SYS_ADMIN @@ -235,20 +235,20 @@ services: - /dev/fuse:/dev/fuse volumes: - /etc/localtime:/etc/localtime:ro -- - /var/lib/ironmount:/var/lib/ironmount -+ - /var/lib/ironmount:/var/lib/ironmount:rshared +- - /var/lib/zerobyte:/var/lib/zerobyte ++ - /var/lib/zerobyte:/var/lib/zerobyte:rshared + - /run/docker/plugins:/run/docker/plugins + - /var/run/docker.sock:/var/run/docker.sock ``` -Restart the Ironmount container to apply the changes: +Restart the Zerobyte container to apply the changes: ```bash docker compose down docker compose up -d ``` -Your Ironmount volumes will now be available as Docker volumes that you can mount into other containers using the `--volume` flag: +Your Zerobyte volumes will now be available as Docker volumes that you can mount into other containers using the `--volume` flag: ```bash docker run -v im-nfs:/path/in/container nginx:latest @@ -267,7 +267,7 @@ volumes: external: true ``` -The volume name format is `im-` where `` is the name you assigned to the volume in Ironmount. You can verify that the volume is available by running: +The volume name format is `im-` where `` is the name you assigned to the volume in Zerobyte. You can verify that the volume is available by running: ```bash docker volume ls @@ -279,7 +279,7 @@ This project includes the following third-party software components: ### Restic -Ironmount includes [Restic](https://github.com/restic/restic) for backup functionality. +Zerobyte includes [Restic](https://github.com/restic/restic) for backup functionality. - **License**: BSD 2-Clause License - **Copyright**: Copyright (c) 2014, Alexander Neumann diff --git a/app/client/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx index 19ef8549..8710b682 100644 --- a/app/client/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -1,4 +1,4 @@ -import { CalendarClock, Database, HardDrive, Mountain, Settings } from "lucide-react"; +import { CalendarClock, Database, HardDrive, Settings } from "lucide-react"; import { Link, NavLink } from "react-router"; import { Sidebar, @@ -46,13 +46,17 @@ export function AppSidebar() { - + Zerobyte Logo - Ironmount + Zerobyte diff --git a/app/client/components/auth-layout.tsx b/app/client/components/auth-layout.tsx index 2435a41e..87c5b7ed 100644 --- a/app/client/components/auth-layout.tsx +++ b/app/client/components/auth-layout.tsx @@ -1,4 +1,3 @@ -import { Mountain } from "lucide-react"; import type { ReactNode } from "react"; type AuthLayoutProps = { @@ -13,8 +12,8 @@ export function AuthLayout({ title, description, children }: AuthLayoutProps) {
- - Ironmount + Zerobyte Logo + Zerobyte
diff --git a/app/client/components/create-repository-form.tsx b/app/client/components/create-repository-form.tsx index 51ba64ab..4c511e31 100644 --- a/app/client/components/create-repository-form.tsx +++ b/app/client/components/create-repository-form.tsx @@ -229,12 +229,12 @@ export const CreateRepositoryForm = ({ - Use Ironmount's password + Use Zerobyte's password Enter password manually - Choose whether to use Ironmount's master password or enter a custom password for the existing + Choose whether to use Zerobyte's master password or enter a custom password for the existing repository. @@ -266,7 +266,7 @@ export const CreateRepositoryForm = ({ Repository Directory
- {form.watch("path") || "/var/lib/ironmount/repositories"} + {form.watch("path") || "/var/lib/zerobyte/repositories"}
- - The directory where the repository will be stored. - + The directory where the repository will be stored. @@ -290,13 +286,9 @@ export const CreateRepositoryForm = ({ Important: Host Mount Required -

- When selecting a custom path, ensure it is mounted from the host machine into the - container. -

+

When selecting a custom path, ensure it is mounted from the host machine into the container.

- If the path is not a host mount, you will lose your repository data when the container - restarts. + If the path is not a host mount, you will lose your repository data when the container restarts.

The default path /var/lib/zerobyte/repositories is @@ -703,6 +695,89 @@ export const CreateRepositoryForm = ({ )} + {watchedBackend === "sftp" && ( + <> + ( + + Host + + + + SFTP server hostname or IP address. + + + )} + /> + ( + + Port + + field.onChange(parseInt(e.target.value, 10))} + /> + + SSH port (default: 22). + + + )} + /> + ( + + User + + + + SSH username for authentication. + + + )} + /> + ( + + Path + + + + Repository path on the SFTP server. + + + )} + /> + ( + + SSH Private Key + +