From ec5cc80980fcafca04902a013f4d2e59bb20b674 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 3 Apr 2022 10:44:36 +0200 Subject: [PATCH] Add time zone selection step --- urbackupclient/client_restore_http.cpp | 146 +++++++++++++++++- urbackupclient/client_restore_http.h | 4 + urbackupclient/dllmain.cpp | 3 + urbackupclient/restorewww/src/App.tsx | 11 +- .../restorewww/src/ConfigRestore.tsx | 17 +- urbackupclient/restorewww/src/Restoring.tsx | 4 +- .../restorewww/src/ReviewRestore.tsx | 4 +- .../restorewww/src/WaitForNetwork.tsx | 2 +- urbackupclient/restorewww/src/WizardState.tsx | 3 + 9 files changed, 176 insertions(+), 18 deletions(-) diff --git a/urbackupclient/client_restore_http.cpp b/urbackupclient/client_restore_http.cpp index f53dd3f8..3408ee94 100644 --- a/urbackupclient/client_restore_http.cpp +++ b/urbackupclient/client_restore_http.cpp @@ -6,6 +6,7 @@ #include "../urbackupcommon/mbrdata.h" #include "../fsimageplugin/IFSImageFactory.h" #include "../urbackupcommon/os_functions.h" +#include "../Interface/Thread.h" extern IFSImageFactory* image_fak; @@ -15,6 +16,8 @@ namespace restore::LoginData g_login_data; int64 res_id = 0; + std::atomic timezone_download_started(false); + struct SRestoreRes { restore::DownloadStatus dl_status; @@ -31,6 +34,24 @@ namespace std::lock_guard lock(g_restore_data_mutex); g_login_data = login_data; } + + const std::string timezone_data_file = "/tmp/timezone_data.json"; + + class DownloadTimezoneData : public IThread + { + public: + void operator()() + { + int rc = system(("curl -f \"https://app.urbackup.com/api/online\" > "+timezone_data_file).c_str()); + + if (rc != 0) + { + Server->deleteFile(timezone_data_file); + } + + delete this; + } + }; } ACTION_IMPL(status) @@ -284,7 +305,14 @@ ACTION_IMPL(has_network_device) JSON::Object ret; ret.set("ok", true); - ret.set("ret", restore::has_network_device()); + bool has_device = restore::has_network_device(); + ret.set("ret", has_device); + + bool did_not_start = false; + if (has_device && timezone_download_started.compare_exchange_strong(did_not_start, true)) + { + Server->createThread(new DownloadTimezoneData(), "dl tz data"); + } restore::writeJsonResponse(tid, ret); } @@ -1240,4 +1268,118 @@ ACTION_IMPL(get_tmpfn) } restore::writeJsonResponse(tid, ret); -} \ No newline at end of file +} + +ACTION_IMPL(get_timezone_data) +{ + std::string data; + int64 starttime = Server->getTimeMS(); + + while (data.empty() && Server->getTimeMS() - starttime < 1000) + { + data = getFile(timezone_data_file); + + if (data.empty()) + { + Server->wait(50); + } + } + + if (data.empty()) + { + JSON::Object ret; + ret.set("ok", false); + restore::writeJsonResponse(tid, ret); + } + else + { + Server->setContentType(tid, "application/json"); + Server->Write(tid, data); + } +} + +static std::vector > getTimezones() +{ + std::vector > ret; + std::string tzdata; + int rc = os_popen("timedatectl list-timezones", tzdata); + if (rc != 0) + { + return ret; + } + + std::vector lines; + Tokenize(tzdata, lines, "\n"); + + for (auto& line : lines) + { + if (line.find("/") != std::string::npos) + { + ret.push_back(std::make_pair(getuntil("/", line), getafter("/", line))); + } + else + { + ret.push_back(std::make_pair(trim(line), "")); + } + } + + return ret; +} + +ACTION_IMPL(get_timezone_areas) +{ + auto tzs = getTimezones(); + + JSON::Object ret; + ret.set("ok", true); + JSON::Array areas; + for (auto tz : tzs) + { + areas.add(tz.first); + } + + ret.set("areas", areas); + restore::writeJsonResponse(tid, ret); +} + +ACTION_IMPL(get_timezone_cities) +{ + std::string area = POST["area"]; + auto tzs = getTimezones(); + + JSON::Object ret; + ret.set("ok", true); + + JSON::Array cities; + for (auto tz : tzs) + { + if(tz.first == area && !tz.second.empty()) + cities.add(tz.second); + } + + ret.set("cities", cities); + restore::writeJsonResponse(tid, ret); +} + +ACTION_IMPL(set_timezone) +{ + std::string tz = POST["tz"]; + + JSON::Object ret; + ret.set("ok", true); + + if (tz.empty() || tz.find('"')!=std::string::npos) + { + ret.set("ok", false); + restore::writeJsonResponse(tid, ret); + return; + } + + int rc = system(("timedatectl set-timezone \"" + tz + "\"").c_str()); + + if (rc != 0) + ret.set("ok", false); + + restore::writeJsonResponse(tid, ret); +} + diff --git a/urbackupclient/client_restore_http.h b/urbackupclient/client_restore_http.h index 8717bb26..25ef4a1c 100644 --- a/urbackupclient/client_restore_http.h +++ b/urbackupclient/client_restore_http.h @@ -40,6 +40,10 @@ namespace Actions ACTION(resize_part); ACTION(capabilities); ACTION(get_tmpfn); + ACTION(get_timezone_data); + ACTION(get_timezone_areas); + ACTION(get_timezone_cities); + ACTION(set_timezone); } namespace restore diff --git a/urbackupclient/dllmain.cpp b/urbackupclient/dllmain.cpp index f3c87f7a..520421d3 100644 --- a/urbackupclient/dllmain.cpp +++ b/urbackupclient/dllmain.cpp @@ -393,6 +393,9 @@ DLLEXPORT void LoadActions(IServer* pServer) ADD_ACTION(resize_part); ADD_ACTION(capabilities); ADD_ACTION(get_tmpfn); + ADD_ACTION(get_timezone_data); + ADD_ACTION(get_timezone_areas); + ADD_ACTION(get_timezone_cities); Server->Log("Started UrBackup Restore HTTP backend...", LL_INFO); return; } diff --git a/urbackupclient/restorewww/src/App.tsx b/urbackupclient/restorewww/src/App.tsx index bbadef0e..004b29b0 100644 --- a/urbackupclient/restorewww/src/App.tsx +++ b/urbackupclient/restorewww/src/App.tsx @@ -16,10 +16,12 @@ import ReviewRestore from './ReviewRestore'; import Restoring from './Restoring'; import SelectKeyboard from './SelectKeyboard'; import ConfigureSpillSpace from './ConfigureSpillSpace'; +import SelectTimezone from './SelectTimezone'; // eslint-disable-next-line react-hooks/exhaustive-deps export const useMountEffect = (fun : React.EffectCallback) => useEffect(fun, []); -export const sleep = (m: number) => new Promise(r => setTimeout(r, m)) +export const sleep = (m: number) => new Promise(r => setTimeout(r, m)); +export const serverTimezoneDef = "Server timezone"; export const toIsoDateTime = (d: Date) => { let y = d.getFullYear(); @@ -41,6 +43,8 @@ function CurrentContent(args: WizardComponent) { return ; case WizardState.WaitForNetwork: return ; + case WizardState.SelectTimezone: + return ; case WizardState.ServerSearch: return ; case WizardState.ConfigureServerConnectionDetails: @@ -98,7 +102,9 @@ function App() { disks: [] }, canKeyboardConfig: true, - canRestoreSpill: true + canRestoreSpill: true, + timezoneArea: serverTimezoneDef, + timezoneCity: "" }); const menuSelected = () => { @@ -224,6 +230,7 @@ function App() { Select keyboard layout } Waiting for network + Select time zone Search for server Configure server connection Wait for connection diff --git a/urbackupclient/restorewww/src/ConfigRestore.tsx b/urbackupclient/restorewww/src/ConfigRestore.tsx index 7c78611d..1bd2a14b 100644 --- a/urbackupclient/restorewww/src/ConfigRestore.tsx +++ b/urbackupclient/restorewww/src/ConfigRestore.tsx @@ -3,14 +3,13 @@ import Checkbox, { CheckboxChangeEvent } from "antd/lib/checkbox/Checkbox"; import produce from "immer"; import { useCallback, useEffect } from "react"; import { useState } from "react"; -import { toIsoDateTime } from "./App"; -import { BackupImage, LocalDisk, WizardComponent, WizardState } from "./WizardState"; +import { serverTimezoneDef, toIsoDateTime } from "./App"; +import { BackupImage, LocalDisk, WizardComponent, WizardState, WizardStateProps } from "./WizardState"; -export function imageDateTime(img: BackupImage) { - if (img.time_str.length>0) +export function imageDateTime(img: BackupImage, props: WizardStateProps) { + if (img.time_str.length>0 && props.timezoneArea===serverTimezoneDef) return img.time_str; - - //Perhaps todo: Add a time zone configuration step and always use this one + return toIsoDateTime(new Date(img.time_s*1000)) } @@ -107,7 +106,7 @@ function ConfigRestore(props: WizardComponent) { for(let img of new_images) { for(let assoc_img of img.assoc) { assoc_img.clientname = img.clientname; - if(assoc_img.letter.length==0) + if(assoc_img.letter.length===0) assoc_img.letter = "sysvol"; } } @@ -226,12 +225,12 @@ function ConfigRestore(props: WizardComponent) {
Select image:
diff --git a/urbackupclient/restorewww/src/Restoring.tsx b/urbackupclient/restorewww/src/Restoring.tsx index 8d774aa7..c9082b61 100644 --- a/urbackupclient/restorewww/src/Restoring.tsx +++ b/urbackupclient/restorewww/src/Restoring.tsx @@ -347,7 +347,7 @@ function Restoring(props: WizardComponent) { const restoreImage = async (img: BackupImage, restoreToPartition: boolean, restoreToDisk: LocalDisk, withSpillSpace: boolean, mbrInfo: MbrInfo) => { - setRestoreAction(img.letter +" - "+imageDateTime(img) + " client "+img.clientname); + setRestoreAction(img.letter +" - "+imageDateTime(img, props.props) + " client "+img.clientname); let partpath: string; let partnum: number; @@ -584,7 +584,7 @@ function Restoring(props: WizardComponent) { addLog("Restoring "+restoreImages.length+" images of client "+props.props.restoreImage.clientname+": "); for(const img of restoreImages) { - addLog("Restoring "+img.letter+" at " + imageDateTime(img)); + addLog("Restoring "+img.letter+" at " + imageDateTime(img, props.props)); } assert(!props.props.restoreToPartition || restoreImages.length===1); diff --git a/urbackupclient/restorewww/src/ReviewRestore.tsx b/urbackupclient/restorewww/src/ReviewRestore.tsx index ca971b86..3ee6d277 100644 --- a/urbackupclient/restorewww/src/ReviewRestore.tsx +++ b/urbackupclient/restorewww/src/ReviewRestore.tsx @@ -1,6 +1,6 @@ import { Alert, Button } from "antd"; import produce from "immer"; -import { toIsoDateTime } from "./App"; +import { imageDateTime } from "./ConfigRestore"; import { WizardComponent, WizardState } from "./WizardState"; @@ -20,7 +20,7 @@ function ReviewRestore(props: WizardComponent) {
{props.props.restoreOnlyMBR ? "MBR and GPT" : "Image"} to restore:
- {props.props.restoreOnlyMBR ? "MBR and GPT" : "Image"} of client {props.props.restoreImage.clientname} of volume {props.props.restoreImage.letter} at {toIsoDateTime(new Date(props.props.restoreImage.time_s*1000))} + {props.props.restoreOnlyMBR ? "MBR and GPT" : "Image"} of client {props.props.restoreImage.clientname} of volume {props.props.restoreImage.letter} at {imageDateTime(props.props.restoreImage, props.props)}

diff --git a/urbackupclient/restorewww/src/WaitForNetwork.tsx b/urbackupclient/restorewww/src/WaitForNetwork.tsx index 4e2da1a7..31645268 100644 --- a/urbackupclient/restorewww/src/WaitForNetwork.tsx +++ b/urbackupclient/restorewww/src/WaitForNetwork.tsx @@ -29,7 +29,7 @@ function WaitForNetwork(props: WizardComponent) { if(jdata && jdata["ret"]) { props.update(produce(props.props, draft => { - draft.state = WizardState.ServerSearch; + draft.state = WizardState.SelectTimezone; draft.max_state = draft.state; })); return; diff --git a/urbackupclient/restorewww/src/WizardState.tsx b/urbackupclient/restorewww/src/WizardState.tsx index 2975d136..050dd8ce 100644 --- a/urbackupclient/restorewww/src/WizardState.tsx +++ b/urbackupclient/restorewww/src/WizardState.tsx @@ -2,6 +2,7 @@ export enum WizardState { Init = 1, SelectKeyboard, WaitForNetwork, + SelectTimezone, ServerSearch, ConfigureServerConnectionDetails, WaitForConnection, @@ -63,6 +64,8 @@ export interface WizardStateProps { spillSpace: SpillSpace; canRestoreSpill: boolean; canKeyboardConfig: boolean; + timezoneArea: string; + timezoneCity: string; } export interface WizardComponent {