2024-03-04 12:21:53 +01:00
|
|
|
// Copyright (C) 2024, The Duplicati Team
|
|
|
|
|
// https://duplicati.com, hello@duplicati.com
|
|
|
|
|
//
|
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
|
|
|
// to deal in the Software without restriction, including without limitation
|
|
|
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
|
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
|
|
|
//
|
|
|
|
|
// The above copyright notice and this permission notice shall be included in
|
|
|
|
|
// all copies or substantial portions of the Software.
|
|
|
|
|
//
|
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
|
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
|
|
// DEALINGS IN THE SOFTWARE.
|
2022-12-30 10:59:29 -08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using Duplicati.Server.Serialization;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using Duplicati.Server.Serialization.Interface;
|
|
|
|
|
using Duplicati.Library.RestAPI;
|
|
|
|
|
|
|
|
|
|
namespace Duplicati.Server.WebServer.RESTMethods
|
|
|
|
|
{
|
|
|
|
|
public class Backup : IRESTMethodGET, IRESTMethodPUT, IRESTMethodPOST, IRESTMethodDELETE, IRESTMethodDocumented
|
|
|
|
|
{
|
|
|
|
|
public class GetResponse
|
|
|
|
|
{
|
|
|
|
|
public class GetResponseData
|
|
|
|
|
{
|
|
|
|
|
public Serialization.Interface.ISchedule Schedule;
|
|
|
|
|
public Serialization.Interface.IBackup Backup;
|
|
|
|
|
public Dictionary<string, string> DisplayNames;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool success;
|
|
|
|
|
|
|
|
|
|
public GetResponseData data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SearchFiles(IBackup backup, string filterstring, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var filter = filterstring;
|
|
|
|
|
var timestring = info.Request.QueryString["time"].Value;
|
|
|
|
|
var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false);
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(timestring) && !allversion)
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prefixonly = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["prefix-only"].Value, false);
|
|
|
|
|
var foldercontents = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["folder-contents"].Value, false);
|
|
|
|
|
var time = new DateTime();
|
|
|
|
|
if (!allversion)
|
|
|
|
|
time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);
|
|
|
|
|
|
|
|
|
|
var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults;
|
|
|
|
|
|
|
|
|
|
var result = new Dictionary<string, object>();
|
|
|
|
|
|
|
|
|
|
foreach(HttpServer.HttpInputItem n in info.Request.QueryString)
|
|
|
|
|
result[n.Name] = n.Value;
|
|
|
|
|
|
|
|
|
|
result["Filesets"] = r.Filesets;
|
|
|
|
|
result["Files"] = r.Files
|
|
|
|
|
// Group directories first - support either directory separator here as we may be restoring data from an alternate platform
|
|
|
|
|
.OrderByDescending(f => (f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("/", StringComparison.Ordinal)) || (!f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("\\", StringComparison.Ordinal)))
|
|
|
|
|
// Sort both groups (directories and files) alphabetically
|
|
|
|
|
.ThenBy(f => f.Path);
|
|
|
|
|
|
|
|
|
|
info.OutputOK(result);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ListFileSets(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var input = info.Request.QueryString;
|
|
|
|
|
var extra = new Dictionary<string, string>
|
|
|
|
|
{
|
|
|
|
|
["list-sets-only"] = "true"
|
|
|
|
|
};
|
|
|
|
|
if (input["include-metadata"].Value != null)
|
|
|
|
|
extra["list-sets-only"] = (!Library.Utility.Utility.ParseBool(input["include-metadata"].Value, false)).ToString();
|
|
|
|
|
if (input["from-remote-only"].Value != null)
|
|
|
|
|
extra["no-local-db"] = Library.Utility.Utility.ParseBool(input["from-remote-only"].Value, false).ToString();
|
|
|
|
|
|
|
|
|
|
var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, backup, extra), false) as Duplicati.Library.Interface.IListResults;
|
|
|
|
|
|
|
|
|
|
if (r.EncryptedFiles && backup.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
info.ReportServerError("encrypted-storage");
|
|
|
|
|
else
|
|
|
|
|
info.OutputOK(r.Filesets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FetchLogData(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath))
|
|
|
|
|
using(var cmd = con.CreateCommand())
|
|
|
|
|
info.OutputOK(LogData.DumpTable(cmd, "LogData", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FetchRemoteLogData(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath))
|
|
|
|
|
using(var cmd = con.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value);
|
|
|
|
|
|
|
|
|
|
// Unwrap raw data to a string
|
|
|
|
|
foreach(var n in dt)
|
|
|
|
|
try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); }
|
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
|
|
info.OutputOK(dt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private void IsDBUsedElseWhere(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void RemovePasswords(IBackup backup)
|
|
|
|
|
{
|
|
|
|
|
backup.SanitizeSettings();
|
|
|
|
|
backup.SanitizeTargetUrl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Export(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false);
|
|
|
|
|
var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false);
|
|
|
|
|
var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false);
|
|
|
|
|
if (!exportPasswords)
|
|
|
|
|
{
|
|
|
|
|
Backup.RemovePasswords(backup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cmdline)
|
|
|
|
|
{
|
|
|
|
|
info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) });
|
|
|
|
|
}
|
|
|
|
|
else if (argsonly)
|
|
|
|
|
{
|
|
|
|
|
var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new {
|
|
|
|
|
Backend = parts.First(),
|
|
|
|
|
Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
|
|
|
|
|
Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal))
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var passphrase = info.Request.QueryString["passphrase"].Value;
|
|
|
|
|
byte[] data = Backup.ExportToJSON(backup, passphrase);
|
|
|
|
|
|
|
|
|
|
string filename = Library.Utility.Uri.UrlEncode(backup.Name) + "-duplicati-config.json";
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(passphrase))
|
|
|
|
|
{
|
|
|
|
|
filename += ".aes";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.Response.ContentLength = data.Length;
|
|
|
|
|
info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
|
|
|
|
|
info.Response.ContentType = "application/octet-stream";
|
|
|
|
|
|
|
|
|
|
info.BodyWriter.SetOK();
|
|
|
|
|
info.Response.SendHeaders();
|
|
|
|
|
info.Response.SendBody(data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static byte[] ExportToJSON(IBackup backup, string passphrase)
|
|
|
|
|
{
|
|
|
|
|
Serializable.ImportExportStructure ipx = FIXMEGlobal.DataConnection.PrepareBackupForExport(backup);
|
|
|
|
|
|
|
|
|
|
byte[] data;
|
|
|
|
|
using (MemoryStream ms = new System.IO.MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
using (StreamWriter sw = new System.IO.StreamWriter(ms))
|
|
|
|
|
{
|
|
|
|
|
Serializer.SerializeJson(sw, ipx, true);
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(passphrase))
|
|
|
|
|
{
|
|
|
|
|
ms.Position = 0;
|
|
|
|
|
using (MemoryStream ms2 = new System.IO.MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
using (Library.Encryption.AESEncryption m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
|
|
|
|
|
{
|
|
|
|
|
m.Encrypt(ms, ms2);
|
|
|
|
|
data = ms2.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
data = ms.ToArray();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RestoreFiles(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var input = info.Request.Form;
|
|
|
|
|
|
|
|
|
|
string[] filters = parsePaths(input["paths"].Value ?? string.Empty);
|
|
|
|
|
|
|
|
|
|
var passphrase = string.IsNullOrEmpty(input["passphrase"].Value) ? null : input["passphrase"].Value;
|
|
|
|
|
|
|
|
|
|
var time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(input["time"].Value, DateTime.Now);
|
|
|
|
|
var restoreTarget = input["restore-path"].Value;
|
|
|
|
|
var overwrite = Duplicati.Library.Utility.Utility.ParseBool(input["overwrite"].Value, false);
|
|
|
|
|
|
|
|
|
|
var permissions = Duplicati.Library.Utility.Utility.ParseBool(input["permissions"].Value, false);
|
|
|
|
|
var skip_metadata = Duplicati.Library.Utility.Utility.ParseBool(input["skip-metadata"].Value, false);
|
|
|
|
|
|
|
|
|
|
var task = Runner.CreateRestoreTask(backup, filters, time, restoreTarget, overwrite, permissions, skip_metadata, passphrase);
|
|
|
|
|
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { TaskID = task.TaskID });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CreateReport(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ReportRemoteSize(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Repair(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
DoRepair(backup, info, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RepairUpdate(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
DoRepair(backup, info, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Vacuum(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var task = Runner.CreateTask(DuplicatiOperation.Vacuum, backup);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Verify(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var task = Runner.CreateTask(DuplicatiOperation.Verify, backup);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new {Status = "OK", ID = task.TaskID});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Compact(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var task = Runner.CreateTask(DuplicatiOperation.Compact, backup);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string[] parsePaths(string paths)
|
|
|
|
|
{
|
|
|
|
|
string[] filters;
|
|
|
|
|
var rawpaths = (paths ?? string.Empty).Trim();
|
|
|
|
|
|
|
|
|
|
// We send the file list as a JSON array to avoid encoding issues with the path separator
|
|
|
|
|
// as it is an allowed character in file and path names.
|
|
|
|
|
// We also accept the old way, for compatibility with the greeno theme
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(rawpaths) && rawpaths.StartsWith("[", StringComparison.Ordinal) && rawpaths.EndsWith("]", StringComparison.Ordinal))
|
|
|
|
|
filters = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(rawpaths);
|
|
|
|
|
else
|
|
|
|
|
filters = paths.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
|
|
|
return filters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DoRepair(IBackup backup, RequestInfo info, bool repairUpdate)
|
|
|
|
|
{
|
|
|
|
|
var input = info.Request.Form;
|
|
|
|
|
string[] filters = null;
|
|
|
|
|
var extra = new Dictionary<string, string>();
|
|
|
|
|
if (input["only-paths"].Value != null)
|
|
|
|
|
extra["repair-only-paths"] = (Library.Utility.Utility.ParseBool(input["only-paths"].Value, false)).ToString();
|
|
|
|
|
if (input["time"].Value != null)
|
|
|
|
|
extra["time"] = input["time"].Value;
|
|
|
|
|
if (input["version"].Value != null)
|
|
|
|
|
extra["version"] = input["version"].Value;
|
|
|
|
|
if (input["paths"].Value != null)
|
|
|
|
|
filters = parsePaths(input["paths"].Value);
|
|
|
|
|
|
|
|
|
|
var task = Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new {Status = "OK", ID = task.TaskID});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RunBackup(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
|
|
|
|
var bt = t == null ? null : t.Backup;
|
|
|
|
|
if (bt != null && backup.ID == bt.ID)
|
|
|
|
|
{
|
|
|
|
|
// Already running
|
|
|
|
|
}
|
|
|
|
|
else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => {
|
|
|
|
|
var bn = x?.Backup;
|
|
|
|
|
return bn == null || bn.ID == backup.ID;
|
|
|
|
|
}))
|
|
|
|
|
{
|
|
|
|
|
// Already in queue
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.OutputOK();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void IsActive(IBackup backup, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
|
|
|
|
var bt = t?.Backup;
|
|
|
|
|
if (bt != null && backup.ID == bt.ID)
|
|
|
|
|
{
|
|
|
|
|
info.OutputOK(new { Status = "OK", Active = true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x =>
|
|
|
|
|
{
|
|
|
|
|
var bn = x?.Backup;
|
|
|
|
|
return bn == null || bn.ID == backup.ID;
|
|
|
|
|
}))
|
|
|
|
|
{
|
|
|
|
|
info.OutputOK(new { Status = "OK", Active = true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
info.OutputOK(new { Status = "OK", Active = false });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateDatabasePath(IBackup backup, RequestInfo info, bool move)
|
|
|
|
|
{
|
|
|
|
|
var np = info.Request.Form["path"].Value;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(np))
|
|
|
|
|
info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
else if (!Path.IsPathRooted(np))
|
|
|
|
|
info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (move && (File.Exists(np) || Directory.Exists(np)))
|
|
|
|
|
info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict);
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (move)
|
|
|
|
|
File.Move(backup.DBPath, np);
|
|
|
|
|
|
|
|
|
|
FIXMEGlobal.DataConnection.UpdateBackupDBPath(backup, np);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void GET(string key, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
|
|
|
|
var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First());
|
|
|
|
|
if (bk == null)
|
|
|
|
|
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (parts.Length > 1)
|
|
|
|
|
{
|
|
|
|
|
var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant();
|
|
|
|
|
|
|
|
|
|
switch (operation)
|
|
|
|
|
{
|
|
|
|
|
case "files":
|
|
|
|
|
var filter = parts.Last().Split(new char[] { '/' }, 2).Skip(1).FirstOrDefault();
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(info.Request.QueryString["filter"].Value))
|
|
|
|
|
filter = info.Request.QueryString["filter"].Value;
|
|
|
|
|
SearchFiles(bk, filter, info);
|
|
|
|
|
return;
|
|
|
|
|
case "log":
|
|
|
|
|
FetchLogData(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
case "remotelog":
|
|
|
|
|
FetchRemoteLogData(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
case "filesets":
|
|
|
|
|
ListFileSets(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
case "export":
|
|
|
|
|
Export(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
case "isdbusedelsewhere":
|
|
|
|
|
IsDBUsedElseWhere(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
case "isactive":
|
|
|
|
|
IsActive(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
default:
|
|
|
|
|
info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var scheduleId = FIXMEGlobal.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID });
|
|
|
|
|
var schedule = scheduleId.Any() ? FIXMEGlobal.DataConnection.GetSchedule(scheduleId.First()) : null;
|
|
|
|
|
var sourcenames = SpecialFolders.GetSourceNames(bk);
|
|
|
|
|
|
|
|
|
|
//TODO: Filter out the password in both settings and the target url
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new GetResponse()
|
|
|
|
|
{
|
|
|
|
|
success = true,
|
|
|
|
|
data = new GetResponse.GetResponseData {
|
|
|
|
|
Schedule = schedule,
|
|
|
|
|
Backup = bk,
|
|
|
|
|
DisplayNames = sourcenames
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void POST(string key, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
|
|
|
|
var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First());
|
|
|
|
|
if (bk == null)
|
|
|
|
|
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (parts.Length > 1)
|
|
|
|
|
{
|
|
|
|
|
var operation = parts.Last().Split(new char[] { '/' }).First().ToLowerInvariant();
|
|
|
|
|
|
|
|
|
|
switch (operation)
|
|
|
|
|
{
|
|
|
|
|
case "deletedb":
|
|
|
|
|
System.IO.File.Delete(bk.DBPath);
|
|
|
|
|
info.OutputOK();
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "movedb":
|
|
|
|
|
UpdateDatabasePath(bk, info, true);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "updatedb":
|
|
|
|
|
UpdateDatabasePath(bk, info, false);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "restore":
|
|
|
|
|
RestoreFiles(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "createreport":
|
|
|
|
|
CreateReport(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "repair":
|
|
|
|
|
Repair(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "repairupdate":
|
|
|
|
|
RepairUpdate(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "vacuum":
|
|
|
|
|
Vacuum(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "verify":
|
|
|
|
|
Verify(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "compact":
|
|
|
|
|
Compact(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "start":
|
|
|
|
|
case "run":
|
|
|
|
|
RunBackup(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "report-remote-size":
|
|
|
|
|
ReportRemoteSize(bk, info);
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case "copytotemp":
|
|
|
|
|
var ipx = Serializer.Deserialize<Database.Backup>(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(bk)));
|
|
|
|
|
|
|
|
|
|
using(var tf = new Duplicati.Library.Utility.TempFile())
|
|
|
|
|
ipx.DBPath = tf;
|
|
|
|
|
ipx.ID = null;
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { status = "OK", ID = FIXMEGlobal.DataConnection.RegisterTemporaryBackup(ipx) });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void PUT(string key, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
string str = info.Request.Form["data"].Value;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(str))
|
2023-03-15 22:02:53 -07:00
|
|
|
str = System.Threading.Tasks.Task.Run(async () =>
|
|
|
|
|
{
|
|
|
|
|
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
|
|
|
|
|
|
|
|
|
return await sr.ReadToEndAsync();
|
|
|
|
|
}).GetAwaiter().GetResult();
|
2022-12-30 10:59:29 -08:00
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(str))
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Backups.AddOrUpdateBackupData data = null;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
data = Serializer.Deserialize<Backups.AddOrUpdateBackupData>(new StringReader(str));
|
|
|
|
|
if (data.Backup == null)
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(key))
|
|
|
|
|
data.Backup.ID = key;
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(data.Backup.ID))
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (data.Backup.IsTemporary)
|
|
|
|
|
{
|
|
|
|
|
var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID);
|
|
|
|
|
if (backup.IsTemporary)
|
|
|
|
|
throw new InvalidDataException("External is temporary but internal is not?");
|
|
|
|
|
|
|
|
|
|
FIXMEGlobal.DataConnection.UpdateTemporaryBackup(backup);
|
|
|
|
|
info.OutputOK();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
lock(FIXMEGlobal.DataConnection.m_lock)
|
|
|
|
|
{
|
|
|
|
|
var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID);
|
|
|
|
|
if (backup == null)
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID))
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule);
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(err))
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//TODO: Merge in real passwords where the placeholder is found
|
|
|
|
|
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.OutputOK();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
if (data == null)
|
|
|
|
|
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
|
|
|
|
else
|
|
|
|
|
info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void DELETE(string key, RequestInfo info)
|
|
|
|
|
{
|
|
|
|
|
var backup = FIXMEGlobal.DataConnection.GetBackup(key);
|
|
|
|
|
if (backup == null)
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var delete_remote_files = Library.Utility.Utility.ParseBool(info.Request.Param["delete-remote-files"].Value, false);
|
|
|
|
|
|
|
|
|
|
if (delete_remote_files)
|
|
|
|
|
{
|
|
|
|
|
var captcha_token = info.Request.Param["captcha-token"].Value;
|
|
|
|
|
var captcha_answer = info.Request.Param["captcha-answer"].Value;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer))
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Captcha.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer))
|
|
|
|
|
{
|
|
|
|
|
info.ReportClientError("Invalid captcha", System.Net.HttpStatusCode.Forbidden);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (FIXMEGlobal.WorkThread.Active)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
//TODO: It's not safe to access the values like this,
|
|
|
|
|
//because the runner thread might interfere
|
|
|
|
|
var nt = FIXMEGlobal.WorkThread.CurrentTask;
|
|
|
|
|
if (backup.Equals(nt == null ? null : nt.Backup))
|
|
|
|
|
{
|
|
|
|
|
bool force;
|
|
|
|
|
if (!bool.TryParse(info.Request.QueryString["force"].Value, out force))
|
|
|
|
|
force = false;
|
|
|
|
|
|
|
|
|
|
if (!force)
|
|
|
|
|
{
|
|
|
|
|
info.OutputError(new { status = "failed", reason = "backup-in-progress" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool hasPaused = FIXMEGlobal.LiveControl.State == LiveControls.LiveControlState.Paused;
|
|
|
|
|
FIXMEGlobal.LiveControl.Pause();
|
|
|
|
|
|
|
|
|
|
for(int i = 0; i < 10; i++)
|
|
|
|
|
if (FIXMEGlobal.WorkThread.Active)
|
|
|
|
|
{
|
|
|
|
|
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
|
|
|
|
if (backup.Equals(t == null ? null : t.Backup))
|
|
|
|
|
System.Threading.Thread.Sleep(1000);
|
|
|
|
|
else
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
if (FIXMEGlobal.WorkThread.Active)
|
|
|
|
|
{
|
|
|
|
|
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
|
|
|
|
if (backup.Equals(t == null ? null : t.Backup))
|
|
|
|
|
{
|
|
|
|
|
if (hasPaused)
|
|
|
|
|
FIXMEGlobal.LiveControl.Resume();
|
|
|
|
|
info.OutputError(new { status = "failed", reason = "backup-unstoppable" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasPaused)
|
|
|
|
|
FIXMEGlobal.LiveControl.Resume();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
info.OutputError(new { status = "error", message = ex.Message });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var extra = new Dictionary<string, string>();
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(info.Request.Param["delete-local-db"].Value))
|
|
|
|
|
extra["delete-local-db"] = info.Request.Param["delete-local-db"].Value;
|
|
|
|
|
if (delete_remote_files)
|
|
|
|
|
extra["delete-remote-files"] = "true";
|
|
|
|
|
|
|
|
|
|
var task = Runner.CreateTask(DuplicatiOperation.Delete, backup, extra);
|
|
|
|
|
FIXMEGlobal.WorkThread.AddTask(task);
|
|
|
|
|
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
|
|
|
|
|
|
|
|
|
info.OutputOK(new { Status = "OK", ID = task.TaskID });
|
|
|
|
|
}
|
|
|
|
|
public string Description { get { return "Retrieves, updates or deletes an existing backup and schedule"; } }
|
|
|
|
|
|
|
|
|
|
public IEnumerable<KeyValuePair<string, Type>> Types
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return new KeyValuePair<string, Type>[] {
|
|
|
|
|
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
|
|
|
|
|
new KeyValuePair<string, Type>(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)),
|
|
|
|
|
new KeyValuePair<string, Type>(HttpServer.Method.Delete, typeof(long))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|