Add time zone selection step

This commit is contained in:
Martin 2022-04-03 10:44:36 +02:00
parent 9fc2d934a0
commit ec5cc80980
9 changed files with 176 additions and 18 deletions

View File

@ -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<bool> timezone_download_started(false);
struct SRestoreRes
{
restore::DownloadStatus dl_status;
@ -31,6 +34,24 @@ namespace
std::lock_guard<std::mutex> 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);
}
}
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<std::pair<std::string, std::string> > getTimezones()
{
std::vector<std::pair<std::string, std::string> > ret;
std::string tzdata;
int rc = os_popen("timedatectl list-timezones", tzdata);
if (rc != 0)
{
return ret;
}
std::vector<std::string> 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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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 <SelectKeyboard props={args.props} update={args.update} />;
case WizardState.WaitForNetwork:
return <WaitForNetwork props={args.props} update={args.update} />;
case WizardState.SelectTimezone:
return <SelectTimezone props={args.props} update={args.update} />;
case WizardState.ServerSearch:
return <ServerSearch props={args.props} update={args.update} />;
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() {
<Menu.Item key={"" + WizardState.SelectKeyboard} disabled={menuItemDisabled(WizardState.SelectKeyboard)}>Select keyboard layout</Menu.Item>
}
<Menu.Item key={"" + WizardState.WaitForNetwork} disabled={menuItemDisabled(WizardState.WaitForNetwork)}>Waiting for network</Menu.Item>
<Menu.Item key={"" + WizardState.SelectTimezone} disabled={menuItemDisabled(WizardState.SelectTimezone)}>Select time zone</Menu.Item>
<Menu.Item key={"" + WizardState.ServerSearch} disabled={menuItemDisabled(WizardState.ServerSearch)}>Search for server</Menu.Item>
<Menu.Item key={"" + WizardState.ConfigureServerConnectionDetails} disabled={menuItemDisabled(WizardState.ConfigureServerConnectionDetails)}>Configure server connection</Menu.Item>
<Menu.Item key={"" + WizardState.WaitForConnection} disabled={menuItemDisabled(WizardState.WaitForConnection)}>Wait for connection</Menu.Item>

View File

@ -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) {
<br />
Select image: <br />
<Select loading={!hasImages} style={{ width: "600px" }}
value={selImage === undefined ? "Please select..." : (selImage.letter + " - " + imageDateTime(selImage))}
value={selImage === undefined ? "Please select..." : (selImage.letter + " - " + imageDateTime(selImage, props.props))}
onChange={(val) => {
setSelImage( getSelImageSet().find( image => image.id===parseInt(val) ) as (BackupImage|undefined));
} }>
{getSelImageSet().map( image => (
<Select.Option key={image.id} value={image.id}>{image.letter} - {imageDateTime(image)}</Select.Option>
<Select.Option key={image.id} value={image.id}>{image.letter} - {imageDateTime(image, props.props)}</Select.Option>
))}
</Select>

View File

@ -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);

View File

@ -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) {
<Alert type="warning" message="Data on the destination disk will be overwritten!" />
<br />
{props.props.restoreOnlyMBR ? "MBR and GPT" : "Image"} to restore: <br />
<strong>{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))}</strong>
<strong>{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)}</strong>
<br />
<br />

View File

@ -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;

View File

@ -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 {