From 426aad4890d2de5d70cd2e0232c0d11c42606c92 Mon Sep 17 00:00:00 2001 From: Gareth Date: Thu, 7 Dec 2023 23:26:32 -0800 Subject: [PATCH] fix: improve Windows path handling --- internal/orchestrator/taskbackup.go | 8 +++++ webui/package.json | 3 +- webui/src/components/URIAutocomplete.tsx | 39 +++++++++++++++++++++--- webui/src/state/buildcfg.ts | 5 +++ webui/src/views/AddPlanModal.tsx | 1 + webui/src/views/App.tsx | 9 +++--- 6 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 webui/src/state/buildcfg.ts diff --git a/internal/orchestrator/taskbackup.go b/internal/orchestrator/taskbackup.go index e0f80ab6..0568c060 100644 --- a/internal/orchestrator/taskbackup.go +++ b/internal/orchestrator/taskbackup.go @@ -103,12 +103,20 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan } lastSent := time.Now() // debounce progress updates, these can endup being very frequent. + var lastFiles []string summary, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) { if time.Since(lastSent) < 250*time.Millisecond { return } lastSent = time.Now() + // prevents flickering output when a status entry omits the CurrentFiles property. Largely cosmetic. + if len(entry.CurrentFiles) == 0 { + entry.CurrentFiles = lastFiles + } else { + lastFiles = entry.CurrentFiles + } + backupOp.OperationBackup.LastStatus = protoutil.BackupProgressEntryToProto(entry) if err := orchestrator.OpLog.Update(op); err != nil { zap.S().Errorf("failed to update oplog with progress for backup: %v", err) diff --git a/webui/package.json b/webui/package.json index 9c2298b6..b38e1c8e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -4,7 +4,8 @@ "description": "", "scripts": { "start": "parcel serve src/index.html", - "build": "RESTICUI_BUILD_VERSION=$(git describe --tags --abbrev=0) parcel build src/index.html", + "build": "RESTICUI_BUILD_VERSION=$(git describe --tags --abbrev=0) UI_OS=unix parcel build src/index.html", + "build-windows": "set UI_OS=windows & set RESTICUI_BUILD_VERSION=$(git describe --tags --abbrev=0) & parcel build src/index.html", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", diff --git a/webui/src/components/URIAutocomplete.tsx b/webui/src/components/URIAutocomplete.tsx index b1528088..16bdbeb7 100644 --- a/webui/src/components/URIAutocomplete.tsx +++ b/webui/src/components/URIAutocomplete.tsx @@ -2,8 +2,10 @@ import { AutoComplete } from "antd"; import React, { useEffect, useState } from "react"; import { ResticUI } from "../../gen/ts/v1/service.pb"; import { StringList } from "../../gen/ts/types/value.pb"; +import { isWindows } from "../state/buildcfg"; let timeout: NodeJS.Timeout | undefined = undefined; +const sep = isWindows ? "\\" : "/"; export const URIAutocomplete = (props: React.PropsWithChildren) => { const [value, setValue] = useState(""); @@ -17,7 +19,7 @@ export const URIAutocomplete = (props: React.PropsWithChildren) => { const onChange = (value: string) => { setValue(value); - const lastSlash = value.lastIndexOf("/"); + const lastSlash = value.lastIndexOf(sep); if (lastSlash !== -1) { value = value.substring(0, lastSlash); } @@ -27,14 +29,14 @@ export const URIAutocomplete = (props: React.PropsWithChildren) => { } timeout = setTimeout(() => { - ResticUI.PathAutocomplete({ value: value + "/" }, { pathPrefix: "/api" }) + ResticUI.PathAutocomplete({ value: value + sep }, { pathPrefix: "/api" }) .then((res: StringList) => { if (!res.values) { return; } const vals = res.values.map((v) => { return { - value: value + "/" + v, + value: value + sep + v, }; }); setOptions(vals); @@ -45,5 +47,34 @@ export const URIAutocomplete = (props: React.PropsWithChildren) => { }, 100); }; - return ; + return ( + { + if (props.globAllowed) { + return Promise.resolve(); + } + if (isWindows) { + if (value.match(/^[a-zA-Z]:\\$/)) { + return Promise.reject( + new Error("Path must start with a drive letter e.g. C:\\") + ); + } else if (value.includes("/")) { + return Promise.reject( + new Error( + "Path must use backslashes e.g. C:\\Users\\MyUsers\\Documents" + ) + ); + } + } + return Promise.resolve(); + }, + }, + ]} + {...props} + /> + ); }; diff --git a/webui/src/state/buildcfg.ts b/webui/src/state/buildcfg.ts new file mode 100644 index 00000000..5166a80c --- /dev/null +++ b/webui/src/state/buildcfg.ts @@ -0,0 +1,5 @@ +export const uios = (process.env.UI_OS || "").trim().toLowerCase(); +export const isWindows = uios === "windows"; +export const uiBuildVersion = ( + process.env.RESTICUI_BUILD_VERSION || "dev" +).trim(); diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx index e5a6353c..24ecb607 100644 --- a/webui/src/views/AddPlanModal.tsx +++ b/webui/src/views/AddPlanModal.tsx @@ -291,6 +291,7 @@ export const AddPlanModal = ({ form.validateFields()} + globAllowed={true} /> { const { @@ -58,9 +59,7 @@ export const App: React.FC = () => { BackRestic{" "} - {process.env.RESTICUI_BUILD_VERSION - ? process.env.RESTICUI_BUILD_VERSION - : ""} + {uiBuildVersion}