mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
36 Commits
v0.17.0-be
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
890bdb8319 | ||
|
|
7091f1f296 | ||
|
|
b394aca7ca | ||
|
|
fdb84374a0 | ||
|
|
c42380b26b | ||
|
|
51ed47c30f | ||
|
|
b8ae10b316 | ||
|
|
f232fc07c1 | ||
|
|
d542318e2c | ||
|
|
2660d91002 | ||
|
|
b155f82575 | ||
|
|
9120153375 | ||
|
|
780fdae63e | ||
|
|
fc482e9729 | ||
|
|
1fe026a76f | ||
|
|
2df1fa53a0 | ||
|
|
da8e9c4ada | ||
|
|
da489fab24 | ||
|
|
e4b8076351 | ||
|
|
70c72f0f9a | ||
|
|
c45b760abc | ||
|
|
9ba26b7599 | ||
|
|
01127ee9d6 | ||
|
|
77f5886110 | ||
|
|
6b6338291b | ||
|
|
2c11b7c7de | ||
|
|
a0fa043207 | ||
|
|
143701820a | ||
|
|
aff875c62f | ||
|
|
e52c25d87b | ||
|
|
4403e3cf19 | ||
|
|
9fec6883f6 | ||
|
|
f4df9e935d | ||
|
|
f326f41599 | ||
|
|
f6b8e7e5a2 | ||
|
|
ff4c1404a6 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -62,8 +62,6 @@ 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/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
|
||||
@@ -76,6 +74,8 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
|
||||
cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
README.md
10
README.md
@@ -40,7 +40,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o
|
||||
```yaml
|
||||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.15
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.17
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -78,7 +78,7 @@ If you want to track a local directory on the same server where Zerobyte is runn
|
||||
```diff
|
||||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.15
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.17
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -146,7 +146,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov
|
||||
```diff
|
||||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.15
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.17
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -205,7 +205,7 @@ In order to enable this feature, you need to change your bind mount `/var/lib/ze
|
||||
```diff
|
||||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.15
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.17
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -236,7 +236,7 @@ In order to enable this feature, you need to run Zerobyte with several items sha
|
||||
```diff
|
||||
services:
|
||||
zerobyte:
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.15
|
||||
image: ghcr.io/nicotsx/zerobyte:v0.17
|
||||
container_name: zerobyte
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
|
||||
@@ -2,6 +2,7 @@ import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||
import { type } from "arktype";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Check, Pencil, Save, X } from "lucide-react";
|
||||
import { cn, slugify } from "~/client/lib/utils";
|
||||
import { deepClean } from "~/utils/object";
|
||||
import { Button } from "./ui/button";
|
||||
@@ -267,6 +268,7 @@ export const CreateRepositoryForm = ({
|
||||
{form.watch("path") || "/var/lib/zerobyte/repositories"}
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={() => setShowPathWarning(true)} size="sm">
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
@@ -278,7 +280,7 @@ export const CreateRepositoryForm = ({
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
Important: Host Mount Required
|
||||
Important: Host mount required
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="space-y-3">
|
||||
<p>When selecting a custom path, ensure it is mounted from the host machine into the container.</p>
|
||||
@@ -320,8 +322,14 @@ export const CreateRepositoryForm = ({
|
||||
/>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>Done</AlertDialogAction>
|
||||
<AlertDialogCancel>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Done
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
@@ -775,6 +783,7 @@ export const CreateRepositoryForm = ({
|
||||
|
||||
{mode === "update" && (
|
||||
<Button type="submit" className="w-full" loading={loading}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { type } from "arktype";
|
||||
import { CheckCircle, Loader2, XCircle } from "lucide-react";
|
||||
import { CheckCircle, Loader2, Pencil, Plug, Save, XCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { cn, slugify } from "~/client/lib/utils";
|
||||
@@ -152,6 +152,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
<div className="text-sm font-mono break-all">{field.value}</div>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => field.onChange("")}>
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
@@ -561,6 +562,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
{!testBackendConnection.isPending && testMessage && !testMessage.success && (
|
||||
<XCircle className="mr-2 h-4 w-4 text-red-500" />
|
||||
)}
|
||||
{!testBackendConnection.isPending && !testMessage && <Plug className="mr-2 h-4 w-4" />}
|
||||
{testBackendConnection.isPending
|
||||
? "Testing..."
|
||||
: testMessage
|
||||
@@ -584,6 +586,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
)}
|
||||
{mode === "update" && (
|
||||
<Button type="submit" className="w-full" loading={loading}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -164,6 +164,7 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleRestore} disabled={isRestoring || !canRestore}>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
{isRestoring
|
||||
? "Restoring..."
|
||||
: selectedPaths.size > 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Database, Eraser, HardDrive, Pencil, Play, Square, Trash2 } from "lucide-react";
|
||||
import { Check, Database, Eraser, HardDrive, Pencil, Play, Square, Trash2, X } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { OnOff } from "~/client/components/onoff";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
@@ -228,8 +228,14 @@ export const ScheduleSummary = (props: Props) => {
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmForget}>Run cleanup</AlertDialogAction>
|
||||
<AlertDialogCancel>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmForget}>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Run cleanup
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { FileIcon } from "lucide-react";
|
||||
import { FileIcon, RotateCcw, Trash2 } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { FileTree } from "~/client/components/file-tree";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
@@ -98,6 +98,7 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
}
|
||||
className={buttonVariants({ variant: "primary", size: "sm" })}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Restore
|
||||
</Link>
|
||||
{onDeleteSnapshot && (
|
||||
@@ -108,6 +109,7 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
disabled={isDeletingSnapshot}
|
||||
loading={isDeletingSnapshot}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{isDeletingSnapshot ? "Deleting..." : "Delete Snapshot"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useId, useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { redirect, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { Save, X } from "lucide-react";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -206,9 +207,11 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
<CreateScheduleForm volume={schedule.volume} initialValues={schedule} onSubmit={handleSubmit} formId={formId} />
|
||||
<div className="flex justify-end mt-4 gap-2">
|
||||
<Button type="submit" className="ml-auto" variant="primary" form={formId} loading={updateSchedule.isPending}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Update schedule
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsEditMode(false)}>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useId, useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Database, HardDrive } from "lucide-react";
|
||||
import { Database, HardDrive, Plus } from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -162,6 +162,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
|
||||
<CreateScheduleForm volume={selectedVolume} onSubmit={handleSubmit} formId={formId} />
|
||||
<div className="flex justify-end mt-4 gap-2">
|
||||
<Button type="submit" variant="primary" form={formId} loading={createSchedule.isPending}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Bell } from "lucide-react";
|
||||
import { Bell, Plus } from "lucide-react";
|
||||
import { useId } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
@@ -68,6 +68,7 @@ export default function CreateNotification() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form={formId} loading={createNotification.isPending}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Destination
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getNotificationDestination } from "~/client/api-client/sdk.gen";
|
||||
import type { Route } from "./+types/notification-details";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { Bell, TestTube2 } from "lucide-react";
|
||||
import { Bell, Save, TestTube2, Trash2, X } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
|
||||
|
||||
@@ -147,6 +147,7 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||
variant="destructive"
|
||||
loading={deleteDestination.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
@@ -174,6 +175,7 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||
<CreateNotificationForm mode="update" formId={formId} onSubmit={handleSubmit} initialValues={data.config} />
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button type="submit" form={formId} loading={updateDestination.isPending}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
@@ -190,8 +192,14 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete}>Delete</AlertDialogAction>
|
||||
<AlertDialogCancel>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Database } from "lucide-react";
|
||||
import { Database, Plus } from "lucide-react";
|
||||
import { useId } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
@@ -79,7 +79,8 @@ export default function CreateRepository() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form={formId} loading={createRepository.isPending}>
|
||||
Create Repository
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create repository
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
||||
button={
|
||||
<Button onClick={() => navigate("/repositories/create")}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Create Repository
|
||||
Create repository
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { cn } from "~/client/lib/utils";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
import { RepositoryInfoTabContent } from "../tabs/info";
|
||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Loader2, Stethoscope, Trash2, X } from "lucide-react";
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: (match: Route.MetaArgs) => [
|
||||
@@ -149,13 +149,17 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
||||
{doctorMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Running Doctor...
|
||||
Running doctor...
|
||||
</>
|
||||
) : (
|
||||
"Run Doctor"
|
||||
<>
|
||||
<Stethoscope className="h-4 w-4 mr-2" />
|
||||
Run doctor
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
@@ -184,11 +188,15 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete repository
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
@@ -198,7 +206,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
||||
<AlertDialog open={showDoctorResults} onOpenChange={setShowDoctorResults}>
|
||||
<AlertDialogContent className="max-w-2xl">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Doctor Results</AlertDialogTitle>
|
||||
<AlertDialogTitle>Doctor results</AlertDialogTitle>
|
||||
<AlertDialogDescription>Repository doctor operation completed</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Check, Save } from "lucide-react";
|
||||
import { Card } from "~/client/components/ui/card";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
@@ -146,6 +147,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
<Button type="submit" disabled={!hasChanges || updateMutation.isPending} loading={updateMutation.isPending}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
@@ -155,12 +157,15 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Update Repository</AlertDialogTitle>
|
||||
<AlertDialogTitle>Update repository</AlertDialogTitle>
|
||||
<AlertDialogDescription>Are you sure you want to update the repository settings?</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmUpdate}>Update</AlertDialogAction>
|
||||
<AlertDialogAction onClick={confirmUpdate}>
|
||||
<Check className="h-4 w-4" />
|
||||
Update
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Database } from "lucide-react";
|
||||
import { Database, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { listBackupSchedulesOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { SnapshotsTable } from "~/client/components/snapshots-table";
|
||||
@@ -128,6 +128,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-muted-foreground">No snapshots match your search.</p>
|
||||
<Button onClick={() => setSearchQuery("")} variant="outline" size="sm">
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Clear search
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Download, KeyRound, User } from "lucide-react";
|
||||
import { Download, KeyRound, User, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
@@ -195,6 +195,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" loading={changePassword.isPending} className="mt-4">
|
||||
<KeyRound className="h-4 w-4 mr-2" />
|
||||
Change Password
|
||||
</Button>
|
||||
</form>
|
||||
@@ -252,9 +253,11 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||
setDownloadPassword("");
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" loading={downloadResticPassword.isPending}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
import { Activity, HeartIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { OnOff } from "~/client/components/onoff";
|
||||
@@ -80,6 +80,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
||||
loading={healthcheck.isPending}
|
||||
onClick={() => healthcheck.mutate({ path: { name: volume.name } })}
|
||||
>
|
||||
<Activity className="h-4 w-4 mr-2" />
|
||||
Run Health Check
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { HardDrive } from "lucide-react";
|
||||
import { HardDrive, Plus } from "lucide-react";
|
||||
import { useId } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
@@ -73,6 +73,7 @@ export default function CreateVolume() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form={formId} loading={createVolume.isPending}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Volume
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useState } from "react";
|
||||
import { Plug, Unplug } from "lucide-react";
|
||||
import { StatusDot } from "~/client/components/status-dot";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
@@ -148,6 +149,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||
loading={mountVol.isPending}
|
||||
className={cn({ hidden: volume.status === "mounted" })}
|
||||
>
|
||||
<Plug className="h-4 w-4 mr-2" />
|
||||
Mount
|
||||
</Button>
|
||||
<Button
|
||||
@@ -156,6 +158,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||
loading={unmountVol.isPending}
|
||||
className={cn({ hidden: volume.status !== "mounted" })}
|
||||
>
|
||||
<Unplug className="h-4 w-4 mr-2" />
|
||||
Unmount
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { Check } from "lucide-react";
|
||||
import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -94,7 +95,10 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmUpdate}>Update</AlertDialogAction>
|
||||
<AlertDialogAction onClick={confirmUpdate}>
|
||||
<Check className="h-4 w-4" />
|
||||
Update
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
@@ -33,10 +34,12 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
||||
const password = await cryptoUtils.decrypt(config.password);
|
||||
|
||||
const source = `//${config.server}/${config.share}`;
|
||||
const options = [
|
||||
`user=${config.username}`,
|
||||
`pass=${config.password}`,
|
||||
`pass=${password}`,
|
||||
`vers=${config.vers}`,
|
||||
`port=${config.port}`,
|
||||
"uid=1000",
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
@@ -49,8 +50,9 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
||||
|
||||
if (config.username && config.password) {
|
||||
const password = await cryptoUtils.decrypt(config.password);
|
||||
const secretsFile = "/etc/davfs2/secrets";
|
||||
const secretsContent = `${source} ${config.username} ${config.password}\n`;
|
||||
const secretsContent = `${source} ${config.username} ${password}\n`;
|
||||
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import slugify from "slugify";
|
||||
import { getCapabilities, parseDockerHost } from "../../core/capabilities";
|
||||
import { db } from "../../db/db";
|
||||
import { volumesTable } from "../../db/schema";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { generateShortId } from "../../utils/id";
|
||||
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
||||
@@ -19,6 +20,23 @@ import { logger } from "../../utils/logger";
|
||||
import { serverEvents } from "../../core/events";
|
||||
import type { BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
async function encryptSensitiveFields(config: BackendConfig): Promise<BackendConfig> {
|
||||
switch (config.backend) {
|
||||
case "smb":
|
||||
return {
|
||||
...config,
|
||||
password: await cryptoUtils.encrypt(config.password),
|
||||
};
|
||||
case "webdav":
|
||||
return {
|
||||
...config,
|
||||
password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
|
||||
};
|
||||
default:
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
const listVolumes = async () => {
|
||||
const volumes = await db.query.volumesTable.findMany({});
|
||||
|
||||
@@ -37,13 +55,14 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||
}
|
||||
|
||||
const shortId = generateShortId();
|
||||
const encryptedConfig = await encryptSensitiveFields(backendConfig);
|
||||
|
||||
const [created] = await db
|
||||
.insert(volumesTable)
|
||||
.values({
|
||||
shortId,
|
||||
name: slug,
|
||||
config: backendConfig,
|
||||
config: encryptedConfig,
|
||||
type: backendConfig.backend,
|
||||
})
|
||||
.returning();
|
||||
@@ -175,11 +194,13 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
||||
await backend.unmount();
|
||||
}
|
||||
|
||||
const encryptedConfig = volumeData.config ? await encryptSensitiveFields(volumeData.config) : undefined;
|
||||
|
||||
const [updated] = await db
|
||||
.update(volumesTable)
|
||||
.set({
|
||||
name: newName,
|
||||
config: volumeData.config,
|
||||
config: encryptedConfig,
|
||||
type: volumeData.config?.backend,
|
||||
autoRemount: volumeData.autoRemount,
|
||||
updatedAt: Date.now(),
|
||||
|
||||
@@ -6,18 +6,26 @@ const keyLength = 32;
|
||||
const encryptionPrefix = "encv1";
|
||||
|
||||
/**
|
||||
* Given a string, encrypts it using a randomly generated salt
|
||||
* Checks if a given string is encrypted by looking for the encryption prefix.
|
||||
*/
|
||||
const isEncrypted = (val?: string): boolean => {
|
||||
return typeof val === "string" && val.startsWith(encryptionPrefix);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a string, encrypts it using a randomly generated salt.
|
||||
* Returns the input unchanged if it's empty or already encrypted.
|
||||
*/
|
||||
const encrypt = async (data: string) => {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data.startsWith(encryptionPrefix)) {
|
||||
if (isEncrypted(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const secret = (await Bun.file(RESTIC_PASS_FILE).text()).trim();
|
||||
const secret = await Bun.file(RESTIC_PASS_FILE).text();
|
||||
|
||||
const salt = crypto.randomBytes(16);
|
||||
const key = crypto.pbkdf2Sync(secret, salt, 100000, keyLength, "sha256");
|
||||
@@ -31,10 +39,15 @@ const encrypt = async (data: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an encrypted string, decrypts it using the salt stored in the string
|
||||
* Given an encrypted string, decrypts it using the salt stored in the string.
|
||||
* Returns the input unchanged if it's not encrypted (for backward compatibility).
|
||||
*/
|
||||
const decrypt = async (encryptedData: string) => {
|
||||
const secret = await Bun.file(RESTIC_PASS_FILE).text();
|
||||
if (!isEncrypted(encryptedData)) {
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
const secret = (await Bun.file(RESTIC_PASS_FILE).text()).trim();
|
||||
|
||||
const parts = encryptedData.split(":").slice(1); // Remove prefix
|
||||
const saltHex = parts.shift() as string;
|
||||
@@ -58,4 +71,5 @@ const decrypt = async (encryptedData: string) => {
|
||||
export const cryptoUtils = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
isEncrypted,
|
||||
};
|
||||
|
||||
@@ -795,7 +795,7 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string
|
||||
};
|
||||
|
||||
const addCommonArgs = (args: string[], env: Record<string, string>) => {
|
||||
args.push("--retry-lock", "1m", "--json");
|
||||
args.push("--json");
|
||||
|
||||
if (env._SFTP_SSH_ARGS) {
|
||||
args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"defaultBranch": "origin/main",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"start": "bun ./dist/server/index.js",
|
||||
"tsc": "react-router typegen && tsc",
|
||||
"lint": "biome check .",
|
||||
"lint:ci": "biome check . --ci",
|
||||
"lint:ci": "biome ci . --changed --error-on-warnings --no-errors-on-unmatched",
|
||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||
"gen:api-client": "openapi-ts",
|
||||
|
||||
Reference in New Issue
Block a user